Skip to content

Commit e329d78

Browse files
πŸ› Fix support for StreamingResponses with dependencies with yield or UploadFiles, close after the response is done (#14099)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 861b22c commit e329d78

14 files changed

+729
-182
lines changed

β€Ždocs/en/docs/advanced/advanced-dependencies.mdβ€Ž

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,91 @@ In the chapters about security, there are utility functions that are implemented
6363
If you understood all this, you already know how those utility tools for security work underneath.
6464

6565
///
66+
67+
## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks }
68+
69+
/// warning
70+
71+
You most probably don't need these technical details.
72+
73+
These details are useful mainly if you had a FastAPI application older than 0.118.0 and you are facing issues with dependencies with `yield`.
74+
75+
///
76+
77+
Dependencies with `yield` have evolved over time to account for the different use cases and to fix some issues, here's a summary of what has changed.
78+
79+
### Dependencies with `yield` and `StreamingResponse`, Technical Details { #dependencies-with-yield-and-streamingresponse-technical-details }
80+
81+
Before FastAPI 0.118.0, if you used a dependency with `yield`, it would run the exit code after the *path operation function* returned but right before sending the response.
82+
83+
The intention was to avoid holding resources for longer than necessary, waiting for the response to travel through the network.
84+
85+
This change also meant that if you returned a `StreamingResponse`, the exit code of the dependency with `yield` would have been already run.
86+
87+
For example, if you had a database session in a dependency with `yield`, the `StreamingResponse` would not be able to use that session while streaming data because the session would have already been closed in the exit code after `yield`.
88+
89+
This behavior was reverted in 0.118.0, to make the exit code after `yield` be executed after the response is sent.
90+
91+
/// info
92+
93+
As you will see below, this is very similar to the behavior before version 0.106.0, but with several improvements and bug fixes for corner cases.
94+
95+
///
96+
97+
#### Use Cases with Early Exit Code { #use-cases-with-early-exit-code }
98+
99+
There are some use cases with specific conditions that could benefit from the old behavior of running the exit code of dependencies with `yield` before sending the response.
100+
101+
For example, imagine you have code that uses a database session in a dependency with `yield` only to verify a user, but the database session is never used again in the *path operation function*, only in the dependency, **and** the response takes a long time to be sent, like a `StreamingResponse` that sends data slowly, but for some reason doesn't use the database.
102+
103+
In this case, the database session would be held until the response is finished being sent, but if you don't use it, then it wouldn't be necessary to hold it.
104+
105+
Here's how it could look like:
106+
107+
{* ../../docs_src/dependencies/tutorial013_an_py310.py *}
108+
109+
The exit code, the automatic closing of the `Session` in:
110+
111+
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[19:21] *}
112+
113+
...would be run after the the response finishes sending the slow data:
114+
115+
{* ../../docs_src/dependencies/tutorial013_an_py310.py ln[30:38] hl[31:33] *}
116+
117+
But as `generate_stream()` doesn't use the database session, it is not really necessary to keep the session open while sending the response.
118+
119+
If you have this specific use case using SQLModel (or SQLAlchemy), you could explicitly close the session after you don't need it anymore:
120+
121+
{* ../../docs_src/dependencies/tutorial014_an_py310.py ln[24:28] hl[28] *}
122+
123+
That way the session would release the database connection, so other requests could use it.
124+
125+
If you have a different use case that needs to exit early from a dependency with `yield`, please create a <a href="https://github.com/fastapi/fastapi/discussions/new?category=questions" class="external-link" target="_blank">GitHub Discussion Question</a> with your specific use case and why you would benefit from having early closing for dependencies with `yield`.
126+
127+
If there are compelling use cases for early closing in dependencies with `yield`, I would consider adding a new way to opt in to early closing.
128+
129+
### Dependencies with `yield` and `except`, Technical Details { #dependencies-with-yield-and-except-technical-details }
130+
131+
Before FastAPI 0.110.0, if you used a dependency with `yield`, and then you captured an exception with `except` in that dependency, and you didn't raise the exception again, the exception would be automatically raised/forwarded to any exception handlers or the internal server error handler.
132+
133+
This was changed in version 0.110.0 to fix unhandled memory consumption from forwarded exceptions without a handler (internal server errors), and to make it consistent with the behavior of regular Python code.
134+
135+
### Background Tasks and Dependencies with `yield`, Technical Details { #background-tasks-and-dependencies-with-yield-technical-details }
136+
137+
Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run.
138+
139+
This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished.
140+
141+
This was changed in FastAPI 0.106.0 with the intention to not hold resources while waiting for the response to travel through the network.
142+
143+
/// tip
144+
145+
Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection).
146+
147+
So, this way you will probably have cleaner code.
148+
149+
///
150+
151+
If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`.
152+
153+
For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function.

β€Ždocs/en/docs/tutorial/dependencies/dependencies-with-yield.mdβ€Ž

Lines changed: 11 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ The yielded value is what is injected into *path operations* and other dependenc
3535

3636
{* ../../docs_src/dependencies/tutorial007.py hl[4] *}
3737

38-
The code following the `yield` statement is executed after creating the response but before sending it:
38+
The code following the `yield` statement is executed after the response:
3939

4040
{* ../../docs_src/dependencies/tutorial007.py hl[5:6] *}
4141

@@ -51,7 +51,7 @@ You can use `async` or regular functions.
5151

5252
If you use a `try` block in a dependency with `yield`, you'll receive any exception that was thrown when using the dependency.
5353

54-
For example, if some code at some point in the middle, in another dependency or in a *path operation*, made a database transaction "rollback" or create any other error, you will receive the exception in your dependency.
54+
For example, if some code at some point in the middle, in another dependency or in a *path operation*, made a database transaction "rollback" or created any other exception, you would receive the exception in your dependency.
5555

5656
So, you can look for that specific exception inside the dependency with `except SomeException`.
5757

@@ -95,9 +95,11 @@ This works thanks to Python's <a href="https://docs.python.org/3/library/context
9595

9696
## Dependencies with `yield` and `HTTPException` { #dependencies-with-yield-and-httpexception }
9797

98-
You saw that you can use dependencies with `yield` and have `try` blocks that catch exceptions.
98+
You saw that you can use dependencies with `yield` and have `try` blocks that try to execute some code and then run some exit code after `finally`.
9999

100-
The same way, you could raise an `HTTPException` or similar in the exit code, after the `yield`.
100+
You can also use `except` to catch the exception that was raised and do something with it.
101+
102+
For example, you can raise a different exception, like `HTTPException`.
101103

102104
/// tip
103105

@@ -109,7 +111,7 @@ But it's there for you if you need it. πŸ€“
109111

110112
{* ../../docs_src/dependencies/tutorial008b_an_py39.py hl[18:22,31] *}
111113

112-
An alternative you could use to catch exceptions (and possibly also raise another `HTTPException`) is to create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.
114+
If you want to catch exceptions and create a custom response based on that, create a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.
113115

114116
## Dependencies with `yield` and `except` { #dependencies-with-yield-and-except }
115117

@@ -121,7 +123,7 @@ In this case, the client will see an *HTTP 500 Internal Server Error* response a
121123

122124
### Always `raise` in Dependencies with `yield` and `except` { #always-raise-in-dependencies-with-yield-and-except }
123125

124-
If you catch an exception in a dependency with `yield`, unless you are raising another `HTTPException` or similar, you should re-raise the original exception.
126+
If you catch an exception in a dependency with `yield`, unless you are raising another `HTTPException` or similar, **you should re-raise the original exception**.
125127

126128
You can re-raise the same exception using `raise`:
127129

@@ -178,48 +180,15 @@ After one of those responses is sent, no other response can be sent.
178180

179181
/// tip
180182

181-
This diagram shows `HTTPException`, but you could also raise any other exception that you catch in a dependency with `yield` or with a [Custom Exception Handler](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank}.
182-
183-
If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`. In most cases you will want to re-raise that same exception or a new one from the dependency with `yield` to make sure it's properly handled.
183+
If you raise any exception in the code from the *path operation function*, it will be passed to the dependencies with yield, including `HTTPException`. In most cases you will want to re-raise that same exception or a new one from the dependency with `yield` to make sure it's properly handled.
184184

