Handler-Optimierung mit benutzerdefinierten Extraktoren in Axum und Actix Web
Olivia Novak
Dev Intern · Leapcell

Einführung
In der Welt der Webentwicklung mit Rust haben Frameworks wie Axum und Actix Web aufgrund ihrer Leistung, Sicherheit und Prägnanz erhebliches an Bedeutung gewonnen. Wenn Anwendungen komplexer werden, sind die Request-Handler oft mit Boilerplate-Code überladen: Parsen von Headern, Validieren von Query-Parametern oder Deserialisieren von Request-Bodies. Dies kann die Kern-Geschäftslogik verschleiern und Handler schwieriger lesbar, testbar und wartbar machen. Glücklicherweise bieten sowohl Axum als auch Actix Web leistungsstarke Mechanismen, um diese Wiederholungen zu abstrahieren: benutzerdefinierte Request-Extraktoren. Durch deren Nutzung können wir unsere Handler-Logik auf ihre reine Essenz reduzieren, was zu saubereren, besser wartbaren und letztendlich angenehmeren Codebasen führt. Dieser Artikel befasst sich mit dem Warum und Wie der Erstellung benutzerdefinierter Extraktoren und demonstriert deren Nützlichkeit bei der Vereinfachung Ihrer Webanwendungs-Handler.
Verständnis von benutzerdefinierten Extraktoren
Bevor wir uns der Implementierung widmen, lassen Sie uns einige Schlüsselbegriffe im Zusammenhang mit benutzerdefinierten Extraktoren in Web-Frameworks klären.
Request Handler: In Web-Frameworks ist ein Handler eine Funktion, die für die Verarbeitung einer eingehenden HTTP-Anfrage und die Rückgabe einer HTTP-Antwort verantwortlich ist. Hier residiert hauptsächlich die Logik Ihrer Anwendung.
Extractor: Ein Extractor ist ein Mechanismus, der es Ihnen ermöglicht, Daten auf eine strukturierte und wiederverwendbare Weise aus einer eingehenden HTTP-Anfrage zu "extrahieren". Anstatt das Request-Objekt manuell innerhalb jedes Handlers zu inspizieren, kapseln Extraktoren diese Logik und stellen direkt als Handler-Argumente gebrauchsfertige Datentypen bereit. Gängige eingebaute Extraktoren sind Json
, Query
, Path
, HeaderMap
und State
.
Middleware: Obwohl verwandt, agieren Middleware-Funktionen auf einer anderen Ebene. Sie können vor oder nach einem Handler ausgeführt werden, um möglicherweise die Anfrage oder Antwort zu modifizieren oder übergreifende Belange wie Logging oder Authentifizierung durchzuführen. Extraktoren hingegen sind speziell dafür konzipiert, Daten zu parsen und an den Handler zu liefern.
Die Kernidee hinter benutzerdefinierten Extraktoren ist, Entwicklern die Möglichkeit zu geben, ihre eigenen Typen zu definieren, die direkt in Handler-Signaturen injiziert werden können. Dies fördert Modularität, Testbarkeit und reduziert Code-Duplizierung. Wenn ein Handler aufgerufen wird, instanziiert das Framework diese benutzerdefinierten Typen automatisch, indem es die notwendigen Informationen aus der eingehenden Request
extrahiert.
Prinzipien benutzerdefinierter Extraktoren
Axum
In Axum ist ein Extractor jeder Typ, der das FromRequestParts
- oder FromRequest
-Trait implementiert.
FromRequestParts
wird verwendet, wenn Ihr Extractor nur unveränderlichen Zugriff auf die Request-Teile (Header, Methode, URI usw.) benötigt und den Request-Body nicht verbraucht.FromRequest
wird verwendet, wenn Ihr Extractor den Request-Body verbrauchen oder die Anfrage modifizieren muss. Bei der Implementierung vonFromRequest
delegieren Sie typischerweise anFromRequestParts
, wenn Sie nur Teile benötigen, oder interagieren direkt mitrequest.into_body()
.
Beide Traits erfordern einen zugehörigen Error
-Typ, der zurückgegeben wird, wenn die Extraktion fehlschlägt, sowie eine asynchrone Methode from_request_parts
oder from_request
.
Actix Web
In Actix Web wird ein benutzerdefinierter Extractor durch Implementierung des FromRequest
-Traits für Ihren benutzerdefinierten Typ erstellt. Dieses Trait stellt eine asynchrone Methode from_request
bereit, die HttpRequest
und Payload
als Argumente entgegennimmt. Sie geben Result<Self, Self::Error>
zurück, wobei `Self::Error Ihr benutzerdefinierter Fehlertyp ist.
Praktische Anwendung: Authentifizierter Benutzer-Extractor
Lassen Sie uns dies anhand eines gängigen Szenarios veranschaulichen: Extrahieren eines authentifizierten Benutzers aus einer Anfrage, typischerweise aus einem Authorization
-Header, der einen JWT enthält.
Axum-Implementierung
Zuerst nehmen wir an, wir haben eine User
-Struktur und eine einfache JWT-Validierungsfunktion.
// In src/models.rs #[derive(Debug, Clone)] pub struct User { pub id: u32, pub username: String, } // In src/auth.rs (vereinfacht für das Beispiel) pub async fn validate_jwt_and_get_user(token: &str) -> Option<User> { // In einer realen Anwendung würde dies die JWT-Dekodierung, Signaturüberprüfung // und möglicherweise eine Datenbanksuche beinhalten. // Zur Vereinfachung prüfen wir nur, ob es "valid_token" ist if token == "valid_token_abc" { Some(User { id: 1, username: "john_doe".to_string() }) } else { None } }
Jetzt definieren wir unseren benutzerdefinierten AuthUser
-Extractor.
// In src/extractors.rs use axum::{ async_trait, extract::{FromRequestParts, TypedHeader}, headers::{authorization::{Bearer, Authorization}, Header}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, }; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUser(pub User); #[async_trait] impl FromRequestParts for AuthUser { type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, _state: &Self::State) -> Result<Self, Self::Rejection> { let TypedHeader(Authorization(bearer)) = parts .extract::<TypedHeader<Authorization<Bearer>>>() .await .map_err(|_| AuthError::InvalidToken)?; let token = bearer.token(); let user = validate_jwt_and_get_user(token) .await .ok_or(AuthError::InvalidToken)?; Ok(AuthUser(user)) } } pub enum AuthError { InvalidToken, // Fügen Sie hier andere relevante Authentifizierungsfehler hinzu } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, error_message) = match self { AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), // Behandeln Sie hier andere Fehler }; (status, Json(json!({"error": error_message}))).into_response() } }
Und hier ist, wie ein Handler ihn verwenden würde:
// In src/main.rs use axum::{routing::get, Router}; use std::net::SocketAddr; use crate::extractors::AuthUser; use crate::models::User; mod auth; mod extractors; mod models; async fn protected_handler(AuthUser(user): AuthUser) -> String { format!("Welcome, {} (ID: {})!", user.username, user.id) } #[tokio::main] async fn main() { let app = Router::new() .route("/protected", get(protected_handler)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
Wenn Sie eine Anfrage an /protected
mit einem gültigen Authorization: Bearer valid_token_abc
-Header senden, erhalten Sie "Welcome, john_doe (ID: 1)!" zurück. Wenn der Token ungültig oder fehlend ist, gibt Axum automatisch eine 401 Unauthorized
mit der in AuthError::into_response
definierten JSON-Fehlermeldung zurück.
Actix Web-Implementierung
Ähnlich für Actix Web verwenden wir dieselbe User
-Struktur und validate_jwt_and_get_user
-Funktion.
// In src/extractors.rs use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web::Bytes, FromRequest, HttpRequest, }; use futures::future::{ready, Ready}; use serde::Serialize; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUserActix(pub User); // Benutzerdefinierter Fehler für Authentifizierungsfehler #[derive(Debug, Serialize)] pub enum AuthErrorActix { MissingToken, InvalidToken, } impl std::fmt::Display for AuthErrorActix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl ResponseError for AuthErrorActix { fn error_response(&self) -> HttpResponse { let (status, message) = match self { AuthErrorActix::MissingToken => (StatusCode::UNAUTHORIZED, "Authorization token not found"), AuthErrorActix::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), }; HttpResponse::build(status) .json(json!({"error": message})) } } impl FromRequest for AuthUserActix { type Error = AuthErrorActix; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let auth_header = req.headers().get(header::AUTHORIZATION); let user_future = async move { let token = auth_header .ok_or(AuthErrorActix::MissingToken)? .to_str() .map_err(|_| AuthErrorActix::InvalidToken)? // Malformed header value .strip_prefix("Bearer ") .ok_or(AuthErrorActix::InvalidToken)?; // Kein Bearer-Token let user = validate_jwt_and_get_user(token) .await .ok_or(AuthErrorActix::InvalidToken)?; Ok(AuthUserActix(user)) }; // Actix FromRequest ist nicht asynchron, daher konvertieren wir einen asynchronen Block in einen Future // und spoilen ihn sofort (oder verwenden einen Helfer, falls verfügbar, für blockierende Operationen). // Für tatsächliche asynchrone Arbeit innerhalb von FromRequest würden Sie normalerweise eine Aufgabe starten oder `web::block` verwenden. // Zur Vereinfachung *und* zur Übereinstimmung mit dem `Ready`-Rückgabetyp: // Wir behandeln `validate_jwt_and_get_user` als "Logik"-Aufruf, der synchron sein oder ein Future ergeben könnte. // Für die Einfachheit dieses Beispiels ist die Verwendung von `ready` mit einem sofortigen Ergebnis akzeptabel, wenn `validate_jwt_and_get_user` synchron ist oder bereits awaited wurde. // Wenn es ASYNCHRON sein MUSS, würden Sie wahrscheinlich `web::block` oder einen benutzerdefinierten Future verwenden. // Zur Demonstration simulieren wir das Ergebnis mit `ready`. ready(req.headers().get(header::AUTHORIZATION) .ok_or(AuthErrorActix::MissingToken) .and_then(|h_value| { h_value.to_str().map_err(|_| AuthErrorActix::InvalidToken) }) .and_then(|s| { s.strip_prefix("Bearer ").ok_or(AuthErrorActix::InvalidToken) }) .and_then(|token| { // In einem realen Szenario müsste dieses await außerhalb von `ready` erfolgen // oder `web::block` für eine synchrone `FromRequest`-Variante verwendet werden. // Zur Demonstration simulieren wir das Ergebnis. futures::executor::block_on(validate_jwt_and_get_user(token)) .ok_or(AuthErrorActix::InvalidToken) }) .map(AuthUserActix)) } }
Und der Actix Web Handler:
// In src/main.rs use actix_web::{get, App, HttpResponse, HttpServer, Responder}; use crate::extractors::AuthUserActix; mod auth; mod extractors; mod models; #[get("/protected")] async fn protected_handler_actix(user: AuthUserActix) -> impl Responder { format!("Welcome, {} (ID: {})!", user.0.username, user.0.id) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(protected_handler_actix) }) .bind(("127.0.0.1", 8080))? .run() .await }
Wenn Sie eine Anfrage an /protected
mit dem Header Authorization: Bearer valid_token_abc
an Actix Web senden, erhalten Sie "Welcome, john_doe (ID: 1)!" zurück. Ungültige oder fehlende Token führen zu einer 401 Unauthorized
-Antwort mit einer JSON-Fehlermeldung.
Vorteile und Anwendungsfälle
Die obigen Beispiele heben mehrere Hauptvorteile von benutzerdefinierten Extraktoren hervor:
- Sauberere Handler: Die Handler-Signatur empfängt direkt das
User
-Objekt, was den Zweck des Handlers sofort klar macht und den Boilerplate-Code im Funktionskörper reduziert. - Wiederverwendbarkeit von Code: Die
AuthUser
(oderAuthUserActix
)-Logik wird einmal definiert und kann für jeden geschützten Handler in Ihrer Anwendung verwendet werden. - Verbesserte Testbarkeit: Die Extraktionslogik ist innerhalb der Implementierung von
FromRequestParts
/FromRequest
isoliert. Dies erleichtert das Schreiben von Unit-Tests für die Extraktionslogik unabhängig von den Handlern. - Fehlerbehandlung: Benutzerdefinierte Fehler können definiert und automatisch in entsprechende HTTP-Antworten konvertiert werden, wodurch die Fehlerbehandlung für spezifische Belange zentralisiert wird.
- Kapselung: Komplexe Logik (wie JWT-Validierung) wird gekapselt und verhindert deren Durchdringen in Handler-Funktionen.
Über die Authentifizierung hinaus sind benutzerdefinierte Extraktoren äußerst nützlich für:
- Extraktion von Tenant-IDs: Für Multi-Tenant-Anwendungen kann ein Extractor einen
X-Tenant-ID
-Header oder ein Subdomain parsen, um den Tenant-Kontext bereitzustellen. - Berechtigungs-/Rollenprüfungen: Extrahieren von Benutzerrollen und Überprüfen, ob diese die erforderlichen Berechtigungen für einen bestimmten Endpunkt haben, bevor der Handler ausgeführt wird.
- Komplexes Parsen von Query-Parametern: Wenn Sie viele zusammenhängende Abfrageparameter haben, die eine logische Einheit bilden (z. B. Paginierungsfilter
page
,limit
,sort_by
), können Sie einen Extractor erstellen, der dieseQuery
-Parameter verwendet und eine einzelnePaginationParams
-Struktur erstellt. - Sitzungsverwaltung: Abrufen oder Aktualisieren von Sitzungsdaten.
- API-Schlüssel-Validierung: Überprüfung auf einen gültigen API-Schlüssel in einem Header.
Fazit
Benutzerdefinierte Request-Extraktoren verbessern die Entwicklererfahrung in Axum und Actix Web erheblich, indem sie es Ihnen ermöglichen, repetitive, bereichsspezifische Logik aus Ihren Handler-Funktionen auszulagern. Durch die Implementierung der Traits FromRequestParts
/ FromRequest
können Sie leistungsstarke, wiederverwendbare Komponenten erstellen, die rohe Request-Daten in strukturierte, gebrauchsfertige Typen für Ihre Geschäftslogik umwandeln. Dies führt zu Handlern, die fokussiert, lesbar und mühelos wartbar sind und Ihren Webentwicklungsprozess in Rust optimieren. Die Nutzung benutzerdefinierter Extraktoren ist ein entscheidender Schritt zum Aufbau robuster, gut architekturierter Webanwendungen.