Zum Inhalt

Fehler behandeln

Es gibt viele Situationen, in denen Sie einem Client, der Ihre API nutzt, einen Fehler mitteilen müssen.

Dieser Client könnte ein Browser mit einem Frontend sein, ein Code von jemand anderem, ein IoT-Gerät usw.

Sie könnten dem Client mitteilen müssen, dass:

  • Der Client nicht genügend Berechtigungen für diese Operation hat.
  • Der Client keinen Zugriff auf diese Ressource hat.
  • Die Ressource, auf die der Client versucht hat, zuzugreifen, nicht existiert.
  • usw.

In diesen Fällen würden Sie normalerweise einen HTTP-Statuscode im Bereich 400 (von 400 bis 499) zurückgeben.

Dies ist vergleichbar mit den HTTP-Statuscodes im Bereich 200 (von 200 bis 299). Diese „200“-Statuscodes bedeuten, dass der Request in irgendeiner Weise erfolgreich war.

Die Statuscodes im Bereich 400 bedeuten hingegen, dass es einen Fehler seitens des Clients gab.

Erinnern Sie sich an all diese „404 Not Found“ Fehler (und Witze)?

HTTPException verwenden

Um HTTP-Responses mit Fehlern an den 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 zusätzlichen Daten, die für APIs relevant sind.

Weil es eine Python-Exception ist, geben Sie sie nicht zurück (return), sondern lösen sie aus (raise).

Das bedeutet auch, wenn Sie sich innerhalb einer Hilfsfunktion befinden, die Sie innerhalb Ihrer Pfadoperation-Funktion aufrufen, und Sie die HTTPException aus dieser Hilfsfunktion heraus auslösen, wird der restliche Code in der Pfadoperation-Funktion nicht ausgeführt. Der Request wird sofort abgebrochen und der HTTP-Error der HTTPException wird an den Client gesendet.

Der Vorteil des Auslösens einer Exception gegenüber dem Zurückgeben eines Wertes wird im Abschnitt über Abhängigkeiten und Sicherheit deutlicher werden.

In diesem Beispiel lösen wir eine Exception mit einem Statuscode von 404 aus, wenn der Client einen Artikel mit einer nicht existierenden ID anfordert:

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 anfordert (ein item_id "foo"), erhält dieser Client einen HTTP-Statuscode 200 und diese JSON-Response:

{
  "item": "The Foo Wrestlers"
}

Aber wenn der Client http://example.com/items/bar anfordert (ein nicht-existierendes item_id "bar"), erhält er einen HTTP-Statuscode 404 (der „Not Found“-Error) und eine JSON-Response wie:

{
  "detail": "Item not found"
}

Tipp

Wenn Sie eine HTTPException auslösen, können Sie dem Parameter detail jeden Wert übergeben, der in JSON konvertiert werden kann, nicht nur str.

Sie könnten ein dict, eine list, usw. übergeben.

Diese werden von FastAPI automatisch gehandhabt und in JSON konvertiert.

Benutzerdefinierte Header hinzufügen

Es gibt Situationen, in denen es nützlich ist, dem HTTP-Error benutzerdefinierte Header hinzuzufügen. Zum Beispiel in einigen Sicherheitsszenarien.

Sie werden es wahrscheinlich nicht direkt in Ihrem Code verwenden müssen.

Aber falls Sie es für ein fortgeschrittenes Szenario benötigen, können Sie benutzerdefinierte Header 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 installieren

Sie können benutzerdefinierte Exceptionhandler mit den gleichen Exception-Werkzeugen von Starlette hinzufügen.

Angenommen, Sie haben eine benutzerdefinierte Exception UnicornException, die Sie (oder eine Bibliothek, die Sie verwenden) raisen könnten.

Und Sie möchten diese Exception global mit FastAPI handhaben.

Sie könnten einen benutzerdefinierten Exceptionhandler mit @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}

Hier, wenn Sie /unicorns/yolo anfordern, wird die Pfadoperation eine UnicornException raisen.

Aber diese wird von unicorn_exception_handler gehandhabt.

Sie erhalten also einen sauberen Fehler mit einem HTTP-Statuscode von 418 und dem JSON-Inhalt:

{"message": "Oops! yolo did something. There goes a rainbow..."}

Technische Details

Sie könnten auch from starlette.requests import Request und from starlette.responses import JSONResponse verwenden.

FastAPI bietet dieselben starlette.responses auch via fastapi.responses an, nur als Annehmlichkeit für Sie, den Entwickler. Aber die meisten verfügbaren Responses kommen direkt von Starlette. Dasselbe gilt für Request.

