FastAPI - Pydantic - Value Error Raises Internal Server Error
If you're not raising an HTTPException
then normally any other uncaught exception will generate a 500 response (an Internal Server Error
). If your intent is to respond with some other custom error message and HTTP status when raising a particular exception - say, ValueError
- then you can use add a global exception handler to your app:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
@app.exception_handler(ValueError)
async def value_error_exception_handler(request: Request, exc: ValueError):
return JSONResponse(
status_code=400,
content={"message": str(exc)},
)
This will give a 400 response (or you can change the status code to whatever you like) like this:
{
"message": "Value Must be within range (0,1000000)"
}
Please note that pydantic
expects that validators raise a ValueError
, TypeError
, or AssertionError
(see docs) which pydantic
will convert into a ValidationError
.
Further, as per FastAPI's documentation:
When a request contains invalid data, FastAPI internally raises a
RequestValidationError
.
and
RequestValidationError
is a sub-class of Pydantic'sValidationError
.
The result of this is that a standard Validation
error raised during pydantic
's Model validation will be translated into a 422 Unprocessable Entity
, and the response body will contain details on why the validation failed.
(As a side note: pydantic
comes with constrained types which allow to constrain basic datatypes without having to write explicit validators.)
If the above is not satisfactory and you'd like to change the behaviour, here's how I would approach it (see here for details on the ValidationError
handling):
from fastapi import Depends, FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.exception_handlers import request_validation_exception_handler
from fastapi.responses import JSONResponse
from pydantic import BaseModel, conint
class RankInput(BaseModel):
# Constrained integer, must be greater that or equal to 0
# and less than or equal to 1 million.
rank: conint(ge=0, le=1_000_000)
async def rank_out_of_bound_handler(request: Request, exc: RequestValidationError):
validation_errors = exc.errors()
for err in validation_errors:
# You could check for other things here as well, e.g. the error type.
if "rank" in err["loc"]:
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"message": "Rank must be in range [0, 1000000]."}
)
# Default response in every other case.
return await request_validation_exception_handler(request, exc)
def get_info_by_rank(rank):
return rank
app = FastAPI(
exception_handlers={RequestValidationError: rank_out_of_bound_handler},
)
@app.get('/rank/{rank}')
async def get_rank(value: RankInput = Depends()):
result = get_info_by_rank(value.rank)
return result
A call to the endpoint now gives:
$ curl -i "http://127.0.0.1:8000/rank/1"
HTTP/1.1 200 OK
date: Sat, 28 Aug 2021 20:47:58 GMT
server: uvicorn
content-length: 1
content-type: application/json
1
$ curl -i "http://127.0.0.1:8000/rank/-1"
HTTP/1.1 400 Bad Request
date: Sat, 28 Aug 2021 20:48:24 GMT
server: uvicorn
content-length: 49
content-type: application/json
{"message":"Rank must be in range [0, 1000000]."}
$ curl -i "http://127.0.0.1:8000/rank/1000001"
HTTP/1.1 400 Bad Request
date: Sat, 28 Aug 2021 20:48:51 GMT
server: uvicorn
content-length: 49
content-type: application/json
{"message":"Rank must be in range [0, 1000000]."}
If you were to add a different endpoint that uses the same model, the exception handler will automatically take care of this as well, e.g.:
@app.get('/other-rank/{rank}')
async def get_other_rank(value: RankInput = Depends()):
result = get_info_by_rank(value.rank)
return result
$ curl -i "http://127.0.0.1:8000/other-rank/-1"
HTTP/1.1 400 Bad Request
date: Sat, 28 Aug 2021 20:54:16 GMT
server: uvicorn
content-length: 49
content-type: application/json
{"message":"Rank must be in range [0, 1000000]."}
If this is not what you're looking for, could you explain why exactly you'd like to raise a ValueError
?