fastapi: cors and error status 500 and handle it in frontend

First check

  • I used the GitHub search to find a similar issue and didn’t find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google “How to X in FastAPI” and didn’t find any information.

Description

When there is 500 error happens, the response header contains no information about cors. The browser console says: ‘Reason: CORS header ‘Access-Control-Allow-Origin’ missing’

I want to check the status code in in frontend(vue) with axios and alert a error message, but there is no error.response.status in axios catch. I guess it is related to the cors bedore the browser complains it in console.

Code to generate error 500

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
import pandas as pd

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=['*'],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.get("/error")
async def five_hundrend_error():
    df = pd.read_csv("not_exists.csv")
    return df.to_dict('records')

Below is the response header of 500. There is no cors info.

HTTP/1.1 500 Internal Server Error
date: Sat, 07 Dec 2019 10:01:24 GMT
server: uvicorn
content-length: 21
content-type: text/plain; charset=utf-8

Is it intended to behave like this? Or it may be a bug?

EDIT: I finally get the 500 error handled in frontend after read the axios doc. I keep it open because I’m still curious about it.

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 10
  • Comments: 16 (3 by maintainers)

Most upvoted comments

@statueofmike I had the same issue recently, you don’t need to wrap each endpoint with try/catch manually, you can do that once in the custom exception-catching middleware.

Assuming that browser sends request from localhost:3000 to api on port 8080, here is an example that returns 500 status code together with CORS headers even though exception is thrown in the endpoint. catch_exceptions_middleware does the trick, the important thing is to use it before CORS middleware is used.

from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import Response

app = FastAPI()


async def catch_exceptions_middleware(request: Request, call_next):
    try:
        return await call_next(request)
    except Exception:
        # you probably want some kind of logging here
        return Response("Internal server error", status_code=500)


app.middleware('http')(catch_exceptions_middleware)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get('/exception')
def get_with_exception():
    raise Exception('some exception')
    return {'test': 'test'}


uvicorn.run(app, host="0.0.0.0", port=8080)

If you receive a 500 response that means you have an error in your backend app code (Python using FastAPI).

It doesn’t have anything to do with CORS. If you have an error, your app won’t be able to send any headers used for CORS. But the problem is not at CORS, is before that.

Check your error logs, from your Python code, where you run Uvicorn.

Thanks for the discussion and ideas here!

On the other side, let me suggest you all to try sentry.io

It has a free tier, and it might change your life 😉

In some cases, it can detect and show errors even better than what you could while debugging locally. And for 500 errors that you want to be sure you know what’s having errors, even during production, that could be a vital tool.

For anyone who comes across this in the future in search of a solution, below is how we’ve decided to handle this using a custom 500 exception handler.

@app.exception_handler(500)
async def custom_http_exception_handler(request, exc):
    error = ErrorResponse(error="Something went wrong")
    error = jsonable_encoder(error.dict())

    response = JSONResponse(content=error, status_code=500)

    # Since the CORSMiddleware is not executed when an unhandled server exception
    # occurs, we need to manually set the CORS headers ourselves if we want the FE
    # to receive a proper JSON 500, opposed to a CORS error.
    # Setting CORS headers on server errors is a bit of a philosophical topic of
    # discussion in many frameworks, and it is currently not handled in FastAPI. 
    # See dotnet core for a recent discussion, where ultimately it was
    # decided to return CORS headers on server failures:
    # https://github.com/dotnet/aspnetcore/issues/2378
    origin = request.headers.get('origin')

    if origin:
        # Have the middleware do the heavy lifting for us to parse
        # all the config, then update our response headers
        cors = CORSMiddleware(
                app=app,
                allow_origins=ALLOWED_ORIGINS,
                allow_credentials=True,
                allow_methods=["*"],
                allow_headers=["*"])

        # Logic directly from Starlette's CORSMiddleware:
        # https://github.com/encode/starlette/blob/master/starlette/middleware/cors.py#L152

        response.headers.update(cors.simple_headers)
        has_cookie = "cookie" in request.headers

        # If request includes any cookie headers, then we must respond
        # with the specific origin instead of '*'.
        if cors.allow_all_origins and has_cookie:
            response.headers["Access-Control-Allow-Origin"] = origin

        # If we only allow specific origins, then we have to mirror back
        # the Origin header in the response.
        elif not cors.allow_all_origins and cors.is_allowed_origin(origin=origin):
            response.headers["Access-Control-Allow-Origin"] = origin
            response.headers.add_vary_header("Origin")

    return response

Thanks for the discussion and ideas here!

On the other side, let me suggest you all to try sentry.io

It has a free tier, and it might change your life 😉

In some cases, it can detect and show errors even better than what you could while debugging locally. And for 500 errors that you want to be sure you know what’s having errors, even during production, that could be a vital tool.

@tiangolo sentry is a good suggestions and should be implemented by the masses, but the problem i think comes with how this error is reported to the user.

What’s happening is the route faults and the caller is receiving a browser message saying ‘CORS header ‘Access-Control-Allow-Origin’ missing’ obscuring the actual error. This causes a wild goose chase in CORS settings on servers and client for some amount of time before someone realizes they should check the server logs for the actual error, even if they’re using sentry. After the realization is CORS headers aren’t sent during default exception handling we Google and stumble on this thread followed by a copy/paste of someone’s work around.

I think this issue is one of friction/confusion which isn’t solved by improving log ingestion. If CORS is misconfigured then a CORS error should be displayed, but if CORS is configured correctly then the CORS error should not appear, and the 500 error would now suggest to the user “check the server logs”. This is, in my experience, how other frameworks function.

I also have this question. I have confusion with a developer who thinks there is a CORS issue when receiving any 500 error response, though the access-control-allow-origin headers are present for all other responses.

As a workaround, I noticed that if I manually do:

from starlette.status import HTTP_500_INTERNAL_SERVER_ERROR

app = FastAPI(...)
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)
...
except Exception as e:
    logging.error(e)
    response.status_code = HTTP_500_INTERNAL_SERVER_ERROR

Then the CORS headers are included in the 500 response.

I apologize if this is just my ignorance of best practices. I expected default 500 responses returned from uncaught Python exceptions to include the CORS headers given in app.add_middleware. It seems to only include them when I build and return a 500 response.

Not providing CORS-related headers in the case of an error is a real antipattern to modern web communication, where CORS is treated as essential and non-negotiable by all common browsers. I don’t think this should be delegated to framework users. Please reopen.

I solved it like this, however it works only if specific exception is given to exception_handler (didn’t work for me with generic Exception)

app = FastAPI()

app.add_middleware(
    CORSMiddleware,
    allow_origins=ALLOWED_ORIGINS,
    allow_methods=["*"],
    allow_headers=["*"],
)

@app.exception_handler(SpecificException)
async def specific_exception_handler(request, exc):
    return JSONResponse(status_code=500, content={"body": "Internal Server Error"})

more info: https://fastapi.tiangolo.com/tutorial/handling-errors/#install-custom-exception-handlers (also check https://www.starlette.io/exceptions/)

what @rigward proposed in https://github.com/tiangolo/fastapi/issues/775#issuecomment-592946834 works BUT is not an optimal solution IMO

@statueofmike @rigward Thanks for your reply. The idea is to handle the unforeseen error to avoid missing cors header.

@tiangolo Thanks for your suggestion.

Thank you, @tiangolo. I’ve found those app code errors. Right now I keep all my endpoints wrapped in a general try/except and manually return 500 responses to preserve the CORS headers for such cases. Is there a better way to have the CORS headers included for future app errors I haven’t identified?