Jump to solution
Verify

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.

Jump to Verify Open PR/Commit
@@ -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
repro.py
# 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)
verify
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
fix.md
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()`
Production impact:
  • 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

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…”
@watttman · 2025-12-17 · confirmation · source
“> is this expected to be resolved in Flask 3.2.0? yes”
@davidism · 2025-12-17 · confirmation · source
“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…”
@relu101 · 2025-09-29 · confirmation · source
“sorry I am new to open source contribution so please forgive if formatting issues,”
@relu101 · 2025-09-29 · source

Failure Signature (Search String)

  • % /tmp/venv/bin/flask --version

Error Message

Stack trace
error.txt
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

repro.py
# 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.

When NOT to use: This fix should not be used if the application relies on the previous teardown behavior.

Option B — Safe version pin Backward-compatible pin

Pin to 3.1.1 until you can upgrade.

When NOT to use: Do not use if you need features or security fixes in newer releases.

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)
onceonly.py
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)

See OnceOnly SDK

When NOT to use: 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.

Fix reference: https://github.com/pallets/flask/pull/5812

Last verified: 2026-02-09. Validate in your environment.

Get updates

We publish verified fixes weekly. No spam.

Subscribe

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

verify
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.