-
-
Notifications
You must be signed in to change notification settings - Fork 949
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow background tasks to run with custom BaseHTTPMiddleware
's
#1441
Conversation
starlette/middleware/base.py
Outdated
if call_next_response and response is not call_next_response: | ||
async with recv_stream: | ||
async for _ in recv_stream: | ||
... |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just discarding the message coro
has sent here.
async with recv_stream: | ||
async for _ in recv_stream: | ||
... # pragma: no cover |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can create a function that performs this logic, if wanted.
if call_next_response and response is not call_next_response: | ||
async with recv_stream: | ||
async for _ in recv_stream: | ||
... # pragma: no cover | ||
await response(scope, receive, send) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me that we will wait for app
to make all of its calls to send_stream.send
before actually processing the response
returned by dispatch_func
. If that is the case, and given that all the messages from app
are being discarded, I think it would be preferable, either to call await response(scope, receive, send)
before draining recv_stream
, or to put draining of recv_stream
in another task of task_group
.
Of course, I do not know the relevant technologies (ASGI/the server implementations/Starlette) as well as you do, so there could very well be a reason that either my analysis is wrong, or that my suggestions are unworkable.
if call_next_response and response is not call_next_response: | |
async with recv_stream: | |
async for _ in recv_stream: | |
... # pragma: no cover | |
await response(scope, receive, send) | |
if call_next_response and response is not call_next_response: | |
async def drain_stream(): | |
async with recv_stream: | |
async for _ in recv_stream: | |
... # pragma: no cover | |
task_group.start_soon(drain_stream) | |
await response(scope, receive, send) |
Unfortunately, it looks like this PR introduces a regression. I'll need to check what. For now, I'm going to close it while I investigate... |
I don't remember what was the supposed regression... 😞 |
I suspect that the change, as written, would fail in the case where someone returned a async def debug_streaming_middleware(request, call_next):
call_next_response = await call_next(request)
return StreamingResponse(
status_code=call_next_response.status_code,
headers=call_next_response.headers,
content=log_content_length(call_next_response.body_iterator),
)
async def log_content_length(bytes_iterator: AsyncIterator[bytes]):
total_length = 0
async for chunk in bytes_iterator:
total_length += len(chunk)
yield chunk
logger.info(f"Body length: {total_length}") In such a case, I think that, in order to fix issues related to background tasks (#919 and #1438), it would be better to think about shielding background tasks from cancellation in order to ensure that they are run even if a client closes the connection. |
This has been proposed in #1654. It's not great to try to fix BaseHTTPMiddleware by adding code somewhere else, and shielding from any cancellation seems like it could open up the door for other issues down the road. I think that a similar but better solution would be to run background tasks outside of the request/response cycle, e.g. by creating an |
BaseHTTPMiddleware
#919This is an old bug. There are some issues that talks about it.
I was debugging #1438, so I'm going to start with that one. Let me show you an example of this issue, retrieved from the mentioned issue:
Running the above code, you'll be able to see:
Looking at that, it led me to the culprit:
starlette/starlette/middleware/base.py
Line 65 in 2b54f42
I've removed that cancellation, and then I understood why it was there. We have a test that handles this case:
starlette/tests/middleware/test_base.py
Lines 148 to 160 in 2b54f42
The test is basically ignoring the
StreamingResponse
object returned bycall_next
. The problem here is that our application uses thesend_stream
(as you can see below), but there's no one to receive that stream, so it blocks. The idea of the above cancellation was to prevent this block.starlette/starlette/middleware/base.py
Lines 27 to 38 in 2b54f42
Ok. Now we solved an issue, but it led to another. What I thought:
Response
object, I'm going to consume the stream from that object.And that is what you see on this PR.
Some references:
BaseHTTPMiddleware
#919 (most famous one)A question so I can test this:
TestClient
?