The Fix
Merged the RequestContext class into the AppContext class, simplifying the internal code for tracking the active context.
Based on closed pallets/flask issue #5804 · PR/commit linked
Production note: This usually shows up under retries/timeouts. Treat it as a side-effect risk until you can verify behavior with a canary + real traffic.
@@ -5,6 +5,10 @@ Unreleased
- Drop support for Python 3.9. :pr:`5730`
- Remove previously deprecated code: ``__version__``. :pr:`5648`
+- ``RequestContext`` has merged with ``AppContext``. ``RequestContext`` is now
+ a deprecated alias. If an app context is already pushed, it is not reused
+ when dispatching a request. This greatly simplifies the internal code for tracking
# flask_teardown_stream_with_context.py
from flask import Flask, g, stream_with_context
def _teardown_request(_):
print("do_teardown_request() called")
g.pop("hello")
app = Flask(__name__)
app.teardown_request(_teardown_request)
@app.get("/stream")
def streamed_response():
g.hello = "world"
def generate():
print("Starting to generate response")
yield f"<p>Hello {g.hello} !</p>"
return stream_with_context(generate())
app.run(debug=True)
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nMerged the RequestContext class into the AppContext class, simplifying the internal code for tracking the active context.\nWhen NOT to use: This fix should not be used if the application relies on the previous teardown behavior.\n\nOption B — Safe version pin\nPin to 3.1.1 until you can upgrade.\nWhen NOT to use: Do not use if you need features or security fixes in newer releases.\n\n
Why This Fix Works in Production
- Trigger: % /tmp/venv/bin/flask --version
- Mechanism: The `teardown_request()` calls are invoked prematurely in the request lifecycle due to changes in `stream_with_context()`
- If left unfixed, this can cause silent data inconsistencies that propagate (bad cache entries, incorrect downstream decisions).
Why This Breaks in Prod
- Shows up under Python 3.13.7 in real deployments (not just unit tests).
- The `teardown_request()` calls are invoked prematurely in the request lifecycle due to changes in `stream_with_context()`
- Surfaces as: % /tmp/venv/bin/flask --version
Proof / Evidence
- GitHub issue: #5804
- Fix PR: https://github.com/pallets/flask/pull/5812
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.70
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.40
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“We are suffering the same issue with 3.1.2, and had to pin Flask to version < 3.1.2. Did not quite get it: is this expected…”
“> is this expected to be resolved in Flask 3.2.0? yes”
“I think we should supress the error as a warning till the issue gets resolves as a side effect by wrapping the teardown call on…”
“sorry I am new to open source contribution so please forgive if formatting issues,”
Failure Signature (Search String)
- % /tmp/venv/bin/flask --version
Error Message
Stack trace
Error Message
-------------
% /tmp/venv/bin/flask --version
Python 3.13.7
Flask 3.1.2
Werkzeug 3.1.3
% /tmp/venv/bin/python flask_teardown_stream_with_context.py
do_teardown_request() called
Starting to generate response
do_teardown_request() called
Debugging middleware caught exception in streamed response at a point where response headers were already sent.
Traceback (most recent call last):
File "/tmp/venv/lib/python3.13/site-packages/flask/helpers.py", line 132, in generator
yield from gen
File "/tmp/flask_teardown_stream_with_context.py", line 21, in generate
yield f"<p>Hello {g.hello} !</p>"
^^^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/ctx.py", line 56, in __getattr__
raise AttributeError(name) from None
AttributeError: hello
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "/tmp/venv/lib/python3.13/site-packages/werkzeug/wsgi.py", line 256, in __next__
return self._next()
~~~~~~~~~~^^
File "/tmp/venv/lib/python3.13/site-packages/werkzeug/wrappers/response.py", line 32, in _iter_encoded
for item in iterable:
^^^^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/helpers.py", line 130, in generator
with app_ctx, req_ctx:
^^^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/ctx.py", li
... (truncated) ...
Minimal Reproduction
# flask_teardown_stream_with_context.py
from flask import Flask, g, stream_with_context
def _teardown_request(_):
print("do_teardown_request() called")
g.pop("hello")
app = Flask(__name__)
app.teardown_request(_teardown_request)
@app.get("/stream")
def streamed_response():
g.hello = "world"
def generate():
print("Starting to generate response")
yield f"<p>Hello {g.hello} !</p>"
return stream_with_context(generate())
app.run(debug=True)
Environment
- Python: 3.13.7
What Broke
Users experience errors and premature teardown calls during response generation, leading to exceptions.
Why It Broke
The `teardown_request()` calls are invoked prematurely in the request lifecycle due to changes in `stream_with_context()`
Fix Options (Details)
Option A — Apply the official fix
Merged the RequestContext class into the AppContext class, simplifying the internal code for tracking the active context.
Option B — Safe version pin Backward-compatible pin
Pin to 3.1.1 until you can upgrade.
Use when you can’t upgrade immediately. Plan a follow-up to upgrade (pins can accumulate security/compat debt).
Option D — Guard side-effects with OnceOnly Guardrail for side-effects
Mitigate duplicate external side-effects under retries/timeouts/agent loops by gating the operation before calling external systems.
- Place OnceOnly between your code/agent and real side-effects (Stripe, emails, CRM, APIs).
- Use a stable key per side-effect (e.g., customer_id + action + idempotency_key).
- Fail-safe: configure fail-open vs fail-closed based on blast radius and spend risk.
- This does NOT fix data corruption; it only prevents duplicate side-effects.
Show example snippet (optional)
from onceonly import OnceOnly
import os
once = OnceOnly(api_key=os.environ["ONCEONLY_API_KEY"], fail_open=True)
# Stable idempotency key per real side-effect.
# Use a request id / job id / webhook delivery id / Stripe event id, etc.
event_id = "evt_..." # replace
key = f"stripe:webhook:{event_id}"
res = once.check_lock(key=key, ttl=3600)
if res.duplicate:
return {"status": "already_processed"}
# Safe to execute the side-effect exactly once.
handle_event(event_id)
Fix reference: https://github.com/pallets/flask/pull/5812
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- This fix should not be used if the application relies on the previous teardown behavior.
- Do not use if you need features or security fixes in newer releases.
- Do not use this to hide logic bugs or data corruption. Use it to block duplicate external side-effects and enforce tool permissions/spend caps.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Capture the exact failing error string in logs and tests so you can reproduce via a minimal script.
- Pin production dependencies and upgrade only with a reproducible test that hits the failing path.
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.