Core Concept: Filters

The fundamental building block of warp is the Filter. A Filter is a trait that represents a piece of logic for processing requests. They are designed to be small, focused, and easily composable.

Filters can: - Match parts of a request (like the path or headers). - Extract values from a request. - Reject requests that don't meet their criteria. - Transform or map extracted values. - Be combined to build complex routing logic.

Composition: and() and or()

The power of warp comes from its compositional filter system. You can chain filters together using and() or provide alternatives using or().

and() - Chaining Requirements

The and() combinator is used to chain filters sequentially. A request must pass through all filters in an and() chain to be successful. Values extracted by each filter are combined and passed to the next step.

use warp::Filter;

// This filter requires:
// 1. The path to start with "hello".
// 2. The next path segment to be a String.
// 3. The presence of a 'user-agent' header.
let route = warp::path("hello")
    .and(warp::path::param::<String>())
    .and(warp::header("user-agent"))
    .map(|param: String, agent: String| {
        format!("Hello {}, whose agent is {}", param, agent)
    });

Notice how the values extracted by param() and header() are passed as arguments to map() in the correct order.

or() - Providing Alternatives

The or() combinator provides a fallback mechanism. If the first filter rejects a request, warp will try the second filter. This is how you build different routes for your API.

use warp::Filter;

let get_route = warp::get().and(warp::path("users")).map(|| "Fetching users");
let post_route = warp::post().and(warp::path("users")).map(|| "Creating a user");

// Try the GET route first. If the method is not GET, it will be rejected,
// and the POST route will be tried instead.
let users_api = get_route.or(post_route);

Extracting Values

Many filters extract information from the request. This data is passed along the filter chain. The filter system uses a tuple-based approach to collect these values. warp automatically flattens nested tuples, so you receive them as clean arguments in your handler functions.

For example, warp::path::param::<u32>() extracts a (u32,). When you and() it with warp::path::param::<String>(), which extracts a (String,), the combined filter extracts a (u32, String). This tuple is then flattened into distinct arguments for map(): |id: u32, name: String| ....

Mapping Extracted Values

Once you have a filter that extracts some values, you need to do something with them. warp provides several combinators for this:

  • map(|a, b, ...| ...): Synchronously transforms the extracted values into a Reply. This is for simple, infallible operations.

    let sum = warp::path!("sum" / u32 / u32)
        .map(|a, b| (a + b).to_string());
  • then(|a, b, ...| async move { ... }): Asynchronously and infallibly transforms extracted values. The closure returns a Future whose output becomes the Reply.

    use std::time::Duration;
    use warp::Filter;
    
    async fn sleepy(seconds: u64) -> String {
        tokio::time::sleep(Duration::from_secs(seconds)).await;
        format!("I waited {} seconds!", seconds)
    }
    
    let wait = warp::path!("wait" / u64).then(sleepy);
  • and_then(|a, b, ...| async move { ... }): Asynchronously and fallibly transforms extracted values. The closure returns a Result<impl Reply, impl Reject>. This is useful for async operations that might fail, such as database queries. If it returns an Err, the request is rejected.

    use warp::Filter;
    
    async fn check_id(id: u32) -> Result<String, warp::Rejection> {
        if id == 0 {
            Err(warp::reject::not_found()) // Reject if ID is 0
        } else {
            Ok(format!("ID {} is valid", id))
        }
    }
    let validate_id = warp::path!("user" / u32).and_then(check_id);