185185
///
186186

187187
## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks { #dependencies-with-yield-httpexception-except-and-background-tasks }
188188

189-
/// warning
190-
191-
You most probably don't need these technical details, you can skip this section and continue below.
192-
193-
These details are useful mainly if you were using a version of FastAPI prior to 0.106.0 and used resources from dependencies with `yield` in background tasks.
194-
195-
///
196-
197-
### Dependencies with `yield` and `except`, Technical Details { #dependencies-with-yield-and-except-technical-details }
198-
199-
Before FastAPI 0.110.0, if you used a dependency with `yield`, and then you captured an exception with `except` in that dependency, and you didn't raise the exception again, the exception would be automatically raised/forwarded to any exception handlers or the internal server error handler.
200-
201-
This was changed in version 0.110.0 to fix unhandled memory consumption from forwarded exceptions without a handler (internal server errors), and to make it consistent with the behavior of regular Python code.
202-
203-
### Background Tasks and Dependencies with `yield`, Technical Details { #background-tasks-and-dependencies-with-yield-technical-details }
204-
205-
Before FastAPI 0.106.0, raising exceptions after `yield` was not possible, the exit code in dependencies with `yield` was executed *after* the response was sent, so [Exception Handlers](../handling-errors.md#install-custom-exception-handlers){.internal-link target=_blank} would have already run.
206-
207-
This was designed this way mainly to allow using the same objects "yielded" by dependencies inside of background tasks, because the exit code would be executed after the background tasks were finished.
208-
209-
Nevertheless, as this would mean waiting for the response to travel through the network while unnecessarily holding a resource in a dependency with yield (for example a database connection), this was changed in FastAPI 0.106.0.
210-
211-
/// tip
212-
213-
Additionally, a background task is normally an independent set of logic that should be handled separately, with its own resources (e.g. its own database connection).
214-
215-
So, this way you will probably have cleaner code.
216-
217-
///
218-
219-
If you used to rely on this behavior, now you should create the resources for background tasks inside the background task itself, and use internally only data that doesn't depend on the resources of dependencies with `yield`.
220-
221-
For example, instead of using the same database session, you would create a new database session inside of the background task, and you would obtain the objects from the database using this new session. And then instead of passing the object from the database as a parameter to the background task function, you would pass the ID of that object and then obtain the object again inside the background task function.
189+
Dependencies with `yield` have evolved over time to cover different use cases and fix some issues.
222190

191+
If you want to see what has changed in different versions of FastAPI, you can read more about it in the advanced guide, in [Advanced Dependencies - Dependencies with `yield`, `HTTPException`, `except` and Background Tasks](../../advanced/advanced-dependencies.md#dependencies-with-yield-httpexception-except-and-background-tasks){.internal-link target=_blank}.
223192
## Context Managers { #context-managers }
224193

225194
### What are "Context Managers" { #what-are-context-managers }
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import time
2+
from typing import Annotated
3+
4+
from fastapi import Depends, FastAPI, HTTPException
5+
from fastapi.responses import StreamingResponse
6+
from sqlmodel import Field, Session, SQLModel, create_engine
7+
8+
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
9+
10+
11+
class User(SQLModel, table=True):
12+
id: int | None = Field(default=None, primary_key=True)
13+
name: str
14+
15+
16+
app = FastAPI()
17+
18+
19+
def get_session():
20+
with Session(engine) as session:
21+
yield session
22+
23+
24+
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
25+
user = session.get(User, user_id)
26+
if not user:
27+
raise HTTPException(status_code=403, detail="Not authorized")
28+
29+
30+
def generate_stream(query: str):
31+
for ch in query:
32+
yield ch
33+
time.sleep(0.1)
34+
35+
36+
@app.get("/generate", dependencies=[Depends(get_user)])
37+
def generate(query: str):
38+
return StreamingResponse(content=generate_stream(query))
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import time
2+
from typing import Annotated
3+
4+
from fastapi import Depends, FastAPI, HTTPException
5+
from fastapi.responses import StreamingResponse
6+
from sqlmodel import Field, Session, SQLModel, create_engine
7+
8+
engine = create_engine("postgresql+psycopg://postgres:postgres@localhost/db")
9+
10+
11+
class User(SQLModel, table=True):
12+
id: int | None = Field(default=None, primary_key=True)
13+
name: str
14+
15+
16+
app = FastAPI()
17+
18+
19+
def get_session():
20+
with Session(engine) as session:
21+
yield session
22+
23+
24+
def get_user(user_id: int, session: Annotated[Session, Depends(get_session)]):
25+
user = session.get(User, user_id)
26+
if not user:
27+
raise HTTPException(status_code=403, detail="Not authorized")
28+
session.close()
29+
30+
31+
def generate_stream(query: str):
32+
for ch in query:
33+
yield ch
34+
time.sleep(0.1)
35+
36+
37+
@app.get("/generate", dependencies=[Depends(get_user)])
38+
def generate(query: str):
39+
return StreamingResponse(content=generate_stream(query))

β€Žfastapi/applications.pyβ€Ž

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
)
2323
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
2424
from fastapi.logger import logger
25+
from fastapi.middleware.asyncexitstack import AsyncExitStackMiddleware
2526
from fastapi.openapi.docs import (
2627
get_redoc_html,
2728
get_swagger_ui_html,
@@ -36,10 +37,12 @@
3637
from starlette.exceptions import HTTPException
3738
from starlette.middleware import Middleware
3839
from starlette.middleware.base import BaseHTTPMiddleware
40+
from starlette.middleware.errors import ServerErrorMiddleware
41+
from starlette.middleware.exceptions import ExceptionMiddleware
3942
from starlette.requests import Request
4043
from starlette.responses import HTMLResponse, JSONResponse, Response
4144
from starlette.routing import BaseRoute
42-
from starlette.types import ASGIApp, Lifespan, Receive, Scope, Send
45+
from starlette.types import ASGIApp, ExceptionHandler, Lifespan, Receive, Scope, Send
4346
from typing_extensions import Annotated, Doc, deprecated
4447

4548
AppType = TypeVar("AppType", bound="FastAPI")
@@ -990,6 +993,54 @@ class Item(BaseModel):
990993
self.middleware_stack: Union[ASGIApp, None] = None
991994
self.setup()
992995

996+
def build_middleware_stack(self) -> ASGIApp:
997+
# Duplicate/override from Starlette to add AsyncExitStackMiddleware
998+
# inside of ExceptionMiddleware, inside of custom user middlewares
999+
debug = self.debug
1000+
error_handler = None
1001+
exception_handlers: dict[Any, ExceptionHandler] = {}
1002+
1003+
for key, value in self.exception_handlers.items():
1004+
if key in (500, Exception):
1005+
error_handler = value
1006+
else:
1007+
exception_handlers[key] = value
1008+
1009+
middleware = (
1010+
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
1011+
+ self.user_middleware
1012+
+ [
1013+
Middleware(
1014+
ExceptionMiddleware, handlers=exception_handlers, debug=debug
1015+
),
1016+
# Add FastAPI-specific AsyncExitStackMiddleware for closing files.
1017+
# Before this was also used for closing dependencies with yield but
1018+
# those now have their own AsyncExitStack, to properly support
1019+
# streaming responses while keeping compatibility with the previous
1020+
# versions (as of writing 0.117.1) that allowed doing
1021+
# except HTTPException inside a dependency with yield.
1022+
# This needs to happen after user middlewares because those create a
1023+
# new contextvars context copy by using a new AnyIO task group.
1024+
# This AsyncExitStack preserves the context for contextvars, not
1025+
# strictly necessary for closing files but it was one of the original
1026+
# intentions.
1027+
# If the AsyncExitStack lived outside of the custom middlewares and
1028+
# contextvars were set, for example in a dependency with 'yield'
1029+
# in that internal contextvars context, the values would not be
1030+
# available in the outer context of the AsyncExitStack.
1031+
# By placing the middleware and the AsyncExitStack here, inside all
1032+
# user middlewares, the same context is used.
1033+
# This is currently not needed, only for closing files, but used to be
1034+
# important when dependencies with yield were closed here.
1035+
Middleware(AsyncExitStackMiddleware),
1036+
]
1037+
)
1038+
1039+
app = self.router
1040+
for cls, args, kwargs in reversed(middleware):
1041+
app = cls(app, *args, **kwargs)
1042+
return app
1043+
9931044
def openapi(self) -> Dict[str, Any]:
9941045
"""
9951046
Generate the OpenAPI schema of the application. This is called by FastAPI
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
from contextlib import AsyncExitStack
2+
3+
from starlette.types import ASGIApp, Receive, Scope, Send
4+
5+
6+
# Used mainly to close files after the request is done, dependencies are closed
7+
# in their own AsyncExitStack
8+
class AsyncExitStackMiddleware:
9+
def __init__(
10+
self, app: ASGIApp, context_name: str = "fastapi_middleware_astack"
11+
) -> None:
12+
self.app = app
13+
self.context_name = context_name
14+
15+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
16+
async with AsyncExitStack() as stack:
17+
scope[self.context_name] = stack
18+
await self.app(scope, receive, send)

0 commit comments

Comments
Β (0)