Skip to content

Commit bf771bd

Browse files
šŸ› Fix unhandled growing memory for internal server errors, refactor dependencies with yield and except to require raising again as in regular Python (#11191)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 6336604 commit bf771bd

17 files changed

+553
-68
lines changed

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

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,63 @@ The same way, you could raise an `HTTPException` or similar in the exit code, af
162162

163163
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}.
164164

165+
## Dependencies with `yield` and `except`
166+
167+
If you catch an exception using `except` in a dependency with `yield` and you don't raise it again (or raise a new exception), FastAPI won't be able to notice there was an exception, the same way that would happen with regular Python:
168+
169+
=== "Python 3.9+"
170+
171+
```Python hl_lines="15-16"
172+
{!> ../../../docs_src/dependencies/tutorial008c_an_py39.py!}
173+
```
174+
175+
=== "Python 3.8+"
176+
177+
```Python hl_lines="14-15"
178+
{!> ../../../docs_src/dependencies/tutorial008c_an.py!}
179+
```
180+
181+
=== "Python 3.8+ non-Annotated"
182+
183+
!!! tip
184+
Prefer to use the `Annotated` version if possible.
185+
186+
```Python hl_lines="13-14"
187+
{!> ../../../docs_src/dependencies/tutorial008c.py!}
188+
```
189+
190+
In this case, the client will see an *HTTP 500 Internal Server Error* response as it should, given that we are not raising an `HTTPException` or similar, but the server will **not have any logs** or any other indication of what was the error. 😱
191+
192+
### Always `raise` in Dependencies with `yield` and `except`
193+
194+
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.
195+
196+
You can re-raise the same exception using `raise`:
197+
198+
=== "Python 3.9+"
199+
200+
```Python hl_lines="17"
201+
{!> ../../../docs_src/dependencies/tutorial008d_an_py39.py!}
202+
```
203+
204+
=== "Python 3.8+"
205+
206+
```Python hl_lines="16"
207+
{!> ../../../docs_src/dependencies/tutorial008d_an.py!}
208+
```
209+
210+
211+
=== "Python 3.8+ non-Annotated"
212+
213+
!!! tip
214+
Prefer to use the `Annotated` version if possible.
215+
216+
```Python hl_lines="15"
217+
{!> ../../../docs_src/dependencies/tutorial008d.py!}
218+
```
219+
220+
Now the client will get the same *HTTP 500 Internal Server Error* response, but the server will have our custom `InternalError` in the logs. šŸ˜Ž
221+
165222
## Execution of dependencies with `yield`
166223

167224
The sequence of execution is more or less like this diagram. Time flows from top to bottom. And each column is one of the parts interacting or executing code.
@@ -187,7 +244,6 @@ participant tasks as Background tasks
187244
operation -->> dep: Raise Exception (e.g. HTTPException)
188245
opt handle
189246
dep -->> dep: Can catch exception, raise a new HTTPException, raise other exception
190-
dep -->> handler: Auto forward exception
191247
end
192248
handler -->> client: HTTP error response
193249
end
@@ -210,15 +266,23 @@ participant tasks as Background tasks
210266
!!! tip
211267
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}.
212268

213-
If you raise any exception, it will be passed to the dependencies with yield, including `HTTPException`, and then **again** to the exception handlers. If there's no exception handler for that exception, it will then be handled by the default internal `ServerErrorMiddleware`, returning a 500 HTTP status code, to let the client know that there was an error in the server.
269+
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.
214270

215-
## Dependencies with `yield`, `HTTPException` and Background Tasks
271+
## Dependencies with `yield`, `HTTPException`, `except` and Background Tasks
216272

217273
!!! warning
218274
You most probably don't need these technical details, you can skip this section and continue below.
219275

220276
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.
221277

278+
### Dependencies with `yield` and `except`, Technical Details
279+
280+
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.
281+
282+
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.
283+
284+
### Background Tasks and Dependencies with `yield`, Technical Details
285+
222286
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.
223287

