starlette: Background tasks don't work with middleware that subclasses `BaseHTTPMiddleware`
When using background tasks with middleware, requests are not processed until the background task has finished.
- Use the example below
- Make several requests in a row - works as expected
- Uncomment
app.add_middleware(TransparentMiddleware)and re-run - Make several requests in a row - subsequent ones are not processed until the 10 second sleep has finished (the first request returns before then though).
The same behaviour occurs with asyncio.sleep (async) and time.sleep (sync, run in threadpool)
import asyncio
import uvicorn
from starlette.applications import Starlette
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.background import BackgroundTask
from starlette.responses import JSONResponse
class TransparentMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request, call_next):
# simple middleware that does absolutely nothing
response = await call_next(request)
return response
app = Starlette(debug=True)
# uncomment to break
# app.add_middleware(TransparentMiddleware)
@app.route("/")
async def test(_):
task = BackgroundTask(asyncio.sleep, 10)
return JSONResponse("hello", background=task)
if __name__ == '__main__':
uvicorn.run(app)
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 45
- Comments: 42 (17 by maintainers)
Commits related to this issue
- Fix: DOS-625 Address action execution blocking new requests * The usage of BackgroundTasks in the new execute_action code was found to be causing blockages of new requests being picked up by the even... — committed to causalens/dara by sam-causalens 5 months ago
- Fix: DOS-625 Address action execution blocking new requests * The usage of BackgroundTasks in the new execute_action code was found to be causing blockages of new requests being picked up by the even... — committed to causalens/dara by sam-causalens 5 months ago
- Fix: DOS-625 Address action execution blocking new requests (#189) * Fix: DOS-625 Address action execution blocking new requests * The usage of BackgroundTasks in the new execute_action code was f... — committed to causalens/dara by sam-causalens 5 months ago
Any updates on this issue ?
Hi all, I had a PR to fix this behavior (#1017 ) but it relied on forcing the response to remain unevaluated, which further confused those users who were relying on having a fully-evaluated response inside their middleware functions (#1022 ). In other words, there are contradictory goals here.
After looking at this
BaseHTTPMiddlewareclass over the last week, I have come to the following conclusions:We should discourage the use of
BaseHTTPMiddlewarein favor of raw ASGI middleware which includes a methoddef __call__(self, scope, receive, send):, andIf people still want to use this middleware class, it should never be used with
StreamingResponse/FileResponseendpoints.Unfortunately, while the
BaseHTTPMiddlewareclass offers an easier interface because it provides arequestargument and promises something like aresponse, this middleware class also encourages users to think about the asgi app functionality in a “complete” or “finished” way. This means this class will either load the entirety of streaming requests into memory (#1012 ) and run the background before returning the response (this issue), or if we fix those problems, that it will then encourage users to leave resources in a pending or open state, an arguably worse result. In short, it’s problematic.Again, these problems should be absent if you avoid subclassing
BaseHTTPMiddleware.Lastly, I think it’s worth leaving this issue open so that other users who have the same problem can see it and see that it remains an open issue.
@ariefprabowo You can use
asyncio.create_task(coro)If anyone finds this and is trying to add headers to responses this is how i ended up doing it using the information from the above discussions.
I got this bug after updating fastapi 0.67.0 -> 0.70.0
This code worked on my project with few midllewares, but after migrating to newer version, “im alive” is not printing in my logs
It happens even i use only one dummy middleware:
After removing all middlewares this code works well as expected
I tried to reproduce this on new pure project, but i could not.
Finally found a workaround by exploiting
run_in_executor:Then in your favorite FastAPI endpoint:
i have same issue and if i add
await asyncio.sleep(1)inside any method that is triggered by task, it is working fine. I don’t understand how it can work but it is working. even i put time.sleep(30) after asyncio.sleep, i get result immediately but process finish 30sec later which is correct.Yes, it is a broader issue with background tasks but that also implies that something should be done differently with how concurrency is being managed in the app. @retnikt see this comment for further discussion.
IMO this isn’t really (just) an issue with
BaseHTTPMiddleware, rather with how background tasks sure implemented; this should be fixed by making them either:Responseby usingasyncio.create_taskor similar, so they actually run in the backgroundI propose that this issue be closed - as far as I can tell, the issues reported here are stale, invalid, or duplicates of other issues:
starlette==0.13.2+uvicorn==0.11.4;anyiohas completely changed the behavior ofBaseHTTPMiddlewareregarding that kind of concurrency and cancellation issues, meaning that I suspect that the issue is stale;GZipMiddlewarecannot have anything to do withBaseHTTPMiddlewareasGZipMiddlewareis not a subclass ofBaseHTTPMiddleware- maybe there is another issue being reported there?Here is how I think we should proceed on this issue:
BaseHTTPMiddlewareis in use, then you may want to check #1438 (and my proposed fix in #1715);I discovered same issue when trying to upgrade my app from fastapi 0.68 to 0.70. Interesting fact is that background tasks work fine on my workstation Ubuntu 20.04 but fail on server with CentOS 7 (same code, python 3.9 & modules versions).
It looks that best solution is to deprecate BaseHTTPMiddleware like mentioned in https://github.com/encode/starlette/issues/1678
Yes, I can reproduce it with the latest version
0.70.0.If I enable the Gzip middleware:
then running a
BackgroundTasksblocks the responses until the background task finishes. If I remove the middleware the background task works as expected and HTTP responses are returned immediately without waiting for itPassing
BackgroundTasktoTemplateResponsefor async functions has been broken for me the last few releases, so I’ve been resorting to simply using:asyncio.create_task(email(...))Perhaps
tests/test_background.pyshould addasyncio.sleep()to emulate work being done if this is breaking?@tomchristie Any insight on this issue?
I’ve found a way how to start long computations and do not block API, check my answer.
@Kruszylo When working with async you need to use asyncio.sleep instead of time.sleep because otherwise it will block the whole process
even
await asyncio.sleep(0.1)would do the job. the key here isawaitswitching asyncio context.Shame for me: never thought about using
asyncio.create_task.But I’m still wondering: running the same testcase under
hypercornsolves this issue, but why? Is it ahypercornbug?What versions of Python, Starlette, and Uvicorn are you using? I’m not able to recreate with Python 3.8.2, Starlette 0.13.3, and Uvicorn 0.11.5.