-
First Check
Commit to Help
Test Case 1: generic Exception handler + middlewarefrom typing import Awaitable, Callable
from contextvars import ContextVar
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception) -> JSONResponse:
# Expect: "Bob"
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error() -> dict:
var.set("Bob")
raise Exception("Something went wrong!")
@app.middleware("http")
async def set_default_user(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
var.set("Alice")
response = await call_next(request)
return responseDescriptionI’m seeing two related problems when using
This looks like a mismatch in how context is propagated to the generic exception handler and/or the wrong handler layer is catching the exception first. Operating SystemmacOS Operating System DetailsNo response FastAPI Version0.118.0 Pydantic Version2.11.7 Python VersionPython 3.12.9 Additional ContextExpected response (200): Actual response (observed): So the response body suggests the handler ran but with lost context, and the logs suggest the default 500 handler got involved. Test Case 2 : specific ValueError handler + middlewarefrom typing import Awaitable, Callable
from contextvars import ContextVar
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(ValueError)
async def handle_value_error(request: Request, exc: ValueError) -> JSONResponse:
# Gets "Bob" as expected
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error() -> dict:
var.set("Bob")
raise ValueError("Something went wrong!")
@app.middleware("http")
async def set_default_user(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
var.set("Alice")
response = await call_next(request)
return responseResponse : log: Test Case 3: catching the exception inside middlewarefrom typing import Callable, Awaitable
from contextvars import ContextVar
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.responses import Response
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.middleware("http")
async def all_exceptions_mw(
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
token = var.set("Alice")
try:
return await call_next(request)
except Exception as exc:
# Expected: "Bob" (set in the route before raising)
# Actual: "Alice"
return JSONResponse({"error": str(exc), "var": var.get()}, status_code=200)
finally:
var.reset(token)
@app.get("/error")
async def error() -> JSONResponse:
var.set("Bob")
raise Exception("Something went wrong!")Response : log: So even though the exception never leaves the middleware (FastAPI’s handler isn’t involved), the context at the point of raising is already lost. Test Case 4: Exception handler without middlewarefrom fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from contextvars import ContextVar
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception):
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error():
var.set("Bob")
raise Exception("Something went wrong!")Response : Server log (full log): The custom handler does execute and the context ( Temporary PatchAfter further debugging, I found that patching def build_middleware_stack(self) -> ASGIApp:
# Duplicate/override from Starlette to add AsyncExitStackMiddleware
# inside of ExceptionMiddleware, inside of custom user middlewares
debug = self.debug
error_handler = None
exception_handlers: dict[Any, ExceptionHandler] = {}
for key, value in self.exception_handlers.items():
- if key in (500, Exception):
+ if key in (500):
error_handler = value
else:
exception_handlers[key] = value
middleware = (
[Middleware(ServerErrorMiddleware, handler=error_handler, debug=debug)]
+ self.user_middleware
+ [
Middleware(ExceptionMiddleware, handlers=exception_handlers, debug=debug),
# Add FastAPI-specific AsyncExitStackMiddleware for closing files.
# Before this was also used for closing dependencies with yield but
# those now have their own AsyncExitStack, to properly support ...
the middleware-level Environment
Full log – generic Exception handler (broken) |
Beta Was this translation helpful? Give feedback.
Replies: 4 comments
-
|
It seems like FastAPI + Starlette are imposing that when an "Exception" is raised and not handled by any specific error handler, it just prints the logs and loses the context. In addition there is a weird behavior when using a middleware. I can reproduce the error as well.
I think this is a bug, rather than a missing feature... The fact that this works as expected... (returns "Bob") from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from contextvars import ContextVar
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception):
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error():
var.set("Bob")
raise ValueError("Something went wrong!")but adding a useless middleware... (returns "default") from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from contextvars import ContextVar
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception):
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error():
var.set("Bob")
raise ValueError("Something went wrong!")
@app.middleware("http")
async def set_default_user(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
response = await call_next(request)
return responseit drives me crazy... |
Beta Was this translation helpful? Give feedback.
-
|
Hello @khirooo and @bonastreyair, In all your examples your are using the BaseHttpMiddleware that execute in another task the endpoint code and for this reason you do no see the expected result. There are already several discussion about it follow this: #14071 (comment), https://discuss.python.org/t/back-propagation-of-contextvar-changes-from-worker-threads/15928. In any case using a pure ASGI middleware will solve your issue. Note about using ContextVar: There are several cases where you can "loose" the context information even without a middleware. class ExampleASGIMiddleware:
def __init__(self, asgi_app):
self.app = asgi_app
async def __call__(self, scope, receive, send):
if scope["type"] != "http":
await self.app(scope, receive, send)
return
token = var.set("AnonymousUser")
try:
await self.app(scope, receive, send)
except Exception as exc:
response = JSONResponse({"error": str(exc), "var": var.get()}, status_code=200)
await response(scope, receive, send)
finally:
var.reset(token)
def fake_user_service():
var.set("Harry")
async def async_fake_user_service():
var.set("Mel")
@app.get("/example-1", dependencies=[Depends(async_fake_user_service)])
async def keep_user_from_service() -> dict:
return {"user": var.get()}
@app.get("/example-2", dependencies=[Depends(fake_user_service)])
async def loose_user_from_service() -> dict:
return {"user": var.get()}More in general all sync endpoint or sync dependency are run in a child context.
About exception and error handlers: I’d strongly discourage patching internals when the framework already provides the right tools. There’s one remaining case: you want to handle exceptions earlier so the middleware chain doesn’t break. This approach leverages the built-in mechanisms rather than modifying internals. |
Beta Was this translation helpful? Give feedback.
-
|
Thanks for the detailed explanation, @luzzodev — it really helps clarify what’s going on under the hood. My understanding
Follow-up
This could be worth investigating further ! |
Beta Was this translation helpful? Give feedback.
-
|
I hope that with this last explanation I’m able to clear up all your doubts. As for the additional questions you asked, I’ll share a comment on each of the scenarios you used as examples. In FastAPI (and partly in Starlette), a middleware stack is built in the following order: The execution flow of a request is LIFO. Purpose of the ServerErrorMiddleware and the ExceptionMiddleware:The ServerErrorMiddleware is generally responsible for always emitting a response from the application, even in the case of an exception. This middleware, whose code you can see here, includes an exception handler that generates a It allows you to define your own exception handler for this middleware by referring to the base exception, e.g." @app.exception_handler(Exception)The ExceptionMiddleware, on the other hand, is the middleware that lets you handle possible exceptions raised in the endpoints through dedicated handlers. Examples include Test case explanationsTest Case 1: generic Exception handler + middlewarefrom typing import Awaitable, Callable
from contextvars import ContextVar
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception) -> JSONResponse:
# Expect: "Bob" --> You should expect Alice not bob here. This is executed from ServerErrorMiddleware
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error() -> dict:
var.set("Bob")
raise Exception("Something went wrong!")
@app.middleware("http")
async def set_default_user(
request: Request, call_next: Callable[[Request], Awaitable[Response]]
) -> Response:
var.set("Alice")
response = await call_next(request) # this will run in a child level
return responseThe generic exception handler is added to the ServerErrorMiddleware, Alice is set before Test Case 3: catching the exception inside middlewarefrom typing import Callable, Awaitable
from contextvars import ContextVar
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from starlette.responses import Response
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.middleware("http")
async def all_exceptions_mw(
request: Request,
call_next: Callable[[Request], Awaitable[Response]],
) -> Response:
token = var.set("Alice")
try:
return await call_next(request)
except Exception as exc:
# Expected: "Bob" (set in the route before raising)
# Actual: "Alice" --> this is correct
return JSONResponse({"error": str(exc), "var": var.get()}, status_code=200)
finally:
var.reset(token)
@app.get("/error")
async def error() -> JSONResponse:
var.set("Bob")
raise Exception("Something went wrong!")The exception will never reach the ServerErrorMiddleware so you will not see the exception logged, same logic about context log. Test Case 4: Exception handler without middlewarefrom fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
from contextvars import ContextVar
var: ContextVar[str] = ContextVar("var", default="default")
app = FastAPI()
@app.exception_handler(Exception)
async def handle_exception(request: Request, exc: Exception):
return JSONResponse({"error": str(exc), "var": var.get()})
@app.get("/error")
async def error():
var.set("Bob")
raise Exception("Something went wrong!")No HTTPMiddlewareBase here, so this time the flow is all on one level, the exception handler is run from ServerErrorMiddleware, this exception will be logged but the stack is much shorter. ConclusionI don’t see any inconsistency, neither in the logging nor in the loss of context. There are clear rules that provide predictability and reproducibility of results. I understand it can feel a bit tricky at first, but I don’t see anything unexpected or that would require further investigation. |
Beta Was this translation helpful? Give feedback.
I hope that with this last explanation I’m able to clear up all your doubts.
What you wrote about what you understood is all correct; I’d only add, for completeness, that the
BaseHTTPMiddleware(the one you use via a decorator) was designed and implemented to provide a simple, ready-to-use tool, but over time it has led to a number of problems or limitations. Personally, I almost never use it and I always prefer a pure ASGI middleware.As for the additional questions you asked, I’ll share a comment on each of the scenarios you used as examples.
Before starting with the examples, let me share a bit more information.
In FastAPI (and partly in Starlette), a middleware stack is built in the f…