toni
Request Lifecycle

Guards

Guards run before route handlers and decide whether the request is authorized to proceed.

A guard is a struct that implements the Guard trait. It has a single method, can_activate, which returns true to allow the request through or false to block it. Guards run after middleware, after routing, and before interceptors and the route handler.

The Guard trait

pub trait Guard: Send + Sync {
    fn can_activate(&self, context: &Context) -> bool;
}

Context gives you access to the request, route metadata, and other request-scoped information.

A simple authentication guard

use toni::{Guard, Context};

pub struct AuthGuard;

impl Guard for AuthGuard {
    fn can_activate(&self, context: &Context) -> bool {
        let req = context.get_request();
        req.headers.get("authorization")
            .and_then(|h| h.strip_prefix("Bearer "))
            .map(|token| !token.is_empty())
            .unwrap_or(false)
    }
}

Role-based guard with route metadata

Guards can read metadata attached to routes with #[set_metadata]:

use toni::{Guard, Context};

pub struct Role(pub Vec<&'static str>);

pub struct RoleGuard;

impl Guard for RoleGuard {
    fn can_activate(&self, context: &Context) -> bool {
        let required = match context.get_metadata::<Role>() {
            Some(role) => &role.0,
            None => return true,  // no role metadata = public route
        };

        let req = context.get_request();
        let user_role = req.headers.get("x-user-role")
            .map(|s| s.as_str())
            .unwrap_or("");

        required.iter().any(|r| *r == user_role)
    }
}

Attach metadata to routes:

#[controller("/admin", pub struct AdminController { /* ... */ })]
impl AdminController {
    #[get("/dashboard")]
    #[set_metadata(Role(vec!["admin"]))]
    fn dashboard(&self) -> HttpResponse { /* ... */ }

    #[get("/reports")]
    #[set_metadata(Role(vec!["admin", "analyst"]))]
    fn reports(&self) -> HttpResponse { /* ... */ }
}

DI-aware guards

Guards that need services (token validation, database lookups) should be implemented as injectables:

use toni::{injectable, Guard, Context};

#[injectable(pub struct JwtGuard {
    #[inject]
    auth_service: AuthService,
})]
#[guard]
impl JwtGuard {
    pub fn new(auth_service: AuthService) -> Self { Self { auth_service } }
}

impl Guard for JwtGuard {
    fn can_activate(&self, context: &Context) -> bool {
        let req = context.get_request();
        let token = req.headers
            .get("authorization")
            .and_then(|h| h.strip_prefix("Bearer "));

        match token {
            Some(t) => self.auth_service.verify_jwt(t),
            None => false,
        }
    }
}

Register in the module's providers:

#[module(
    controllers: [ProtectedController],
    providers: [JwtGuard, AuthService],
)]
pub struct AppModule;

Applying guards

Global (every route)

let mut factory = ToniFactory::new();
factory.use_global_guards(Arc::new(AuthGuard));
let mut app = factory.create_with(AppModule, AxumAdapter::new()).await;

Controller level

#[controller("/admin", pub struct AdminController { /* ... */ })]
#[use_guards(AuthGuard {}, RoleGuard {})]
impl AdminController {
    // all routes in this controller require auth + role
}

Method level

#[controller("/posts", pub struct PostsController { /* ... */ })]
impl PostsController {
    #[get("")]
    fn find_all(&self) -> HttpResponse { /* public */ }

    #[delete("/:id")]
    #[use_guards(AuthGuard {})]   // only this route requires auth
    fn remove(&self) -> HttpResponse { /* protected */ }
}

When multiple guards are applied, they run in the order listed. If any returns false, the request is blocked and the remaining guards are not called.

What happens when a guard blocks

When can_activate returns false, Toni responds with 403 Forbidden. To return a different status or body, throw an HttpError from within the guard instead:

impl Guard for StrictAuthGuard {
    fn can_activate(&self, context: &Context) -> bool {
        let req = context.get_request();
        if req.headers.get("authorization").is_none() {
            // Panic with an HttpError — the error handler chain picks it up
            // (advanced pattern — usually returning false is sufficient)
        }
        false
    }
}

For custom error responses from guards, pair them with an error handler that intercepts the 403 and shapes it.

On this page