warp
warp is a super-easy, composable, web server framework for warp speeds. If you are hard-core, you can learn from the documentation of warp, I personally prefer looking at working examples.
This books is mostly sharing the examples that can be found in the source repository of warp with some extra examples an explanation.
The goal of this book
Make it easier to get started using warp and to contribute back examples and documentation to the warp project.
As of this writing in April 2025, the last commit to the warp repository was on Jul 23, 2024 and the last release of warp is v0.3.7
that came out April 5th 2024.
It seem warp is currently not being maintained so for now I won't even attempt to send contributions to the project.
Nevertheless it seems to be in use including in the mdbook project and that is the main motivation for me to learn a bit about warp.
How to use the examples?
In order to try the examples you can clone our fork of the repository.
git clone https://github.com/szabgab/warp/
cd into the created folder:
cd warp
Run the Hello World example:
cargo run --example hello
Examples
Welcome to the examples! These show off warp
's functionality and explain how to use it.
Getting Started
To get started, run examples/hello.rs
with:
> cargo run --example hello
This will start a simple "hello world" service running on your localhost port 3030.
Open another terminal and run:
> curl http://localhost:3030/hi
Hello, World!%
Congratulations, you have just run your first warp service!
You can run other examples with cargo run --example [example name]
:
hello.rs
- Just a basic "Hello World" API- Hello World using functions
- Hello World using modules
- Hello setting content type with_heather
routing.rs
- Builds up a more complex set of routes and shows how to combine filtersbody.rs
- What's a good API without parsing data from the request body?headers.rs
- Parsing data from the request headersrejections.rs
- Your APIs are obviously perfect, but for silly others who call them incorrectly you'll want to define errors for themfutures.rs
- Wait, wait! ... Or how to integrate futures into filterstodos.rs
- Putting this all together with a proper app
Further Use Cases
Serving HTML and Other Files
file.rs
- Serving static filesdir.rs
- Or a whole directory of fileshandlebars_template.rs
- Using Handlebars to fill in an HTML template
Websockets
Hooray! warp
also includes built-in support for WebSockets
websockets.rs
- Basic handling of a WebSocket upgradewebsockets_chat.rs
- Full WebSocket app
Server-Side Events
sse.rs
- Basic Server-Side Eventsse_chat.rs
- Full SSE app
TLS
tls.rs
- can i haz security?
Autoreloading
autoreload.rs
- Change some code and watch the server reload automatically!
Debugging
tracing.rs
- Warp has built-in support for rich diagnostics withtracing
!
Custom HTTP Methods
custom_methods.rs
- It is also possible to use Warp with custom HTTP methods.
Other
Hello World
#![deny(warnings)] use warp::Filter; #[tokio::main] async fn main() { // Match any request and return hello world! let routes = warp::any().map(|| "Hello, World!"); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Run the example:
cargo run --example hello
Using your browser visit http://localhost:3030/
You will see the browser displaying Hello, World!.
In another terminal you can check the site using curl sending a GET
request to the URL:
$ curl http://localhost:3030
Hello, World!
This is a very simple example that has many issues. We'll see some of them and step-by-step we'll improve.
Problems
This example will respond to any
request with the same content.
A GET
request to any other path:
curl http://localhost:3030/hi
A POST
request to any path:
curl -X POST http://localhost:3030/oups
We cannot test this code without launching a server.
I don't have much to say about it. It is what it is. We'll soon have a way to test the code easily.
This code sets the Content-type to text/plain
.
In order to see this change the content of the string to <b>Hello</b>, World!
, that is we would like to return some HTML as well.
Using Ctrl-C stop the server process and run it again.
If you reload the web page at http://localhost:3030/
you will see it display the HTML tag instead of making the work bold.
This happens when the server returns the content with the Content-type
set to text/plain
.
Using the -i
flag of curl
we can see the header that shows the content-type being text/plain
.
$ curl -i http://localhost:3030
HTTP/1.1 200 OK
content-type: text/plain; charset=utf-8
content-length: 20
date: Sat, 12 Apr 2025 07:11:14 GMT
<b>Hello</b>, World!
Hello World using functions
#![deny(warnings)] use std::convert::Infallible; use warp::Filter; #[tokio::main] async fn main() { let routes = setup_routes(); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } pub fn setup_routes() -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { warp::path!().and(warp::get()).and_then(say_hello) } pub async fn say_hello() -> Result<impl warp::Reply, Infallible> { Ok(warp::reply::html("Hello, <b>World</b>!")) } #[cfg(test)] mod test_hello_using_functions { #[tokio::test] async fn test_hello() { use super::setup_routes; use warp::test::request; use warp::http::StatusCode; let routes = setup_routes(); let response = request() .method("GET") .path("/") .reply(&routes) .await; assert_eq!(response.status(), StatusCode::OK); assert_eq!(response.body(), "Hello, <b>World</b>!"); } #[tokio::test] async fn test_other_path() { use super::setup_routes; use warp::test::request; use warp::http::StatusCode; let routes = setup_routes(); let response = request() .method("GET") .path("/hi") .reply(&routes) .await; assert_eq!(response.status(), StatusCode::NOT_FOUND); assert_eq!(response.body(), ""); } #[tokio::test] async fn test_post_method() { use super::setup_routes; use warp::test::request; use warp::http::StatusCode; let routes = setup_routes(); let response = request() .method("POST") .path("/") .reply(&routes) .await; assert_eq!(response.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(response.body(), "HTTP method not allowed"); } }
In this example we have a function called setup_routes
that creates all the mappings between URL pathes and requst methods (such as GET
and POST
) on one hand and functions that will fulfill those requests on the other hand. In this first example we have only one route that deals with the empty path (or /
if you wish) as defined using the warp::path!()
macro and the GET
request method as defined by the warp::get()
function call.
This request is mapped to the arbitrarily named say_hello
function that will return some HTML using the warp::reply::html
function call.
We can run this example with the following command:
cargo run --example hello_using_functions
Then we can visit http://localhost:3030/
and observe that the response is "Hello, World!". The word World being bold and we don't see the HTML tags.
That's because instead of returning a plain string using Ok("Hello, <b>World</b>!")
, the warp::reply::html
function call set the Content-type
to be
text/html
.
We can easily observe this using curl
in another terminal:
$ curl -i http://localhost:3030/
HTTP/1.1 200 OK
content-type: text/html; charset=utf-8
content-length: 20
date: Mon, 14 Apr 2025 15:41:21 GMT
Hello, <b>World</b>!
We can also check other pathes and observe they return a 404 Not Found
with an empty page:
$ curl -i http://localhost:3030/hi
HTTP/1.1 404 Not Found
content-length: 0
date: Mon, 14 Apr 2025 15:42:02 GMT
Similarily using the POST
method will yield a 405 Method Not Allowed
error with some text:
$ curl -i -X POST http://localhost:3030/
HTTP/1.1 405 Method Not Allowed
content-type: text/plain; charset=utf-8
content-length: 23
date: Mon, 14 Apr 2025 15:43:07 GMT
HTTP method not allowed
Testing
The second part of the file is the test. It tests the application through the route-handler (the Filter in warp terms) so it can test everything besides the web-server itself.
There are 3 test-cases. The first one checking the happy path, when the user accesses the main page with a GET
request.
The other two checks the two other requests that return various error status codes. It might be strange at first to test
invalid requests, but even in a real-world application we'd want to make sure that invalid requests show the expected error
message and not some garbage or some internal information. So it is a good practice to have tests for some invalid requests.
We can run the tests by running the following command:
cargo test --example hello_using_functions
```<style>
footer {
text-align: center;
text-wrap: balance;
margin-top: 5rem;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
footer p {
margin: 0;
}
</style>
<footer><p>Copyright © 2025 • Created with ❤️ by the authors of warp and <a href="https://szabgab.com/">Gabor Szabo</a></p>
</footer>
Hello World using modules
#![deny(warnings)] #[tokio::main] async fn main() { let routes = filters::setup_routes(); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } mod filters { use super::handlers; use warp::Filter; pub fn setup_routes() -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { warp::path!().and(warp::get()).and_then(handlers::say_hello) } } mod handlers { use std::convert::Infallible; pub async fn say_hello() -> Result<impl warp::Reply, Infallible> { Ok(warp::reply::html("Hello, <b>World</b>!")) } }
To run this example type in the following:
cargo run --example hello_using_modules
Then we can visit http://localhost:3030/
.
The behaviour is the same as in the Hello using functions case, but this structure of code will help with larger code-bases.
Hello setting content type with_heathe
#![deny(warnings)] use warp::Filter; #[tokio::main] async fn main() { // Match any request and return hello world! let routes = warp::any() .map(|| warp::reply::with_header("<b>Hello</b>, World!", "Content-Type", "text/html")); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
This is just another way to set the content type to text/html
using the with_header
function.
Routing
#![deny(warnings)] use warp::Filter; #[tokio::main] async fn main() { pretty_env_logger::init(); // We'll start simple, and gradually show how you combine these powers // into super powers! // GET / let hello_world = warp::path::end().map(|| "Hello, World at root!"); // GET /hi let hi = warp::path("hi").map(|| "Hello, World!"); // How about multiple segments? First, we could use the `path!` macro: // // GET /hello/from/warp let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| "Hello from warp!"); // Fine, but how do I handle parameters in paths? // // GET /sum/:u32/:u32 let sum = warp::path!("sum" / u32 / u32).map(|a, b| format!("{} + {} = {}", a, b, a + b)); // Any type that implements FromStr can be used, and in any order: // // GET /:u16/times/:u16 let times = warp::path!(u16 / "times" / u16).map(|a, b| format!("{} times {} = {}", a, b, a * b)); // Oh shoot, those math routes should be mounted at a different path, // is that possible? Yep. // // GET /math/sum/:u32/:u32 // GET /math/:u16/times/:u16 let math = warp::path("math"); let _sum = math.and(sum); let _times = math.and(times); // What! And? What's that do? // // It combines the filters in a sort of "this and then that" order. In // fact, it's exactly what the `path!` macro has been doing internally. // // GET /bye/:string let bye = warp::path("bye") .and(warp::path::param()) .map(|name: String| format!("Good bye, {}!", name)); // Ah, can filters do things besides `and`? // // Why, yes they can! They can also `or`! As you might expect, `or` creates // a "this or else that" chain of filters. If the first doesn't succeed, // then it tries the other. // // So, those `math` routes could have been mounted all as one, with `or`. // // GET /math/sum/:u32/:u32 // GET /math/:u16/times/:u16 let math = warp::path("math").and(sum.or(times)); // We can use the end() filter to match a shorter path let help = warp::path("math") // Careful! Omitting the following line would make this filter match // requests to /math/sum/:u32/:u32 and /math/:u16/times/:u16 .and(warp::path::end()) .map(|| "This is the Math API. Try calling /math/sum/:u32/:u32 or /math/:u16/times/:u16"); let math = help.or(math); // Let's let people know that the `sum` and `times` routes are under `math`. let sum = sum.map(|output| format!("(This route has moved to /math/sum/:u16/:u16) {}", output)); let times = times.map(|output| format!("(This route has moved to /math/:u16/times/:u16) {}", output)); // It turns out, using `or` is how you combine everything together into // a single API. (We also actually haven't been enforcing that the // method is GET, so we'll do that too!) // // GET / // GET /hi // GET /hello/from/warp // GET /bye/:string // GET /math/sum/:u32/:u32 // GET /math/:u16/times/:u16 let routes = warp::get().and( hello_world .or(hi) .or(hello_from_warp) .or(bye) .or(math) .or(sum) .or(times), ); // Note that composing filters for many routes may increase compile times (because it uses a lot of generics). // If you wish to use dynamic dispatch instead and speed up compile times while // making it slightly slower at runtime, you can use Filter::boxed(). warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Body
#![deny(warnings)] use serde_derive::{Deserialize, Serialize}; use warp::Filter; #[derive(Deserialize, Serialize)] struct Employee { name: String, rate: u32, } #[tokio::main] async fn main() { pretty_env_logger::init(); // POST /employees/:rate {"name":"Sean","rate":2} let promote = warp::post() .and(warp::path("employees")) .and(warp::path::param::<u32>()) // Only accept bodies smaller than 16kb... .and(warp::body::content_length_limit(1024 * 16)) .and(warp::body::json()) .map(|rate, mut employee: Employee| { employee.rate = rate; warp::reply::json(&employee) }); warp::serve(promote).run(([127, 0, 0, 1], 3030)).await }
Headers
#![deny(warnings)] use std::net::SocketAddr; use warp::Filter; /// Create a server that requires header conditions: /// /// - `Host` is a `SocketAddr` /// - `Accept` is exactly `*/*` /// /// If these conditions don't match, a 404 is returned. #[tokio::main] async fn main() { pretty_env_logger::init(); // For this example, we assume no DNS was used, // so the Host header should be an address. let host = warp::header::<SocketAddr>("host"); // Match when we get `accept: */*` exactly. let accept_stars = warp::header::exact("accept", "*/*"); let routes = host .and(accept_stars) .map(|addr| format!("accepting stars on {}", addr)); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Rejections
#![deny(warnings)] use std::convert::Infallible; use std::error::Error; use std::num::NonZeroU16; use serde_derive::{Deserialize, Serialize}; use warp::http::StatusCode; use warp::{reject, Filter, Rejection, Reply}; /// Rejections represent cases where a filter should not continue processing /// the request, but a different filter *could* process it. #[tokio::main] async fn main() { let math = warp::path!("math" / u16); let div_with_header = math .and(warp::get()) .and(div_by()) .map(|num: u16, denom: NonZeroU16| { warp::reply::json(&Math { op: format!("{} / {}", num, denom), output: num / denom.get(), }) }); let div_with_body = math.and(warp::post()) .and(warp::body::json()) .map(|num: u16, body: DenomRequest| { warp::reply::json(&Math { op: format!("{} / {}", num, body.denom), output: num / body.denom.get(), }) }); let routes = div_with_header.or(div_with_body).recover(handle_rejection); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } /// Extract a denominator from a "div-by" header, or reject with DivideByZero. fn div_by() -> impl Filter<Extract = (NonZeroU16,), Error = Rejection> + Copy { warp::header::<u16>("div-by").and_then(|n: u16| async move { if let Some(denom) = NonZeroU16::new(n) { Ok(denom) } else { Err(reject::custom(DivideByZero)) } }) } #[derive(Deserialize)] struct DenomRequest { pub denom: NonZeroU16, } #[derive(Debug)] struct DivideByZero; impl reject::Reject for DivideByZero {} // JSON replies /// A successful math operation. #[derive(Serialize)] struct Math { op: String, output: u16, } /// An API error serializable to JSON. #[derive(Serialize)] struct ErrorMessage { code: u16, message: String, } // This function receives a `Rejection` and tries to return a custom // value, otherwise simply passes the rejection along. async fn handle_rejection(err: Rejection) -> Result<impl Reply, Infallible> { let code; let message; if err.is_not_found() { code = StatusCode::NOT_FOUND; message = "NOT_FOUND"; } else if let Some(DivideByZero) = err.find() { code = StatusCode::BAD_REQUEST; message = "DIVIDE_BY_ZERO"; } else if let Some(e) = err.find::<warp::filters::body::BodyDeserializeError>() { // This error happens if the body could not be deserialized correctly // We can use the cause to analyze the error and customize the error message message = match e.source() { Some(cause) => { if cause.to_string().contains("denom") { "FIELD_ERROR: denom" } else { "BAD_REQUEST" } } None => "BAD_REQUEST", }; code = StatusCode::BAD_REQUEST; } else if let Some(_) = err.find::<warp::reject::MethodNotAllowed>() { // We can handle a specific error, here METHOD_NOT_ALLOWED, // and render it however we want code = StatusCode::METHOD_NOT_ALLOWED; message = "METHOD_NOT_ALLOWED"; } else { // We should have expected this... Just log and say its a 500 eprintln!("unhandled rejection: {:?}", err); code = StatusCode::INTERNAL_SERVER_ERROR; message = "UNHANDLED_REJECTION"; } let json = warp::reply::json(&ErrorMessage { code: code.as_u16(), message: message.into(), }); Ok(warp::reply::with_status(json, code)) }
Futures
#![deny(warnings)] use std::convert::Infallible; use std::str::FromStr; use std::time::Duration; use warp::Filter; #[tokio::main] async fn main() { // Match `/:Seconds`... let routes = warp::path::param() // and_then create a `Future` that will simply wait N seconds... .and_then(sleepy); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } async fn sleepy(Seconds(seconds): Seconds) -> Result<impl warp::Reply, Infallible> { tokio::time::sleep(Duration::from_secs(seconds)).await; Ok(format!("I waited {} seconds!", seconds)) } /// A newtype to enforce our maximum allowed seconds. struct Seconds(u64); impl FromStr for Seconds { type Err = (); fn from_str(src: &str) -> Result<Self, Self::Err> { src.parse::<u64>().map_err(|_| ()).and_then(|num| { if num <= 5 { Ok(Seconds(num)) } else { Err(()) } }) } }
todos
#![deny(warnings)] use std::env; use warp::Filter; /// Provides a RESTful web server managing some Todos. /// /// API will be: /// /// - `GET /todos`: return a JSON list of Todos. /// - `POST /todos`: create a new Todo. /// - `PUT /todos/:id`: update a specific Todo. /// - `DELETE /todos/:id`: delete a specific Todo. #[tokio::main] async fn main() { if env::var_os("RUST_LOG").is_none() { // Set `RUST_LOG=todos=debug` to see debug logs, // this only shows access logs. env::set_var("RUST_LOG", "todos=info"); } pretty_env_logger::init(); let db = models::blank_db(); let api = filters::todos(db); // View access logs by setting `RUST_LOG=todos`. let routes = api.with(warp::log("todos")); // Start up the server... warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } mod filters { use super::handlers; use super::models::{Db, ListOptions, Todo}; use warp::Filter; /// The 4 TODOs filters combined. pub fn todos( db: Db, ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { todos_list(db.clone()) .or(todos_create(db.clone())) .or(todos_update(db.clone())) .or(todos_delete(db)) } /// GET /todos?offset=3&limit=5 pub fn todos_list( db: Db, ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { warp::path!("todos") .and(warp::get()) .and(warp::query::<ListOptions>()) .and(with_db(db)) .and_then(handlers::list_todos) } /// POST /todos with JSON body pub fn todos_create( db: Db, ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { warp::path!("todos") .and(warp::post()) .and(json_body()) .and(with_db(db)) .and_then(handlers::create_todo) } /// PUT /todos/:id with JSON body pub fn todos_update( db: Db, ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { warp::path!("todos" / u64) .and(warp::put()) .and(json_body()) .and(with_db(db)) .and_then(handlers::update_todo) } /// DELETE /todos/:id pub fn todos_delete( db: Db, ) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone { // We'll make one of our endpoints admin-only to show how authentication filters are used let admin_only = warp::header::exact("authorization", "Bearer admin"); warp::path!("todos" / u64) // It is important to put the auth check _after_ the path filters. // If we put the auth check before, the request `PUT /todos/invalid-string` // would try this filter and reject because the authorization header doesn't match, // rather because the param is wrong for that other path. .and(admin_only) .and(warp::delete()) .and(with_db(db)) .and_then(handlers::delete_todo) } fn with_db(db: Db) -> impl Filter<Extract = (Db,), Error = std::convert::Infallible> + Clone { warp::any().map(move || db.clone()) } fn json_body() -> impl Filter<Extract = (Todo,), Error = warp::Rejection> + Clone { // When accepting a body, we want a JSON body // (and to reject huge payloads)... warp::body::content_length_limit(1024 * 16).and(warp::body::json()) } } /// These are our API handlers, the ends of each filter chain. /// Notice how thanks to using `Filter::and`, we can define a function /// with the exact arguments we'd expect from each filter in the chain. /// No tuples are needed, it's auto flattened for the functions. mod handlers { use super::models::{Db, ListOptions, Todo}; use std::convert::Infallible; use warp::http::StatusCode; pub async fn list_todos(opts: ListOptions, db: Db) -> Result<impl warp::Reply, Infallible> { // Just return a JSON array of todos, applying the limit and offset. let todos = db.lock().await; let todos: Vec<Todo> = todos .clone() .into_iter() .skip(opts.offset.unwrap_or(0)) .take(opts.limit.unwrap_or(std::usize::MAX)) .collect(); Ok(warp::reply::json(&todos)) } pub async fn create_todo(create: Todo, db: Db) -> Result<impl warp::Reply, Infallible> { log::debug!("create_todo: {:?}", create); let mut vec = db.lock().await; for todo in vec.iter() { if todo.id == create.id { log::debug!(" -> id already exists: {}", create.id); // Todo with id already exists, return `400 BadRequest`. return Ok(StatusCode::BAD_REQUEST); } } // No existing Todo with id, so insert and return `201 Created`. vec.push(create); Ok(StatusCode::CREATED) } pub async fn update_todo( id: u64, update: Todo, db: Db, ) -> Result<impl warp::Reply, Infallible> { log::debug!("update_todo: id={}, todo={:?}", id, update); let mut vec = db.lock().await; // Look for the specified Todo... for todo in vec.iter_mut() { if todo.id == id { *todo = update; return Ok(StatusCode::OK); } } log::debug!(" -> todo id not found!"); // If the for loop didn't return OK, then the ID doesn't exist... Ok(StatusCode::NOT_FOUND) } pub async fn delete_todo(id: u64, db: Db) -> Result<impl warp::Reply, Infallible> { log::debug!("delete_todo: id={}", id); let mut vec = db.lock().await; let len = vec.len(); vec.retain(|todo| { // Retain all Todos that aren't this id... // In other words, remove all that *are* this id... todo.id != id }); // If the vec is smaller, we found and deleted a Todo! let deleted = vec.len() != len; if deleted { // respond with a `204 No Content`, which means successful, // yet no body expected... Ok(StatusCode::NO_CONTENT) } else { log::debug!(" -> todo id not found!"); Ok(StatusCode::NOT_FOUND) } } } mod models { use serde_derive::{Deserialize, Serialize}; use std::sync::Arc; use tokio::sync::Mutex; /// So we don't have to tackle how different database work, we'll just use /// a simple in-memory DB, a vector synchronized by a mutex. pub type Db = Arc<Mutex<Vec<Todo>>>; pub fn blank_db() -> Db { Arc::new(Mutex::new(Vec::new())) } #[derive(Debug, Deserialize, Serialize, Clone)] pub struct Todo { pub id: u64, pub text: String, pub completed: bool, } // The query parameters for list_todos. #[derive(Debug, Deserialize)] pub struct ListOptions { pub offset: Option<usize>, pub limit: Option<usize>, } } #[cfg(test)] mod tests { use warp::http::StatusCode; use warp::test::request; use super::{ filters, models::{self, Todo}, }; #[tokio::test] async fn test_post() { let db = models::blank_db(); let api = filters::todos(db); let resp = request() .method("POST") .path("/todos") .json(&todo1()) .reply(&api) .await; assert_eq!(resp.status(), StatusCode::CREATED); } #[tokio::test] async fn test_post_conflict() { let db = models::blank_db(); db.lock().await.push(todo1()); let api = filters::todos(db); let resp = request() .method("POST") .path("/todos") .json(&todo1()) .reply(&api) .await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[tokio::test] async fn test_put_unknown() { let _ = pretty_env_logger::try_init(); let db = models::blank_db(); let api = filters::todos(db); let resp = request() .method("PUT") .path("/todos/1") .header("authorization", "Bearer admin") .json(&todo1()) .reply(&api) .await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } fn todo1() -> Todo { Todo { id: 1, text: "test 1".into(), completed: false, } } }
file
#![deny(warnings)] use warp::Filter; #[tokio::main] async fn main() { pretty_env_logger::init(); let readme = warp::get() .and(warp::path::end()) .and(warp::fs::file("./README.md")); // dir already requires GET... let examples = warp::path("ex").and(warp::fs::dir("./examples/")); // GET / => README.md // GET /ex/... => ./examples/.. let routes = readme.or(examples); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
dir
#![deny(warnings)] #[tokio::main] async fn main() { pretty_env_logger::init(); warp::serve(warp::fs::dir("examples/dir")) .run(([127, 0, 0, 1], 3030)) .await; }
<!DOCTYPE html>
<html>
<head>
<title>dir/index.html</title>
</head>
<body>
<h1>Welcome to Dir</h1>
<a href="/another.html">another page</a>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<title>dir/another.html</title>
</head>
<body>
<h1>Welcome to Another Page</h1>
<a href="/">back</a>
</body>
</html>
Handlebars template
#![deny(warnings)] use std::sync::Arc; use handlebars::Handlebars; use serde::Serialize; use serde_json::json; use warp::Filter; struct WithTemplate<T: Serialize> { name: &'static str, value: T, } fn render<T>(template: WithTemplate<T>, hbs: Arc<Handlebars<'_>>) -> impl warp::Reply where T: Serialize, { let render = hbs .render(template.name, &template.value) .unwrap_or_else(|err| err.to_string()); warp::reply::html(render) } #[tokio::main] async fn main() { let template = "<!DOCTYPE html> <html> <head> <title>Warp Handlebars template example</title> </head> <body> <h1>Hello {{user}}!</h1> </body> </html>"; let mut hb = Handlebars::new(); // register the template hb.register_template_string("template.html", template) .unwrap(); // Turn Handlebars instance into a Filter so we can combine it // easily with others... let hb = Arc::new(hb); // Create a reusable closure to render template let handlebars = move |with_template| render(with_template, hb.clone()); //GET / let route = warp::get() .and(warp::path::end()) .map(|| WithTemplate { name: "template.html", value: json!({"user" : "Warp"}), }) .map(handlebars); warp::serve(route).run(([127, 0, 0, 1], 3030)).await; }
Websockets
#![deny(warnings)] use futures_util::{FutureExt, StreamExt}; use warp::Filter; #[tokio::main] async fn main() { pretty_env_logger::init(); let routes = warp::path("echo") // The `ws()` filter will prepare the Websocket handshake. .and(warp::ws()) .map(|ws: warp::ws::Ws| { // And then our closure will be called when it completes... ws.on_upgrade(|websocket| { // Just echo all messages back... let (tx, rx) = websocket.split(); rx.forward(tx).map(|result| { if let Err(e) = result { eprintln!("websocket error: {:?}", e); } }) }) }); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Websockets chat
// #![deny(warnings)] use std::collections::HashMap; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, }; use futures_util::{SinkExt, StreamExt, TryFutureExt}; use tokio::sync::{mpsc, RwLock}; use tokio_stream::wrappers::UnboundedReceiverStream; use warp::ws::{Message, WebSocket}; use warp::Filter; /// Our global unique user id counter. static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); /// Our state of currently connected users. /// /// - Key is their id /// - Value is a sender of `warp::ws::Message` type Users = Arc<RwLock<HashMap<usize, mpsc::UnboundedSender<Message>>>>; #[tokio::main] async fn main() { pretty_env_logger::init(); // Keep track of all connected users, key is usize, value // is a websocket sender. let users = Users::default(); // Turn our "state" into a new Filter... let users = warp::any().map(move || users.clone()); // GET /chat -> websocket upgrade let chat = warp::path("chat") // The `ws()` filter will prepare Websocket handshake... .and(warp::ws()) .and(users) .map(|ws: warp::ws::Ws, users| { // This will call our function if the handshake succeeds. ws.on_upgrade(move |socket| user_connected(socket, users)) }); // GET / -> index html let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML)); let routes = index.or(chat); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } async fn user_connected(ws: WebSocket, users: Users) { // Use a counter to assign a new unique ID for this user. let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); eprintln!("new chat user: {}", my_id); // Split the socket into a sender and receive of messages. let (mut user_ws_tx, mut user_ws_rx) = ws.split(); // Use an unbounded channel to handle buffering and flushing of messages // to the websocket... let (tx, rx) = mpsc::unbounded_channel(); let mut rx = UnboundedReceiverStream::new(rx); tokio::task::spawn(async move { while let Some(message) = rx.next().await { user_ws_tx .send(message) .unwrap_or_else(|e| { eprintln!("websocket send error: {}", e); }) .await; } }); // Save the sender in our list of connected users. users.write().await.insert(my_id, tx); // Return a `Future` that is basically a state machine managing // this specific user's connection. // Every time the user sends a message, broadcast it to // all other users... while let Some(result) = user_ws_rx.next().await { let msg = match result { Ok(msg) => msg, Err(e) => { eprintln!("websocket error(uid={}): {}", my_id, e); break; } }; user_message(my_id, msg, &users).await; } // user_ws_rx stream will keep processing as long as the user stays // connected. Once they disconnect, then... user_disconnected(my_id, &users).await; } async fn user_message(my_id: usize, msg: Message, users: &Users) { // Skip any non-Text messages... let msg = if let Ok(s) = msg.to_str() { s } else { return; }; let new_msg = format!("<User#{}>: {}", my_id, msg); // New message from this user, send it to everyone else (except same uid)... for (&uid, tx) in users.read().await.iter() { if my_id != uid { if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) { // The tx is disconnected, our `user_disconnected` code // should be happening in another task, nothing more to // do here. } } } } async fn user_disconnected(my_id: usize, users: &Users) { eprintln!("good bye user: {}", my_id); // Stream closed up, so remove from the user list users.write().await.remove(&my_id); } static INDEX_HTML: &str = r#"<!DOCTYPE html> <html lang="en"> <head> <title>Warp Chat</title> </head> <body> <h1>Warp chat</h1> <div id="chat"> <p><em>Connecting...</em></p> </div> <input type="text" id="text" /> <button type="button" id="send">Send</button> <script type="text/javascript"> const chat = document.getElementById('chat'); const text = document.getElementById('text'); const uri = 'ws://' + location.host + '/chat'; const ws = new WebSocket(uri); function message(data) { const line = document.createElement('p'); line.innerText = data; chat.appendChild(line); } ws.onopen = function() { chat.innerHTML = '<p><em>Connected!</em></p>'; }; ws.onmessage = function(msg) { message(msg.data); }; ws.onclose = function() { chat.getElementsByTagName('em')[0].innerText = 'Disconnected!'; }; send.onclick = function() { const msg = text.value; ws.send(msg); text.value = ''; message('<You>: ' + msg); }; </script> </body> </html> "#;
SSE
use futures_util::StreamExt; use std::convert::Infallible; use std::time::Duration; use tokio::time::interval; use tokio_stream::wrappers::IntervalStream; use warp::{sse::Event, Filter}; // create server-sent event fn sse_counter(counter: u64) -> Result<Event, Infallible> { Ok(warp::sse::Event::default().data(counter.to_string())) } #[tokio::main] async fn main() { pretty_env_logger::init(); let routes = warp::path("ticks").and(warp::get()).map(|| { let mut counter: u64 = 0; // create server event source let interval = interval(Duration::from_secs(1)); let stream = IntervalStream::new(interval); let event_stream = stream.map(move |_| { counter += 1; sse_counter(counter) }); // reply using server-sent events warp::sse::reply(event_stream) }); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
SSE Chat
use futures_util::{Stream, StreamExt}; use std::collections::HashMap; use std::sync::{ atomic::{AtomicUsize, Ordering}, Arc, Mutex, }; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use warp::{sse::Event, Filter}; #[tokio::main] async fn main() { pretty_env_logger::init(); // Keep track of all connected users, key is usize, value // is an event stream sender. let users = Arc::new(Mutex::new(HashMap::new())); // Turn our "state" into a new Filter... let users = warp::any().map(move || users.clone()); // POST /chat -> send message let chat_send = warp::path("chat") .and(warp::post()) .and(warp::path::param::<usize>()) .and(warp::body::content_length_limit(500)) .and( warp::body::bytes().and_then(|body: bytes::Bytes| async move { std::str::from_utf8(&body) .map(String::from) .map_err(|_e| warp::reject::custom(NotUtf8)) }), ) .and(users.clone()) .map(|my_id, msg, users| { user_message(my_id, msg, &users); warp::reply() }); // GET /chat -> messages stream let chat_recv = warp::path("chat").and(warp::get()).and(users).map(|users| { // reply using server-sent events let stream = user_connected(users); warp::sse::reply(warp::sse::keep_alive().stream(stream)) }); // GET / -> index html let index = warp::path::end().map(|| { warp::http::Response::builder() .header("content-type", "text/html; charset=utf-8") .body(INDEX_HTML) }); let routes = index.or(chat_recv).or(chat_send); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; } /// Our global unique user id counter. static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); /// Message variants. #[derive(Debug)] enum Message { UserId(usize), Reply(String), } #[derive(Debug)] struct NotUtf8; impl warp::reject::Reject for NotUtf8 {} /// Our state of currently connected users. /// /// - Key is their id /// - Value is a sender of `Message` type Users = Arc<Mutex<HashMap<usize, mpsc::UnboundedSender<Message>>>>; fn user_connected(users: Users) -> impl Stream<Item = Result<Event, warp::Error>> + Send + 'static { // Use a counter to assign a new unique ID for this user. let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); eprintln!("new chat user: {}", my_id); // Use an unbounded channel to handle buffering and flushing of messages // to the event source... let (tx, rx) = mpsc::unbounded_channel(); let rx = UnboundedReceiverStream::new(rx); tx.send(Message::UserId(my_id)) // rx is right above, so this cannot fail .unwrap(); // Save the sender in our list of connected users. users.lock().unwrap().insert(my_id, tx); // Convert messages into Server-Sent Events and return resulting stream. rx.map(|msg| match msg { Message::UserId(my_id) => Ok(Event::default().event("user").data(my_id.to_string())), Message::Reply(reply) => Ok(Event::default().data(reply)), }) } fn user_message(my_id: usize, msg: String, users: &Users) { let new_msg = format!("<User#{}>: {}", my_id, msg); // New message from this user, send it to everyone else (except same uid)... // // We use `retain` instead of a for loop so that we can reap any user that // appears to have disconnected. users.lock().unwrap().retain(|uid, tx| { if my_id == *uid { // don't send to same user, but do retain true } else { // If not `is_ok`, the SSE stream is gone, and so don't retain tx.send(Message::Reply(new_msg.clone())).is_ok() } }); } static INDEX_HTML: &str = r#" <!DOCTYPE html> <html> <head> <title>Warp Chat</title> </head> <body> <h1>warp chat</h1> <div id="chat"> <p><em>Connecting...</em></p> </div> <input type="text" id="text" /> <button type="button" id="send">Send</button> <script type="text/javascript"> var uri = 'http://' + location.host + '/chat'; var sse = new EventSource(uri); function message(data) { var line = document.createElement('p'); line.innerText = data; chat.appendChild(line); } sse.onopen = function() { chat.innerHTML = "<p><em>Connected!</em></p>"; } var user_id; sse.addEventListener("user", function(msg) { user_id = msg.data; }); sse.onmessage = function(msg) { message(msg.data); }; send.onclick = function() { var msg = text.value; var xhr = new XMLHttpRequest(); xhr.open("POST", uri + '/' + user_id, true); xhr.send(msg); text.value = ''; message('<You>: ' + msg); }; </script> </body> </html> "#;
TLS
#![deny(warnings)] // Don't copy this `cfg`, it's only needed because this file is within // the warp repository. // Instead, specify the "tls" feature in your warp dependency declaration. #[cfg(feature = "tls")] #[tokio::main] async fn main() { use warp::Filter; // Match any request and return hello world! let routes = warp::any().map(|| "Hello, World!"); warp::serve(routes) .tls() // RSA .cert_path("examples/tls/cert.pem") .key_path("examples/tls/key.rsa") // ECC // .cert_path("examples/tls/cert.ecc.pem") // .key_path("examples/tls/key.ecc") .run(([127, 0, 0, 1], 3030)) .await; } #[cfg(not(feature = "tls"))] fn main() { eprintln!("Requires the `tls` feature."); }
Autoreload
#![deny(warnings)] use hyper::server::Server; use listenfd::ListenFd; use std::convert::Infallible; use warp::Filter; /// You'll need to install `systemfd` and `cargo-watch`: /// ``` /// cargo install systemfd cargo-watch /// ``` /// And run with: /// ``` /// systemfd --no-pid -s http::3030 -- cargo watch -x 'run --example autoreload' /// ``` #[tokio::main] async fn main() { // Match any request and return hello world! let routes = warp::any().map(|| "Hello, World!"); // hyper let's us build a server from a TcpListener (which will be // useful shortly). Thus, we'll need to convert our `warp::Filter` into // a `hyper::service::MakeService` for use with a `hyper::server::Server`. let svc = warp::service(routes); let make_svc = hyper::service::make_service_fn(|_: _| { // the clone is there because not all warp filters impl Copy let svc = svc.clone(); async move { Ok::<_, Infallible>(svc) } }); let mut listenfd = ListenFd::from_env(); // if listenfd doesn't take a TcpListener (i.e. we're not running via // the command above), we fall back to explicitly binding to a given // host:port. let server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() { Server::from_tcp(l).unwrap() } else { Server::bind(&([127, 0, 0, 1], 3030).into()) }; server.serve(make_svc).await.unwrap(); }
Tracing
//! [`tracing`] is a framework for instrumenting Rust programs to //! collect scoped, structured, and async-aware diagnostics. This example //! demonstrates how the `warp::trace` module can be used to instrument `warp` //! applications with `tracing`. //! //! [`tracing`]: https://crates.io/crates/tracing #![deny(warnings)] use tracing_subscriber::fmt::format::FmtSpan; use warp::Filter; #[tokio::main] async fn main() { // Filter traces based on the RUST_LOG env var, or, if it's not set, // default to show the output of the example. let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=info,warp=debug".to_owned()); // Configure the default `tracing` subscriber. // The `fmt` subscriber from the `tracing-subscriber` crate logs `tracing` // events to stdout. Other subscribers are available for integrating with // distributed tracing systems such as OpenTelemetry. tracing_subscriber::fmt() // Use the filter we built above to determine which traces to record. .with_env_filter(filter) // Record an event when each span closes. This can be used to time our // routes' durations! .with_span_events(FmtSpan::CLOSE) .init(); let hello = warp::path("hello") .and(warp::get()) // When the `hello` route is called, emit a `tracing` event. .map(|| { tracing::info!("saying hello..."); "Hello, World!" }) // Wrap the route in a `tracing` span to add the route's name as context // to any events that occur inside it. .with(warp::trace::named("hello")); let goodbye = warp::path("goodbye") .and(warp::get()) .map(|| { tracing::info!("saying goodbye..."); "So long and thanks for all the fish!" }) // We can also provide our own custom `tracing` spans to wrap a route. .with(warp::trace(|info| { // Construct our own custom span for this route. tracing::info_span!("goodbye", req.path = ?info.path()) })); let routes = hello .or(goodbye) // Wrap all the routes with a filter that creates a `tracing` span for // each request we receive, including data about the request. .with(warp::trace::request()); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Custom HTTP Methods
#![deny(warnings)] use std::net::SocketAddr; use warp::hyper::StatusCode; use warp::{hyper::Method, reject, Filter, Rejection, Reply}; #[derive(Debug)] struct MethodError; impl reject::Reject for MethodError {} const FOO_METHOD: &'static str = "FOO"; const BAR_METHOD: &'static str = "BAR"; fn method(name: &'static str) -> impl Filter<Extract = (), Error = Rejection> + Clone { warp::method() .and_then(move |m: Method| async move { if m == name { Ok(()) } else { Err(reject::custom(MethodError)) } }) .untuple_one() } pub async fn handle_not_found(reject: Rejection) -> Result<impl Reply, Rejection> { if reject.is_not_found() { Ok(StatusCode::NOT_FOUND) } else { Err(reject) } } pub async fn handle_custom(reject: Rejection) -> Result<impl Reply, Rejection> { if reject.find::<MethodError>().is_some() { Ok(StatusCode::METHOD_NOT_ALLOWED) } else { Err(reject) } } #[tokio::main] async fn main() -> Result<(), Box<dyn std::error::Error>> { let address: SocketAddr = "[::]:3030".parse()?; let foo_route = method(FOO_METHOD) .and(warp::path!("foo")) .map(|| "Success") .recover(handle_not_found); let bar_route = method(BAR_METHOD) .and(warp::path!("bar")) .map(|| "Success") .recover(handle_not_found); warp::serve(foo_route.or(bar_route).recover(handle_custom)) .run(address) .await; Ok(()) }
Other
The following examples were not listed in the examples/README.md file.
Compression
#![deny(warnings)] use warp::Filter; #[tokio::main] async fn main() { let file = warp::path("todos").and(warp::fs::file("./examples/todos.rs")); // NOTE: You could double compress something by adding a compression // filter here, a la // ``` // let file = warp::path("todos") // .and(warp::fs::file("./examples/todos.rs")) // .with(warp::compression::brotli()); // ``` // This would result in a browser error, or downloading a file whose contents // are compressed let dir = warp::path("ws_chat").and(warp::fs::file("./examples/websockets_chat.rs")); let file_and_dir = warp::get() .and(file.or(dir)) .with(warp::compression::gzip()); let examples = warp::path("ex") .and(warp::fs::dir("./examples/")) .with(warp::compression::deflate()); // GET /todos => gzip -> toods.rs // GET /ws_chat => gzip -> ws_chat.rs // GET /ex/... => deflate -> ./examples/... let routes = file_and_dir.or(examples); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Dyn Reply
#![deny(warnings)] use warp::{http::StatusCode, Filter}; async fn dyn_reply(word: String) -> Result<Box<dyn warp::Reply>, warp::Rejection> { if &word == "hello" { Ok(Box::new("world")) } else { Ok(Box::new(StatusCode::BAD_REQUEST)) } } #[tokio::main] async fn main() { let routes = warp::path::param().and_then(dyn_reply); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Multipart
use bytes::BufMut; use futures_util::TryStreamExt; use warp::multipart::FormData; use warp::Filter; #[tokio::main] async fn main() { // Running curl -F file=@.gitignore 'localhost:3030/' should print [("file", ".gitignore", "\n/target\n**/*.rs.bk\nCargo.lock\n.idea/\nwarp.iml\n")] let route = warp::multipart::form().and_then(|form: FormData| async move { let field_names: Vec<_> = form .and_then(|mut field| async move { let mut bytes: Vec<u8> = Vec::new(); // field.data() only returns a piece of the content, you should call over it until it replies None while let Some(content) = field.data().await { let content = content.unwrap(); bytes.put(content); } Ok(( field.name().to_string(), field.filename().unwrap().to_string(), String::from_utf8_lossy(&*bytes).to_string(), )) }) .try_collect() .await .unwrap(); Ok::<_, warp::Rejection>(format!("{:?}", field_names)) }); warp::serve(route).run(([127, 0, 0, 1], 3030)).await; }
Query string
use serde_derive::{Deserialize, Serialize}; use std::collections::HashMap; use warp::{ http::{Response, StatusCode}, Filter, }; #[derive(Deserialize, Serialize)] struct MyObject { key1: String, key2: u32, } #[tokio::main] async fn main() { pretty_env_logger::init(); // get /example1?key=value // demonstrates an optional parameter. let example1 = warp::get() .and(warp::path("example1")) .and(warp::query::<HashMap<String, String>>()) .map(|p: HashMap<String, String>| match p.get("key") { Some(key) => Response::builder().body(format!("key = {}", key)), None => Response::builder().body(String::from("No \"key\" param in query.")), }); // get /example2?key1=value&key2=42 // uses the query string to populate a custom object let example2 = warp::get() .and(warp::path("example2")) .and(warp::query::<MyObject>()) .map(|p: MyObject| { Response::builder().body(format!("key1 = {}, key2 = {}", p.key1, p.key2)) }); let opt_query = warp::query::<MyObject>() .map(Some) .or_else(|_| async { Ok::<(Option<MyObject>,), std::convert::Infallible>((None,)) }); // get /example3?key1=value&key2=42 // builds on example2 but adds custom error handling let example3 = warp::get() .and(warp::path("example3")) .and(opt_query) .map(|p: Option<MyObject>| match p { Some(obj) => { Response::builder().body(format!("key1 = {}, key2 = {}", obj.key1, obj.key2)) } None => Response::builder() .status(StatusCode::BAD_REQUEST) .body(String::from("Failed to decode query param.")), }); warp::serve(example1.or(example2).or(example3)) .run(([127, 0, 0, 1], 3030)) .await }
Returning
use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; // Option 1: BoxedFilter // Note that this may be useful for shortening compile times when you are composing many filters. // Boxing the filters will use dynamic dispatch and speed up compilation while // making it slightly slower at runtime. pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { warp::path("assets").and(warp::fs::dir("./assets")).boxed() } // Option 2: impl Filter + Clone pub fn index_filter() -> impl Filter<Extract = (&'static str,), Error = Rejection> + Clone { warp::path::end().map(|| "Index page") } #[tokio::main] async fn main() { let routes = index_filter().or(assets_filter()); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }
Stream
use bytes::Buf; use futures_util::{Stream, StreamExt}; use warp::{reply::Response, Filter, Reply}; #[tokio::main] async fn main() { // Running curl -T /path/to/a/file 'localhost:3030/' should echo back the content of the file, // or an HTTP 413 error if the configured size limit is exceeded. let route = warp::body::content_length_limit(65536) .and(warp::body::stream()) .then(handler); warp::serve(route).run(([127, 0, 0, 1], 3030)).await; } async fn handler( mut body: impl Stream<Item = Result<impl Buf, warp::Error>> + Unpin + Send + Sync, ) -> Response { let mut collected: Vec<u8> = vec![]; while let Some(buf) = body.next().await { let mut buf = buf.unwrap(); while buf.remaining() > 0 { let chunk = buf.chunk(); let chunk_len = chunk.len(); collected.extend_from_slice(chunk); buf.advance(chunk_len); } } println!("Sending {} bytes", collected.len()); collected.into_response() }
Unix socket
#![deny(warnings)] #[cfg(unix)] #[tokio::main] async fn main() { use tokio::net::UnixListener; use tokio_stream::wrappers::UnixListenerStream; pretty_env_logger::init(); let listener = UnixListener::bind("/tmp/warp.sock").unwrap(); let incoming = UnixListenerStream::new(listener); warp::serve(warp::fs::dir("examples/dir")) .run_incoming(incoming) .await; } #[cfg(not(unix))] #[tokio::main] async fn main() { panic!("Must run under Unix-like platform!"); }
Wrapping
#![deny(warnings)] use warp::Filter; fn hello_wrapper<F, T>( filter: F, ) -> impl Filter<Extract = (&'static str,)> + Clone + Send + Sync + 'static where F: Filter<Extract = (T,), Error = std::convert::Infallible> + Clone + Send + Sync + 'static, F::Extract: warp::Reply, { warp::any() .map(|| { println!("before filter"); }) .untuple_one() .and(filter) .map(|_arg| "wrapped hello world") } #[tokio::main] async fn main() { // Match any request and return hello world! let routes = warp::any() .map(|| "hello world") .boxed() .recover(|_err| async { Ok("recovered") }) // wrap the filter with hello_wrapper .with(warp::wrap_fn(hello_wrapper)); warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; }