3.1.2 regression: `stream_with_context` triggers `teardown_request()` calls before response generation
Hello,
I believe the changes to stream_with_context() in https://github.com/pallets/flask/pull/5799/commits/9822a0351574790cb66c652fcc396ad7aa2b09d8 introduced a bug where the teardown_request() callables are invoked too early in the request/response lifecycle (and actually invoked twice, before generating the response and a second time after the end of the request). Take the following example:
# 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)
In 3.1.1:
% /tmp/venv/bin/flask --version
Python 3.13.7
Flask 3.1.1
Werkzeug 3.1.3
% /tmp/venv/bin/python flask_teardown_stream_with_context.py
[…]
Starting to generate response
127.0.0.1 - - [01/Sep/2025 16:07:05] "GET /stream HTTP/1.1" 200 -
do_teardown_request() called
In 3.1.2:
% /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", line 443, in __exit__
self.pop(exc_value)
~~~~~~~~^^^^^^^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/ctx.py", line 410, in pop
self.app.do_teardown_request(exc)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/app.py", line 1356, in do_teardown_request
self.ensure_sync(func)(exc)
~~~~~~~~~~~~~~~~~~~~~~^^^^^
File "/tmp/flask_teardown_stream_with_context.py", line 7, in _teardown_request
g.pop("hello")
~~~~~^^^^^^^^^
File "/tmp/venv/lib/python3.13/site-packages/flask/ctx.py", line 88, in pop
return self.__dict__.pop(name)
~~~~~~~~~~~~~~~~~^^^^^^
KeyError: 'hello'
127.0.0.1 - - [01/Sep/2025 16:09:35] "GET /stream HTTP/1.1" 200 -
Specifically,
do_teardown_request() called
Starting to generate response
do_teardown_request() called
So _teardown_request() is called before flask start to iterate on the response generator.
This is a simplified version of our own code; I'm not sure we can actually expect g to still be available during response generators, but given it worked in 3.1.1 and the phrasing / intent of teardown_request(), I'd expect it not to be called before the response is actually generated. Note also that removing the code which causes the error, i.e. the g access, and keeping just the print() for debugging, will still show _teardown_request() being called twice.
It's not obvious to me where exactly the bug is triggered. Adding a traceback.print_stack() call to _teardown_request():
- in 3.1.1, the only call, once the request is done, is triggered by https://github.com/pallets/flask/blob/7fff56f5172c48b6f3aedf17ee14ef5c2533dfd1/src/flask/helpers.py#L115 ⇒ https://github.com/pallets/flask/blob/330123258e8c3dc391cbe55ab1ed94891ca83af3/src/flask/ctx.py#L443
- in 3.1.2, the (new) first call before entering the response generator is triggered by https://github.com/pallets/flask/blob/330123258e8c3dc391cbe55ab1ed94891ca83af3/src/flask/app.py#L1527 ; the second is similar to 3.1.1, i.e. https://github.com/pallets/flask/blob/330123258e8c3dc391cbe55ab1ed94891ca83af3/src/flask/helpers.py#L130 ⇒ https://github.com/pallets/flask/blob/330123258e8c3dc391cbe55ab1ed94891ca83af3/src/flask/ctx.py#L443
Environment:
- Python version: 3.13
- Flask version: 3.1.2
This will be fixed as a side effect of #5812 in 3.2. I'm not sure how to fix it in the mean time, without reintroducing the other issue, but I'm open to reviewing a PR if you can figure it out sooner.
I do understand how this is an issue, but note that the docs already call out that you can't make assumptions about how many times teardown functions will run or what will have or have not run before them. That's something you should address regardless of this being fixed.
This will be fixed as a side effect of https://github.com/pallets/flask/pull/5812 in 3.2. I'm not sure how to fix it in the mean time
Seems like it does the trick indeed, thanks. The fact that it won't be fixed until then is not really an issue for us, I've just pinned flask to <3.1.2 until we can upgrade.
the docs already call out that you can't make assumptions about how many times teardown functions will run or what will have or have not run before them
https://flask.palletsprojects.com/en/stable/reqcontext/#teardown-callbacks here ? It's not immediately obvious to me that they could run more than once, but even then this isn't the original issue we encountered, because our teardown callbacks are indeed idempotent — the g.pop() was used here to exhibit the issue.
My issue was more around when it can be called: in this specific case, even before entering the generator passed to stream_with_context(). I wouldn't expect a teardown callback to be called at this point, considering do_teardown_request() is « Called after the request is dispatched and the response is returned ».
Is there an expected release date for 3.2.0? We also encountered this problem in OTEL.
Hi I tested the code of @noirbee and indeed it does give the same error (venv-flask) ➜ flask git:(fix-streaming-teardown) ✗ flask --version Python 3.13.5 Flask 3.2.0.dev0 Werkzeug 3.1.3
- Running on http://127.0.0.1:5000 Press CTRL+C to quit
- Restarting with stat
- Debugger is active!
- Debugger PIN: 140-638-840
Starting to generate response
127.0.0.1 - - [29/Sep/2025 23:09:47] "GET /stream HTTP/1.1" 200 -
do_teardown_request() called
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 "/Users/anand/Desktop/oss/flask/src/flask/app.py", line 1500, in call
return self.wsgi_app(environ, start_response)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
File "/Users/anand/Desktop/oss/flask/src/flask/app.py", line 1491, in wsgi_app
ctx.pop(error)
File "/Users/anand/Desktop/oss/flask/src/flask/ctx.py", line 474, in pop self.app.do_teardown_request(exc) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^ File "/Users/anand/Desktop/oss/flask/src/flask/app.py", line 1339, in do_teardown_request self.ensure_sync(func)(exc) ~~~~~~~~~~~~~~~~~~~~~~^^^^^ File "/Users/anand/Desktop/oss/flask/1.py", line 7, in _teardown_request g.pop("hello") ~~~~~^^^^^^^^^ File "/Users/anand/Desktop/oss/flask/src/flask/ctx.py", line 88, in pop return self.dict.pop(name) ~~~~~~~~~~~~~~~~~^^^^^^ KeyError: 'hello'
But: when I add decorator for streaming with context it works correctly
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"
@stream_with_context # change here
def generate():
print("Starting to generate response")
yield f"<p>Hello {g.hello} !</p>"
return stream_with_context(generate())
app.run(debug=True)
Even Replacing g.pop("hello") with g.pop("hello", None) works suggesting its being trying to popped multiple times and cant find hence error
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 the lines of:
from contextlib import suppress
def do_teardown_request(self, exc: BaseException | None = None) -> None: """Called after the request is dispatched and the response is finalized.""" req = _cv_app.get().request
for name in chain(req.blueprints, (None,)):
if name in self.teardown_request_funcs:
for func in reversed(self.teardown_request_funcs[name]):
with suppress(KeyError):
self.ensure_sync(func)(exc)
request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc)
sorry I am new to open source contribution so please forgive if formatting issues,
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 to be resolved in Flask 3.2.0? Thanks
is this expected to be resolved in Flask 3.2.0?
yes