-
-
Notifications
You must be signed in to change notification settings - Fork 16.6k
Description
Hello,
I believe the changes to stream_with_context() in 9822a03 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 ⇒
Line 115 in 7fff56f
with ctx: Line 443 in 3301232
self.pop(exc_value) - in 3.1.2, the (new) first call before entering the response generator is triggered by ; the second is similar to 3.1.1, i.e.
Line 1527 in 3301232
ctx.pop(error) ⇒Line 130 in 3301232
with app_ctx, req_ctx: Line 443 in 3301232
self.pop(exc_value)
Environment:
- Python version: 3.13
- Flask version: 3.1.2