Erstellen von benutzerdefiniertem Middleware in FastAPI zur Verbesserung der API-Kontrolle
Wenhao Wang
Dev Intern · Leapcell

Einführung
In der Welt der Backend-Entwicklung bedeutet das Erstellen robuster und skalierbarer APIs oft mehr als nur die Definition von Endpunkten. Wenn Ihre Anwendung wächst, werden Sie wiederkehrende Aufgaben feststellen, die für fast jede eingehende Anfrage oder ausgehende Antwort ausgeführt werden müssen. Diese Aufgaben können Authentifizierung, Protokollierung, Fehlerbehandlung, Hinzufügen benutzerdefinierter Header oder sogar die Änderung von Anfrage-/Antwortkörpern umfassen. Obwohl diese Funktionalitäten über einzelne Endpunkt-Handler verteilt werden können, führt dieser Ansatz schnell zu Code-Duplizierung, verringerter Wartbarkeit und mangelnder klarer Trennung von Belangen. Hier glänzt das Konzept der Middleware. Middleware bietet einen leistungsstarken, eleganten und zentralisierten Mechanismus, um Anfragen und Antworten auf globaler Ebene abzufangen und zu verarbeiten, lange bevor sie Ihre Kernanwendungslogik erreichen oder nachdem Ihre Logik ihre Arbeit abgeschlossen hat. Durch die Nutzung von Middleware können wir die Kontrolle, Flexibilität und architektonische Sauberkeit unserer APIs erheblich verbessern. Dieser Artikel befasst sich mit den praktischen Aspekten des Erstellens von benutzerdefiniertem Middleware in FastAPI und Starlette, zwei beliebten Python-Web-Frameworks, um diese gemeinsamen Herausforderungen zu bewältigen und Ihren Backend-Entwicklungsprozess zu optimieren.
Middleware in Web-Frameworks verstehen
Bevor wir uns mit den Implementierungsdetails befassen, wollen wir ein gemeinsames Verständnis der beteiligten Kernkonzepte aufbauen.
Was ist Middleware? Im Wesentlichen ist Middleware ein Softwareteil, der als Vermittler zwischen einer Anfrage und der Kernlogik einer Anwendung oder zwischen der Kernlogik der Anwendung und einer Antwort fungiert. Betrachten Sie es als eine Reihe von Kontrollpunkten oder Filtern, durch die Anfragen (und die entsprechenden Antworten) geleitet werden. Jedes Stück Middleware kann spezifische Aktionen ausführen, möglicherweise die Anfrage oder Antwort ändern und sie dann an die nächste Schicht in der Kette weiterleiten.
Starlette und ASGI
FastAPI baut auf Starlette auf, einem leichtgewichtigen ASGI-Framework. ASGI (Asynchronous Server Gateway Interface) ist ein geistiger Nachfolger von WSGI, der für die Handhabung asynchroner Operationen entwickelt wurde. Middleware in Starlette (und damit in FastAPI) ist von Natur aus asynchron. Das bedeutet, dass Middleware-Funktionen async
sind und await
-Operationen ausführen, wenn sie die Aufrufe der Anwendung verarbeiten.
Wichtige Middleware-Merkmale:
- Abfangen: Middleware kann sowohl eingehende Anfragen als auch ausgehende Antworten abfangen.
- Ausführungsreihenfolge: Die Reihenfolge, in der Middleware definiert wird, ist entscheidend. Anfragen durchlaufen die Middleware-Kette von oben nach unten, während Antworten sie von unten nach oben durchlaufen.
- Änderung: Middleware kann Anforderungs-Header, Körper, Abfrageparameter sowie Antwortstatuscodes, Header und Körper ändern.
- Short-Circuiting: Middleware kann den Anfrage-Antwort-Zyklus frühzeitig beenden, z. B. durch Rückgabe einer Fehlerantwort, ohne jemals die Hauptanwendungslogik zu erreichen.
Benutzerdefiniertes Middleware erstellen
Lassen Sie uns untersuchen, wie benutzerdefiniertes Middleware mit praktischen Beispielen erstellt wird. Wir werden verschiedene Arten von gängigen Middleware-Anwendungsfällen abdecken.
Protokollierung von Anfragedauern
Ein sehr gängiger Anwendungsfall ist die Protokollierung der Zeit, die für die Verarbeitung jeder Anfrage benötigt wird, was für Leistungsüberwachung und Debugging von unschätzbarem Wert ist.
# main.py from fastapi import FastAPI, Request, Response import time import logging # Logging konfigurieren logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) app = FastAPI() @app.middleware("http") async def add_process_time_header(request: Request, call_next): start_time = time.time() response = await call_next(request) process_time = time.time() - start_time response.headers["X-Process-Time"] = str(process_time) logger.info(f"Request: {request.method} {request.url.path} processed in {process_time:.4f}s") return response @app.get("/") async def read_root(): return {"message": "Hello FastAPI!"} @app.get("/items/{item_id}") async def read_item(item_id: int): # Verarbeitungszeit simulieren await asyncio.sleep(0.1) return {"item_id": item_id}
In diesem Beispiel:
- Wir definieren eine
async
-Funktionadd_process_time_header
. - Sie nimmt
request: Request
undcall_next
als Argumente entgegen.call_next
ist ein aufrufbares Objekt, das die "nächste" Operation in der Middleware-Kette darstellt und schließlich zu Ihrem Routen-Handler führt. - Wir erfassen die
start_time
, rufenawait call_next(request)
auf, um die Kontrolle an die nächste Middleware oder den Routen-Handler zu übergeben, und berechnen dann dieprocess_time
nach Erhalt der Antwort. - Schließlich fügen wir der Antwort einen benutzerdefinierten Header
X-Process-Time
hinzu und protokollieren die Dauer.
Um dies auszuführen, speichern Sie es als main.py
und führen Sie uvicorn main:app --reload
aus.
Machen Sie dann eine Anfrage: curl -v http://localhost:8000/items/123
. Sie sehen den Header X-Process-Time
in der Antwort.
Benutzerdefiniertes Authentifizierungs-Middleware
Stellen wir uns vor, Sie haben ein einfaches tokenbasiertes Authentifizierungssystem, bei dem ein bestimmter X-API-Key
-Header vorhanden und gültig sein muss.
# main_auth.py from fastapi import FastAPI, Request, HTTPException, status from starlette.responses import JSONResponse app = FastAPI() SECRET_API_KEY = "supersecretkey" @app.middleware("http") async def api_key_auth_middleware(request: Request, call_next): if request.url.path.startswith("/public"): # Zugriff auf öffentliche Pfade ohne API-Schlüssel zulassen response = await call_next(request) return response api_key = request.headers.get("X-API-Key") if not api_key or api_key != SECRET_API_KEY: return JSONResponse( status_code=status.HTTP_401_UNAUTHORIZED, content={"detail": "Unauthorized: Invalid or missing X-API-Key"} ) response = await call_next(request) return response @app.get("/protected") async def protected_route(): return {"message": "Welcome, authorized user!"} @app.get("/public") async def public_route(): return {"message": "This is a public endpoint."}
Hier:
- Wir prüfen auf den Header
X-API-Key
. - Wenn er fehlt oder ungültig ist, geben wir sofort eine
JSONResponse
mit dem Status401 Unauthorized
zurück, wodurch die Anfrage effektiv unterbrochen wird und verhindert wird, dass sie denprotected_route
erreicht. - Wir demonstrieren auch, wie Middleware ihre Logik bedingt anwenden kann, sodass öffentliche Pfade die Authentifizierungsprüfung umgehen können.
Testen Sie mit:
curl http://localhost:8000/protected
(wird nicht autorisiert sein)curl -H "X-API-Key: wrongkey" http://localhost:8000/protected
(wird nicht autorisiert sein)curl -H "X-API-Key: supersecretkey" http://localhost:8000/protected
(wird autorisiert sein)curl http://localhost:8000/public
(wird ohne API-Schlüssel autorisiert sein)
Ändern des Anfrage- oder Antwortkörpers (Fortgeschritten)
Das Ändern des Anfragekörpers kann schwierig sein, da das Request
-Objekt von FastAPI/Starlette den Körper verbraucht, wenn Sie darauf zugreifen. Um ihn zu ändern, müssen Sie oft den Körper lesen, ihn ändern, ein neues Request
-Objekt (oder ein Mock-Objekt) erstellen und es dann weitergeben. Das Ändern des Antwortkörpers ist einfacher.
Lassen Sie uns zeigen, wie ein Antwortkörper geändert wird, um einen Zeitstempel hinzuzufügen.
# main_transform.py import json from fastapi import FastAPI, Request, Response from starlette.background import BackgroundTask app = FastAPI() @app.middleware("http") async def add_timestamp_to_response(request: Request, call_next): response = await call_next(request) # Nur JSON-Antworten ändern if "application/json" in response.headers.get("content-type", ""): # Den rohen Antwortkörper lesen response_body = b"" async for chunk in response.body_iterator: response_body += chunk # Dekodieren, ändern und neu kodieren try: data = json.loads(response_body) data["timestamp_utc"] = time.time() modified_body = json.dumps(data).encode("utf-8") # Ein neues Response-Objekt mit dem geänderten Körper erstellen # Und ursprünglichen Statuscode und Header kopieren return Response( content=modified_body, status_code=response.status_code, media_type="application/json", headers=dict(response.headers), background=response.background ) except json.JSONDecodeError: # Wenn kein gültiges JSON vorhanden ist, einfach die ursprüngliche Antwort zurückgeben pass return response @app.get("/data") async def get_data(): return {"value": 123, "description": "some data"}
Dieses Beispiel ist komplexer:
- Wir iterieren durch
response.body_iterator
, um den ursprünglichen Antworthkörper wieder zusammenzusetzen. Dies ist entscheidend, da der Körper ein Stream ist und nur einmal gelesen werden kann. - Wir dekodieren ihn, fügen unser Feld
timestamp_utc
hinzu und kodieren ihn neu. - Entscheidend ist, dass wir ein neues
Response
-Objekt mit dem geänderten Inhalt zurückgeben und dabei den ursprünglichenstatus_code
,media_type
,headers
undbackground
-Aufgaben beibehalten. Dies stellt sicher, dass die Integrität der ursprünglichen Antwort vollständig erhalten bleibt und nur der Inhalt des Körpers geändert wird.
Mit curl http://localhost:8000/data
sehen Sie ein Feld timestamp_utc
, das der JSON-Antwort hinzugefügt wurde.
Anwendungsszenarien
Benutzerdefiniertes Middleware kann in zahlreichen Szenarien angewendet werden:
- API-Schlüssel-/Token-Validierung: Wie gezeigt, zur Authentifizierung und Autorisierung.
- Anfrage-/Antwort-Protokollierung: Zum Debugging, Auditing und zur Leistungsüberwachung.
- Einschleusen benutzerdefinierter Header: Hinzufügen von Korrelations-IDs, Tracing-Informationen oder Sicherheitsheadern.
- Fehlerbehandlung: Erfassen spezifischer Ausnahmen global und Rückgabe konsistenter Fehlerantworten.
- Ratenbegrenzung: Missbrauch verhindern, indem die Anzahl der Anfragen von einer bestimmten IP oder einem bestimmten Benutzer begrenzt wird.
- CORS-Verwaltung: Obwohl FastAPI integriertes CORS-Middleware bietet, könnten Sie bei Bedarf benutzerdefinierte, granularere Logik erstellen.
- Datentransformation: Normalisieren eingehender Anfragedaten oder Anreichern ausgehender Antwortdaten.
Fazit
Benutzerdefiniertes Middleware in FastAPI und Starlette bietet einen leistungsstarken und flexiblen Ansatz zur Verwaltung von durchquerenden Belangen in Ihren Webanwendungen. Durch die Zentralisierung gängiger Funktionalitäten wie Authentifizierung, Protokollierung und Datenbearbeitung können Sie die Code-Duplizierung erheblich reduzieren, die Wartbarkeit verbessern und eine granulare Kontrolle über den Anfrage-Antwort-Lebenszyklus gewinnen. Die Nutzung dieses Musters ermöglicht es Ihnen, robustere, skalierbarere und architektonisch sauberere APIs zu erstellen.