Custom 500 or Exception handlers do not run through middleware like other handled exceptions.
See original GitHub issueChecklist
- The bug is reproducible against the latest release and/or
master
. - There are no similar issues or pull requests to fix it yet.
Describe the bug
To reproduce
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import JSONResponse
app = Starlette(routes=routes, middleware=middleware)
async def not_found(request, exc):
return JSONResponse(content={"error": 404}, status_code=exc.status_code)
async def server_error(request, exc):
return JSONResponse(content={"error": 500}, status_code=exc.status_code)
exception_handlers = {
404: not_found,
500: server_error
}
middleware = [
Middleware(CORSMiddleware, allow_origins=['*'])
]
app = Starlette(routes=routes, middleware=middleware, exception_handlers=exception_handlers)
Expected behavior
both 404 and 500 errors will respond with their relevant json response, and correct CORS headers
Actual behavior
the 404 error will response with correct CORS headers but the 500 error will not.
Debugging material
Because Exception
and 500
are special cased in build_middleware_stack
any error bubbles up to the outer most ServerErrorMiddleware
which is outside the other middleware, instead of being handled by the ExceptionMiddleware
and passing through the custom middleware.
see: https://github.com/encode/starlette/blob/master/starlette/applications.py#L74-L77
I can see four possible fixes, none of them perfect…
-
handle custom 500 errors in
ExceptionMiddleware
and preventServerErrorMiddleware
showing its helpful debug info when a custom handler is present. -
have
ServerErrorMiddleware
run the response of the error handler through the middleware stack again, and if that fails resort to existing behaviour, adding a bunch of complexity. -
move the debuging info to a separate middleware and put that in front of the ErrorMiddleware, which would then cause it to trigger for every exception, but would allow
ExceptionMiddleware
handle 500s as expected, andServerErrorMiddleware
becomes a last resort. -
update https://www.starlette.io/exceptions/ to explain 500 errors are handled differently, and to get the above behaviour something like the following work-around is required:
@app.middleware("http") def exception_middleware(request, call_next) try: return await call_next(request) except Exception: return JSONResponse(content={"error": 500}, status_code=500))
Environment
- OS: N/A
- Python version: N/A
- Starlette version: 0.13.6 and master branch
Additional Context
#1116 seems to be have similar but refers to the generic error_response
on ServerErrorMiddleware
, this is similar but specifically about custom error handlers so i didn’t want to hijack that issue.
Issue Analytics
- State:
- Created 2 years ago
- Reactions:7
- Comments:17 (7 by maintainers)
Top GitHub Comments
We serialise errors to Problem JSON format and display them in the browser, including 500 errors. Just because an error has occurred on the server, it doesn’t mean that you can’t show a useful error to the user containing the details of what has gone wrong. Right now, the obvious approach you get from following the documentation fails. Instead you need a clumsy workaround that goes against the documentation’s recommendation.
That’s precisely the point. If you have an SPA loading API requests and there is a 500 error, the CORS headers are what allows you to see what is going on with the broken state, instead of just "Sorry, something failed but I can’t tell you what because I’m not allowed to look at the details”.
That… seems totally okay to me.
If you have an unhandled exception you really do just want to break out with a 500 error, with minimal other interference.
It’s a broken state that you do want to see, and it shouldn’t at all matter what CORS headers it does or doesn’t include.
(Also it’d be perfectly possible for the user to get a 5xx response from some intermediary, such as an overloaded server, or a gateway proxy, and you wouldn’t be seeing any CORS headers on those either)