Entwicklung benutzerdefinierter Extraktoren in Axum und Actix Web
Min-jun Kim
Dev Intern · Leapcell

Anforderungsinformationen mit benutzerdefinierten Extraktoren freischalten
In der Welt der Rust-Webentwicklung ist die Extraktion spezifischer Informationen aus eingehenden HTTP-Anforderungen eine grundlegende und häufig durchgeführte Aufgabe. Ob es sich um Benutzerauthentifizierungs-Tokens, geparste JSON-Bodies oder Abfrageparameter handelt, die Fähigkeit, sauber und ergonomisch auf diese Daten zuzugreifen, wirkt sich erheblich auf die Lesbarkeit und Wartbarkeit der Handler Ihrer Anwendung aus. Sowohl Axum als auch Actix Web, zwei beliebte Web-Frameworks in Rust, bieten leistungsstarke Mechanismen zur Erreichung dieses Ziels: das FromRequest-Trait. Dieses Trait ist der Eckpfeiler für die Erstellung benutzerdefinierter Extraktoren und ermöglicht es Entwicklern, komplexe Logik zur Datenextraktion in eine wiederverwendbare und idiomatische Form zu kapseln. Das Verständnis und die Nutzung von FromRequest befähigen Sie, sauberere, robustere Web-APIs zu schreiben und sich über Boilerplate hinaus zu einer hochgradig ausdrucksstarken und verwaltbaren Codierung zu bewegen.
Die Macht von FromRequest
Bevor wir uns mit der Erstellung unserer eigenen Extraktoren befassen, wollen wir die Kernkonzepte klar verstehen.
- Extractor: Im Kontext von Web-Frameworks ist ein Extraktor ein Typ, der ein bestimmtes Trait (wie
FromRequest) implementiert, das es ihm ermöglicht, automatisch aus der eingehenden HTTP-Anforderung konstruiert zu werden. Dieser Vorgang geschieht, bevor Ihre Handlerfunktion aufgerufen wird. - Asynchrone Kontexte: Sowohl Axum als auch Actix Web sind asynchrone Frameworks. Das bedeutet, dass die Extraktion von Daten aus einer Anforderung oft das
awaitvon Operationen (wie das Lesen des Anforderungskörpers) beinhaltet. Daher wird dieFromRequest-Implementierung typischerweise selbst eineasync-Funktion sein, die einFuturezurückgibt. - Fehlerbehandlung: Extraktoren können fehlschlagen. Zum Beispiel, wenn ein erforderlicher Header fehlt oder ein JSON-Body fehlerhaft ist. Eine robuste
FromRequest-Implementierung muss definieren, wie diese Extraktionsfehler behandelt werden und welche Art von Fehlerantwort an den Client zurückgegeben wird.
Das FromRequest-Trait ist das Herzstück, wie Axum und Actix Web Ihren Handlerfunktionen nahtlos strukturierte Daten zur Verfügung stellen. Wenn eine Anforderung ankommt, prüft das Framework die Typen der Argumente in Ihrem Handler. Wenn der Typ eines Arguments FromRequest implementiert, ruft das Framework dessen from_request-Methode auf und übergibt den Kontext der eingehenden Anforderung. Diese Methode versucht dann, eine Instanz des Argumenttyps zu konstruieren. Wenn dies erfolgreich ist, wird die Instanz an Ihren Handler übergeben; wenn sie fehlschlägt, wird eine entsprechende Fehlerantwort an den Client zurückgegeben, ohne dass Ihr Handler jemals aufgerufen wird. Dieser Mechanismus fördert eine klare Trennung der Zuständigkeiten und hält Ihre Handler rein auf die Geschäftslogik konzentriert.
Erstellen eines benutzerdefinierten Extraktors in Axum
Stellen wir uns vor, wir möchten einen "Client-ID"-Header aus jeder Anforderung extrahieren und sicherstellen, dass es sich um eine gültige UUID handelt. Wir werden einen benutzerdefinierten ClientId-Extraktor erstellen.
// Axum Beispiel use axum::{ async_trait, extract::{FromRequestParts, Request}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, Router, }; use std::fmt; uuid::Uuid; // Unser benutzerdefinierter Fehlertyp für ClientId-Extraktionsfehler #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "Ungültiger oder fehlender Client-ID-Header") } } // Konvertieren Sie unseren Fehler in eine HTTP-Antwort impl IntoResponse for ClientIdError { fn into_response(self) -> Response { (StatusCode::BAD_REQUEST, "Fehlender oder ungültiger Client-ID-Header").into_response() } } // Die benutzerdefinierte Extraktor-Struktur #[derive(Debug)] pub struct ClientId(pub Uuid); // Implementieren Sie FromRequestParts für unseren ClientId // Axums FromRequestParts wird zum Extrahieren von Daten aus Anforderungsheadern, URI usw. verwendet. // FromRequest wird zum Extrahieren aus dem Body oder anderen asynchronen Operationen verwendet, die die Anforderung verbrauchen. #[async_trait] impl FromRequestParts for ClientId { type Rejection = ClientIdError; async fn from_request_parts(parts: &mut Parts, _state: &()) -> Result<Self, Self::Rejection> { let headers = &parts.headers; let client_id_header = headers .get("Client-ID") .ok_or(ClientIdError)? // Fehler, wenn der Header fehlt .to_str() .map_err(|_| ClientIdError)?; let client_id = Uuid::parse_str(client_id_header).map_err(|_| ClientIdError)?; // Fehler, wenn keine gültige UUID Ok(ClientId(client_id)) } } // Ein Beispiel-Handler, der unseren benutzerdefinierten Extraktor verwendet async fn handle_request_with_client_id(ClientId(client_id): ClientId) -> Json<String> { Json(format!("Hallo vom Client: {}", client_id)) } #[tokio::main] async fn main() { let app = Router::new().route("/hello", axum::routing::get(handle_request_with_client_id)); println!("Axum-Server läuft auf http://127.0.0.1:3000"); axum::Server::bind(&"0.0.0.1:3000".parse().unwrap()) .serve(app.into_make_service()) .await .unwrap(); }
Im Axum-Beispiel implementieren wir FromRequestParts, da wir nur die Teile der Anforderung (Header) betrachten und nicht den Anforderungstext verbrauchen. Wenn wir einen JSON-Body parsen würden, würden wir stattdessen FromRequest implementieren. Beachten Sie, wie ClientIdError IntoResponse implementiert, was es Axum ermöglicht, unsere Extraktionsfehler automatisch in eine ordnungsgemäße HTTP-Antwort umzuwandeln.
Erstellen eines benutzerdefinierten Extraktors in Actix Web
Lassen Sie uns nun einen ähnlichen ClientId-Extraktor für Actix Web implementieren. Actix Webs FromRequest-Trait behandelt sowohl Header als auch potenziell asynchrone Operationen direkt.
// Actix Web Beispiel use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web, App, FromRequest, HttpRequest, HttpResponse, HttpServer, }; use futures::future::{ok, Ready}; use std::{fmt, ops::Deref}; uuid::Uuid; // Unser benutzerdefinierter Fehlertyp für ClientId-Extraktionsfehler #[derive(Debug)] struct ClientIdError; impl fmt::Display for ClientIdError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Ungültiger oder fehlender Client-ID-Header") } } // Konvertieren Sie unseren Fehler in eine HTTP-Antwort impl ResponseError for ClientIdError { fn error_response(&self) -> HttpResponse { HttpResponse::build(self.status_code()) .body(self.to_string()) } fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } } // Die benutzerdefinierte Extraktor-Struktur #[derive(Debug)] pub struct ClientId(pub Uuid); // Implementieren Sie Deref, um einfach auf die innere Uuid zuzugreifen impl Deref for ClientId { type Target = Uuid; fn deref(&self) -> &Self::Target { &self.0 } } // Implementieren Sie FromRequest für unseren ClientId // Actix Webs FromRequest funktioniert sowohl für die Header- als auch für die Body-Extraktion. impl FromRequest for ClientId { type Error = ClientIdError; type Future = Ready<Result<Self, Self::Error>>; type Config = (); fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let client_id_header = req.headers().get("Client-ID"); let result = match client_id_header { Some(header_value) => { header_value .to_str() .ok() .and_then(|s| Uuid::parse_str(s).ok()) .map(|uuid| ClientId(uuid)) .ok_or(ClientIdError) } None => Err(ClientIdError), }; ok(result) } } // Ein Beispiel-Handler, der unseren benutzerdefinierten Extraktor verwendet async fn handle_request_with_client_id(client_id: ClientId) -> HttpResponse { HttpResponse::Ok().body(format!("Hallo vom Client: {}", client_id.0)) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().service(web::resource("/hello").to(handle_request_with_client_id)) }) .bind("127.0.0.1:8080")? // .run() .await }
Im Actix Web-Beispiel empfängt die from_request-Methode von FromRequest eine HttpRequest-Referenz und eine mutable Payload. Da unser ClientId-Extraktor nur Headerinformationen benötigt, müssen wir nicht mit der Payload interagieren. Die Deref-Implementierung für ClientId ist ein schöner Zusatz, der es Ihnen ermöglicht, client_id.to_string() anstelle von client_id.0.to_string() direkt zu verwenden.
Anwendungsfälle und Best Practices
Benutzerdefinierte Extraktoren sind unglaublich vielseitig und können in zahlreichen Szenarien angewendet werden:
- Authentifizierung und Autorisierung: Extrahieren von JWTs, API-Schlüsseln oder Sitzungs-Tokens und deren Validierung.
- Mandanten-ID-Extraktion: Extrahieren einer Mandanten-ID aus einem Header, einer Subdomäne oder einem Pfad in mandantenfähigen Anwendungen.
- Komplexes Abfrageparameter-Parsing: Wenn Abfrageparameter eine spezifische Validierung oder Transformation erfordern, die über das hinausgeht, was integrierte Extraktoren bieten.
- Body-Vorverarbeitung: Verschlüsseln oder Dekomprimieren von Anforderungstexten, bevor sie den Haupt-Handler erreichen.
- Benutzerkontext: Laden einer
User-Struktur aus einer Datenbank basierend auf einer authentifizierten ID und deren Einspeisung in den Handler.
Bei der Gestaltung benutzerdefinierter Extraktoren sollten Sie diese Best Practices berücksichtigen:
- Klare Fehlerbehandlung: Definieren Sie immer einen spezifischen Fehlertyp für Ihren Extraktor und implementieren Sie das Fehlerkonvertierungs-Trait des Frameworks (
IntoResponsefür Axum,ResponseErrorfür Actix Web). Dies gewährleistet klare und umsetzbare Fehlermeldungen für Clients. - Modularität: Halten Sie Extraktoren auf eine einzige Verantwortung fokussiert. Wenn ein Extraktor zu komplex wird, sollten Sie ihn in kleinere, komponierbare Einheiten zerlegen.
- Leistung: Achten Sie auf leistungskritische Operationen innerhalb Ihres Extraktors, insbesondere wenn diese mit Festplatten-I/O oder Netzwerkanfragen verbunden sind. Cachen Sie Ergebnisse, wo immer dies angebracht ist.
- Testbarkeit: Entwerfen Sie Ihre Extraktoren so, dass sie leicht isoliert getestet werden können. Vermeiden Sie eine zu starke Abhängigkeit von komplexen Framework-Interna, die schwer zu simulieren sind.
- Dokumentation: Dokumentieren Sie klar, was Ihr Extraktor erwartet (z. B. erforderliche Header, Body-Format) und was er liefert.
Verbesserung der Ergonomie in der Webentwicklung
Benutzerdefinierte Extraktoren, die auf dem FromRequest-Trait in Axum und Actix Web aufbauen, sind ein leistungsstarkes Feature, das die Ergonomie, Modularität und Testbarkeit Ihrer Rust-Webanwendungen erheblich verbessert. Durch die Kapselung der Logik zur Extraktion und Validierung von Anforderungsinformationen ermöglichen sie es Handlerfunktionen, sauber, prägnant und rein auf die Geschäftslogik fokussiert zu bleiben, was zu wartbareren und robusteren Codebasen führt.

