Skip to content

fix: keep the request container open until a streamed response closes - #44

Merged
lesnik512 merged 1 commit into
mainfrom
fix/stream-with-context
Oct 17, 2026
Merged

lesnik512 merged 1 commit into
mainfrom
fix/stream-with-context

Conversation

@lesnik512

Copy link
Copy Markdown
Member

On Flask 3.1.2 and later, a stream_with_context generator got ContainerClosedError when it resolved a REQUEST dependency. For streamed responses, the request container now closes when the response closes.

Cause

Since Flask 3.1.2, stream_with_context only records the request and app contexts when it is created, and pushes them again when the body is iterated (flask/helpers.py, with app_ctx, req_ctx: inside generator()). wsgi_app pops the request context right after returning the response. The app context then has one token, so AppContext.pop runs do_teardown_appcontext (flask/ctx.py) and our close runs before the body is iterated. Iterating pushes the same contexts again, so g and the closed container come back, and the pop at the end runs teardown a second time. On Flask 3.0 the generator pushed the context at creation, so the first pop was a no-op and teardown ran once, after the body.

A teardown_appcontext handler has no public way to know that a stream will push the context again, so the fix moves the decision to the response.

Fix

  • setup_di connects a flask.request_finished receiver for the app. It gets the final response after all after_request hooks and the session save. If the response is streamed (response.is_streamed) and not direct_passthrough, it registers child.close_sync with response.call_on_close and marks the child as deferred on g.
  • The teardown close skips a deferred child, so every teardown handler runs before the close, on both teardown passes.
  • Werkzeug calls call_on_close functions from Response.close, which the WSGI server calls after sending the body or when the client disconnects. HEAD and 204/304 responses go through that close too.
  • direct_passthrough responses (for example send_file) are handed to the server as the raw iterable, so Response.close never runs. Those still close at teardown.
  • If an exception propagates out of the request, request_finished never fires and teardown closes as before.

Plain generator responses without stream_with_context are deferred as well, so a dependency resolved in the view stays live while the body streams. Before, it was finalized before the first chunk went out.

Public API only: flask.request_finished, Response.is_streamed, Response.direct_passthrough, Response.call_on_close.

Tests

  • The streaming test now requires the same live dependency inside the generator, and one finalizer that runs only after the response closes.
  • A plain generator response finalizes after the body.
  • A direct_passthrough response still finalizes at teardown.

Ran locally on Flask 3.1.3, and with the floor constraints on Flask 3.0.0, 3.1.1 and 3.1.2.

Closes #43

@lesnik512
lesnik512 merged commit 0c25ba0 into main Oct 17, 2026
12 checks passed
@lesnik512
lesnik512 deleted the fix/stream-with-context branch October 17, 2026 14:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

stream_with_context generators can't resolve REQUEST dependencies on Flask 3.1.2+

1 participant