Fastapi: [QUESTION] cors and error status 500 and handle it in frontend

Created on 7 Dec 2019  路  8Comments  路  Source: tiangolo/fastapi

First check

  • [x] I used the GitHub search to find a similar issue and didn't find it.
  • [x] I searched the FastAPI documentation, with the integrated search.
  • [x] 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.

question

Most helpful comment

@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.

```import uvicorn
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)

All 8 comments

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.

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.

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?

@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.

```import uvicorn
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)

Ran into this myself as well. Almost didn't think that adding a handler for Exception worked, but I missed the very, very vital...

... the important thing is to use it before CORS middleware is used.

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 :wink:

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.

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

@tiangolo Thanks for your suggestion.

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
Was this page helpful?
0 / 5 - 0 ratings