Hay muchas situaciones en las que necesitas reportar un error a un cliente que está usando tu API.
Este cliente podría ser un navegador con un frontend, un código de otra persona, un dispositivo IoT, etc.
Podrías necesitar decirle al cliente que:
El cliente no tiene suficientes privilegios para esa operación.
El cliente no tiene acceso a ese recurso.
El item que el cliente intentaba acceder no existe.
etc.
En estos casos, normalmente retornarías un código de estado HTTP en el rango de 400 (de 400 a 499).
Esto es similar a los códigos de estado HTTP 200 (de 200 a 299). Esos códigos de estado "200" significan que de alguna manera hubo un "éxito" en la petición.
Los códigos de estado en el rango 400 significan que hubo un error por parte del cliente.
¿Recuerdas todos esos errores "404 Not Found" (y chistes)?
fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinitems:raiseHTTPException(status_code=404,detail="Item not found")return{"item":items[item_id]}
HTTPException es una excepción normal de Python con datos adicionales relevantes para APIs.
Como es una excepción de Python, no la retornas, la lanzas.
Esto también significa que si estás dentro de una función utilitaria que estás llamando dentro de tu función de operación de path, y lanzas la HTTPException desde dentro de esa función utilitaria, no ejecutará el resto del código en la función de operación de path, terminará esa petición inmediatamente y enviará el error HTTP de la HTTPException al cliente.
El beneficio de lanzar una excepción en lugar de retornar un valor será más evidente en la sección sobre Dependencias y Seguridad.
En este ejemplo, cuando el cliente solicita un item por un ID que no existe, lanza una excepción con un código de estado 404:
fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items/{item_id}")asyncdefread_item(item_id:str):ifitem_idnotinitems:raiseHTTPException(status_code=404,detail="Item not found")return{"item":items[item_id]}
Si el cliente solicita http://example.com/items/foo (un item_id"foo"), ese cliente recibirá un código de estado HTTP 200, y una respuesta JSON de:
{"item":"The Foo Wrestlers"}
Pero si el cliente solicita http://example.com/items/bar (un item_id"bar" que no existe), ese cliente recibirá un código de estado HTTP 404 (el error "not found"), y una respuesta JSON de:
{"detail":"Item not found"}
Consejo
Al lanzar una HTTPException, puedes pasar cualquier valor que pueda ser convertido a JSON como parámetro detail, no solo str.
Podrías pasar un dict, una list, etc.
Son manejados automáticamente por FastAPI y convertidos a JSON.
Hay algunas situaciones en las que es útil poder añadir headers personalizados al error HTTP. Por ejemplo, para algunos tipos de seguridad.
Probablemente no necesites usarlo directamente en tu código.
Pero en caso de que lo necesites para un escenario avanzado, puedes añadir headers personalizados:
fromfastapiimportFastAPI,HTTPExceptionapp=FastAPI()items={"foo":"The Foo Wrestlers"}@app.get("/items-header/{item_id}")asyncdefread_item_header(item_id:str):ifitem_idnotinitems:raiseHTTPException(status_code=404,detail="Item not found",headers={"X-Error":"There goes my error"},)return{"item":items[item_id]}
Instalar manejadores de excepciones personalizados¶
Digamos que tienes una excepción personalizada UnicornException que tú (o una librería que usas) podrían lanzar.
Y quieres manejar esta excepción globalmente con FastAPI.
Podrías añadir un manejador de excepciones personalizado con @app.exception_handler():
fromfastapiimportFastAPI,Requestfromfastapi.responsesimportJSONResponseclassUnicornException(Exception):def__init__(self,name:str):self.name=nameapp=FastAPI()@app.exception_handler(UnicornException)asyncdefunicorn_exception_handler(request:Request,exc:UnicornException):returnJSONResponse(status_code=418,content={"message":f"Oops! {exc.name} did something. There goes a rainbow..."},)@app.get("/unicorns/{name}")asyncdefread_unicorn(name:str):ifname=="yolo":raiseUnicornException(name=name)return{"unicorn_name":name}
Aquí, si solicitas /unicorns/yolo, la operación de pathlanzará una UnicornException.
Pero será manejada por el unicorn_exception_handler.
Así que recibirás un error limpio, con un código de estado HTTP 418 y un contenido JSON de:
{"message":"Oops! yolo did something. There goes a rainbow..."}
Detalles Técnicos
También podrías usar from starlette.requests import Request y from starlette.responses import JSONResponse.
FastAPI proporciona las mismas starlette.responses como fastapi.responses solo como una comodidad para ti, el desarrollador. Pero la mayoría de las respuestas disponibles vienen directamente de Starlette. Lo mismo con Request.
Sobrescribir los manejadores de excepciones por defecto¶
FastAPI tiene algunos manejadores de excepciones por defecto.
Estos manejadores se encargan de retornar las respuestas JSON por defecto cuando lanzas una HTTPException y cuando la petición tiene datos inválidos.
Puedes sobrescribir estos manejadores de excepciones con los tuyos.
Sobrescribir las excepciones de validación de peticiones¶
Cuando una petición contiene datos inválidos, FastAPI internamente lanza un RequestValidationError.
Y también incluye un manejador de excepciones por defecto para ello.
Para sobrescribirlo, importa el RequestValidationError y úsalo con @app.exception_handler(RequestValidationError) para decorar el manejador de excepciones.
El manejador de excepciones recibirá una Request y la excepción.
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exceptionsimportRequestValidationErrorfromfastapi.responsesimportPlainTextResponsefromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefhttp_exception_handler(request,exc):returnPlainTextResponse(str(exc.detail),status_code=exc.status_code)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc:RequestValidationError):message="Validation errors:"forerrorinexc.errors():message+=f"\nField: {error['loc']}, Error: {error['msg']}"returnPlainTextResponse(message,status_code=400)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(status_code=418,detail="Nope! I don't like 3.")return{"item_id":item_id}
Ahora, si vas a /items/foo, en lugar de obtener el error JSON por defecto con:
{"detail":[{"loc":["path","item_id"],"msg":"value is not a valid integer","type":"type_error.integer"}]}
obtendrás una versión en texto, con:
Validation errors:
Field: ('path', 'item_id'), Error: Input should be a valid integer, unable to parse string as an integer
Sobrescribir el manejador de errores de HTTPException¶
De la misma manera, puedes sobrescribir el manejador de HTTPException.
Por ejemplo, podrías querer retornar una respuesta en texto plano en lugar de JSON para estos errores:
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exceptionsimportRequestValidationErrorfromfastapi.responsesimportPlainTextResponsefromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefhttp_exception_handler(request,exc):returnPlainTextResponse(str(exc.detail),status_code=exc.status_code)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc:RequestValidationError):message="Validation errors:"forerrorinexc.errors():message+=f"\nField: {error['loc']}, Error: {error['msg']}"returnPlainTextResponse(message,status_code=400)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(status_code=418,detail="Nope! I don't like 3.")return{"item_id":item_id}
Detalles Técnicos
También podrías usar from starlette.responses import PlainTextResponse.
FastAPI proporciona las mismas starlette.responses que fastapi.responses solo como una conveniencia para ti, el desarrollador. Pero la mayoría de las respuestas disponibles vienen directamente de Starlette.
Aviso
Ten en mente que el RequestValidationError contiene la información del nombre del archivo y la línea donde ocurre el error de validación para que puedas mostrarlo en tus logs con la información relevante si quieres.
Pero eso significa que si simplemente lo conviertes a un string y retornas esa información directamente, podrías estar filtrando un poco de información sobre tu sistema, por eso aquí el código extrae y muestra cada error de manera independiente.
Recibirás una respuesta diciéndote que los datos son inválidos conteniendo el body recibido:
{"detail":[{"loc":["body","size"],"msg":"value is not a valid integer","type":"type_error.integer"}],"body":{"title":"towel","size":"XL"}}
HTTPException de FastAPI vs HTTPException de Starlette¶
FastAPI tiene su propio HTTPException.
Y la clase de error HTTPException de FastAPI hereda de la clase de error HTTPException de Starlette.
La única diferencia es que el HTTPException de FastAPI acepta cualquier dato convertible a JSON para el campo detail, mientras que el HTTPException de Starlette solo acepta strings para ello.
Así que puedes seguir lanzando el HTTPException de FastAPI como normalmente en tu código.
Pero cuando registras un manejador de excepciones, deberías registrarlo para el HTTPException de Starlette.
De esta manera, si cualquier parte del código interno de Starlette, o una extensión o plug-in de Starlette, lanza un HTTPException de Starlette, tu manejador podrá capturarlo y manejarlo.
En este ejemplo, para poder tener ambos HTTPException en el mismo código, la excepción de Starlette se renombra a StarletteHTTPException:
Reutilizar los manejadores de excepciones de FastAPI¶
Si quieres usar la excepción junto con los mismos manejadores de excepciones por defecto de FastAPI, puedes importar y reutilizar los manejadores de excepciones por defecto de fastapi.exception_handlers:
fromfastapiimportFastAPI,HTTPExceptionfromfastapi.exception_handlersimport(http_exception_handler,request_validation_exception_handler,)fromfastapi.exceptionsimportRequestValidationErrorfromstarlette.exceptionsimportHTTPExceptionasStarletteHTTPExceptionapp=FastAPI()@app.exception_handler(StarletteHTTPException)asyncdefcustom_http_exception_handler(request,exc):print(f"OMG! An HTTP error!: {repr(exc)}")returnawaithttp_exception_handler(request,exc)@app.exception_handler(RequestValidationError)asyncdefvalidation_exception_handler(request,exc):print(f"OMG! The client sent invalid data!: {exc}")returnawaitrequest_validation_exception_handler(request,exc)@app.get("/items/{item_id}")asyncdefread_item(item_id:int):ifitem_id==3:raiseHTTPException(status_code=418,detail="Nope! I don't like 3.")return{"item_id":item_id}
En este ejemplo solo estás imprimiendo el error con un mensaje muy expresivo, pero te haces una idea. Puedes usar la excepción y luego simplemente reutilizar los manejadores de excepciones por defecto.