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 aReply. 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 aFuturewhose output becomes theReply.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 aResult<impl Reply, impl Reject>. This is useful for async operations that might fail, such as database queries. If it returns anErr, 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);