Skip to content

3.1.2 regression: stream_with_context triggers teardown_request() calls before response generation #5804

@noirbee

Description

@noirbee

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():

Environment:

  • Python version: 3.13
  • Flask version: 3.1.2

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions