Volga
Home
Agent Skill
API Docs
GitHub
  • English
  • Русский
Home
Agent Skill
API Docs
GitHub
  • English
  • Русский
  • Home
  • Getting Started

    • Quick Start
    • Handlers
    • Route Parameters
    • Query Parameters
    • Route Groups
  • Requests & Responses

    • Headers
    • Handling JSON
    • Handling Form Data
    • Working with Files
    • Multipart Responses
    • Raw Request Body
    • Cookies
    • Validating Requests
  • Middleware & Infrastructure

    • Basic Middleware
    • Custom Middleware
    • Parameterized Middleware
    • Response Compression
    • Request Decompression
    • CORS (Cross-Origin Resource Sharing)
    • Static Files
    • Rate Limiting
    • Configuration Files
    • OpenAPI
  • Security & Access

    • Authentication and Authorization
    • OAuth 2.1 & OpenID Connect
    • OAuth 2.1 Client
    • Machine-to-Machine Grants
    • DPoP — Sender-Constrained Tokens
  • Reliability & Observability

    • Global Error Handling
    • Tracing & Logging
    • Request cancellation
    • Graceful Shutdown
  • Protocols & Realtime

    • HTTP Versions and Methods
    • HTTPS
    • WebSockets
    • Server-Sent Events (SSE)
  • Advanced Patterns

    • Dependency Injection
    • Custom Handling of HEAD, OPTIONS, and TRACE Methods
  • Agent Skill

Basic Middleware

Volga provides several tools to help you build middleware pipelines, whether for individual routes, route groups, or the entire application.

Enabling Middleware

If you're not using the full feature set, make sure to explicitly enable the middleware feature in your Cargo.toml:

[dependencies]
volga = { version = "...", features = ["middleware"] }

Filters

The filter() method allows you to define conditional logic (such as validation or access control) for a specific route or a group of routes.

use volga::{App, Path};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Allow only positive numbers
    app.group("/positive", |g| {
        g.filter(|Path((x, y)): Path<(i32, i32)>| async move { x >= 0 && y >= 0 });
        g.map_get("/sum/{x}/{y}", sum);
    });
    
    // Allow only negative numbers
    app.map_get("/negative/sum/{x}/{y}", sum)
        .filter(|Path((x, y)): Path<(i32, i32)>| async move { x < 0 && y < 0 });

    app.run().await
}

async fn sum(x: i32, y: i32) -> i32 {
    x + y
}

What a Filter Returns

A filter's verdict is a bool, (), a FilterResult or a Result<(), E>. A request it stops goes to the error handler, with the status of what the filter returned:

The filter returnsAnswer
false, or FilterResult::err()400 Bad Request with the message Request rejected by filter
Err of anything with a status — Error, StatusCode, (StatusCode, E), std::io::Error, ValidationError, OAuthError, Problem, a type of your own implementing IntoErrorthat status, as the same Err would get from a handler
Err of a string or a Box<dyn std::error::Error + Send + Sync>400 Bad Request with that message

A filter refuses a request for any reason — a missing API key as often as a bad parameter — so the message for false names none. Before 0.14.0 it said Validation: One or more request parameters are incorrect, and since a global filter also decides requests no route matches, a request for a path that does not exist was told the same. A filter that wants a status or a message of its own returns Err.

The last row is the one difference from a handler, where a string is a 500: an error without a status of its own, returned from a filter, is taken as the reason the request is refused. Since 0.12.0, an error with a status keeps it — an OAuthError answers 401 for invalid_token and 403 for insufficient_scope, an Error keeps its instance and attached response.

use volga::{App, headers::HttpHeaders, http::StatusCode};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // 401 Unauthorized without the header
    app.filter(|headers: HttpHeaders| match headers.get_raw("x-api-key") {
        Some(_) => Ok(()),
        None => Err((StatusCode::UNAUTHORIZED, "missing API key")),
    });

    app.map_get("/sum/{x}/{y}", |x: i32, y: i32| x + y);

    app.run().await
}

FilterResult::with_error() takes the same types and answers as Err with that error does.

A std::error::Error is not enough

A filter's Err must be Error or implement IntoError. A type that only implements std::error::Error — ParseIntError, an error from another crate — does not compile there; give it a status with .map_err(|err| (StatusCode::BAD_REQUEST, err)), or wrap it into an error type of your own that implements IntoError.

Handling Incoming Requests

The tap_req() method gives you access to the HttpRequestMut, allowing you to inspect or mutate request before request processing.

use volga::{App, HttpRequestMut, headers};

headers! {
    (CustomHeader, "x-custom-header")
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app
        .map_get("/sum/{x}/{y}", |x: i32, y: i32| async move { x + y })
        .tap_req(|mut req: HttpRequestMut| async move { 
            req.try_insert_header::<CustomHeader>("Custom Value")?;
            Ok(req)
        });

    app.run().await
}

Handling Successful Responses

Use the map_ok() method to transform or augment successful HTTP responses.

use volga::{App, HttpResponse, HttpResult, headers};

headers! {
    (CustomHeader, "x-custom-header")
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    app.group("/positive", |g| {
        g.map_ok(group_response);
        g.map_get("/sum/{x}/{y}", sum);
    });

    app.map_get("/negative/sum/{x}/{y}", sum)
        .map_ok(route_response);

    app.run().await
}

async fn group_response(mut resp: HttpResponse) -> HttpResult {
    resp.try_insert_header::<CustomHeader>("for-group")?;
    Ok(resp)
}

async fn route_response(mut resp: HttpResponse) -> HttpResult {
    resp.try_insert_header::<CustomHeader>("for-route")?;
    Ok(resp)
}

async fn sum(x: i32, y: i32) -> i32 {
    x + y
}

Handling Errors

The map_err() method lets you define custom error handlers - globally, per route, or for route groups.

use volga::{App, error::{Error, ProblemDetails}};
use std::io::Error as IoError;

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();
    
    app
        .map_get("/error", produce_err)
        .map_err(handle_err);

    app.run().await
}

async fn handle_err(error: Error) -> ProblemDetails {
    ProblemDetails::from(error)
}

async fn produce_err() -> IoError {
    IoError::other("some error")
}

Since 0.12.0 whatever Err the handler returns reaches this map_err as an Error — Err(StatusCode::NOT_FOUND) and an Err of your own error type included.

Tips

By attaching map_err() to the App, you configure a global error handler. You can read more about advanced error handling here.

Synchronous Middleware

Since v0.11.0 filter(), map_ok(), map_err() and tap_req() also accept a plain fn or a closure that returns its verdict, response or request directly, when there is nothing to await — on App, a route and a route group alike, just as handlers do:

use volga::{App, HttpResponse, HttpResult, Path, headers, headers::HttpHeaders};

headers! {
    (CustomHeader, "x-custom-header")
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let mut app = App::new();

    // Reject requests without an API key
    app.filter(|headers: HttpHeaders| headers.get_raw("x-api-key").is_some());

    app.map_get("/positive/sum/{x}/{y}", |x: i32, y: i32| x + y)
        .filter(|Path((x, y)): Path<(i32, i32)>| x >= 0 && y >= 0)
        .map_ok(tag_response);

    app.run().await
}

fn tag_response(mut resp: HttpResponse) -> HttpResult {
    resp.try_insert_header::<CustomHeader>("sync")?;
    Ok(resp)
}

wrap(), with() and attach() stay asynchronous, since what they exist for is awaiting next.

Examples

  • Request filter example
  • Response handler example
Last Updated: 10/20/26, 3:26 PM
Next
Custom Middleware