Die Default-Exceptionhandler überschreiben

FastAPI hat einige Default-Exceptionhandler.

Diese Handler sind dafür verantwortlich, die Default-JSON-Responses zurückzugeben, wenn Sie eine HTTPException raisen und wenn der Request ungültige Daten enthält.

Sie können diese Exceptionhandler mit Ihren eigenen überschreiben.

Überschreiben von Request-Validierungs-Exceptions

Wenn ein Request ungültige Daten enthält, löst FastAPI intern einen RequestValidationError aus.

Und es enthält auch einen Default-Exceptionhandler für diesen.

Um diesen zu überschreiben, importieren Sie den RequestValidationError und verwenden Sie ihn mit @app.exception_handler(RequestValidationError), um den Exceptionhandler zu dekorieren.

Der Exceptionhandler erhält einen Request und die Exception.

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 zu /items/foo gehen, erhalten Sie anstelle des standardmäßigen JSON-Fehlers mit:

{
    "detail": [
        {
            "loc": [
                "path",
                "item_id"
            ],
            "msg": "value is not a valid integer",
            "type": "type_error.integer"
        }
    ]
}

eine Textversion mit:

1 validation error
path -> item_id
  value is not a valid integer (type=type_error.integer)

RequestValidationError vs. ValidationError

Achtung

Dies sind technische Details, die Sie überspringen können, wenn sie für Sie jetzt nicht wichtig sind.

RequestValidationError ist eine Unterklasse von Pydantics ValidationError.

FastAPI verwendet diesen so, dass, wenn Sie ein Pydantic-Modell in response_model verwenden und Ihre Daten einen Fehler haben, Sie den Fehler in Ihrem Log sehen.

Aber der Client/Benutzer wird ihn nicht sehen. Stattdessen erhält der Client einen „Internal Server Error“ mit einem HTTP-Statuscode 500.

Es sollte so sein, denn wenn Sie einen Pydantic ValidationError in Ihrer Response oder irgendwo anders in Ihrem Code haben (nicht im Request des Clients), ist es tatsächlich ein Fehler 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.

Überschreiben des HTTPException-Fehlerhandlers

Auf die gleiche Weise 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önnten auch from starlette.responses import PlainTextResponse verwenden.

FastAPI bietet dieselben starlette.responses auch via fastapi.responses an, nur als Annehmlichkeit für Sie, den Entwickler. Aber die meisten verfügbaren Responses kommen direkt von Starlette.

Verwenden des RequestValidationError-Bodys

Der RequestValidationError enthält den empfangenen body mit den ungültigen Daten.

Sie könnten diesen während der Entwicklung Ihrer Anwendung verwenden, um den Body zu loggen und zu debuggen, ihn an den Benutzer zurückzugeben usw.

from fastapi import FastAPI, Request
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=422,
        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

Versuchen Sie nun, einen ungültigen Artikel zu senden:

{
  "title": "towel",
  "size": "XL"
}

Sie erhalten eine Response, die Ihnen sagt, dass die Daten ungültig sind und die 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 die HTTPException-Fehlerklasse von FastAPI erbt von der HTTPException-Fehlerklasse von Starlette.

Der einzige Unterschied besteht darin, dass die HTTPException von FastAPI beliebige JSON-konvertierbare Daten für das detail-Feld akzeptiert, während die HTTPException von Starlette nur Strings dafür akzeptiert.

Sie können also weiterhin die HTTPException von FastAPI wie üblich in Ihrem Code auslösen.

Aber wenn Sie einen Exceptionhandler registrieren, sollten Sie ihn für die HTTPException von Starlette registrieren.

Auf diese Weise, wenn irgendein Teil des internen Codes von Starlette, oder eine Starlette-Erweiterung oder ein Plug-in, eine Starlette HTTPException auslöst, wird Ihr Handler in der Lage sein, diese abzufangen und zu handhaben.

Um in diesem Beispiel beide HTTPExceptions im selben Code zu haben, wird die Exception von Starlette zu StarletteHTTPException umbenannt:

from starlette.exceptions import HTTPException as StarletteHTTPException

Die Exceptionhandler von FastAPI wiederverwenden

Wenn Sie die Exception zusammen mit den gleichen Default-Exceptionhandlern von FastAPI verwenden möchten, können Sie die Default-Exceptionhandler aus 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 geben Sie nur den Fehler mit einer sehr ausdrucksstarken Nachricht aus, aber Sie verstehen das Prinzip. Sie können die Exception verwenden und dann einfach die Default-Exceptionhandler wiederverwenden.