Fehlerbehandlung¶
Es gibt viele Situationen, in denen Sie einem Client, der Ihre API benutzt, einen Fehler zurückgeben müssen.
Dieser Client könnte ein Browser mit einem Frontend, Code von jemand anderem, ein IoT-Gerät, usw., sein.
Sie müssten beispielsweise einem Client sagen:
- Dass er nicht die notwendigen Berechtigungen hat, eine Aktion auszuführen.
- Dass er zu einer Ressource keinen Zugriff hat.
- Dass die Ressource, auf die er zugreifen möchte, nicht existiert.
- usw.
In diesen Fällen geben Sie normalerweise einen HTTP-Statuscode im Bereich 400 (400 bis 499) zurück.
Das ist vergleichbar mit den HTTP-Statuscodes im Bereich 200 (von 200 bis 299). Diese „200“er Statuscodes bedeuten, dass der Request in einem bestimmten Aspekt ein „Success“ („Erfolg“) war.
Die Statuscodes im 400er-Bereich bedeuten hingegen, dass es einen Fehler gab.
Erinnern Sie sich an all diese 404 Not Found Fehler (und Witze)?
HTTPException
verwenden¶
Um HTTP-Responses mit Fehlern zum Client zurückzugeben, verwenden Sie HTTPException
.
HTTPException
importieren¶
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
Eine HTTPException
in Ihrem Code auslösen¶
HTTPException
ist eine normale Python-Exception mit einigen zusätzlichen Daten, die für APIs relevant sind.
Weil es eine Python-Exception ist, geben Sie sie nicht zurück, (return
), sondern Sie lösen sie aus (raise
).
Das bedeutet auch, wenn Sie in einer Hilfsfunktion sind, die Sie von ihrer Pfadoperation-Funktion aus aufrufen, und Sie lösen eine HTTPException
von innerhalb dieser Hilfsfunktion aus, dann wird der Rest der Pfadoperation-Funktion nicht ausgeführt, sondern der Request wird sofort abgebrochen und der HTTP-Error der HTTP-Exception
wird zum Client gesendet.
Der Vorteil, eine Exception auszulösen (raise
), statt sie zurückzugeben (return
) wird im Abschnitt über Abhängigkeiten und Sicherheit klarer werden.
Im folgenden Beispiel lösen wir, wenn der Client eine ID anfragt, die nicht existiert, eine Exception mit dem Statuscode 404
aus.
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items/{item_id}")
async def read_item(item_id: str):
if item_id not in items:
raise HTTPException(status_code=404, detail="Item not found")
return {"item": items[item_id]}
Die resultierende Response¶
Wenn der Client http://example.com/items/foo
anfragt (ein item_id
"foo"
), erhält dieser Client einen HTTP-Statuscode 200 und folgende JSON-Response:
{
"item": "The Foo Wrestlers"
}
Aber wenn der Client http://example.com/items/bar
anfragt (ein nicht-existierendes item_id
"bar"
), erhält er einen HTTP-Statuscode 404 (der „Not Found“-Fehler), und eine JSON-Response wie folgt:
{
"detail": "Item not found"
}
Tipp
Wenn Sie eine HTTPException
auslösen, können Sie dem Parameter detail
jeden Wert übergeben, der nach JSON konvertiert werden kann, nicht nur str
.
Zum Beispiel ein dict
, eine list
, usw.
Das wird automatisch von FastAPI gehandhabt und der Wert nach JSON konvertiert.
Benutzerdefinierte Header hinzufügen¶
Es gibt Situationen, da ist es nützlich, dem HTTP-Error benutzerdefinierte Header hinzufügen zu können, etwa in einigen Sicherheitsszenarien.
Sie müssen das wahrscheinlich nicht direkt in ihrem Code verwenden.
Aber falls es in einem fortgeschrittenen Szenario notwendig ist, können Sie benutzerdefinierte Header wie folgt hinzufügen:
from fastapi import FastAPI, HTTPException
app = FastAPI()
items = {"foo": "The Foo Wrestlers"}
@app.get("/items-header/{item_id}")
async def read_item_header(item_id: str):
if item_id not in items:
raise HTTPException(
status_code=404,
detail="Item not found",
headers={"X-Error": "There goes my error"},
)
return {"item": items[item_id]}
Benutzerdefinierte Exceptionhandler definieren¶
Sie können benutzerdefinierte Exceptionhandler hinzufügen, mithilfe derselben Werkzeuge für Exceptions von Starlette.
Nehmen wir an, Sie haben eine benutzerdefinierte Exception UnicornException
, die Sie (oder eine Bibliothek, die Sie verwenden) raise
n könnten.
Und Sie möchten diese Exception global mit FastAPI handhaben.
Sie könnten einen benutzerdefinierten Exceptionhandler mittels @app.exception_handler()
hinzufügen:
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse
class UnicornException(Exception):
def __init__(self, name: str):
self.name = name
app = FastAPI()
@app.exception_handler(UnicornException)
async def unicorn_exception_handler(request: Request, exc: UnicornException):
return JSONResponse(
status_code=418,
content={"message": f"Oops! {exc.name} did something. There goes a rainbow..."},
)
@app.get("/unicorns/{name}")
async def read_unicorn(name: str):
if name == "yolo":
raise UnicornException(name=name)
return {"unicorn_name": name}
Wenn Sie nun /unicorns/yolo
anfragen, raise
d die Pfadoperation eine UnicornException
.
Aber diese wird von unicorn_exception_handler
gehandhabt.
Sie erhalten also einen sauberen Error mit einem Statuscode 418
und dem JSON-Inhalt:
{"message": "Oops! yolo did something. There goes a rainbow..."}
Technische Details
Sie können auch from starlette.requests import Request
und from starlette.responses import JSONResponse
verwenden.
FastAPI bietet dieselben starlette.responses
auch via fastapi.responses
an, als Annehmlichkeit für Sie, den Entwickler. Die meisten verfügbaren Responses kommen aber direkt von Starlette. Das Gleiche gilt für Request
.
Die Default-Exceptionhandler überschreiben¶
FastAPI hat einige Default-Exceptionhandler.
Diese Handler kümmern sich darum, Default-JSON-Responses zurückzugeben, wenn Sie eine HTTPException
raise
n, und wenn der Request ungültige Daten enthält.
Sie können diese Exceptionhandler mit ihren eigenen überschreiben.
Requestvalidierung-Exceptions überschreiben¶
Wenn ein Request ungültige Daten enthält, löst FastAPI intern einen RequestValidationError
aus.
Und bietet auch einen Default-Exceptionhandler dafür.
Um diesen zu überschreiben, importieren Sie den RequestValidationError
und verwenden Sie ihn in @app.exception_handler(RequestValidationError)
, um Ihren Exceptionhandler zu dekorieren.
Der Exceptionhandler wird einen Request
und die Exception entgegennehmen.
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
Wenn Sie nun /items/foo
besuchen, erhalten Sie statt des Default-JSON-Errors:
{
"detail": [
{
"loc": [
"path",
"item_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}
eine Textversion:
1 validation error
path -> item_id
value is not a valid integer (type=type_error.integer)
RequestValidationError
vs. ValidationError
¶
Achtung
Das folgende sind technische Details, die Sie überspringen können, wenn sie für Sie nicht wichtig sind.
RequestValidationError
ist eine Unterklasse von Pydantics ValidationError
.
FastAPI verwendet diesen, sodass Sie, wenn Sie ein Pydantic-Modell für response_model
verwenden, und ihre Daten fehlerhaft sind, einen Fehler in ihrem Log sehen.
Aber der Client/Benutzer sieht ihn nicht. Stattdessen erhält der Client einen „Internal Server Error“ mit einem HTTP-Statuscode 500
.
Das ist, wie es sein sollte, denn wenn Sie einen Pydantic-ValidationError
in Ihrer Response oder irgendwo sonst in ihrem Code haben (es sei denn, im Request des Clients), ist das tatsächlich ein Bug in ihrem Code.
Und während Sie den Fehler beheben, sollten ihre Clients/Benutzer keinen Zugriff auf interne Informationen über den Fehler haben, da das eine Sicherheitslücke aufdecken könnte.
den HTTPException
-Handler überschreiben¶
Genauso können Sie den HTTPException
-Handler überschreiben.
Zum Beispiel könnten Sie eine Klartext-Response statt JSON für diese Fehler zurückgeben wollen:
from fastapi import FastAPI, HTTPException
from fastapi.exceptions import RequestValidationError
from fastapi.responses import PlainTextResponse
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(str(exc.detail), status_code=exc.status_code)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
return PlainTextResponse(str(exc), status_code=400)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
Technische Details
Sie können auch from starlette.responses import PlainTextResponse
verwenden.
FastAPI bietet dieselben starlette.responses
auch via fastapi.responses
an, als Annehmlichkeit für Sie, den Entwickler. Die meisten verfügbaren Responses kommen aber direkt von Starlette.
Den RequestValidationError
-Body verwenden¶
Der RequestValidationError
enthält den empfangenen body
mit den ungültigen Daten.
Sie könnten diesen verwenden, während Sie Ihre Anwendung entwickeln, um den Body zu loggen und zu debuggen, ihn zum Benutzer zurückzugeben, usw.
from fastapi import FastAPI, Request, status
from fastapi.encoders import jsonable_encoder
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from pydantic import BaseModel
app = FastAPI()
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
)
class Item(BaseModel):
title: str
size: int
@app.post("/items/")
async def create_item(item: Item):
return item
Jetzt versuchen Sie, einen ungültigen Artikel zu senden:
{
"title": "towel",
"size": "XL"
}
Sie erhalten eine Response, die Ihnen sagt, dass die Daten ungültig sind, und welche den empfangenen Body enthält.
{
"detail": [
{
"loc": [
"body",
"size"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
],
"body": {
"title": "towel",
"size": "XL"
}
}
FastAPIs HTTPException
vs. Starlettes HTTPException
¶
FastAPI hat seine eigene HTTPException
.
Und FastAPIs HTTPException
-Fehlerklasse erbt von Starlettes HTTPException
-Fehlerklasse.
Der einzige Unterschied besteht darin, dass FastAPIs HTTPException
alles für das Feld detail
akzeptiert, was nach JSON konvertiert werden kann, während Starlettes HTTPException
nur Strings zulässt.
Sie können also weiterhin FastAPIs HTTPException
wie üblich in Ihrem Code auslösen.
Aber wenn Sie einen Exceptionhandler registrieren, registrieren Sie ihn für Starlettes HTTPException
.
Auf diese Weise wird Ihr Handler, wenn irgendein Teil von Starlettes internem Code, oder eine Starlette-Erweiterung, oder -Plugin eine Starlette-HTTPException
auslöst, in der Lage sein, diese zu fangen und zu handhaben.
Damit wir in diesem Beispiel beide HTTPException
s im selben Code haben können, benennen wir Starlettes Exception um zu StarletteHTTPException
:
from starlette.exceptions import HTTPException as StarletteHTTPException
FastAPIs Exceptionhandler wiederverwenden¶
Wenn Sie die Exception zusammen mit denselben Default-Exceptionhandlern von FastAPI verwenden möchten, können Sie die Default-Exceptionhandler von fastapi.Exception_handlers
importieren und wiederverwenden:
from fastapi import FastAPI, HTTPException
from fastapi.exception_handlers import (
http_exception_handler,
request_validation_exception_handler,
)
from fastapi.exceptions import RequestValidationError
from starlette.exceptions import HTTPException as StarletteHTTPException
app = FastAPI()
@app.exception_handler(StarletteHTTPException)
async def custom_http_exception_handler(request, exc):
print(f"OMG! An HTTP error!: {repr(exc)}")
return await http_exception_handler(request, exc)
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request, exc):
print(f"OMG! The client sent invalid data!: {exc}")
return await request_validation_exception_handler(request, exc)
@app.get("/items/{item_id}")
async def read_item(item_id: int):
if item_id == 3:
raise HTTPException(status_code=418, detail="Nope! I don't like 3.")
return {"item_id": item_id}
In diesem Beispiel print
en Sie nur den Fehler mit einer sehr ausdrucksstarken Nachricht, aber Sie sehen, worauf wir hinauswollen. Sie können mit der Exception etwas machen und dann einfach die Default-Exceptionhandler wiederverwenden.