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 aFuture
whose 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);