224288
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.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
from fastapi import Depends, FastAPI, HTTPException
2+
3+
app = FastAPI()
4+
5+
6+
class InternalError(Exception):
7+
pass
8+
9+
10+
def get_username():
11+
try:
12+
yield "Rick"
13+
except InternalError:
14+
print("Oops, we didn't raise again, Britney 😱")
15+
16+
17+
@app.get("/items/{item_id}")
18+
def get_item(item_id: str, username: str = Depends(get_username)):
19+
if item_id == "portal-gun":
20+
raise InternalError(
21+
f"The portal gun is too dangerous to be owned by {username}"
22+
)
23+
if item_id != "plumbus":
24+
raise HTTPException(
25+
status_code=404, detail="Item not found, there's only a plumbus here"
26+
)
27+
return item_id
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastapi import Depends, FastAPI, HTTPException
2+
from typing_extensions import Annotated
3+
4+
app = FastAPI()
5+
6+
7+
class InternalError(Exception):
8+
pass
9+
10+
11+
def get_username():
12+
try:
13+
yield "Rick"
14+
except InternalError:
15+
print("Oops, we didn't raise again, Britney 😱")
16+
17+
18+
@app.get("/items/{item_id}")
19+
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
20+
if item_id == "portal-gun":
21+
raise InternalError(
22+
f"The portal gun is too dangerous to be owned by {username}"
23+
)
24+
if item_id != "plumbus":
25+
raise HTTPException(
26+
status_code=404, detail="Item not found, there's only a plumbus here"
27+
)
28+
return item_id
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from typing import Annotated
2+
3+
from fastapi import Depends, FastAPI, HTTPException
4+
5+
app = FastAPI()
6+
7+
8+
class InternalError(Exception):
9+
pass
10+
11+
12+
def get_username():
13+
try:
14+
yield "Rick"
15+
except InternalError:
16+
print("Oops, we didn't raise again, Britney 😱")
17+
18+
19+
@app.get("/items/{item_id}")
20+
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
21+
if item_id == "portal-gun":
22+
raise InternalError(
23+
f"The portal gun is too dangerous to be owned by {username}"
24+
)
25+
if item_id != "plumbus":
26+
raise HTTPException(
27+
status_code=404, detail="Item not found, there's only a plumbus here"
28+
)
29+
return item_id
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from fastapi import Depends, FastAPI, HTTPException
2+
3+
app = FastAPI()
4+
5+
6+
class InternalError(Exception):
7+
pass
8+
9+
10+
def get_username():
11+
try:
12+
yield "Rick"
13+
except InternalError:
14+
print("We don't swallow the internal error here, we raise again šŸ˜Ž")
15+
raise
16+
17+
18+
@app.get("/items/{item_id}")
19+
def get_item(item_id: str, username: str = Depends(get_username)):
20+
if item_id == "portal-gun":
21+
raise InternalError(
22+
f"The portal gun is too dangerous to be owned by {username}"
23+
)
24+
if item_id != "plumbus":
25+
raise HTTPException(
26+
status_code=404, detail="Item not found, there's only a plumbus here"
27+
)
28+
return item_id
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from fastapi import Depends, FastAPI, HTTPException
2+
from typing_extensions import Annotated
3+
4+
app = FastAPI()
5+
6+
7+
class InternalError(Exception):
8+
pass
9+
10+
11+
def get_username():
12+
try:
13+
yield "Rick"
14+
except InternalError:
15+
print("We don't swallow the internal error here, we raise again šŸ˜Ž")
16+
raise
17+
18+
19+
@app.get("/items/{item_id}")
20+
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
21+
if item_id == "portal-gun":
22+
raise InternalError(
23+
f"The portal gun is too dangerous to be owned by {username}"
24+
)
25+
if item_id != "plumbus":
26+
raise HTTPException(
27+
status_code=404, detail="Item not found, there's only a plumbus here"
28+
)
29+
return item_id
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
from typing import Annotated
2+
3+
from fastapi import Depends, FastAPI, HTTPException
4+
5+
app = FastAPI()
6+
7+
8+
class InternalError(Exception):
9+
pass
10+
11+
12+
def get_username():
13+
try:
14+
yield "Rick"
15+
except InternalError:
16+
print("We don't swallow the internal error here, we raise again šŸ˜Ž")
17+
raise
18+
19+
20+
@app.get("/items/{item_id}")
21+
def get_item(item_id: str, username: Annotated[str, Depends(get_username)]):
22+
if item_id == "portal-gun":
23+
raise InternalError(
24+
f"The portal gun is too dangerous to be owned by {username}"
25+
)
26+
if item_id != "plumbus":
27+
raise HTTPException(
28+
status_code=404, detail="Item not found, there's only a plumbus here"
29+
)
30+
return item_id

0 commit comments

Comments
Ā (0)