toni
Extractors

Custom Extractors

Implement FromRequest to extract any data from incoming requests with full type safety.

When the built-in extractors don't cover your case, implement FromRequest on your own type. The framework will call it automatically whenever your type appears in a handler signature.

The FromRequest trait

pub trait FromRequest: Sized {
    type Error: std::fmt::Display;
    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error>;
}

If from_request returns Err, the handler is not called and the error flows to the error handler chain.

Example: Bearer token extractor

use toni::extractors::FromRequest;
use toni::HttpRequest;

pub struct BearerToken(pub String);

impl FromRequest for BearerToken {
    type Error = String;

    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
        req.headers
            .get("authorization")
            .ok_or_else(|| "Missing Authorization header".to_string())
            .and_then(|header| {
                header
                    .strip_prefix("Bearer ")
                    .ok_or_else(|| "Authorization header must start with 'Bearer '".to_string())
                    .map(|token| BearerToken(token.to_string()))
            })
    }
}

// Usage
#[get("/me")]
fn get_me(&self, BearerToken(token): BearerToken) -> HttpResponse {
    // token is guaranteed to be present and stripped of "Bearer " prefix
}

Example: Pagination extractor

Encapsulate common query param patterns:

use serde::Deserialize;
use toni::extractors::{FromRequest, Query};
use toni::HttpRequest;

pub struct Pagination {
    pub page: u32,
    pub per_page: u32,
    pub offset: u32,
}

impl FromRequest for Pagination {
    type Error = String;

    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
        #[derive(Deserialize, Default)]
        struct RawPagination {
            page: Option<u32>,
            per_page: Option<u32>,
        }

        let Query(raw) = Query::<RawPagination>::from_request(req)
            .map_err(|e| e.to_string())?;

        let page = raw.page.unwrap_or(1).max(1);
        let per_page = raw.per_page.unwrap_or(20).min(100);
        let offset = (page - 1) * per_page;

        Ok(Pagination { page, per_page, offset })
    }
}

// Clean handler signatures
#[get("")]
fn list(&self, pagination: Pagination) -> HttpResponse {
    let items = self.service.find_page(pagination.offset, pagination.per_page);
    HttpResponse::ok().json(serde_json::json!({
        "items": items,
        "page": pagination.page,
        "per_page": pagination.per_page,
    })).build()
}

Example: Request context extractor

Extract data set by middleware (user session, request ID, etc.):

use toni::extractors::FromRequest;
use toni::HttpRequest;

pub struct RequestId(pub String);

impl FromRequest for RequestId {
    type Error = String;

    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
        // X-Request-ID is set by middleware upstream
        let id = req.headers
            .get("x-request-id")
            .cloned()
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
        Ok(RequestId(id))
    }
}

Combining extractors

Custom extractors can compose built-in ones:

pub struct AuthenticatedRequest {
    pub user_id: u32,
    pub body: serde_json::Value,
}

impl FromRequest for AuthenticatedRequest {
    type Error = String;

    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
        let user_id: u32 = req.headers
            .get("x-user-id")
            .ok_or("Unauthenticated")?
            .parse()
            .map_err(|_| "Invalid user ID")?;

        let body = req.body_as_json::<serde_json::Value>()
            .map_err(|e| e.to_string())?;

        Ok(AuthenticatedRequest { user_id, body })
    }
}

Validation in custom extractors

Custom extractors can run validation inline. For validator crate integration, pair with Validated<T> by implementing ValidatableExtractor:

use toni::extractors::{FromRequest, ValidatableExtractor};

pub struct TypedBody<T>(pub T);

impl<T: serde::de::DeserializeOwned> FromRequest for TypedBody<T> {
    type Error = String;
    fn from_request(req: &HttpRequest) -> Result<Self, Self::Error> {
        let body = req.body_as_json::<T>().map_err(|e| e.to_string())?;
        Ok(TypedBody(body))
    }
}

impl<T: validator::Validate + serde::de::DeserializeOwned> ValidatableExtractor for TypedBody<T> {
    fn get_inner(&self) -> &dyn validator::Validate {
        &self.0
    }
}

// Now Validated<TypedBody<MyDto>> works

On this page