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:

  1. Concept Explanation: Deep dive into custom extractors with FromRequestParts and FromRequest.
  2. Live Coding: Implement custom extractors for authentication, query params, and JSON body validation.
  3. Best Practices & Pitfalls: How to design robust extractors.
  4. Tasks: Practice by building your own extractors.

Learning Outcomes:

  • Understand the difference between FromRequestParts and FromRequest in 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 FromRequestParts for things like authentication headers, cookies, or path/query params.
  • Use FromRequest when 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 Rejection type.
  • 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 (FromRequestParts or FromRequest).
  • 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 FromRequest to parse and validate a JSON body (e.g., check for required fields).
  • Bonus: Combine multiple extractors in a single handler for advanced validation.