Advanced State & Extractors - FromRequestParts & FromRequest (Complete)
1. Welcome and Icebreaker:
If you could automatically extract any info from every request, what would it be? (e.g., user ID, API key, client IP, etc.)
2. Outline the Session:
Agenda:
- Concept Explanation: Deep dive into custom extractors with
FromRequestPartsandFromRequest. - Live Coding: Implement custom extractors for authentication, query params, and JSON body validation.
- Best Practices & Pitfalls: How to design robust extractors.
- Tasks: Practice by building your own extractors.
Learning Outcomes:
- Understand the difference between
FromRequestPartsandFromRequestin Axum. - Create custom extractors for headers, query params, and request bodies.
- Handle errors and rejections gracefully in extractors.
Expectations: Ask questions, try the code, and experiment with your own extractors!
3. Concept Explanation:
Extractors in Axum are types that implement logic to pull data from HTTP requests. They help you keep handler code clean and reusable. There are two main traits:
FromRequestParts: For extracting from request metadata (headers, extensions, URI, etc). Does not consume the body.FromRequest: For extracting from the full request, including the body (e.g., JSON, form data).
When to use which?
- Use
FromRequestPartsfor things like authentication headers, cookies, or path/query params. - Use
FromRequestwhen you need to parse the body (e.g., JSON payloads).
Custom Extractors:
- Encapsulate logic for authentication, validation, or custom parsing.
- Return clear errors using the
Rejectiontype. - Can be composed for complex scenarios.
4. Live Coding Demonstration:
Let’s see three examples: header extraction, query param extraction, and JSON body validation. Add serde = { version = "1", features = ["derive"] } and url = "2" for the examples below, alongside Axum 0.8 and Tokio 1. The body-consuming extractor must be the last handler argument.
a) Custom Header Extractor (Authentication)
use axum::{extract::FromRequestParts, http::{request::Parts, StatusCode}};
struct AuthUser(String);
impl<S: Send + Sync> FromRequestParts<S> for AuthUser {
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let user = parts.headers.get("x-user")
.ok_or((StatusCode::UNAUTHORIZED, "Missing x-user header"))?
.to_str()
.map_err(|_| (StatusCode::BAD_REQUEST, "Invalid x-user header"))?;
Ok(AuthUser(user.to_owned()))
}
}
// Usage in a handler:
async fn handler(AuthUser(user): AuthUser) -> String {
format!("Hello, {}!", user)
}
This is only a demonstration of header parsing: a client can set x-user to any value. Do not treat it as authentication without verifying a signed token or a trusted upstream identity.
b) Required Query Parameter Extractor
use axum::{extract::FromRequestParts, http::{request::Parts, StatusCode}};
struct RequiredQuery(String);
impl<S: Send + Sync> FromRequestParts<S> for RequiredQuery {
type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
let query = parts.uri.query().unwrap_or("");
for (k, v) in url::form_urlencoded::parse(query.as_bytes()) {
if k == "q" {
return Ok(RequiredQuery(v.into_owned()));
}
}
Err((StatusCode::BAD_REQUEST, "Missing 'q' query parameter"))
}
}
// Usage:
async fn search(RequiredQuery(q): RequiredQuery) -> String {
format!("You searched for: {}", q)
}
c) JSON Body Validation with FromRequest
use axum::{extract::{FromRequest, Request}, http::StatusCode, Json};
use serde::Deserialize;
#[derive(Deserialize)]
struct Input {
name: String,
age: u8,
}
struct ValidatedInput(Input);
impl<S: Send + Sync> FromRequest<S> for ValidatedInput {
type Rejection = (StatusCode, String);
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let Json(input) = Json::<Input>::from_request(req, state).await
.map_err(|rejection| (rejection.status(), rejection.body_text()))?;
if input.age < 18 {
Err((StatusCode::UNPROCESSABLE_ENTITY, "Must be at least 18".to_string()))
} else {
Ok(ValidatedInput(input))
}
}
}
// Usage:
async fn register(ValidatedInput(input): ValidatedInput) -> String {
format!("Welcome, {}!", input.name)
}
Best Practices:
- Use custom extractors for reusable, testable logic.
- Always handle missing or malformed data with clear error messages.
- Compose extractors for complex scenarios (e.g., auth + validation).
Common Pitfalls:
- Not handling all error cases (missing headers, invalid body, etc).
- Forgetting to implement the required traits (
FromRequestPartsorFromRequest). - Consuming the body in
FromRequestParts(not allowed).
5. Tasks
- Write a custom extractor for a required query parameter (e.g.,
page). - Implement an extractor that validates a token from headers (e.g.,
Authorization). - Use
FromRequestto parse and validate a JSON body (e.g., check for required fields). - Bonus: Combine multiple extractors in a single handler for advanced validation.