├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── examples ├── README.md ├── autoreload.rs ├── body.rs ├── compression.rs ├── custom_methods.rs ├── dir.rs ├── dir │ ├── another.html │ └── index.html ├── dyn_reply.rs ├── file.rs ├── futures.rs ├── handlebars_template.rs ├── headers.rs ├── hello.rs ├── multipart.rs ├── query_string.rs ├── rejections.rs ├── returning.rs ├── routing.rs ├── sse.rs ├── sse_chat.rs ├── stream.rs ├── tls.rs ├── tls │ ├── cert.ecc.pem │ ├── cert.pem │ ├── key.ecc │ └── key.rsa ├── todos.rs ├── tracing.rs ├── unix_socket.rs ├── websockets.rs ├── websockets_chat.rs └── wrapping.rs ├── src ├── error.rs ├── filter │ ├── and.rs │ ├── and_then.rs │ ├── boxed.rs │ ├── map.rs │ ├── map_err.rs │ ├── mod.rs │ ├── or.rs │ ├── or_else.rs │ ├── recover.rs │ ├── service.rs │ ├── then.rs │ ├── unify.rs │ ├── untuple_one.rs │ └── wrap.rs ├── filters │ ├── addr.rs │ ├── any.rs │ ├── body.rs │ ├── compression.rs │ ├── cookie.rs │ ├── cors.rs │ ├── ext.rs │ ├── fs.rs │ ├── header.rs │ ├── host.rs │ ├── log.rs │ ├── method.rs │ ├── mod.rs │ ├── multipart.rs │ ├── path.rs │ ├── query.rs │ ├── reply.rs │ ├── sse.rs │ ├── trace.rs │ └── ws.rs ├── generic.rs ├── lib.rs ├── redirect.rs ├── reject.rs ├── reply.rs ├── route.rs ├── server.rs ├── service.rs ├── test.rs ├── tls.rs └── transport.rs └── tests ├── addr.rs ├── body.rs ├── cookie.rs ├── cors.rs ├── ext.rs ├── filter.rs ├── fs.rs ├── header.rs ├── host.rs ├── method.rs ├── multipart.rs ├── path.rs ├── query.rs ├── redirect.rs ├── reply_with.rs ├── tracing.rs └── ws.rs /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [seanmonstar] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Version** 11 | List the versions of all `warp` crates you are using. The easiest way to get 12 | this information is using `cargo-tree`. 13 | 14 | `cargo install cargo-tree` 15 | (see install here: https://github.com/sfackler/cargo-tree) 16 | 17 | Then: 18 | 19 | `cargo tree | grep warp` 20 | 21 | **Platform** 22 | The output of `uname -a` (UNIX), or version and 32 or 64-bit (Windows) 23 | 24 | **Description** 25 | Enter your issue details here. 26 | One way to structure the description: 27 | 28 | [short summary of the bug] 29 | 30 | I tried this code: 31 | 32 | [code sample that causes the bug] 33 | 34 | I expected to see this happen: [explanation] 35 | 36 | Instead, this happened: [explanation] 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: https://discord.gg/RFsPjyt 5 | about: 'Please post your question on the #warp discord channel. You may 6 | also be able to find help at https://users.rust-lang.org/.' 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | env: 9 | RUST_BACKTRACE: 1 10 | 11 | jobs: 12 | ci-pass: 13 | name: CI is green 14 | runs-on: ubuntu-latest 15 | needs: 16 | - style 17 | - test 18 | - doc 19 | steps: 20 | - run: exit 0 21 | 22 | style: 23 | name: Check Style 24 | runs-on: ubuntu-latest 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Install rust 30 | uses: dtolnay/rust-toolchain@stable 31 | with: 32 | components: rustfmt 33 | 34 | - run: cargo fmt --all --check 35 | 36 | test: 37 | name: Test 38 | needs: [style] 39 | runs-on: ubuntu-latest 40 | 41 | strategy: 42 | matrix: 43 | build: [stable, beta, nightly, tls, no-default-features, compression] 44 | 45 | include: 46 | - build: beta 47 | rust: beta 48 | - build: nightly 49 | rust: nightly 50 | benches: true 51 | - build: tls 52 | features: "--features tls" 53 | - build: no-default-features 54 | features: "--no-default-features" 55 | - build: compression 56 | features: "--features compression" 57 | 58 | steps: 59 | - name: Checkout 60 | uses: actions/checkout@v3 61 | 62 | - name: Install rust 63 | uses: dtolnay/rust-toolchain@master 64 | with: 65 | toolchain: ${{ matrix.rust || 'stable' }} 66 | 67 | - name: Test 68 | run: cargo test ${{ matrix.features }} 69 | 70 | - name: Test all benches 71 | if: matrix.benches 72 | run: cargo test --benches ${{ matrix.features }} 73 | 74 | doc: 75 | name: Build docs 76 | needs: [style, test] 77 | runs-on: ubuntu-latest 78 | steps: 79 | - name: Checkout 80 | uses: actions/checkout@v3 81 | 82 | - name: Install Rust 83 | uses: dtolnay/rust-toolchain@nightly 84 | 85 | - name: cargo doc 86 | run: cargo rustdoc -- -D broken_intra_doc_links 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target 3 | **/*.rs.bk 4 | Cargo.lock 5 | .idea/ 6 | warp.iml 7 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warp" 3 | version = "0.3.7" 4 | description = "serve the web at warp speeds" 5 | authors = ["Sean McArthur "] 6 | license = "MIT" 7 | readme = "README.md" 8 | documentation = "https://docs.rs/warp" 9 | repository = "https://github.com/seanmonstar/warp" 10 | categories = ["web-programming::http-server"] 11 | keywords = ["warp", "server", "http", "hyper"] 12 | autotests = true 13 | autoexamples = true 14 | edition = "2018" 15 | 16 | [package.metadata.docs.rs] 17 | all-features = true 18 | 19 | [dependencies] 20 | async-compression = { version = "0.4.5", features = ["tokio"], optional = true } 21 | bytes = "1.0" 22 | futures-util = { version = "0.3", default-features = false, features = ["sink"] } 23 | futures-channel = { version = "0.3.17", features = ["sink"]} 24 | headers = "0.3.5" 25 | http = "0.2" 26 | hyper = { version = "0.14", features = ["stream", "server", "http1", "http2", "tcp", "client"] } 27 | log = "0.4" 28 | mime = "0.3" 29 | mime_guess = "2.0.0" 30 | multer = { version = "2.1.0", optional = true } 31 | scoped-tls = "1.0" 32 | serde = "1.0" 33 | serde_json = "1.0" 34 | serde_urlencoded = "0.7.1" 35 | tokio = { version = "1.0", features = ["fs", "sync", "time"] } 36 | tokio-util = { version = "0.7.1", features = ["io"] } 37 | tracing = { version = "0.1.21", default-features = false, features = ["log", "std"] } 38 | tower-service = "0.3" 39 | tokio-tungstenite = { version = "0.21", optional = true } 40 | percent-encoding = "2.1" 41 | pin-project = "1.0" 42 | tokio-rustls = { version = "0.26", default-features = false, features = ["logging", "tls12", "ring"], optional = true } 43 | rustls-pemfile = { version = "2.0", optional = true } 44 | 45 | [dev-dependencies] 46 | pretty_env_logger = "0.5" 47 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 48 | tracing-log = "0.2" 49 | serde_derive = "1.0" 50 | handlebars = "6.0" 51 | tokio = { version = "1.0", features = ["macros", "rt-multi-thread"] } 52 | tokio-stream = { version = "0.1.1", features = ["net"] } 53 | listenfd = "1.0" 54 | 55 | [features] 56 | default = ["multipart", "websocket"] 57 | multipart = ["multer"] 58 | websocket = ["tokio-tungstenite"] 59 | tls = ["tokio-rustls", "rustls-pemfile"] 60 | 61 | # Enable compression-related filters 62 | compression = ["compression-brotli", "compression-gzip"] 63 | compression-brotli = ["async-compression/brotli"] 64 | compression-gzip = ["async-compression/deflate", "async-compression/gzip"] 65 | 66 | [profile.release] 67 | codegen-units = 1 68 | incremental = false 69 | 70 | [profile.bench] 71 | codegen-units = 1 72 | incremental = false 73 | 74 | [[test]] 75 | name = "multipart" 76 | required-features = ["multipart"] 77 | 78 | [[test]] 79 | name = "ws" 80 | required-features = ["websocket"] 81 | 82 | [[example]] 83 | name = "compression" 84 | required-features = ["compression"] 85 | 86 | [[example]] 87 | name = "unix_socket" 88 | 89 | [[example]] 90 | name = "websockets" 91 | required-features = ["websocket"] 92 | 93 | [[example]] 94 | name = "websockets_chat" 95 | required-features = ["websocket"] 96 | 97 | [[example]] 98 | name = "query_string" 99 | 100 | 101 | [[example]] 102 | name = "multipart" 103 | required-features = ["multipart"] 104 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018-2020 Sean McArthur 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # warp 2 | 3 | [![crates.io](https://img.shields.io/crates/v/warp.svg)](https://crates.io/crates/warp) 4 | [![Released API docs](https://docs.rs/warp/badge.svg)](https://docs.rs/warp) 5 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 6 | [![GHA Build Status](https://github.com/seanmonstar/warp/workflows/CI/badge.svg)](https://github.com/seanmonstar/warp/actions?query=workflow%3ACI) 7 | [![Discord chat][discord-badge]][discord-url] 8 | 9 | A super-easy, composable, web server framework for warp speeds. 10 | 11 | The fundamental building block of `warp` is the `Filter`: they can be combined 12 | and composed to express rich requirements on requests. 13 | 14 | Thanks to its `Filter` system, warp provides these out of the box: 15 | 16 | * Path routing and parameter extraction 17 | * Header requirements and extraction 18 | * Query string deserialization 19 | * JSON and Form bodies 20 | * Multipart form data 21 | * Static Files and Directories 22 | * Websockets 23 | * Access logging 24 | * Gzip, Deflate, and Brotli compression 25 | 26 | Since it builds on top of [hyper](https://hyper.rs), you automatically get: 27 | 28 | - HTTP/1 29 | - HTTP/2 30 | - Asynchronous 31 | - One of the fastest HTTP implementations 32 | - Tested and **correct** 33 | 34 | ## Example 35 | 36 | Add warp and Tokio to your dependencies: 37 | 38 | ```toml 39 | tokio = { version = "1", features = ["full"] } 40 | warp = "0.3" 41 | ``` 42 | 43 | And then get started in your `main.rs`: 44 | 45 | ```rust 46 | use warp::Filter; 47 | 48 | #[tokio::main] 49 | async fn main() { 50 | // GET /hello/warp => 200 OK with body "Hello, warp!" 51 | let hello = warp::path!("hello" / String) 52 | .map(|name| format!("Hello, {}!", name)); 53 | 54 | warp::serve(hello) 55 | .run(([127, 0, 0, 1], 3030)) 56 | .await; 57 | } 58 | ``` 59 | 60 | For more information you can check the [docs](https://docs.rs/warp) or the [examples](https://github.com/seanmonstar/warp/tree/master/examples). 61 | 62 | [discord-badge]: https://img.shields.io/discord/500028886025895936.svg?logo=discord 63 | [discord-url]: https://discord.gg/RFsPjyt 64 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Welcome to the examples! These show off `warp`'s functionality and explain how to use it. 4 | 5 | ## Getting Started 6 | 7 | To get started, run `examples/hello.rs` with: 8 | 9 | ```bash 10 | > cargo run --example hello 11 | ``` 12 | 13 | This will start a simple "hello world" service running on your localhost port 3030. 14 | 15 | Open another terminal and run: 16 | 17 | ```bash 18 | > curl http://localhost:3030/hi 19 | Hello, World!% 20 | ``` 21 | 22 | Congratulations, you have just run your first warp service! 23 | 24 | You can run other examples with `cargo run --example [example name]`: 25 | 26 | - [`hello.rs`](./hello.rs) - Just a basic "Hello World" API 27 | - [`routing.rs`](./routing.rs) - Builds up a more complex set of routes and shows how to combine filters 28 | - [`body.rs`](./body.rs) - What's a good API without parsing data from the request body? 29 | - [`headers.rs`](./headers.rs) - Parsing data from the request headers 30 | - [`rejections.rs`](./rejections.rs) - Your APIs are obviously perfect, but for silly others who call them incorrectly you'll want to define errors for them 31 | - [`futures.rs`](./futures.rs) - Wait, wait! ... Or how to integrate futures into filters 32 | - [`todos.rs`](./todos.rs) - Putting this all together with a proper app 33 | 34 | ## Further Use Cases 35 | 36 | ### Serving HTML and Other Files 37 | 38 | - [`file.rs`](./file.rs) - Serving static files 39 | - [`dir.rs`](./dir.rs) - Or a whole directory of files 40 | - [`handlebars_template.rs`](./handlebars_template.rs) - Using Handlebars to fill in an HTML template 41 | 42 | ### Websockets 43 | 44 | Hooray! `warp` also includes built-in support for WebSockets 45 | 46 | - [`websockets.rs`](./websockets.rs) - Basic handling of a WebSocket upgrade 47 | - [`websockets_chat.rs`](./websockets_chat.rs) - Full WebSocket app 48 | 49 | ### Server-Side Events 50 | 51 | - [`sse.rs`](./sse.rs) - Basic Server-Side Event 52 | - [`sse_chat.rs`](./sse_chat.rs) - Full SSE app 53 | 54 | ### TLS 55 | 56 | - [`tls.rs`](./tls.rs) - can i haz security? 57 | 58 | ### Autoreloading 59 | 60 | - [`autoreload.rs`](./autoreload.rs) - Change some code and watch the server reload automatically! 61 | 62 | ### Debugging 63 | 64 | - [`tracing.rs`](./tracing.rs) - Warp has built-in support for rich diagnostics with [`tracing`](https://docs.rs/tracing)! 65 | 66 | ## Custom HTTP Methods 67 | 68 | - [`custom_methods.rs`](./custom_methods.rs) - It is also possible to use Warp with custom HTTP methods. 69 | -------------------------------------------------------------------------------- /examples/autoreload.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use hyper::server::Server; 3 | use listenfd::ListenFd; 4 | use std::convert::Infallible; 5 | use warp::Filter; 6 | 7 | /// You'll need to install `systemfd` and `cargo-watch`: 8 | /// ``` 9 | /// cargo install systemfd cargo-watch 10 | /// ``` 11 | /// And run with: 12 | /// ``` 13 | /// systemfd --no-pid -s http::3030 -- cargo watch -x 'run --example autoreload' 14 | /// ``` 15 | #[tokio::main] 16 | async fn main() { 17 | // Match any request and return hello world! 18 | let routes = warp::any().map(|| "Hello, World!"); 19 | 20 | // hyper let's us build a server from a TcpListener (which will be 21 | // useful shortly). Thus, we'll need to convert our `warp::Filter` into 22 | // a `hyper::service::MakeService` for use with a `hyper::server::Server`. 23 | let svc = warp::service(routes); 24 | 25 | let make_svc = hyper::service::make_service_fn(|_: _| { 26 | // the clone is there because not all warp filters impl Copy 27 | let svc = svc.clone(); 28 | async move { Ok::<_, Infallible>(svc) } 29 | }); 30 | 31 | let mut listenfd = ListenFd::from_env(); 32 | // if listenfd doesn't take a TcpListener (i.e. we're not running via 33 | // the command above), we fall back to explicitly binding to a given 34 | // host:port. 35 | let server = if let Some(l) = listenfd.take_tcp_listener(0).unwrap() { 36 | Server::from_tcp(l).unwrap() 37 | } else { 38 | Server::bind(&([127, 0, 0, 1], 3030).into()) 39 | }; 40 | 41 | server.serve(make_svc).await.unwrap(); 42 | } 43 | -------------------------------------------------------------------------------- /examples/body.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use serde_derive::{Deserialize, Serialize}; 4 | 5 | use warp::Filter; 6 | 7 | #[derive(Deserialize, Serialize)] 8 | struct Employee { 9 | name: String, 10 | rate: u32, 11 | } 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | pretty_env_logger::init(); 16 | 17 | // POST /employees/:rate {"name":"Sean","rate":2} 18 | let promote = warp::post() 19 | .and(warp::path("employees")) 20 | .and(warp::path::param::()) 21 | // Only accept bodies smaller than 16kb... 22 | .and(warp::body::content_length_limit(1024 * 16)) 23 | .and(warp::body::json()) 24 | .map(|rate, mut employee: Employee| { 25 | employee.rate = rate; 26 | warp::reply::json(&employee) 27 | }); 28 | 29 | warp::serve(promote).run(([127, 0, 0, 1], 3030)).await 30 | } 31 | -------------------------------------------------------------------------------- /examples/compression.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use warp::Filter; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | let file = warp::path("todos").and(warp::fs::file("./examples/todos.rs")); 8 | // NOTE: You could double compress something by adding a compression 9 | // filter here, a la 10 | // ``` 11 | // let file = warp::path("todos") 12 | // .and(warp::fs::file("./examples/todos.rs")) 13 | // .with(warp::compression::brotli()); 14 | // ``` 15 | // This would result in a browser error, or downloading a file whose contents 16 | // are compressed 17 | 18 | let dir = warp::path("ws_chat").and(warp::fs::file("./examples/websockets_chat.rs")); 19 | 20 | let file_and_dir = warp::get() 21 | .and(file.or(dir)) 22 | .with(warp::compression::gzip()); 23 | 24 | let examples = warp::path("ex") 25 | .and(warp::fs::dir("./examples/")) 26 | .with(warp::compression::deflate()); 27 | 28 | // GET /todos => gzip -> toods.rs 29 | // GET /ws_chat => gzip -> ws_chat.rs 30 | // GET /ex/... => deflate -> ./examples/... 31 | let routes = file_and_dir.or(examples); 32 | 33 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 34 | } 35 | -------------------------------------------------------------------------------- /examples/custom_methods.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::net::SocketAddr; 3 | 4 | use warp::hyper::StatusCode; 5 | use warp::{hyper::Method, reject, Filter, Rejection, Reply}; 6 | 7 | #[derive(Debug)] 8 | struct MethodError; 9 | impl reject::Reject for MethodError {} 10 | 11 | const FOO_METHOD: &'static str = "FOO"; 12 | const BAR_METHOD: &'static str = "BAR"; 13 | 14 | fn method(name: &'static str) -> impl Filter + Clone { 15 | warp::method() 16 | .and_then(move |m: Method| async move { 17 | if m == name { 18 | Ok(()) 19 | } else { 20 | Err(reject::custom(MethodError)) 21 | } 22 | }) 23 | .untuple_one() 24 | } 25 | 26 | pub async fn handle_not_found(reject: Rejection) -> Result { 27 | if reject.is_not_found() { 28 | Ok(StatusCode::NOT_FOUND) 29 | } else { 30 | Err(reject) 31 | } 32 | } 33 | 34 | pub async fn handle_custom(reject: Rejection) -> Result { 35 | if reject.find::().is_some() { 36 | Ok(StatusCode::METHOD_NOT_ALLOWED) 37 | } else { 38 | Err(reject) 39 | } 40 | } 41 | 42 | #[tokio::main] 43 | async fn main() -> Result<(), Box> { 44 | let address: SocketAddr = "[::]:3030".parse()?; 45 | 46 | let foo_route = method(FOO_METHOD) 47 | .and(warp::path!("foo")) 48 | .map(|| "Success") 49 | .recover(handle_not_found); 50 | 51 | let bar_route = method(BAR_METHOD) 52 | .and(warp::path!("bar")) 53 | .map(|| "Success") 54 | .recover(handle_not_found); 55 | 56 | warp::serve(foo_route.or(bar_route).recover(handle_custom)) 57 | .run(address) 58 | .await; 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /examples/dir.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | #[tokio::main] 4 | async fn main() { 5 | pretty_env_logger::init(); 6 | 7 | warp::serve(warp::fs::dir("examples/dir")) 8 | .run(([127, 0, 0, 1], 3030)) 9 | .await; 10 | } 11 | -------------------------------------------------------------------------------- /examples/dir/another.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dir/another.html 5 | 6 | 7 |

Welcome to Another Page

8 | back 9 | 10 | -------------------------------------------------------------------------------- /examples/dir/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dir/index.html 5 | 6 | 7 |

Welcome to Dir

8 | another page 9 | 10 | -------------------------------------------------------------------------------- /examples/dyn_reply.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::{http::StatusCode, Filter}; 3 | 4 | async fn dyn_reply(word: String) -> Result, warp::Rejection> { 5 | if &word == "hello" { 6 | Ok(Box::new("world")) 7 | } else { 8 | Ok(Box::new(StatusCode::BAD_REQUEST)) 9 | } 10 | } 11 | 12 | #[tokio::main] 13 | async fn main() { 14 | let routes = warp::path::param().and_then(dyn_reply); 15 | 16 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 17 | } 18 | -------------------------------------------------------------------------------- /examples/file.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use warp::Filter; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | pretty_env_logger::init(); 8 | 9 | let readme = warp::get() 10 | .and(warp::path::end()) 11 | .and(warp::fs::file("./README.md")); 12 | 13 | // dir already requires GET... 14 | let examples = warp::path("ex").and(warp::fs::dir("./examples/")); 15 | 16 | // GET / => README.md 17 | // GET /ex/... => ./examples/.. 18 | let routes = readme.or(examples); 19 | 20 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 21 | } 22 | -------------------------------------------------------------------------------- /examples/futures.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use std::convert::Infallible; 4 | use std::str::FromStr; 5 | use std::time::Duration; 6 | use warp::Filter; 7 | 8 | #[tokio::main] 9 | async fn main() { 10 | // Match `/:Seconds`... 11 | let routes = warp::path::param() 12 | // and_then create a `Future` that will simply wait N seconds... 13 | .and_then(sleepy); 14 | 15 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 16 | } 17 | 18 | async fn sleepy(Seconds(seconds): Seconds) -> Result { 19 | tokio::time::sleep(Duration::from_secs(seconds)).await; 20 | Ok(format!("I waited {} seconds!", seconds)) 21 | } 22 | 23 | /// A newtype to enforce our maximum allowed seconds. 24 | struct Seconds(u64); 25 | 26 | impl FromStr for Seconds { 27 | type Err = (); 28 | fn from_str(src: &str) -> Result { 29 | src.parse::().map_err(|_| ()).and_then(|num| { 30 | if num <= 5 { 31 | Ok(Seconds(num)) 32 | } else { 33 | Err(()) 34 | } 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /examples/handlebars_template.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::sync::Arc; 3 | 4 | use handlebars::Handlebars; 5 | use serde::Serialize; 6 | use serde_json::json; 7 | use warp::Filter; 8 | 9 | struct WithTemplate { 10 | name: &'static str, 11 | value: T, 12 | } 13 | 14 | fn render(template: WithTemplate, hbs: Arc>) -> impl warp::Reply 15 | where 16 | T: Serialize, 17 | { 18 | let render = hbs 19 | .render(template.name, &template.value) 20 | .unwrap_or_else(|err| err.to_string()); 21 | warp::reply::html(render) 22 | } 23 | 24 | #[tokio::main] 25 | async fn main() { 26 | let template = " 27 | 28 | 29 | Warp Handlebars template example 30 | 31 | 32 |

Hello {{user}}!

33 | 34 | "; 35 | 36 | let mut hb = Handlebars::new(); 37 | // register the template 38 | hb.register_template_string("template.html", template) 39 | .unwrap(); 40 | 41 | // Turn Handlebars instance into a Filter so we can combine it 42 | // easily with others... 43 | let hb = Arc::new(hb); 44 | 45 | // Create a reusable closure to render template 46 | let handlebars = move |with_template| render(with_template, hb.clone()); 47 | 48 | //GET / 49 | let route = warp::get() 50 | .and(warp::path::end()) 51 | .map(|| WithTemplate { 52 | name: "template.html", 53 | value: json!({"user" : "Warp"}), 54 | }) 55 | .map(handlebars); 56 | 57 | warp::serve(route).run(([127, 0, 0, 1], 3030)).await; 58 | } 59 | -------------------------------------------------------------------------------- /examples/headers.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::net::SocketAddr; 3 | use warp::Filter; 4 | 5 | /// Create a server that requires header conditions: 6 | /// 7 | /// - `Host` is a `SocketAddr` 8 | /// - `Accept` is exactly `*/*` 9 | /// 10 | /// If these conditions don't match, a 404 is returned. 11 | #[tokio::main] 12 | async fn main() { 13 | pretty_env_logger::init(); 14 | 15 | // For this example, we assume no DNS was used, 16 | // so the Host header should be an address. 17 | let host = warp::header::("host"); 18 | 19 | // Match when we get `accept: */*` exactly. 20 | let accept_stars = warp::header::exact("accept", "*/*"); 21 | 22 | let routes = host 23 | .and(accept_stars) 24 | .map(|addr| format!("accepting stars on {}", addr)); 25 | 26 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 27 | } 28 | -------------------------------------------------------------------------------- /examples/hello.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::Filter; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | // Match any request and return hello world! 7 | let routes = warp::any().map(|| "Hello, World!"); 8 | 9 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 10 | } 11 | -------------------------------------------------------------------------------- /examples/multipart.rs: -------------------------------------------------------------------------------- 1 | use bytes::BufMut; 2 | use futures_util::TryStreamExt; 3 | use warp::multipart::FormData; 4 | use warp::Filter; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | // Running curl -F file=@.gitignore 'localhost:3030/' should print [("file", ".gitignore", "\n/target\n**/*.rs.bk\nCargo.lock\n.idea/\nwarp.iml\n")] 9 | let route = warp::multipart::form().and_then(|form: FormData| async move { 10 | let field_names: Vec<_> = form 11 | .and_then(|mut field| async move { 12 | let mut bytes: Vec = Vec::new(); 13 | 14 | // field.data() only returns a piece of the content, you should call over it until it replies None 15 | while let Some(content) = field.data().await { 16 | let content = content.unwrap(); 17 | bytes.put(content); 18 | } 19 | Ok(( 20 | field.name().to_string(), 21 | field.filename().unwrap().to_string(), 22 | String::from_utf8_lossy(&*bytes).to_string(), 23 | )) 24 | }) 25 | .try_collect() 26 | .await 27 | .unwrap(); 28 | 29 | Ok::<_, warp::Rejection>(format!("{:?}", field_names)) 30 | }); 31 | warp::serve(route).run(([127, 0, 0, 1], 3030)).await; 32 | } 33 | -------------------------------------------------------------------------------- /examples/query_string.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use warp::{ 4 | http::{Response, StatusCode}, 5 | Filter, 6 | }; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | struct MyObject { 10 | key1: String, 11 | key2: u32, 12 | } 13 | 14 | #[tokio::main] 15 | async fn main() { 16 | pretty_env_logger::init(); 17 | 18 | // get /example1?key=value 19 | // demonstrates an optional parameter. 20 | let example1 = warp::get() 21 | .and(warp::path("example1")) 22 | .and(warp::query::>()) 23 | .map(|p: HashMap| match p.get("key") { 24 | Some(key) => Response::builder().body(format!("key = {}", key)), 25 | None => Response::builder().body(String::from("No \"key\" param in query.")), 26 | }); 27 | 28 | // get /example2?key1=value&key2=42 29 | // uses the query string to populate a custom object 30 | let example2 = warp::get() 31 | .and(warp::path("example2")) 32 | .and(warp::query::()) 33 | .map(|p: MyObject| { 34 | Response::builder().body(format!("key1 = {}, key2 = {}", p.key1, p.key2)) 35 | }); 36 | 37 | let opt_query = warp::query::() 38 | .map(Some) 39 | .or_else(|_| async { Ok::<(Option,), std::convert::Infallible>((None,)) }); 40 | 41 | // get /example3?key1=value&key2=42 42 | // builds on example2 but adds custom error handling 43 | let example3 = 44 | warp::get() 45 | .and(warp::path("example3")) 46 | .and(opt_query) 47 | .map(|p: Option| match p { 48 | Some(obj) => { 49 | Response::builder().body(format!("key1 = {}, key2 = {}", obj.key1, obj.key2)) 50 | } 51 | None => Response::builder() 52 | .status(StatusCode::BAD_REQUEST) 53 | .body(String::from("Failed to decode query param.")), 54 | }); 55 | 56 | warp::serve(example1.or(example2).or(example3)) 57 | .run(([127, 0, 0, 1], 3030)) 58 | .await 59 | } 60 | -------------------------------------------------------------------------------- /examples/rejections.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use std::convert::Infallible; 4 | use std::error::Error; 5 | use std::num::NonZeroU16; 6 | 7 | use serde_derive::{Deserialize, Serialize}; 8 | use warp::http::StatusCode; 9 | use warp::{reject, Filter, Rejection, Reply}; 10 | 11 | /// Rejections represent cases where a filter should not continue processing 12 | /// the request, but a different filter *could* process it. 13 | #[tokio::main] 14 | async fn main() { 15 | let math = warp::path!("math" / u16); 16 | let div_with_header = math 17 | .and(warp::get()) 18 | .and(div_by()) 19 | .map(|num: u16, denom: NonZeroU16| { 20 | warp::reply::json(&Math { 21 | op: format!("{} / {}", num, denom), 22 | output: num / denom.get(), 23 | }) 24 | }); 25 | 26 | let div_with_body = 27 | math.and(warp::post()) 28 | .and(warp::body::json()) 29 | .map(|num: u16, body: DenomRequest| { 30 | warp::reply::json(&Math { 31 | op: format!("{} / {}", num, body.denom), 32 | output: num / body.denom.get(), 33 | }) 34 | }); 35 | 36 | let routes = div_with_header.or(div_with_body).recover(handle_rejection); 37 | 38 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 39 | } 40 | 41 | /// Extract a denominator from a "div-by" header, or reject with DivideByZero. 42 | fn div_by() -> impl Filter + Copy { 43 | warp::header::("div-by").and_then(|n: u16| async move { 44 | if let Some(denom) = NonZeroU16::new(n) { 45 | Ok(denom) 46 | } else { 47 | Err(reject::custom(DivideByZero)) 48 | } 49 | }) 50 | } 51 | 52 | #[derive(Deserialize)] 53 | struct DenomRequest { 54 | pub denom: NonZeroU16, 55 | } 56 | 57 | #[derive(Debug)] 58 | struct DivideByZero; 59 | 60 | impl reject::Reject for DivideByZero {} 61 | 62 | // JSON replies 63 | 64 | /// A successful math operation. 65 | #[derive(Serialize)] 66 | struct Math { 67 | op: String, 68 | output: u16, 69 | } 70 | 71 | /// An API error serializable to JSON. 72 | #[derive(Serialize)] 73 | struct ErrorMessage { 74 | code: u16, 75 | message: String, 76 | } 77 | 78 | // This function receives a `Rejection` and tries to return a custom 79 | // value, otherwise simply passes the rejection along. 80 | async fn handle_rejection(err: Rejection) -> Result { 81 | let code; 82 | let message; 83 | 84 | if err.is_not_found() { 85 | code = StatusCode::NOT_FOUND; 86 | message = "NOT_FOUND"; 87 | } else if let Some(DivideByZero) = err.find() { 88 | code = StatusCode::BAD_REQUEST; 89 | message = "DIVIDE_BY_ZERO"; 90 | } else if let Some(e) = err.find::() { 91 | // This error happens if the body could not be deserialized correctly 92 | // We can use the cause to analyze the error and customize the error message 93 | message = match e.source() { 94 | Some(cause) => { 95 | if cause.to_string().contains("denom") { 96 | "FIELD_ERROR: denom" 97 | } else { 98 | "BAD_REQUEST" 99 | } 100 | } 101 | None => "BAD_REQUEST", 102 | }; 103 | code = StatusCode::BAD_REQUEST; 104 | } else if let Some(_) = err.find::() { 105 | // We can handle a specific error, here METHOD_NOT_ALLOWED, 106 | // and render it however we want 107 | code = StatusCode::METHOD_NOT_ALLOWED; 108 | message = "METHOD_NOT_ALLOWED"; 109 | } else { 110 | // We should have expected this... Just log and say its a 500 111 | eprintln!("unhandled rejection: {:?}", err); 112 | code = StatusCode::INTERNAL_SERVER_ERROR; 113 | message = "UNHANDLED_REJECTION"; 114 | } 115 | 116 | let json = warp::reply::json(&ErrorMessage { 117 | code: code.as_u16(), 118 | message: message.into(), 119 | }); 120 | 121 | Ok(warp::reply::with_status(json, code)) 122 | } 123 | -------------------------------------------------------------------------------- /examples/returning.rs: -------------------------------------------------------------------------------- 1 | use warp::{filters::BoxedFilter, Filter, Rejection, Reply}; 2 | 3 | // Option 1: BoxedFilter 4 | // Note that this may be useful for shortening compile times when you are composing many filters. 5 | // Boxing the filters will use dynamic dispatch and speed up compilation while 6 | // making it slightly slower at runtime. 7 | pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { 8 | warp::path("assets").and(warp::fs::dir("./assets")).boxed() 9 | } 10 | 11 | // Option 2: impl Filter + Clone 12 | pub fn index_filter() -> impl Filter + Clone { 13 | warp::path::end().map(|| "Index page") 14 | } 15 | 16 | #[tokio::main] 17 | async fn main() { 18 | let routes = index_filter().or(assets_filter()); 19 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 20 | } 21 | -------------------------------------------------------------------------------- /examples/routing.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use warp::Filter; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | pretty_env_logger::init(); 8 | 9 | // We'll start simple, and gradually show how you combine these powers 10 | // into super powers! 11 | 12 | // GET / 13 | let hello_world = warp::path::end().map(|| "Hello, World at root!"); 14 | 15 | // GET /hi 16 | let hi = warp::path("hi").map(|| "Hello, World!"); 17 | 18 | // How about multiple segments? First, we could use the `path!` macro: 19 | // 20 | // GET /hello/from/warp 21 | let hello_from_warp = warp::path!("hello" / "from" / "warp").map(|| "Hello from warp!"); 22 | 23 | // Fine, but how do I handle parameters in paths? 24 | // 25 | // GET /sum/:u32/:u32 26 | let sum = warp::path!("sum" / u32 / u32).map(|a, b| format!("{} + {} = {}", a, b, a + b)); 27 | 28 | // Any type that implements FromStr can be used, and in any order: 29 | // 30 | // GET /:u16/times/:u16 31 | let times = 32 | warp::path!(u16 / "times" / u16).map(|a, b| format!("{} times {} = {}", a, b, a * b)); 33 | 34 | // Oh shoot, those math routes should be mounted at a different path, 35 | // is that possible? Yep. 36 | // 37 | // GET /math/sum/:u32/:u32 38 | // GET /math/:u16/times/:u16 39 | let math = warp::path("math"); 40 | let _sum = math.and(sum); 41 | let _times = math.and(times); 42 | 43 | // What! And? What's that do? 44 | // 45 | // It combines the filters in a sort of "this and then that" order. In 46 | // fact, it's exactly what the `path!` macro has been doing internally. 47 | // 48 | // GET /bye/:string 49 | let bye = warp::path("bye") 50 | .and(warp::path::param()) 51 | .map(|name: String| format!("Good bye, {}!", name)); 52 | 53 | // Ah, can filters do things besides `and`? 54 | // 55 | // Why, yes they can! They can also `or`! As you might expect, `or` creates 56 | // a "this or else that" chain of filters. If the first doesn't succeed, 57 | // then it tries the other. 58 | // 59 | // So, those `math` routes could have been mounted all as one, with `or`. 60 | // 61 | // GET /math/sum/:u32/:u32 62 | // GET /math/:u16/times/:u16 63 | let math = warp::path("math").and(sum.or(times)); 64 | 65 | // We can use the end() filter to match a shorter path 66 | let help = warp::path("math") 67 | // Careful! Omitting the following line would make this filter match 68 | // requests to /math/sum/:u32/:u32 and /math/:u16/times/:u16 69 | .and(warp::path::end()) 70 | .map(|| "This is the Math API. Try calling /math/sum/:u32/:u32 or /math/:u16/times/:u16"); 71 | let math = help.or(math); 72 | 73 | // Let's let people know that the `sum` and `times` routes are under `math`. 74 | let sum = sum.map(|output| format!("(This route has moved to /math/sum/:u16/:u16) {}", output)); 75 | let times = 76 | times.map(|output| format!("(This route has moved to /math/:u16/times/:u16) {}", output)); 77 | 78 | // It turns out, using `or` is how you combine everything together into 79 | // a single API. (We also actually haven't been enforcing that the 80 | // method is GET, so we'll do that too!) 81 | // 82 | // GET / 83 | // GET /hi 84 | // GET /hello/from/warp 85 | // GET /bye/:string 86 | // GET /math/sum/:u32/:u32 87 | // GET /math/:u16/times/:u16 88 | 89 | let routes = warp::get().and( 90 | hello_world 91 | .or(hi) 92 | .or(hello_from_warp) 93 | .or(bye) 94 | .or(math) 95 | .or(sum) 96 | .or(times), 97 | ); 98 | 99 | // Note that composing filters for many routes may increase compile times (because it uses a lot of generics). 100 | // If you wish to use dynamic dispatch instead and speed up compile times while 101 | // making it slightly slower at runtime, you can use Filter::boxed(). 102 | 103 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 104 | } 105 | -------------------------------------------------------------------------------- /examples/sse.rs: -------------------------------------------------------------------------------- 1 | use futures_util::StreamExt; 2 | use std::convert::Infallible; 3 | use std::time::Duration; 4 | use tokio::time::interval; 5 | use tokio_stream::wrappers::IntervalStream; 6 | use warp::{sse::Event, Filter}; 7 | 8 | // create server-sent event 9 | fn sse_counter(counter: u64) -> Result { 10 | Ok(warp::sse::Event::default().data(counter.to_string())) 11 | } 12 | 13 | #[tokio::main] 14 | async fn main() { 15 | pretty_env_logger::init(); 16 | 17 | let routes = warp::path("ticks").and(warp::get()).map(|| { 18 | let mut counter: u64 = 0; 19 | // create server event source 20 | let interval = interval(Duration::from_secs(1)); 21 | let stream = IntervalStream::new(interval); 22 | let event_stream = stream.map(move |_| { 23 | counter += 1; 24 | sse_counter(counter) 25 | }); 26 | // reply using server-sent events 27 | warp::sse::reply(event_stream) 28 | }); 29 | 30 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 31 | } 32 | -------------------------------------------------------------------------------- /examples/sse_chat.rs: -------------------------------------------------------------------------------- 1 | use futures_util::{Stream, StreamExt}; 2 | use std::collections::HashMap; 3 | use std::sync::{ 4 | atomic::{AtomicUsize, Ordering}, 5 | Arc, Mutex, 6 | }; 7 | use tokio::sync::mpsc; 8 | use tokio_stream::wrappers::UnboundedReceiverStream; 9 | use warp::{sse::Event, Filter}; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | pretty_env_logger::init(); 14 | 15 | // Keep track of all connected users, key is usize, value 16 | // is an event stream sender. 17 | let users = Arc::new(Mutex::new(HashMap::new())); 18 | // Turn our "state" into a new Filter... 19 | let users = warp::any().map(move || users.clone()); 20 | 21 | // POST /chat -> send message 22 | let chat_send = warp::path("chat") 23 | .and(warp::post()) 24 | .and(warp::path::param::()) 25 | .and(warp::body::content_length_limit(500)) 26 | .and( 27 | warp::body::bytes().and_then(|body: bytes::Bytes| async move { 28 | std::str::from_utf8(&body) 29 | .map(String::from) 30 | .map_err(|_e| warp::reject::custom(NotUtf8)) 31 | }), 32 | ) 33 | .and(users.clone()) 34 | .map(|my_id, msg, users| { 35 | user_message(my_id, msg, &users); 36 | warp::reply() 37 | }); 38 | 39 | // GET /chat -> messages stream 40 | let chat_recv = warp::path("chat").and(warp::get()).and(users).map(|users| { 41 | // reply using server-sent events 42 | let stream = user_connected(users); 43 | warp::sse::reply(warp::sse::keep_alive().stream(stream)) 44 | }); 45 | 46 | // GET / -> index html 47 | let index = warp::path::end().map(|| { 48 | warp::http::Response::builder() 49 | .header("content-type", "text/html; charset=utf-8") 50 | .body(INDEX_HTML) 51 | }); 52 | 53 | let routes = index.or(chat_recv).or(chat_send); 54 | 55 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 56 | } 57 | 58 | /// Our global unique user id counter. 59 | static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); 60 | 61 | /// Message variants. 62 | #[derive(Debug)] 63 | enum Message { 64 | UserId(usize), 65 | Reply(String), 66 | } 67 | 68 | #[derive(Debug)] 69 | struct NotUtf8; 70 | impl warp::reject::Reject for NotUtf8 {} 71 | 72 | /// Our state of currently connected users. 73 | /// 74 | /// - Key is their id 75 | /// - Value is a sender of `Message` 76 | type Users = Arc>>>; 77 | 78 | fn user_connected(users: Users) -> impl Stream> + Send + 'static { 79 | // Use a counter to assign a new unique ID for this user. 80 | let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); 81 | 82 | eprintln!("new chat user: {}", my_id); 83 | 84 | // Use an unbounded channel to handle buffering and flushing of messages 85 | // to the event source... 86 | let (tx, rx) = mpsc::unbounded_channel(); 87 | let rx = UnboundedReceiverStream::new(rx); 88 | 89 | tx.send(Message::UserId(my_id)) 90 | // rx is right above, so this cannot fail 91 | .unwrap(); 92 | 93 | // Save the sender in our list of connected users. 94 | users.lock().unwrap().insert(my_id, tx); 95 | 96 | // Convert messages into Server-Sent Events and return resulting stream. 97 | rx.map(|msg| match msg { 98 | Message::UserId(my_id) => Ok(Event::default().event("user").data(my_id.to_string())), 99 | Message::Reply(reply) => Ok(Event::default().data(reply)), 100 | }) 101 | } 102 | 103 | fn user_message(my_id: usize, msg: String, users: &Users) { 104 | let new_msg = format!(": {}", my_id, msg); 105 | 106 | // New message from this user, send it to everyone else (except same uid)... 107 | // 108 | // We use `retain` instead of a for loop so that we can reap any user that 109 | // appears to have disconnected. 110 | users.lock().unwrap().retain(|uid, tx| { 111 | if my_id == *uid { 112 | // don't send to same user, but do retain 113 | true 114 | } else { 115 | // If not `is_ok`, the SSE stream is gone, and so don't retain 116 | tx.send(Message::Reply(new_msg.clone())).is_ok() 117 | } 118 | }); 119 | } 120 | 121 | static INDEX_HTML: &str = r#" 122 | 123 | 124 | 125 | Warp Chat 126 | 127 | 128 |

warp chat

129 |
130 |

Connecting...

131 |
132 | 133 | 134 | 161 | 162 | 163 | "#; 164 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | use bytes::Buf; 2 | use futures_util::{Stream, StreamExt}; 3 | use warp::{reply::Response, Filter, Reply}; 4 | 5 | #[tokio::main] 6 | async fn main() { 7 | // Running curl -T /path/to/a/file 'localhost:3030/' should echo back the content of the file, 8 | // or an HTTP 413 error if the configured size limit is exceeded. 9 | let route = warp::body::content_length_limit(65536) 10 | .and(warp::body::stream()) 11 | .then(handler); 12 | warp::serve(route).run(([127, 0, 0, 1], 3030)).await; 13 | } 14 | 15 | async fn handler( 16 | mut body: impl Stream> + Unpin + Send + Sync, 17 | ) -> Response { 18 | let mut collected: Vec = vec![]; 19 | while let Some(buf) = body.next().await { 20 | let mut buf = buf.unwrap(); 21 | while buf.remaining() > 0 { 22 | let chunk = buf.chunk(); 23 | let chunk_len = chunk.len(); 24 | collected.extend_from_slice(chunk); 25 | buf.advance(chunk_len); 26 | } 27 | } 28 | println!("Sending {} bytes", collected.len()); 29 | collected.into_response() 30 | } 31 | -------------------------------------------------------------------------------- /examples/tls.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | // Don't copy this `cfg`, it's only needed because this file is within 4 | // the warp repository. 5 | // Instead, specify the "tls" feature in your warp dependency declaration. 6 | #[cfg(feature = "tls")] 7 | #[tokio::main] 8 | async fn main() { 9 | use warp::Filter; 10 | 11 | // Match any request and return hello world! 12 | let routes = warp::any().map(|| "Hello, World!"); 13 | 14 | warp::serve(routes) 15 | .tls() 16 | // RSA 17 | .cert_path("examples/tls/cert.pem") 18 | .key_path("examples/tls/key.rsa") 19 | // ECC 20 | // .cert_path("examples/tls/cert.ecc.pem") 21 | // .key_path("examples/tls/key.ecc") 22 | .run(([127, 0, 0, 1], 3030)) 23 | .await; 24 | } 25 | 26 | #[cfg(not(feature = "tls"))] 27 | fn main() { 28 | eprintln!("Requires the `tls` feature."); 29 | } 30 | -------------------------------------------------------------------------------- /examples/tls/cert.ecc.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICGjCCAb+gAwIBAgIUEbF3/5NuJeGvIywbwta91AkJxTgwCgYIKoZIzj0EAwIw 3 | YjELMAkGA1UEBhMCREUxEDAOBgNVBAgMB0dlcm1hbnkxEDAOBgNVBAcMB0xlaXB6 4 | aWcxEjAQBgNVBAMMCWxvY2FsLmRldjEbMBkGCSqGSIb3DQEJARYMaGlAbG9jYWwu 5 | ZGV2MB4XDTI0MDcyMzA3MDMzOFoXDTI3MDcyMzA3MDMzOFowYjELMAkGA1UEBhMC 6 | REUxEDAOBgNVBAgMB0dlcm1hbnkxEDAOBgNVBAcMB0xlaXB6aWcxEjAQBgNVBAMM 7 | CWxvY2FsLmRldjEbMBkGCSqGSIb3DQEJARYMaGlAbG9jYWwuZGV2MFkwEwYHKoZI 8 | zj0CAQYIKoZIzj0DAQcDQgAE2UeBetF/oh43g+pMkmX15YzXJA29tkGEO+k7OBhW 9 | FpHQ7LVOsnocchEfjGVrJlJ0xPxst5p6UpjM6EgX6CkZh6NTMFEwHQYDVR0OBBYE 10 | FFERjkqZLloI4V6XcrsutHi0oiwtMB8GA1UdIwQYMBaAFFERjkqZLloI4V6Xcrsu 11 | tHi0oiwtMA8GA1UdEwEB/wQFMAMBAf8wCgYIKoZIzj0EAwIDSQAwRgIhAM3fWlv0 12 | 5OW660apTKBgNxtQQ/JW4mdeuqBuI1+Jea6DAiEAhb1Kpcksg1iyMLJ4uMQvUePY 13 | smRQQ67YgoLE+W5JAuw= 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /examples/tls/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIEADCCAmigAwIBAgICAcgwDQYJKoZIhvcNAQELBQAwLDEqMCgGA1UEAwwhcG9u 3 | eXRvd24gUlNBIGxldmVsIDIgaW50ZXJtZWRpYXRlMB4XDTE2MDgxMzE2MDcwNFoX 4 | DTIyMDIwMzE2MDcwNFowGTEXMBUGA1UEAwwOdGVzdHNlcnZlci5jb20wggEiMA0G 5 | CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCpVhh1/FNP2qvWenbZSghari/UThwe 6 | dynfnHG7gc3JmygkEdErWBO/CHzHgsx7biVE5b8sZYNEDKFojyoPHGWK2bQM/FTy 7 | niJCgNCLdn6hUqqxLAml3cxGW77hAWu94THDGB1qFe+eFiAUnDmob8gNZtAzT6Ky 8 | b/JGJdrEU0wj+Rd7wUb4kpLInNH/Jc+oz2ii2AjNbGOZXnRz7h7Kv3sO9vABByYe 9 | LcCj3qnhejHMqVhbAT1MD6zQ2+YKBjE52MsQKU/xhUpu9KkUyLh0cxkh3zrFiKh4 10 | Vuvtc+n7aeOv2jJmOl1dr0XLlSHBlmoKqH6dCTSbddQLmlK7dms8vE01AgMBAAGj 11 | gb4wgbswDAYDVR0TAQH/BAIwADALBgNVHQ8EBAMCBsAwHQYDVR0OBBYEFMeUzGYV 12 | bXwJNQVbY1+A8YXYZY8pMEIGA1UdIwQ7MDmAFJvEsUi7+D8vp8xcWvnEdVBGkpoW 13 | oR6kHDAaMRgwFgYDVQQDDA9wb255dG93biBSU0EgQ0GCAXswOwYDVR0RBDQwMoIO 14 | dGVzdHNlcnZlci5jb22CFXNlY29uZC50ZXN0c2VydmVyLmNvbYIJbG9jYWxob3N0 15 | MA0GCSqGSIb3DQEBCwUAA4IBgQBsk5ivAaRAcNgjc7LEiWXFkMg703AqDDNx7kB1 16 | RDgLalLvrjOfOp2jsDfST7N1tKLBSQ9bMw9X4Jve+j7XXRUthcwuoYTeeo+Cy0/T 17 | 1Q78ctoX74E2nB958zwmtRykGrgE/6JAJDwGcgpY9kBPycGxTlCN926uGxHsDwVs 18 | 98cL6ZXptMLTR6T2XP36dAJZuOICSqmCSbFR8knc/gjUO36rXTxhwci8iDbmEVaf 19 | BHpgBXGU5+SQ+QM++v6bHGf4LNQC5NZ4e4xvGax8ioYu/BRsB/T3Lx+RlItz4zdU 20 | XuxCNcm3nhQV2ZHquRdbSdoyIxV5kJXel4wCmOhWIq7A2OBKdu5fQzIAzzLi65EN 21 | RPAKsKB4h7hGgvciZQ7dsMrlGw0DLdJ6UrFyiR5Io7dXYT/+JP91lP5xsl6Lhg9O 22 | FgALt7GSYRm2cZdgi9pO9rRr83Br1VjQT1vHz6yoZMXSqc4A2zcN2a2ZVq//rHvc 23 | FZygs8miAhWPzqnpmgTj1cPiU1M= 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /examples/tls/key.ecc: -------------------------------------------------------------------------------- 1 | -----BEGIN EC PRIVATE KEY----- 2 | MHcCAQEEIPwp3LAnLEyWe2lLz66Y3QCCJ/BEMJheTM0shZnnSw6toAoGCCqGSM49 3 | AwEHoUQDQgAE2UeBetF/oh43g+pMkmX15YzXJA29tkGEO+k7OBhWFpHQ7LVOsnoc 4 | chEfjGVrJlJ0xPxst5p6UpjM6EgX6CkZhw== 5 | -----END EC PRIVATE KEY----- 6 | -------------------------------------------------------------------------------- /examples/tls/key.rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAqVYYdfxTT9qr1np22UoIWq4v1E4cHncp35xxu4HNyZsoJBHR 3 | K1gTvwh8x4LMe24lROW/LGWDRAyhaI8qDxxlitm0DPxU8p4iQoDQi3Z+oVKqsSwJ 4 | pd3MRlu+4QFrveExwxgdahXvnhYgFJw5qG/IDWbQM0+ism/yRiXaxFNMI/kXe8FG 5 | +JKSyJzR/yXPqM9ootgIzWxjmV50c+4eyr97DvbwAQcmHi3Ao96p4XoxzKlYWwE9 6 | TA+s0NvmCgYxOdjLEClP8YVKbvSpFMi4dHMZId86xYioeFbr7XPp+2njr9oyZjpd 7 | Xa9Fy5UhwZZqCqh+nQk0m3XUC5pSu3ZrPLxNNQIDAQABAoIBAFKtZJgGsK6md4vq 8 | kyiYSufrcBLaaEQ/rkQtYCJKyC0NAlZKFLRy9oEpJbNLm4cQSkYPXn3Qunx5Jj2k 9 | 2MYz+SgIDy7f7KHgr52Ew020dzNQ52JFvBgt6NTZaqL1TKOS1fcJSSNIvouTBerK 10 | NCSXHzfb4P+MfEVe/w1c4ilE+kH9SzdEo2jK/sRbzHIY8TX0JbmQ4SCLLayr22YG 11 | usIxtIYcWt3MMP/G2luRnYzzBCje5MXdpAhlHLi4TB6x4h5PmBKYc57uOVNngKLd 12 | YyrQKcszW4Nx5v0a4HG3A5EtUXNCco1+5asXOg2lYphQYVh2R+1wgu5WiDjDVu+6 13 | EYgjFSkCgYEA0NBk6FDoxE/4L/4iJ4zIhu9BptN8Je/uS5c6wRejNC/VqQyw7SHb 14 | hRFNrXPvq5Y+2bI/DxtdzZLKAMXOMjDjj0XEgfOIn2aveOo3uE7zf1i+njxwQhPu 15 | uSYA9AlBZiKGr2PCYSDPnViHOspVJjxRuAgyWM1Qf+CTC0D95aj0oz8CgYEAz5n4 16 | Cb3/WfUHxMJLljJ7PlVmlQpF5Hk3AOR9+vtqTtdxRjuxW6DH2uAHBDdC3OgppUN4 17 | CFj55kzc2HUuiHtmPtx8mK6G+otT7Lww+nLSFL4PvZ6CYxqcio5MPnoYd+pCxrXY 18 | JFo2W7e4FkBOxb5PF5So5plg+d0z/QiA7aFP1osCgYEAtgi1rwC5qkm8prn4tFm6 19 | hkcVCIXc+IWNS0Bu693bXKdGr7RsmIynff1zpf4ntYGpEMaeymClCY0ppDrMYlzU 20 | RBYiFNdlBvDRj6s/H+FTzHRk2DT/99rAhY9nzVY0OQFoQIXK8jlURGrkmI/CYy66 21 | XqBmo5t4zcHM7kaeEBOWEKkCgYAYnO6VaRtPNQfYwhhoFFAcUc+5t+AVeHGW/4AY 22 | M5qlAlIBu64JaQSI5KqwS0T4H+ZgG6Gti68FKPO+DhaYQ9kZdtam23pRVhd7J8y+ 23 | xMI3h1kiaBqZWVxZ6QkNFzizbui/2mtn0/JB6YQ/zxwHwcpqx0tHG8Qtm5ZAV7PB 24 | eLCYhQKBgQDALJxU/6hMTdytEU5CLOBSMby45YD/RrfQrl2gl/vA0etPrto4RkVq 25 | UrkDO/9W4mZORClN3knxEFSTlYi8YOboxdlynpFfhcs82wFChs+Ydp1eEsVHAqtu 26 | T+uzn0sroycBiBfVB949LExnzGDFUkhG0i2c2InarQYLTsIyHCIDEA== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /examples/tracing.rs: -------------------------------------------------------------------------------- 1 | //! [`tracing`] is a framework for instrumenting Rust programs to 2 | //! collect scoped, structured, and async-aware diagnostics. This example 3 | //! demonstrates how the `warp::trace` module can be used to instrument `warp` 4 | //! applications with `tracing`. 5 | //! 6 | //! [`tracing`]: https://crates.io/crates/tracing 7 | #![deny(warnings)] 8 | use tracing_subscriber::fmt::format::FmtSpan; 9 | use warp::Filter; 10 | 11 | #[tokio::main] 12 | async fn main() { 13 | // Filter traces based on the RUST_LOG env var, or, if it's not set, 14 | // default to show the output of the example. 15 | let filter = std::env::var("RUST_LOG").unwrap_or_else(|_| "tracing=info,warp=debug".to_owned()); 16 | 17 | // Configure the default `tracing` subscriber. 18 | // The `fmt` subscriber from the `tracing-subscriber` crate logs `tracing` 19 | // events to stdout. Other subscribers are available for integrating with 20 | // distributed tracing systems such as OpenTelemetry. 21 | tracing_subscriber::fmt() 22 | // Use the filter we built above to determine which traces to record. 23 | .with_env_filter(filter) 24 | // Record an event when each span closes. This can be used to time our 25 | // routes' durations! 26 | .with_span_events(FmtSpan::CLOSE) 27 | .init(); 28 | 29 | let hello = warp::path("hello") 30 | .and(warp::get()) 31 | // When the `hello` route is called, emit a `tracing` event. 32 | .map(|| { 33 | tracing::info!("saying hello..."); 34 | "Hello, World!" 35 | }) 36 | // Wrap the route in a `tracing` span to add the route's name as context 37 | // to any events that occur inside it. 38 | .with(warp::trace::named("hello")); 39 | 40 | let goodbye = warp::path("goodbye") 41 | .and(warp::get()) 42 | .map(|| { 43 | tracing::info!("saying goodbye..."); 44 | "So long and thanks for all the fish!" 45 | }) 46 | // We can also provide our own custom `tracing` spans to wrap a route. 47 | .with(warp::trace(|info| { 48 | // Construct our own custom span for this route. 49 | tracing::info_span!("goodbye", req.path = ?info.path()) 50 | })); 51 | 52 | let routes = hello 53 | .or(goodbye) 54 | // Wrap all the routes with a filter that creates a `tracing` span for 55 | // each request we receive, including data about the request. 56 | .with(warp::trace::request()); 57 | 58 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 59 | } 60 | -------------------------------------------------------------------------------- /examples/unix_socket.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | #[cfg(unix)] 4 | #[tokio::main] 5 | async fn main() { 6 | use tokio::net::UnixListener; 7 | use tokio_stream::wrappers::UnixListenerStream; 8 | 9 | pretty_env_logger::init(); 10 | 11 | let listener = UnixListener::bind("/tmp/warp.sock").unwrap(); 12 | let incoming = UnixListenerStream::new(listener); 13 | warp::serve(warp::fs::dir("examples/dir")) 14 | .run_incoming(incoming) 15 | .await; 16 | } 17 | 18 | #[cfg(not(unix))] 19 | #[tokio::main] 20 | async fn main() { 21 | panic!("Must run under Unix-like platform!"); 22 | } 23 | -------------------------------------------------------------------------------- /examples/websockets.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use futures_util::{FutureExt, StreamExt}; 4 | use warp::Filter; 5 | 6 | #[tokio::main] 7 | async fn main() { 8 | pretty_env_logger::init(); 9 | 10 | let routes = warp::path("echo") 11 | // The `ws()` filter will prepare the Websocket handshake. 12 | .and(warp::ws()) 13 | .map(|ws: warp::ws::Ws| { 14 | // And then our closure will be called when it completes... 15 | ws.on_upgrade(|websocket| { 16 | // Just echo all messages back... 17 | let (tx, rx) = websocket.split(); 18 | rx.forward(tx).map(|result| { 19 | if let Err(e) = result { 20 | eprintln!("websocket error: {:?}", e); 21 | } 22 | }) 23 | }) 24 | }); 25 | 26 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 27 | } 28 | -------------------------------------------------------------------------------- /examples/websockets_chat.rs: -------------------------------------------------------------------------------- 1 | // #![deny(warnings)] 2 | use std::collections::HashMap; 3 | use std::sync::{ 4 | atomic::{AtomicUsize, Ordering}, 5 | Arc, 6 | }; 7 | 8 | use futures_util::{SinkExt, StreamExt, TryFutureExt}; 9 | use tokio::sync::{mpsc, RwLock}; 10 | use tokio_stream::wrappers::UnboundedReceiverStream; 11 | use warp::ws::{Message, WebSocket}; 12 | use warp::Filter; 13 | 14 | /// Our global unique user id counter. 15 | static NEXT_USER_ID: AtomicUsize = AtomicUsize::new(1); 16 | 17 | /// Our state of currently connected users. 18 | /// 19 | /// - Key is their id 20 | /// - Value is a sender of `warp::ws::Message` 21 | type Users = Arc>>>; 22 | 23 | #[tokio::main] 24 | async fn main() { 25 | pretty_env_logger::init(); 26 | 27 | // Keep track of all connected users, key is usize, value 28 | // is a websocket sender. 29 | let users = Users::default(); 30 | // Turn our "state" into a new Filter... 31 | let users = warp::any().map(move || users.clone()); 32 | 33 | // GET /chat -> websocket upgrade 34 | let chat = warp::path("chat") 35 | // The `ws()` filter will prepare Websocket handshake... 36 | .and(warp::ws()) 37 | .and(users) 38 | .map(|ws: warp::ws::Ws, users| { 39 | // This will call our function if the handshake succeeds. 40 | ws.on_upgrade(move |socket| user_connected(socket, users)) 41 | }); 42 | 43 | // GET / -> index html 44 | let index = warp::path::end().map(|| warp::reply::html(INDEX_HTML)); 45 | 46 | let routes = index.or(chat); 47 | 48 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 49 | } 50 | 51 | async fn user_connected(ws: WebSocket, users: Users) { 52 | // Use a counter to assign a new unique ID for this user. 53 | let my_id = NEXT_USER_ID.fetch_add(1, Ordering::Relaxed); 54 | 55 | eprintln!("new chat user: {}", my_id); 56 | 57 | // Split the socket into a sender and receive of messages. 58 | let (mut user_ws_tx, mut user_ws_rx) = ws.split(); 59 | 60 | // Use an unbounded channel to handle buffering and flushing of messages 61 | // to the websocket... 62 | let (tx, rx) = mpsc::unbounded_channel(); 63 | let mut rx = UnboundedReceiverStream::new(rx); 64 | 65 | tokio::task::spawn(async move { 66 | while let Some(message) = rx.next().await { 67 | user_ws_tx 68 | .send(message) 69 | .unwrap_or_else(|e| { 70 | eprintln!("websocket send error: {}", e); 71 | }) 72 | .await; 73 | } 74 | }); 75 | 76 | // Save the sender in our list of connected users. 77 | users.write().await.insert(my_id, tx); 78 | 79 | // Return a `Future` that is basically a state machine managing 80 | // this specific user's connection. 81 | 82 | // Every time the user sends a message, broadcast it to 83 | // all other users... 84 | while let Some(result) = user_ws_rx.next().await { 85 | let msg = match result { 86 | Ok(msg) => msg, 87 | Err(e) => { 88 | eprintln!("websocket error(uid={}): {}", my_id, e); 89 | break; 90 | } 91 | }; 92 | user_message(my_id, msg, &users).await; 93 | } 94 | 95 | // user_ws_rx stream will keep processing as long as the user stays 96 | // connected. Once they disconnect, then... 97 | user_disconnected(my_id, &users).await; 98 | } 99 | 100 | async fn user_message(my_id: usize, msg: Message, users: &Users) { 101 | // Skip any non-Text messages... 102 | let msg = if let Ok(s) = msg.to_str() { 103 | s 104 | } else { 105 | return; 106 | }; 107 | 108 | let new_msg = format!(": {}", my_id, msg); 109 | 110 | // New message from this user, send it to everyone else (except same uid)... 111 | for (&uid, tx) in users.read().await.iter() { 112 | if my_id != uid { 113 | if let Err(_disconnected) = tx.send(Message::text(new_msg.clone())) { 114 | // The tx is disconnected, our `user_disconnected` code 115 | // should be happening in another task, nothing more to 116 | // do here. 117 | } 118 | } 119 | } 120 | } 121 | 122 | async fn user_disconnected(my_id: usize, users: &Users) { 123 | eprintln!("good bye user: {}", my_id); 124 | 125 | // Stream closed up, so remove from the user list 126 | users.write().await.remove(&my_id); 127 | } 128 | 129 | static INDEX_HTML: &str = r#" 130 | 131 | 132 | Warp Chat 133 | 134 | 135 |

Warp chat

136 |
137 |

Connecting...

138 |
139 | 140 | 141 | 173 | 174 | 175 | "#; 176 | -------------------------------------------------------------------------------- /examples/wrapping.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::Filter; 3 | 4 | fn hello_wrapper( 5 | filter: F, 6 | ) -> impl Filter + Clone + Send + Sync + 'static 7 | where 8 | F: Filter + Clone + Send + Sync + 'static, 9 | F::Extract: warp::Reply, 10 | { 11 | warp::any() 12 | .map(|| { 13 | println!("before filter"); 14 | }) 15 | .untuple_one() 16 | .and(filter) 17 | .map(|_arg| "wrapped hello world") 18 | } 19 | 20 | #[tokio::main] 21 | async fn main() { 22 | // Match any request and return hello world! 23 | let routes = warp::any() 24 | .map(|| "hello world") 25 | .boxed() 26 | .recover(|_err| async { Ok("recovered") }) 27 | // wrap the filter with hello_wrapper 28 | .with(warp::wrap_fn(hello_wrapper)); 29 | 30 | warp::serve(routes).run(([127, 0, 0, 1], 3030)).await; 31 | } 32 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::error::Error as StdError; 3 | use std::fmt; 4 | 5 | type BoxError = Box; 6 | 7 | /// Errors that can happen inside warp. 8 | pub struct Error { 9 | inner: BoxError, 10 | } 11 | 12 | impl Error { 13 | pub(crate) fn new>(err: E) -> Error { 14 | Error { inner: err.into() } 15 | } 16 | } 17 | 18 | impl fmt::Debug for Error { 19 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 20 | // Skip showing worthless `Error { .. }` wrapper. 21 | fmt::Debug::fmt(&self.inner, f) 22 | } 23 | } 24 | 25 | impl fmt::Display for Error { 26 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 27 | fmt::Display::fmt(&self.inner, f) 28 | } 29 | } 30 | 31 | impl StdError for Error { 32 | fn source(&self) -> Option<&(dyn StdError + 'static)> { 33 | Some(self.inner.as_ref()) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(infallible: Infallible) -> Error { 39 | match infallible {} 40 | } 41 | } 42 | 43 | #[test] 44 | fn error_size_of() { 45 | assert_eq!( 46 | ::std::mem::size_of::(), 47 | ::std::mem::size_of::() * 2 48 | ); 49 | } 50 | 51 | #[test] 52 | fn error_source() { 53 | let e = Error::new(std::fmt::Error {}); 54 | assert!(e.source().unwrap().is::()); 55 | } 56 | 57 | macro_rules! unit_error { 58 | ( 59 | $(#[$docs:meta])* 60 | $pub:vis $typ:ident: $display:literal 61 | ) => ( 62 | $(#[$docs])* 63 | $pub struct $typ { _p: (), } 64 | 65 | impl ::std::fmt::Debug for $typ { 66 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 67 | f.debug_struct(stringify!($typ)).finish() 68 | } 69 | } 70 | 71 | impl ::std::fmt::Display for $typ { 72 | fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { 73 | f.write_str($display) 74 | } 75 | } 76 | 77 | impl ::std::error::Error for $typ {} 78 | ) 79 | } 80 | -------------------------------------------------------------------------------- /src/filter/and.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::ready; 6 | use pin_project::pin_project; 7 | 8 | use super::{Combine, Filter, FilterBase, Internal, Tuple}; 9 | use crate::generic::CombinedTuples; 10 | use crate::reject::CombineRejection; 11 | 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct And { 14 | pub(super) first: T, 15 | pub(super) second: U, 16 | } 17 | 18 | impl FilterBase for And 19 | where 20 | T: Filter, 21 | T::Extract: Send, 22 | U: Filter + Clone + Send, 23 | ::HList: Combine<::HList> + Send, 24 | CombinedTuples: Send, 25 | U::Error: CombineRejection, 26 | { 27 | type Extract = CombinedTuples; 28 | type Error = >::One; 29 | type Future = AndFuture; 30 | 31 | fn filter(&self, _: Internal) -> Self::Future { 32 | AndFuture { 33 | state: State::First(self.first.filter(Internal), self.second.clone()), 34 | } 35 | } 36 | } 37 | 38 | #[allow(missing_debug_implementations)] 39 | #[pin_project] 40 | pub struct AndFuture { 41 | #[pin] 42 | state: State, 43 | } 44 | 45 | #[pin_project(project = StateProj)] 46 | enum State { 47 | First(#[pin] T, U), 48 | Second(Option, #[pin] U::Future), 49 | Done, 50 | } 51 | 52 | impl Future for AndFuture 53 | where 54 | T: Filter, 55 | U: Filter, 56 | ::HList: Combine<::HList> + Send, 57 | U::Error: CombineRejection, 58 | { 59 | type Output = Result< 60 | CombinedTuples, 61 | >::One, 62 | >; 63 | 64 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 65 | self.project().state.poll(cx) 66 | } 67 | } 68 | 69 | impl Future for State 70 | where 71 | T: Future>, 72 | U: Filter, 73 | TE: Tuple, 74 | TE::HList: Combine<::HList> + Send, 75 | U::Error: CombineRejection, 76 | { 77 | type Output = Result, >::One>; 78 | 79 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 80 | loop { 81 | match self.as_mut().project() { 82 | StateProj::First(first, second) => { 83 | let ex1 = ready!(first.poll(cx))?; 84 | let fut2 = second.filter(Internal); 85 | self.set(State::Second(Some(ex1), fut2)); 86 | } 87 | StateProj::Second(ex1, second) => { 88 | let ex2 = ready!(second.poll(cx))?; 89 | let ex3 = ex1.take().unwrap().combine(ex2); 90 | self.set(State::Done); 91 | return Poll::Ready(Ok(ex3)); 92 | } 93 | StateProj::Done => panic!("polled after complete"), 94 | } 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/filter/and_then.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Func, Internal}; 9 | use crate::reject::CombineRejection; 10 | 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct AndThen { 13 | pub(super) filter: T, 14 | pub(super) callback: F, 15 | } 16 | 17 | impl FilterBase for AndThen 18 | where 19 | T: Filter, 20 | F: Func + Clone + Send, 21 | F::Output: TryFuture + Send, 22 | ::Error: CombineRejection, 23 | { 24 | type Extract = (::Ok,); 25 | type Error = <::Error as CombineRejection>::One; 26 | type Future = AndThenFuture; 27 | #[inline] 28 | fn filter(&self, _: Internal) -> Self::Future { 29 | AndThenFuture { 30 | state: State::First(self.filter.filter(Internal), self.callback.clone()), 31 | } 32 | } 33 | } 34 | 35 | #[allow(missing_debug_implementations)] 36 | #[pin_project] 37 | pub struct AndThenFuture 38 | where 39 | T: Filter, 40 | F: Func, 41 | F::Output: TryFuture + Send, 42 | ::Error: CombineRejection, 43 | { 44 | #[pin] 45 | state: State, 46 | } 47 | 48 | #[pin_project(project = StateProj)] 49 | enum State 50 | where 51 | T: TryFuture, 52 | F: Func, 53 | F::Output: TryFuture + Send, 54 | ::Error: CombineRejection, 55 | { 56 | First(#[pin] T, F), 57 | Second(#[pin] F::Output), 58 | Done, 59 | } 60 | 61 | impl Future for AndThenFuture 62 | where 63 | T: Filter, 64 | F: Func, 65 | F::Output: TryFuture + Send, 66 | ::Error: CombineRejection, 67 | { 68 | type Output = Result< 69 | (::Ok,), 70 | <::Error as CombineRejection>::One, 71 | >; 72 | 73 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 74 | self.project().state.poll(cx) 75 | } 76 | } 77 | 78 | impl Future for State 79 | where 80 | T: TryFuture, 81 | F: Func, 82 | F::Output: TryFuture + Send, 83 | ::Error: CombineRejection, 84 | { 85 | type Output = Result< 86 | (::Ok,), 87 | <::Error as CombineRejection>::One, 88 | >; 89 | 90 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 91 | loop { 92 | match self.as_mut().project() { 93 | StateProj::First(first, second) => { 94 | let ex1 = ready!(first.try_poll(cx))?; 95 | let fut2 = second.call(ex1); 96 | self.set(State::Second(fut2)); 97 | } 98 | StateProj::Second(second) => { 99 | let ex2 = match ready!(second.try_poll(cx)) { 100 | Ok(item) => Ok((item,)), 101 | Err(err) => Err(From::from(err)), 102 | }; 103 | self.set(State::Done); 104 | return Poll::Ready(ex2); 105 | } 106 | StateProj::Done => panic!("polled after complete"), 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/filter/boxed.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::future::Future; 3 | use std::pin::Pin; 4 | use std::sync::Arc; 5 | 6 | use futures_util::TryFutureExt; 7 | 8 | use super::{Filter, FilterBase, Internal, Tuple}; 9 | use crate::reject::Rejection; 10 | 11 | /// A type representing a boxed [`Filter`](crate::Filter) trait object. 12 | /// 13 | /// The filter inside is a dynamic trait object. The purpose of this type is 14 | /// to ease returning `Filter`s from other functions. 15 | /// 16 | /// To create one, call `Filter::boxed` on any filter. 17 | /// 18 | /// # Examples 19 | /// 20 | /// ``` 21 | /// use warp::{Filter, filters::BoxedFilter, Reply}; 22 | /// 23 | /// pub fn assets_filter() -> BoxedFilter<(impl Reply,)> { 24 | /// warp::path("assets") 25 | /// .and(warp::fs::dir("./assets")) 26 | /// .boxed() 27 | /// } 28 | /// ``` 29 | /// 30 | pub struct BoxedFilter { 31 | filter: Arc< 32 | dyn Filter< 33 | Extract = T, 34 | Error = Rejection, 35 | Future = Pin> + Send>>, 36 | > + Send 37 | + Sync, 38 | >, 39 | } 40 | 41 | impl BoxedFilter { 42 | pub(super) fn new(filter: F) -> BoxedFilter 43 | where 44 | F: Filter + Send + Sync + 'static, 45 | F::Error: Into, 46 | { 47 | BoxedFilter { 48 | filter: Arc::new(BoxingFilter { 49 | filter: filter.map_err(super::Internal, Into::into), 50 | }), 51 | } 52 | } 53 | } 54 | 55 | impl Clone for BoxedFilter { 56 | fn clone(&self) -> BoxedFilter { 57 | BoxedFilter { 58 | filter: self.filter.clone(), 59 | } 60 | } 61 | } 62 | 63 | impl fmt::Debug for BoxedFilter { 64 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 65 | f.debug_struct("BoxedFilter").finish() 66 | } 67 | } 68 | 69 | fn _assert_send() { 70 | fn _assert() {} 71 | _assert::>(); 72 | } 73 | 74 | impl FilterBase for BoxedFilter { 75 | type Extract = T; 76 | type Error = Rejection; 77 | type Future = Pin> + Send>>; 78 | 79 | fn filter(&self, _: Internal) -> Self::Future { 80 | self.filter.filter(Internal) 81 | } 82 | } 83 | 84 | struct BoxingFilter { 85 | filter: F, 86 | } 87 | 88 | impl FilterBase for BoxingFilter 89 | where 90 | F: Filter, 91 | F::Future: Send + 'static, 92 | { 93 | type Extract = F::Extract; 94 | type Error = F::Error; 95 | type Future = Pin> + Send>>; 96 | 97 | fn filter(&self, _: Internal) -> Self::Future { 98 | Box::pin(self.filter.filter(Internal).into_future()) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/filter/map.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Func, Internal}; 9 | 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct Map { 12 | pub(super) filter: T, 13 | pub(super) callback: F, 14 | } 15 | 16 | impl FilterBase for Map 17 | where 18 | T: Filter, 19 | F: Func + Clone + Send, 20 | { 21 | type Extract = (F::Output,); 22 | type Error = T::Error; 23 | type Future = MapFuture; 24 | #[inline] 25 | fn filter(&self, _: Internal) -> Self::Future { 26 | MapFuture { 27 | extract: self.filter.filter(Internal), 28 | callback: self.callback.clone(), 29 | } 30 | } 31 | } 32 | 33 | #[allow(missing_debug_implementations)] 34 | #[pin_project] 35 | pub struct MapFuture { 36 | #[pin] 37 | extract: T::Future, 38 | callback: F, 39 | } 40 | 41 | impl Future for MapFuture 42 | where 43 | T: Filter, 44 | F: Func, 45 | { 46 | type Output = Result<(F::Output,), T::Error>; 47 | 48 | #[inline] 49 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 50 | let pin = self.project(); 51 | match ready!(pin.extract.try_poll(cx)) { 52 | Ok(ex) => { 53 | let ex = (pin.callback.call(ex),); 54 | Poll::Ready(Ok(ex)) 55 | } 56 | Err(err) => Poll::Ready(Err(err)), 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/filter/map_err.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::TryFuture; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Internal}; 9 | use crate::reject::IsReject; 10 | 11 | #[derive(Clone, Copy, Debug)] 12 | pub struct MapErr { 13 | pub(super) filter: T, 14 | pub(super) callback: F, 15 | } 16 | 17 | impl FilterBase for MapErr 18 | where 19 | T: Filter, 20 | F: Fn(T::Error) -> E + Clone + Send, 21 | E: IsReject, 22 | { 23 | type Extract = T::Extract; 24 | type Error = E; 25 | type Future = MapErrFuture; 26 | #[inline] 27 | fn filter(&self, _: Internal) -> Self::Future { 28 | MapErrFuture { 29 | extract: self.filter.filter(Internal), 30 | callback: self.callback.clone(), 31 | } 32 | } 33 | } 34 | 35 | #[allow(missing_debug_implementations)] 36 | #[pin_project] 37 | pub struct MapErrFuture { 38 | #[pin] 39 | extract: T::Future, 40 | callback: F, 41 | } 42 | 43 | impl Future for MapErrFuture 44 | where 45 | T: Filter, 46 | F: Fn(T::Error) -> E, 47 | { 48 | type Output = Result; 49 | 50 | #[inline] 51 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 52 | self.as_mut() 53 | .project() 54 | .extract 55 | .try_poll(cx) 56 | .map_err(|err| (self.callback)(err)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/filter/or.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Internal}; 9 | use crate::generic::Either; 10 | use crate::reject::CombineRejection; 11 | use crate::route; 12 | 13 | type Combined = >::Combined; 14 | 15 | #[derive(Clone, Copy, Debug)] 16 | pub struct Or { 17 | pub(super) first: T, 18 | pub(super) second: U, 19 | } 20 | 21 | impl FilterBase for Or 22 | where 23 | T: Filter, 24 | U: Filter + Clone + Send, 25 | U::Error: CombineRejection, 26 | { 27 | type Extract = (Either,); 28 | //type Error = >::Combined; 29 | type Error = Combined; 30 | type Future = EitherFuture; 31 | 32 | fn filter(&self, _: Internal) -> Self::Future { 33 | let idx = route::with(|route| route.matched_path_index()); 34 | EitherFuture { 35 | state: State::First(self.first.filter(Internal), self.second.clone()), 36 | original_path_index: PathIndex(idx), 37 | } 38 | } 39 | } 40 | 41 | #[allow(missing_debug_implementations)] 42 | #[pin_project] 43 | pub struct EitherFuture { 44 | #[pin] 45 | state: State, 46 | original_path_index: PathIndex, 47 | } 48 | 49 | #[pin_project(project = StateProj)] 50 | enum State { 51 | First(#[pin] T::Future, U), 52 | Second(Option, #[pin] U::Future), 53 | Done, 54 | } 55 | 56 | #[derive(Copy, Clone)] 57 | struct PathIndex(usize); 58 | 59 | impl PathIndex { 60 | fn reset_path(&self) { 61 | route::with(|route| route.reset_matched_path_index(self.0)); 62 | } 63 | } 64 | 65 | impl Future for EitherFuture 66 | where 67 | T: Filter, 68 | U: Filter, 69 | U::Error: CombineRejection, 70 | { 71 | type Output = Result<(Either,), Combined>; 72 | 73 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 74 | loop { 75 | let pin = self.as_mut().project(); 76 | let (err1, fut2) = match pin.state.project() { 77 | StateProj::First(first, second) => match ready!(first.try_poll(cx)) { 78 | Ok(ex1) => { 79 | return Poll::Ready(Ok((Either::A(ex1),))); 80 | } 81 | Err(e) => { 82 | pin.original_path_index.reset_path(); 83 | (e, second.filter(Internal)) 84 | } 85 | }, 86 | StateProj::Second(err1, second) => { 87 | let ex2 = match ready!(second.try_poll(cx)) { 88 | Ok(ex2) => Ok((Either::B(ex2),)), 89 | Err(e) => { 90 | pin.original_path_index.reset_path(); 91 | let err1 = err1.take().expect("polled after complete"); 92 | Err(e.combine(err1)) 93 | } 94 | }; 95 | self.set(EitherFuture { 96 | state: State::Done, 97 | ..*self 98 | }); 99 | return Poll::Ready(ex2); 100 | } 101 | StateProj::Done => panic!("polled after complete"), 102 | }; 103 | 104 | self.set(EitherFuture { 105 | state: State::Second(Some(err1), fut2), 106 | ..*self 107 | }); 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/filter/or_else.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Func, Internal}; 9 | use crate::reject::IsReject; 10 | use crate::route; 11 | 12 | #[derive(Clone, Copy, Debug)] 13 | pub struct OrElse { 14 | pub(super) filter: T, 15 | pub(super) callback: F, 16 | } 17 | 18 | impl FilterBase for OrElse 19 | where 20 | T: Filter, 21 | F: Func + Clone + Send, 22 | F::Output: TryFuture + Send, 23 | ::Error: IsReject, 24 | { 25 | type Extract = ::Ok; 26 | type Error = ::Error; 27 | type Future = OrElseFuture; 28 | #[inline] 29 | fn filter(&self, _: Internal) -> Self::Future { 30 | let idx = route::with(|route| route.matched_path_index()); 31 | OrElseFuture { 32 | state: State::First(self.filter.filter(Internal), self.callback.clone()), 33 | original_path_index: PathIndex(idx), 34 | } 35 | } 36 | } 37 | 38 | #[allow(missing_debug_implementations)] 39 | #[pin_project] 40 | pub struct OrElseFuture 41 | where 42 | T: Filter, 43 | F: Func, 44 | F::Output: TryFuture + Send, 45 | { 46 | #[pin] 47 | state: State, 48 | original_path_index: PathIndex, 49 | } 50 | 51 | #[pin_project(project = StateProj)] 52 | enum State 53 | where 54 | T: Filter, 55 | F: Func, 56 | F::Output: TryFuture + Send, 57 | { 58 | First(#[pin] T::Future, F), 59 | Second(#[pin] F::Output), 60 | Done, 61 | } 62 | 63 | #[derive(Copy, Clone)] 64 | struct PathIndex(usize); 65 | 66 | impl PathIndex { 67 | fn reset_path(&self) { 68 | route::with(|route| route.reset_matched_path_index(self.0)); 69 | } 70 | } 71 | 72 | impl Future for OrElseFuture 73 | where 74 | T: Filter, 75 | F: Func, 76 | F::Output: TryFuture + Send, 77 | { 78 | type Output = Result<::Ok, ::Error>; 79 | 80 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 81 | loop { 82 | let pin = self.as_mut().project(); 83 | let (err, second) = match pin.state.project() { 84 | StateProj::First(first, second) => match ready!(first.try_poll(cx)) { 85 | Ok(ex) => return Poll::Ready(Ok(ex)), 86 | Err(err) => (err, second), 87 | }, 88 | StateProj::Second(second) => { 89 | let ex2 = ready!(second.try_poll(cx)); 90 | self.set(OrElseFuture { 91 | state: State::Done, 92 | ..*self 93 | }); 94 | return Poll::Ready(ex2); 95 | } 96 | StateProj::Done => panic!("polled after complete"), 97 | }; 98 | 99 | pin.original_path_index.reset_path(); 100 | let fut2 = second.call(err); 101 | self.set(OrElseFuture { 102 | state: State::Second(fut2), 103 | ..*self 104 | }); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/filter/recover.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Func, Internal}; 9 | use crate::generic::Either; 10 | use crate::reject::IsReject; 11 | use crate::route; 12 | 13 | #[derive(Clone, Copy, Debug)] 14 | pub struct Recover { 15 | pub(super) filter: T, 16 | pub(super) callback: F, 17 | } 18 | 19 | impl FilterBase for Recover 20 | where 21 | T: Filter, 22 | F: Func + Clone + Send, 23 | F::Output: TryFuture + Send, 24 | ::Error: IsReject, 25 | { 26 | type Extract = (Either::Ok,)>,); 27 | type Error = ::Error; 28 | type Future = RecoverFuture; 29 | #[inline] 30 | fn filter(&self, _: Internal) -> Self::Future { 31 | let idx = route::with(|route| route.matched_path_index()); 32 | RecoverFuture { 33 | state: State::First(self.filter.filter(Internal), self.callback.clone()), 34 | original_path_index: PathIndex(idx), 35 | } 36 | } 37 | } 38 | 39 | #[allow(missing_debug_implementations)] 40 | #[pin_project] 41 | pub struct RecoverFuture 42 | where 43 | T: Filter, 44 | F: Func, 45 | F::Output: TryFuture + Send, 46 | ::Error: IsReject, 47 | { 48 | #[pin] 49 | state: State, 50 | original_path_index: PathIndex, 51 | } 52 | 53 | #[pin_project(project = StateProj)] 54 | enum State 55 | where 56 | T: Filter, 57 | F: Func, 58 | F::Output: TryFuture + Send, 59 | ::Error: IsReject, 60 | { 61 | First(#[pin] T::Future, F), 62 | Second(#[pin] F::Output), 63 | Done, 64 | } 65 | 66 | #[derive(Copy, Clone)] 67 | struct PathIndex(usize); 68 | 69 | impl PathIndex { 70 | fn reset_path(&self) { 71 | route::with(|route| route.reset_matched_path_index(self.0)); 72 | } 73 | } 74 | 75 | impl Future for RecoverFuture 76 | where 77 | T: Filter, 78 | F: Func, 79 | F::Output: TryFuture + Send, 80 | ::Error: IsReject, 81 | { 82 | type Output = Result< 83 | (Either::Ok,)>,), 84 | ::Error, 85 | >; 86 | 87 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 88 | loop { 89 | let pin = self.as_mut().project(); 90 | let (err, second) = match pin.state.project() { 91 | StateProj::First(first, second) => match ready!(first.try_poll(cx)) { 92 | Ok(ex) => return Poll::Ready(Ok((Either::A(ex),))), 93 | Err(err) => (err, second), 94 | }, 95 | StateProj::Second(second) => { 96 | let ex2 = match ready!(second.try_poll(cx)) { 97 | Ok(ex2) => Ok((Either::B((ex2,)),)), 98 | Err(e) => Err(e), 99 | }; 100 | self.set(RecoverFuture { 101 | state: State::Done, 102 | ..*self 103 | }); 104 | return Poll::Ready(ex2); 105 | } 106 | StateProj::Done => panic!("polled after complete"), 107 | }; 108 | 109 | pin.original_path_index.reset_path(); 110 | let fut2 = second.call(err); 111 | self.set(RecoverFuture { 112 | state: State::Second(fut2), 113 | ..*self 114 | }); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/filter/service.rs: -------------------------------------------------------------------------------- 1 | use std::convert::Infallible; 2 | use std::future::Future; 3 | use std::net::SocketAddr; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use futures_util::future::TryFuture; 8 | use hyper::service::Service; 9 | use pin_project::pin_project; 10 | 11 | use crate::reject::IsReject; 12 | use crate::reply::{Reply, Response}; 13 | use crate::route::{self, Route}; 14 | use crate::{Filter, Request}; 15 | 16 | /// Convert a `Filter` into a `Service`. 17 | /// 18 | /// Filters are normally what APIs are built on in warp. However, it can be 19 | /// useful to convert a `Filter` into a [`Service`][Service], such as if 20 | /// further customizing a `hyper::Service`, or if wanting to make use of 21 | /// the greater [Tower][tower] set of middleware. 22 | /// 23 | /// # Example 24 | /// 25 | /// Running a `warp::Filter` on a regular `hyper::Server`: 26 | /// 27 | /// ``` 28 | /// # async fn run() -> Result<(), Box> { 29 | /// use std::convert::Infallible; 30 | /// use warp::Filter; 31 | /// 32 | /// // Our Filter... 33 | /// let route = warp::any().map(|| "Hello From Warp!"); 34 | /// 35 | /// // Convert it into a `Service`... 36 | /// let svc = warp::service(route); 37 | /// 38 | /// // Typical hyper setup... 39 | /// let make_svc = hyper::service::make_service_fn(move |_| async move { 40 | /// Ok::<_, Infallible>(svc) 41 | /// }); 42 | /// 43 | /// hyper::Server::bind(&([127, 0, 0, 1], 3030).into()) 44 | /// .serve(make_svc) 45 | /// .await?; 46 | /// # Ok(()) 47 | /// # } 48 | /// ``` 49 | /// 50 | /// [Service]: https://docs.rs/hyper/0.13.*/hyper/service/trait.Service.html 51 | /// [tower]: https://docs.rs/tower 52 | pub fn service(filter: F) -> FilteredService 53 | where 54 | F: Filter, 55 | ::Ok: Reply, 56 | ::Error: IsReject, 57 | { 58 | FilteredService { filter } 59 | } 60 | 61 | #[derive(Copy, Clone, Debug)] 62 | pub struct FilteredService { 63 | filter: F, 64 | } 65 | 66 | impl FilteredService 67 | where 68 | F: Filter, 69 | ::Ok: Reply, 70 | ::Error: IsReject, 71 | { 72 | #[inline] 73 | pub(crate) fn call_with_addr( 74 | &self, 75 | req: Request, 76 | remote_addr: Option, 77 | ) -> FilteredFuture { 78 | debug_assert!(!route::is_set(), "nested route::set calls"); 79 | 80 | let route = Route::new(req, remote_addr); 81 | let fut = route::set(&route, || self.filter.filter(super::Internal)); 82 | FilteredFuture { future: fut, route } 83 | } 84 | } 85 | 86 | impl Service for FilteredService 87 | where 88 | F: Filter, 89 | ::Ok: Reply, 90 | ::Error: IsReject, 91 | { 92 | type Response = Response; 93 | type Error = Infallible; 94 | type Future = FilteredFuture; 95 | 96 | fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { 97 | Poll::Ready(Ok(())) 98 | } 99 | 100 | #[inline] 101 | fn call(&mut self, req: Request) -> Self::Future { 102 | self.call_with_addr(req, None) 103 | } 104 | } 105 | 106 | #[pin_project] 107 | #[derive(Debug)] 108 | pub struct FilteredFuture { 109 | #[pin] 110 | future: F, 111 | route: ::std::cell::RefCell, 112 | } 113 | 114 | impl Future for FilteredFuture 115 | where 116 | F: TryFuture, 117 | F::Ok: Reply, 118 | F::Error: IsReject, 119 | { 120 | type Output = Result; 121 | 122 | #[inline] 123 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 124 | debug_assert!(!route::is_set(), "nested route::set calls"); 125 | 126 | let pin = self.project(); 127 | let fut = pin.future; 128 | match route::set(pin.route, || fut.try_poll(cx)) { 129 | Poll::Ready(Ok(ok)) => Poll::Ready(Ok(ok.into_response())), 130 | Poll::Pending => Poll::Pending, 131 | Poll::Ready(Err(err)) => { 132 | tracing::debug!("rejected: {:?}", err); 133 | Poll::Ready(Ok(err.into_response())) 134 | } 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/filter/then.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Func, Internal}; 9 | 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct Then { 12 | pub(super) filter: T, 13 | pub(super) callback: F, 14 | } 15 | 16 | impl FilterBase for Then 17 | where 18 | T: Filter, 19 | F: Func + Clone + Send, 20 | F::Output: Future + Send, 21 | { 22 | type Extract = (::Output,); 23 | type Error = T::Error; 24 | type Future = ThenFuture; 25 | #[inline] 26 | fn filter(&self, _: Internal) -> Self::Future { 27 | ThenFuture { 28 | state: State::First(self.filter.filter(Internal), self.callback.clone()), 29 | } 30 | } 31 | } 32 | 33 | #[allow(missing_debug_implementations)] 34 | #[pin_project] 35 | pub struct ThenFuture 36 | where 37 | T: Filter, 38 | F: Func, 39 | F::Output: Future + Send, 40 | { 41 | #[pin] 42 | state: State, 43 | } 44 | 45 | #[pin_project(project = StateProj)] 46 | enum State 47 | where 48 | T: TryFuture, 49 | F: Func, 50 | F::Output: Future + Send, 51 | { 52 | First(#[pin] T, F), 53 | Second(#[pin] F::Output), 54 | Done, 55 | } 56 | 57 | impl Future for ThenFuture 58 | where 59 | T: Filter, 60 | F: Func, 61 | F::Output: Future + Send, 62 | { 63 | type Output = Result<(::Output,), T::Error>; 64 | 65 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 66 | self.project().state.poll(cx) 67 | } 68 | } 69 | 70 | impl Future for State 71 | where 72 | T: TryFuture, 73 | F: Func, 74 | F::Output: Future + Send, 75 | { 76 | type Output = Result<(::Output,), T::Error>; 77 | 78 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 79 | loop { 80 | match self.as_mut().project() { 81 | StateProj::First(first, second) => { 82 | let ex1 = ready!(first.try_poll(cx))?; 83 | let fut2 = second.call(ex1); 84 | self.set(State::Second(fut2)); 85 | } 86 | StateProj::Second(second) => { 87 | let ex2 = (ready!(second.poll(cx)),); 88 | self.set(State::Done); 89 | return Poll::Ready(Ok(ex2)); 90 | } 91 | StateProj::Done => panic!("polled after complete"), 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/filter/unify.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Either, Filter, FilterBase, Internal, Tuple}; 9 | 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct Unify { 12 | pub(super) filter: F, 13 | } 14 | 15 | impl FilterBase for Unify 16 | where 17 | F: Filter,)>, 18 | T: Tuple, 19 | { 20 | type Extract = T; 21 | type Error = F::Error; 22 | type Future = UnifyFuture; 23 | #[inline] 24 | fn filter(&self, _: Internal) -> Self::Future { 25 | UnifyFuture { 26 | inner: self.filter.filter(Internal), 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_debug_implementations)] 32 | #[pin_project] 33 | pub struct UnifyFuture { 34 | #[pin] 35 | inner: F, 36 | } 37 | 38 | impl Future for UnifyFuture 39 | where 40 | F: TryFuture,)>, 41 | { 42 | type Output = Result; 43 | 44 | #[inline] 45 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 46 | Poll::Ready(match ready!(self.project().inner.try_poll(cx))? { 47 | (Either::A(x),) | (Either::B(x),) => Ok(x), 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/filter/untuple_one.rs: -------------------------------------------------------------------------------- 1 | use std::future::Future; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_util::{ready, TryFuture}; 6 | use pin_project::pin_project; 7 | 8 | use super::{Filter, FilterBase, Internal, Tuple}; 9 | 10 | #[derive(Clone, Copy, Debug)] 11 | pub struct UntupleOne { 12 | pub(super) filter: F, 13 | } 14 | 15 | impl FilterBase for UntupleOne 16 | where 17 | F: Filter, 18 | T: Tuple, 19 | { 20 | type Extract = T; 21 | type Error = F::Error; 22 | type Future = UntupleOneFuture; 23 | #[inline] 24 | fn filter(&self, _: Internal) -> Self::Future { 25 | UntupleOneFuture { 26 | extract: self.filter.filter(Internal), 27 | } 28 | } 29 | } 30 | 31 | #[allow(missing_debug_implementations)] 32 | #[pin_project] 33 | pub struct UntupleOneFuture { 34 | #[pin] 35 | extract: F::Future, 36 | } 37 | 38 | impl Future for UntupleOneFuture 39 | where 40 | F: Filter, 41 | T: Tuple, 42 | { 43 | type Output = Result; 44 | 45 | #[inline] 46 | fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 47 | match ready!(self.project().extract.try_poll(cx)) { 48 | Ok((t,)) => Poll::Ready(Ok(t)), 49 | Err(err) => Poll::Ready(Err(err)), 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/filter/wrap.rs: -------------------------------------------------------------------------------- 1 | use super::Filter; 2 | 3 | pub trait WrapSealed { 4 | type Wrapped: Filter; 5 | 6 | fn wrap(&self, filter: F) -> Self::Wrapped; 7 | } 8 | 9 | impl<'a, T, F> WrapSealed for &'a T 10 | where 11 | T: WrapSealed, 12 | F: Filter, 13 | { 14 | type Wrapped = T::Wrapped; 15 | fn wrap(&self, filter: F) -> Self::Wrapped { 16 | (*self).wrap(filter) 17 | } 18 | } 19 | 20 | pub trait Wrap: WrapSealed {} 21 | 22 | impl Wrap for T 23 | where 24 | T: WrapSealed, 25 | F: Filter, 26 | { 27 | } 28 | 29 | /// Combines received filter with pre and after filters 30 | /// 31 | /// # Example 32 | /// 33 | /// ``` 34 | /// use crate::warp::Filter; 35 | /// 36 | /// let route = warp::any() 37 | /// .map(|| "hello world") 38 | /// .with(warp::wrap_fn(|filter| filter)); 39 | /// ``` 40 | /// 41 | /// You can find the full example in the [usage example](https://github.com/seanmonstar/warp/blob/master/examples/wrapping.rs). 42 | pub fn wrap_fn(func: F) -> WrapFn 43 | where 44 | F: Fn(T) -> U, 45 | T: Filter, 46 | U: Filter, 47 | { 48 | WrapFn { func } 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct WrapFn { 53 | func: F, 54 | } 55 | 56 | impl WrapSealed for WrapFn 57 | where 58 | F: Fn(T) -> U, 59 | T: Filter, 60 | U: Filter, 61 | { 62 | type Wrapped = U; 63 | 64 | fn wrap(&self, filter: T) -> Self::Wrapped { 65 | (self.func)(filter) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/filters/addr.rs: -------------------------------------------------------------------------------- 1 | //! Socket Address filters. 2 | 3 | use std::convert::Infallible; 4 | use std::net::SocketAddr; 5 | 6 | use crate::filter::{filter_fn_one, Filter}; 7 | 8 | /// Creates a `Filter` to get the remote address of the connection. 9 | /// 10 | /// If the underlying transport doesn't use socket addresses, this will yield 11 | /// `None`. 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use std::net::SocketAddr; 17 | /// use warp::Filter; 18 | /// 19 | /// let route = warp::addr::remote() 20 | /// .map(|addr: Option| { 21 | /// println!("remote address = {:?}", addr); 22 | /// }); 23 | /// ``` 24 | pub fn remote() -> impl Filter,), Error = Infallible> + Copy { 25 | filter_fn_one(|route| futures_util::future::ok(route.remote_addr())) 26 | } 27 | -------------------------------------------------------------------------------- /src/filters/any.rs: -------------------------------------------------------------------------------- 1 | //! A filter that matches any route. 2 | use std::convert::Infallible; 3 | use std::future::Future; 4 | use std::pin::Pin; 5 | use std::task::{Context, Poll}; 6 | 7 | use crate::filter::{Filter, FilterBase, Internal}; 8 | 9 | /// A [`Filter`](crate::Filter) that matches any route. 10 | /// 11 | /// This can be a useful building block to build new filters from, 12 | /// since [`Filter`] is otherwise a sealed trait. 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use warp::Filter; 18 | /// 19 | /// let route = warp::any() 20 | /// .map(|| { 21 | /// "I always return this string!" 22 | /// }); 23 | /// ``` 24 | /// 25 | /// This could allow creating a single `impl Filter` returning a specific 26 | /// reply, that can then be used as the end of several different filter 27 | /// chains. 28 | /// 29 | /// Another use case is turning some clone-able resource into a `Filter`, 30 | /// thus allowing to easily `and` it together with others. 31 | /// 32 | /// ``` 33 | /// use std::sync::Arc; 34 | /// use warp::Filter; 35 | /// 36 | /// let state = Arc::new(vec![33, 41]); 37 | /// let with_state = warp::any().map(move || state.clone()); 38 | /// 39 | /// // Now we could `and` with any other filter: 40 | /// 41 | /// let route = warp::path::param() 42 | /// .and(with_state) 43 | /// .map(|param_id: u32, db: Arc>| { 44 | /// db.contains(¶m_id) 45 | /// }); 46 | /// ``` 47 | pub fn any() -> impl Filter + Copy { 48 | Any 49 | } 50 | 51 | #[derive(Copy, Clone)] 52 | #[allow(missing_debug_implementations)] 53 | struct Any; 54 | 55 | impl FilterBase for Any { 56 | type Extract = (); 57 | type Error = Infallible; 58 | type Future = AnyFut; 59 | 60 | #[inline] 61 | fn filter(&self, _: Internal) -> Self::Future { 62 | AnyFut 63 | } 64 | } 65 | 66 | #[allow(missing_debug_implementations)] 67 | struct AnyFut; 68 | 69 | impl Future for AnyFut { 70 | type Output = Result<(), Infallible>; 71 | 72 | #[inline] 73 | fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll { 74 | Poll::Ready(Ok(())) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/filters/cookie.rs: -------------------------------------------------------------------------------- 1 | //! Cookie Filters 2 | 3 | use futures_util::future; 4 | use headers::Cookie; 5 | 6 | use super::header; 7 | use crate::filter::{Filter, One}; 8 | use crate::reject::Rejection; 9 | use std::convert::Infallible; 10 | use std::str::FromStr; 11 | 12 | /// Creates a `Filter` that requires a cookie by name. 13 | /// 14 | /// If found, extracts the value of the cookie, otherwise rejects. 15 | pub fn cookie(name: &'static str) -> impl Filter, Error = Rejection> + Copy 16 | where 17 | T: FromStr + Send + 'static, 18 | { 19 | header::header2().and_then(move |cookie: Cookie| { 20 | let cookie = cookie 21 | .get(name) 22 | .ok_or_else(|| crate::reject::missing_cookie(name)) 23 | .and_then(|s| T::from_str(s).map_err(|_| crate::reject::missing_cookie(name))); 24 | future::ready(cookie) 25 | }) 26 | } 27 | 28 | /// Creates a `Filter` that looks for an optional cookie by name. 29 | /// 30 | /// If found, extracts the value of the cookie, otherwise continues 31 | /// the request, extracting `None`. 32 | pub fn optional( 33 | name: &'static str, 34 | ) -> impl Filter>, Error = Infallible> + Copy 35 | where 36 | T: FromStr + Send + 'static, 37 | { 38 | header::optional2().map(move |opt: Option| { 39 | let cookie = opt.and_then(|cookie| cookie.get(name).map(|x| T::from_str(x))); 40 | match cookie { 41 | Some(Ok(t)) => Some(t), 42 | Some(Err(_)) => None, 43 | None => None, 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/filters/ext.rs: -------------------------------------------------------------------------------- 1 | //! Request Extensions 2 | 3 | use std::convert::Infallible; 4 | 5 | use futures_util::future; 6 | 7 | use crate::filter::{filter_fn_one, Filter}; 8 | use crate::reject::{self, Rejection}; 9 | 10 | /// Get a previously set extension of the current route. 11 | /// 12 | /// If the extension doesn't exist, this rejects with a `MissingExtension`. 13 | pub fn get( 14 | ) -> impl Filter + Copy { 15 | filter_fn_one(|route| { 16 | let route = route 17 | .extensions() 18 | .get::() 19 | .cloned() 20 | .ok_or_else(|| reject::known(MissingExtension { _p: () })); 21 | future::ready(route) 22 | }) 23 | } 24 | 25 | /// Get a previously set extension of the current route. 26 | /// 27 | /// If the extension doesn't exist, it yields `None`. 28 | pub fn optional( 29 | ) -> impl Filter,), Error = Infallible> + Copy { 30 | filter_fn_one(|route| future::ok(route.extensions().get::().cloned())) 31 | } 32 | 33 | unit_error! { 34 | /// An error used to reject if `get` cannot find the extension. 35 | pub MissingExtension: "Missing request extension" 36 | } 37 | -------------------------------------------------------------------------------- /src/filters/header.rs: -------------------------------------------------------------------------------- 1 | //! Header Filters 2 | //! 3 | //! These filters are used to interact with the Request HTTP headers. Some 4 | //! of them, like `exact` and `exact_ignore_case`, are just predicates, 5 | //! they don't extract any values. The `header` filter allows parsing 6 | //! a type from any header. 7 | use std::convert::Infallible; 8 | use std::str::FromStr; 9 | 10 | use futures_util::future; 11 | use headers::{Header, HeaderMapExt}; 12 | use http::header::HeaderValue; 13 | use http::HeaderMap; 14 | 15 | use crate::filter::{filter_fn, filter_fn_one, Filter, One}; 16 | use crate::reject::{self, Rejection}; 17 | 18 | /// Create a `Filter` that tries to parse the specified header. 19 | /// 20 | /// This `Filter` will look for a header with supplied name, and try to 21 | /// parse to a `T`, otherwise rejects the request. 22 | /// 23 | /// # Example 24 | /// 25 | /// ``` 26 | /// use std::net::SocketAddr; 27 | /// 28 | /// // Parse `content-length: 100` as a `u64` 29 | /// let content_length = warp::header::("content-length"); 30 | /// 31 | /// // Parse `host: 127.0.0.1:8080` as a `SocketAddr 32 | /// let local_host = warp::header::("host"); 33 | /// 34 | /// // Parse `foo: bar` into a `String` 35 | /// let foo = warp::header::("foo"); 36 | /// ``` 37 | pub fn header( 38 | name: &'static str, 39 | ) -> impl Filter, Error = Rejection> + Copy { 40 | filter_fn_one(move |route| { 41 | tracing::trace!("header({:?})", name); 42 | let route = route 43 | .headers() 44 | .get(name) 45 | .ok_or_else(|| reject::missing_header(name)) 46 | .and_then(|value| value.to_str().map_err(|_| reject::invalid_header(name))) 47 | .and_then(|s| T::from_str(s).map_err(|_| reject::invalid_header(name))); 48 | future::ready(route) 49 | }) 50 | } 51 | 52 | pub(crate) fn header2( 53 | ) -> impl Filter, Error = Rejection> + Copy { 54 | filter_fn_one(move |route| { 55 | tracing::trace!("header2({:?})", T::name()); 56 | let route = route 57 | .headers() 58 | .typed_get() 59 | .ok_or_else(|| reject::invalid_header(T::name().as_str())); 60 | future::ready(route) 61 | }) 62 | } 63 | 64 | /// Create a `Filter` that tries to parse the specified header, if it exists. 65 | /// 66 | /// If the header does not exist, it yields `None`. Otherwise, it will try to 67 | /// parse as a `T`, and if it fails, a invalid header rejection is return. If 68 | /// successful, the filter yields `Some(T)`. 69 | /// 70 | /// # Example 71 | /// 72 | /// ``` 73 | /// // Grab the `authorization` header if it exists. 74 | /// let opt_auth = warp::header::optional::("authorization"); 75 | /// ``` 76 | pub fn optional( 77 | name: &'static str, 78 | ) -> impl Filter>, Error = Rejection> + Copy 79 | where 80 | T: FromStr + Send + 'static, 81 | { 82 | filter_fn_one(move |route| { 83 | tracing::trace!("optional({:?})", name); 84 | let result = route.headers().get(name).map(|value| { 85 | value 86 | .to_str() 87 | .map_err(|_| reject::invalid_header(name))? 88 | .parse::() 89 | .map_err(|_| reject::invalid_header(name)) 90 | }); 91 | 92 | match result { 93 | Some(Ok(t)) => future::ok(Some(t)), 94 | Some(Err(e)) => future::err(e), 95 | None => future::ok(None), 96 | } 97 | }) 98 | } 99 | 100 | pub(crate) fn optional2() -> impl Filter>, Error = Infallible> + Copy 101 | where 102 | T: Header + Send + 'static, 103 | { 104 | filter_fn_one(move |route| future::ready(Ok(route.headers().typed_get()))) 105 | } 106 | 107 | /* TODO 108 | pub fn exact2(header: T) -> impl FilterClone 109 | where 110 | T: Header + PartialEq + Clone + Send, 111 | { 112 | filter_fn(move |route| { 113 | tracing::trace!("exact2({:?})", T::NAME); 114 | route.headers() 115 | .typed_get::() 116 | .and_then(|val| if val == header { 117 | Some(()) 118 | } else { 119 | None 120 | }) 121 | .ok_or_else(|| reject::bad_request()) 122 | }) 123 | } 124 | */ 125 | 126 | /// Create a `Filter` that requires a header to match the value exactly. 127 | /// 128 | /// This `Filter` will look for a header with supplied name and the exact 129 | /// value, otherwise rejects the request. 130 | /// 131 | /// # Example 132 | /// 133 | /// ``` 134 | /// // Require `dnt: 1` header to be set. 135 | /// let must_dnt = warp::header::exact("dnt", "1"); 136 | /// ``` 137 | pub fn exact( 138 | name: &'static str, 139 | value: &'static str, 140 | ) -> impl Filter + Copy { 141 | filter_fn(move |route| { 142 | tracing::trace!("exact?({:?}, {:?})", name, value); 143 | let route = route 144 | .headers() 145 | .get(name) 146 | .ok_or_else(|| reject::missing_header(name)) 147 | .and_then(|val| { 148 | if val == value { 149 | Ok(()) 150 | } else { 151 | Err(reject::invalid_header(name)) 152 | } 153 | }); 154 | future::ready(route) 155 | }) 156 | } 157 | 158 | /// Create a `Filter` that requires a header to match the value exactly. 159 | /// 160 | /// This `Filter` will look for a header with supplied name and the exact 161 | /// value, ignoring ASCII case, otherwise rejects the request. 162 | /// 163 | /// # Example 164 | /// 165 | /// ``` 166 | /// // Require `connection: keep-alive` header to be set. 167 | /// let keep_alive = warp::header::exact_ignore_case("connection", "keep-alive"); 168 | /// ``` 169 | pub fn exact_ignore_case( 170 | name: &'static str, 171 | value: &'static str, 172 | ) -> impl Filter + Copy { 173 | filter_fn(move |route| { 174 | tracing::trace!("exact_ignore_case({:?}, {:?})", name, value); 175 | let route = route 176 | .headers() 177 | .get(name) 178 | .ok_or_else(|| reject::missing_header(name)) 179 | .and_then(|val| { 180 | if val.as_bytes().eq_ignore_ascii_case(value.as_bytes()) { 181 | Ok(()) 182 | } else { 183 | Err(reject::invalid_header(name)) 184 | } 185 | }); 186 | future::ready(route) 187 | }) 188 | } 189 | 190 | /// Create a `Filter` that gets a `HeaderValue` for the name. 191 | /// 192 | /// # Example 193 | /// 194 | /// ``` 195 | /// use warp::{Filter, http::header::HeaderValue}; 196 | /// 197 | /// let filter = warp::header::value("x-token") 198 | /// .map(|value: HeaderValue| { 199 | /// format!("header value bytes: {:?}", value) 200 | /// }); 201 | /// ``` 202 | pub fn value( 203 | name: &'static str, 204 | ) -> impl Filter, Error = Rejection> + Copy { 205 | filter_fn_one(move |route| { 206 | tracing::trace!("value({:?})", name); 207 | let route = route 208 | .headers() 209 | .get(name) 210 | .cloned() 211 | .ok_or_else(|| reject::missing_header(name)); 212 | future::ready(route) 213 | }) 214 | } 215 | 216 | /// Create a `Filter` that returns a clone of the request's `HeaderMap`. 217 | /// 218 | /// # Example 219 | /// 220 | /// ``` 221 | /// use warp::{Filter, http::HeaderMap}; 222 | /// 223 | /// let headers = warp::header::headers_cloned() 224 | /// .map(|headers: HeaderMap| { 225 | /// format!("header count: {}", headers.len()) 226 | /// }); 227 | /// ``` 228 | pub fn headers_cloned() -> impl Filter, Error = Infallible> + Copy { 229 | filter_fn_one(|route| future::ok(route.headers().clone())) 230 | } 231 | -------------------------------------------------------------------------------- /src/filters/host.rs: -------------------------------------------------------------------------------- 1 | //! Host ("authority") filter 2 | //! 3 | use crate::filter::{filter_fn_one, Filter, One}; 4 | use crate::reject::{self, Rejection}; 5 | use futures_util::future; 6 | pub use http::uri::Authority; 7 | use std::str::FromStr; 8 | 9 | /// Creates a `Filter` that requires a specific authority (target server's 10 | /// host and port) in the request. 11 | /// 12 | /// Authority is specified either in the `Host` header or in the target URI. 13 | /// 14 | /// # Example 15 | /// 16 | /// ``` 17 | /// use warp::Filter; 18 | /// 19 | /// let multihost = 20 | /// warp::host::exact("foo.com").map(|| "you've reached foo.com") 21 | /// .or(warp::host::exact("bar.com").map(|| "you've reached bar.com")); 22 | /// ``` 23 | pub fn exact(expected: &str) -> impl Filter + Clone { 24 | let expected = Authority::from_str(expected).expect("invalid host/authority"); 25 | optional() 26 | .and_then(move |option: Option| match option { 27 | Some(authority) if authority == expected => future::ok(()), 28 | _ => future::err(reject::not_found()), 29 | }) 30 | .untuple_one() 31 | } 32 | 33 | /// Creates a `Filter` that looks for an authority (target server's host 34 | /// and port) in the request. 35 | /// 36 | /// Authority is specified either in the `Host` header or in the target URI. 37 | /// 38 | /// If found, extracts the `Authority`, otherwise continues the request, 39 | /// extracting `None`. 40 | /// 41 | /// Rejects with `400 Bad Request` if the `Host` header is malformed or if there 42 | /// is a mismatch between the `Host` header and the target URI. 43 | /// 44 | /// # Example 45 | /// 46 | /// ``` 47 | /// use warp::{Filter, host::Authority}; 48 | /// 49 | /// let host = warp::host::optional() 50 | /// .map(|authority: Option| { 51 | /// if let Some(a) = authority { 52 | /// format!("{} is currently not at home", a.host()) 53 | /// } else { 54 | /// "please state who you're trying to reach".to_owned() 55 | /// } 56 | /// }); 57 | /// ``` 58 | pub fn optional() -> impl Filter>, Error = Rejection> + Copy { 59 | filter_fn_one(move |route| { 60 | // The authority can be sent by clients in various ways: 61 | // 62 | // 1) in the "target URI" 63 | // a) serialized in the start line (HTTP/1.1 proxy requests) 64 | // b) serialized in `:authority` pseudo-header (HTTP/2 generated - "SHOULD") 65 | // 2) in the `Host` header (HTTP/1.1 origin requests, HTTP/2 converted) 66 | // 67 | // Hyper transparently handles 1a/1b, but not 2, so we must look at both. 68 | 69 | let from_uri = route.uri().authority(); 70 | 71 | let name = "host"; 72 | let from_header = route.headers() 73 | .get(name) 74 | .map(|value| 75 | // Header present, parse it 76 | value.to_str().map_err(|_| reject::invalid_header(name)) 77 | .and_then(|value| Authority::from_str(value).map_err(|_| reject::invalid_header(name))) 78 | ); 79 | 80 | future::ready(match (from_uri, from_header) { 81 | // no authority in the request (HTTP/1.0 or non-conforming) 82 | (None, None) => Ok(None), 83 | 84 | // authority specified in either or both matching 85 | (Some(a), None) => Ok(Some(a.clone())), 86 | (None, Some(Ok(a))) => Ok(Some(a)), 87 | (Some(a), Some(Ok(b))) if *a == b => Ok(Some(b)), 88 | 89 | // mismatch 90 | (Some(_), Some(Ok(_))) => Err(reject::invalid_header(name)), 91 | 92 | // parse error 93 | (_, Some(Err(r))) => Err(r), 94 | }) 95 | }) 96 | } 97 | -------------------------------------------------------------------------------- /src/filters/log.rs: -------------------------------------------------------------------------------- 1 | //! Logger Filters 2 | 3 | use std::fmt; 4 | use std::net::SocketAddr; 5 | use std::time::{Duration, Instant}; 6 | 7 | use http::{header, StatusCode}; 8 | 9 | use crate::filter::{Filter, WrapSealed}; 10 | use crate::reject::IsReject; 11 | use crate::reply::Reply; 12 | use crate::route::Route; 13 | 14 | use self::internal::WithLog; 15 | 16 | /// Create a wrapping [`Filter`](crate::Filter) with the specified `name` as the `target`. 17 | /// 18 | /// This uses the default access logging format, and log records produced 19 | /// will have their `target` set to `name`. 20 | /// 21 | /// # Example 22 | /// 23 | /// ``` 24 | /// use warp::Filter; 25 | /// 26 | /// // If using something like `pretty_env_logger`, 27 | /// // view logs by setting `RUST_LOG=example::api`. 28 | /// let log = warp::log("example::api"); 29 | /// let route = warp::any() 30 | /// .map(warp::reply) 31 | /// .with(log); 32 | /// ``` 33 | pub fn log(name: &'static str) -> Log) + Copy> { 34 | let func = move |info: Info<'_>| { 35 | // TODO? 36 | // - response content length? 37 | log::info!( 38 | target: name, 39 | "{} \"{} {} {:?}\" {} \"{}\" \"{}\" {:?}", 40 | OptFmt(info.route.remote_addr()), 41 | info.method(), 42 | info.path(), 43 | info.route.version(), 44 | info.status().as_u16(), 45 | OptFmt(info.referer()), 46 | OptFmt(info.user_agent()), 47 | info.elapsed(), 48 | ); 49 | }; 50 | Log { func } 51 | } 52 | 53 | /// Create a wrapping [`Filter`](crate::Filter) that receives `warp::log::Info`. 54 | /// 55 | /// # Example 56 | /// 57 | /// ``` 58 | /// use warp::Filter; 59 | /// 60 | /// let log = warp::log::custom(|info| { 61 | /// // Use a log macro, or slog, or println, or whatever! 62 | /// eprintln!( 63 | /// "{} {} {}", 64 | /// info.method(), 65 | /// info.path(), 66 | /// info.status(), 67 | /// ); 68 | /// }); 69 | /// let route = warp::any() 70 | /// .map(warp::reply) 71 | /// .with(log); 72 | /// ``` 73 | pub fn custom(func: F) -> Log 74 | where 75 | F: Fn(Info<'_>), 76 | { 77 | Log { func } 78 | } 79 | 80 | /// Decorates a [`Filter`] to log requests and responses. 81 | #[derive(Clone, Copy, Debug)] 82 | pub struct Log { 83 | func: F, 84 | } 85 | 86 | /// Information about the request/response that can be used to prepare log lines. 87 | #[allow(missing_debug_implementations)] 88 | pub struct Info<'a> { 89 | route: &'a Route, 90 | start: Instant, 91 | status: StatusCode, 92 | } 93 | 94 | impl WrapSealed for Log 95 | where 96 | FN: Fn(Info<'_>) + Clone + Send, 97 | F: Filter + Clone + Send, 98 | F::Extract: Reply, 99 | F::Error: IsReject, 100 | { 101 | type Wrapped = WithLog; 102 | 103 | fn wrap(&self, filter: F) -> Self::Wrapped { 104 | WithLog { 105 | filter, 106 | log: self.clone(), 107 | } 108 | } 109 | } 110 | 111 | impl<'a> Info<'a> { 112 | /// View the remote `SocketAddr` of the request. 113 | pub fn remote_addr(&self) -> Option { 114 | self.route.remote_addr() 115 | } 116 | 117 | /// View the `http::Method` of the request. 118 | pub fn method(&self) -> &http::Method { 119 | self.route.method() 120 | } 121 | 122 | /// View the URI path of the request. 123 | pub fn path(&self) -> &str { 124 | self.route.full_path() 125 | } 126 | 127 | /// View the `http::Version` of the request. 128 | pub fn version(&self) -> http::Version { 129 | self.route.version() 130 | } 131 | 132 | /// View the `http::StatusCode` of the response. 133 | pub fn status(&self) -> http::StatusCode { 134 | self.status 135 | } 136 | 137 | /// View the referer of the request. 138 | pub fn referer(&self) -> Option<&str> { 139 | self.route 140 | .headers() 141 | .get(header::REFERER) 142 | .and_then(|v| v.to_str().ok()) 143 | } 144 | 145 | /// View the user agent of the request. 146 | pub fn user_agent(&self) -> Option<&str> { 147 | self.route 148 | .headers() 149 | .get(header::USER_AGENT) 150 | .and_then(|v| v.to_str().ok()) 151 | } 152 | 153 | /// View the `Duration` that elapsed for the request. 154 | pub fn elapsed(&self) -> Duration { 155 | tokio::time::Instant::now().into_std() - self.start 156 | } 157 | 158 | /// View the host of the request 159 | pub fn host(&self) -> Option<&str> { 160 | self.route 161 | .headers() 162 | .get(header::HOST) 163 | .and_then(|v| v.to_str().ok()) 164 | } 165 | 166 | /// Access the full headers of the request 167 | pub fn request_headers(&self) -> &http::HeaderMap { 168 | self.route.headers() 169 | } 170 | } 171 | 172 | struct OptFmt(Option); 173 | 174 | impl fmt::Display for OptFmt { 175 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 176 | if let Some(ref t) = self.0 { 177 | fmt::Display::fmt(t, f) 178 | } else { 179 | f.write_str("-") 180 | } 181 | } 182 | } 183 | 184 | mod internal { 185 | use std::future::Future; 186 | use std::pin::Pin; 187 | use std::task::{Context, Poll}; 188 | use std::time::Instant; 189 | 190 | use futures_util::{ready, TryFuture}; 191 | use pin_project::pin_project; 192 | 193 | use super::{Info, Log}; 194 | use crate::filter::{Filter, FilterBase, Internal}; 195 | use crate::reject::IsReject; 196 | use crate::reply::{Reply, Response}; 197 | use crate::route; 198 | 199 | #[allow(missing_debug_implementations)] 200 | pub struct Logged(pub(super) Response); 201 | 202 | impl Reply for Logged { 203 | #[inline] 204 | fn into_response(self) -> Response { 205 | self.0 206 | } 207 | } 208 | 209 | #[allow(missing_debug_implementations)] 210 | #[derive(Clone, Copy)] 211 | pub struct WithLog { 212 | pub(super) filter: F, 213 | pub(super) log: Log, 214 | } 215 | 216 | impl FilterBase for WithLog 217 | where 218 | FN: Fn(Info<'_>) + Clone + Send, 219 | F: Filter + Clone + Send, 220 | F::Extract: Reply, 221 | F::Error: IsReject, 222 | { 223 | type Extract = (Logged,); 224 | type Error = F::Error; 225 | type Future = WithLogFuture; 226 | 227 | fn filter(&self, _: Internal) -> Self::Future { 228 | let started = tokio::time::Instant::now().into_std(); 229 | WithLogFuture { 230 | log: self.log.clone(), 231 | future: self.filter.filter(Internal), 232 | started, 233 | } 234 | } 235 | } 236 | 237 | #[allow(missing_debug_implementations)] 238 | #[pin_project] 239 | pub struct WithLogFuture { 240 | log: Log, 241 | #[pin] 242 | future: F, 243 | started: Instant, 244 | } 245 | 246 | impl Future for WithLogFuture 247 | where 248 | FN: Fn(Info<'_>), 249 | F: TryFuture, 250 | F::Ok: Reply, 251 | F::Error: IsReject, 252 | { 253 | type Output = Result<(Logged,), F::Error>; 254 | 255 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 256 | let pin = self.as_mut().project(); 257 | let (result, status) = match ready!(pin.future.try_poll(cx)) { 258 | Ok(reply) => { 259 | let resp = reply.into_response(); 260 | let status = resp.status(); 261 | (Poll::Ready(Ok((Logged(resp),))), status) 262 | } 263 | Err(reject) => { 264 | let status = reject.status(); 265 | (Poll::Ready(Err(reject)), status) 266 | } 267 | }; 268 | 269 | route::with(|route| { 270 | (self.log.func)(Info { 271 | route, 272 | start: self.started, 273 | status, 274 | }); 275 | }); 276 | 277 | result 278 | } 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/filters/method.rs: -------------------------------------------------------------------------------- 1 | //! HTTP Method filters. 2 | //! 3 | //! The filters deal with the HTTP Method part of a request. Several here will 4 | //! match the request `Method`, and if not matched, will reject the request 5 | //! with a `405 Method Not Allowed`. 6 | //! 7 | //! There is also [`warp::method()`](method), which never rejects 8 | //! a request, and just extracts the method to be used in your filter chains. 9 | use futures_util::future; 10 | use http::Method; 11 | 12 | use crate::filter::{filter_fn, filter_fn_one, Filter, One}; 13 | use crate::reject::Rejection; 14 | use std::convert::Infallible; 15 | 16 | /// Create a `Filter` that requires the request method to be `GET`. 17 | /// 18 | /// # Example 19 | /// 20 | /// ``` 21 | /// use warp::Filter; 22 | /// 23 | /// let get_only = warp::get().map(warp::reply); 24 | /// ``` 25 | pub fn get() -> impl Filter + Copy { 26 | method_is(|| &Method::GET) 27 | } 28 | 29 | /// Create a `Filter` that requires the request method to be `POST`. 30 | /// 31 | /// # Example 32 | /// 33 | /// ``` 34 | /// use warp::Filter; 35 | /// 36 | /// let post_only = warp::post().map(warp::reply); 37 | /// ``` 38 | pub fn post() -> impl Filter + Copy { 39 | method_is(|| &Method::POST) 40 | } 41 | 42 | /// Create a `Filter` that requires the request method to be `PUT`. 43 | /// 44 | /// # Example 45 | /// 46 | /// ``` 47 | /// use warp::Filter; 48 | /// 49 | /// let put_only = warp::put().map(warp::reply); 50 | /// ``` 51 | pub fn put() -> impl Filter + Copy { 52 | method_is(|| &Method::PUT) 53 | } 54 | 55 | /// Create a `Filter` that requires the request method to be `DELETE`. 56 | /// 57 | /// # Example 58 | /// 59 | /// ``` 60 | /// use warp::Filter; 61 | /// 62 | /// let delete_only = warp::delete().map(warp::reply); 63 | /// ``` 64 | pub fn delete() -> impl Filter + Copy { 65 | method_is(|| &Method::DELETE) 66 | } 67 | 68 | /// Create a `Filter` that requires the request method to be `HEAD`. 69 | /// 70 | /// # Example 71 | /// 72 | /// ``` 73 | /// use warp::Filter; 74 | /// 75 | /// let head_only = warp::head().map(warp::reply); 76 | /// ``` 77 | pub fn head() -> impl Filter + Copy { 78 | method_is(|| &Method::HEAD) 79 | } 80 | 81 | /// Create a `Filter` that requires the request method to be `OPTIONS`. 82 | /// 83 | /// # Example 84 | /// 85 | /// ``` 86 | /// use warp::Filter; 87 | /// 88 | /// let options_only = warp::options().map(warp::reply); 89 | /// ``` 90 | pub fn options() -> impl Filter + Copy { 91 | method_is(|| &Method::OPTIONS) 92 | } 93 | 94 | /// Create a `Filter` that requires the request method to be `PATCH`. 95 | /// 96 | /// # Example 97 | /// 98 | /// ``` 99 | /// use warp::Filter; 100 | /// 101 | /// let patch_only = warp::patch().map(warp::reply); 102 | /// ``` 103 | pub fn patch() -> impl Filter + Copy { 104 | method_is(|| &Method::PATCH) 105 | } 106 | 107 | /// Extract the `Method` from the request. 108 | /// 109 | /// This never rejects a request. 110 | /// 111 | /// # Example 112 | /// 113 | /// ``` 114 | /// use warp::Filter; 115 | /// 116 | /// let route = warp::method() 117 | /// .map(|method| { 118 | /// format!("You sent a {} request!", method) 119 | /// }); 120 | /// ``` 121 | pub fn method() -> impl Filter, Error = Infallible> + Copy { 122 | filter_fn_one(|route| future::ok::<_, Infallible>(route.method().clone())) 123 | } 124 | 125 | // NOTE: This takes a static function instead of `&'static Method` directly 126 | // so that the `impl Filter` can be zero-sized. Moving it around should be 127 | // cheaper than holding a single static pointer (which would make it 1 word). 128 | fn method_is(func: F) -> impl Filter + Copy 129 | where 130 | F: Fn() -> &'static Method + Copy, 131 | { 132 | filter_fn(move |route| { 133 | let method = func(); 134 | tracing::trace!("method::{:?}?: {:?}", method, route.method()); 135 | if route.method() == method { 136 | future::ok(()) 137 | } else { 138 | future::err(crate::reject::method_not_allowed()) 139 | } 140 | }) 141 | } 142 | 143 | #[cfg(test)] 144 | mod tests { 145 | #[test] 146 | fn method_size_of() { 147 | // See comment on `method_is` function. 148 | assert_eq!(std::mem::size_of_val(&super::get()), 0,); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/filters/mod.rs: -------------------------------------------------------------------------------- 1 | //! Built-in Filters 2 | //! 3 | //! This module mostly serves as documentation to group together the list of 4 | //! built-in filters. Most of these are available at more convenient paths. 5 | 6 | pub mod addr; 7 | pub mod any; 8 | pub mod body; 9 | #[cfg(any(feature = "compression-brotli", feature = "compression-gzip"))] 10 | pub mod compression; 11 | pub mod cookie; 12 | pub mod cors; 13 | pub mod ext; 14 | pub mod fs; 15 | pub mod header; 16 | pub mod host; 17 | pub mod log; 18 | pub mod method; 19 | #[cfg(feature = "multipart")] 20 | pub mod multipart; 21 | pub mod path; 22 | pub mod query; 23 | pub mod reply; 24 | pub mod sse; 25 | pub mod trace; 26 | #[cfg(feature = "websocket")] 27 | pub mod ws; 28 | 29 | pub use crate::filter::BoxedFilter; 30 | -------------------------------------------------------------------------------- /src/filters/multipart.rs: -------------------------------------------------------------------------------- 1 | //! Multipart body filters 2 | //! 3 | //! [`Filter`](crate::Filter)s that extract a multipart body for a route. 4 | 5 | use std::error::Error as StdError; 6 | use std::fmt::{Display, Formatter}; 7 | use std::future::Future; 8 | use std::pin::Pin; 9 | use std::task::{Context, Poll}; 10 | use std::{fmt, io}; 11 | 12 | use bytes::{Buf, Bytes}; 13 | use futures_util::{future, Stream}; 14 | use headers::ContentType; 15 | use hyper::Body; 16 | use mime::Mime; 17 | use multer::{Field as PartInner, Multipart as FormDataInner}; 18 | 19 | use crate::filter::{Filter, FilterBase, Internal}; 20 | use crate::reject::{self, Rejection}; 21 | 22 | // If not otherwise configured, default to 2MB. 23 | const DEFAULT_FORM_DATA_MAX_LENGTH: u64 = 1024 * 1024 * 2; 24 | 25 | /// A [`Filter`](crate::Filter) to extract a `multipart/form-data` body from a request. 26 | /// 27 | /// Create with the `warp::multipart::form()` function. 28 | #[derive(Debug, Clone)] 29 | pub struct FormOptions { 30 | max_length: Option, 31 | } 32 | 33 | /// A `Stream` of multipart/form-data `Part`s. 34 | /// 35 | /// Extracted with a `warp::multipart::form` filter. 36 | pub struct FormData { 37 | inner: FormDataInner<'static>, 38 | } 39 | 40 | /// A single "part" of a multipart/form-data body. 41 | /// 42 | /// Yielded from the `FormData` stream. 43 | pub struct Part { 44 | part: PartInner<'static>, 45 | } 46 | 47 | /// Create a [`Filter`](crate::Filter) to extract a `multipart/form-data` body from a request. 48 | /// 49 | /// The extracted `FormData` type is a `Stream` of `Part`s, and each `Part` 50 | /// in turn is a `Stream` of bytes. 51 | pub fn form() -> FormOptions { 52 | FormOptions { 53 | max_length: Some(DEFAULT_FORM_DATA_MAX_LENGTH), 54 | } 55 | } 56 | 57 | // ===== impl Form ===== 58 | 59 | impl FormOptions { 60 | /// Set the maximum byte length allowed for this body. 61 | /// 62 | /// `max_length(None)` means that maximum byte length is not checked. 63 | /// Defaults to 2MB. 64 | pub fn max_length(mut self, max: impl Into>) -> Self { 65 | self.max_length = max.into(); 66 | self 67 | } 68 | } 69 | 70 | type FormFut = Pin> + Send>>; 71 | 72 | impl FilterBase for FormOptions { 73 | type Extract = (FormData,); 74 | type Error = Rejection; 75 | type Future = FormFut; 76 | 77 | fn filter(&self, _: Internal) -> Self::Future { 78 | let boundary = super::header::header2::().and_then(|ct| { 79 | let mime = Mime::from(ct); 80 | let mime = mime 81 | .get_param("boundary") 82 | .map(|v| v.to_string()) 83 | .ok_or_else(|| reject::invalid_header("content-type")); 84 | future::ready(mime) 85 | }); 86 | 87 | let filt = boundary 88 | .and(super::body::body()) 89 | .map(|boundary: String, body| { 90 | let body = BodyIoError(body); 91 | FormData { 92 | inner: FormDataInner::new(body, &boundary), 93 | } 94 | }); 95 | 96 | if let Some(max_length) = self.max_length { 97 | Box::pin( 98 | super::body::content_length_limit(max_length) 99 | .and(filt) 100 | .filter(Internal), 101 | ) 102 | } else { 103 | Box::pin(filt.filter(Internal)) 104 | } 105 | } 106 | } 107 | 108 | // ===== impl FormData ===== 109 | 110 | impl fmt::Debug for FormData { 111 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 112 | f.debug_struct("FormData").finish() 113 | } 114 | } 115 | 116 | impl Stream for FormData { 117 | type Item = Result; 118 | 119 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 120 | match self.inner.poll_next_field(cx) { 121 | Poll::Pending => Poll::Pending, 122 | Poll::Ready(Ok(Some(part))) => { 123 | if part.name().is_some() || part.file_name().is_some() { 124 | Poll::Ready(Some(Ok(Part { part }))) 125 | } else { 126 | Poll::Ready(Some(Err(crate::Error::new(MultipartFieldMissingName)))) 127 | } 128 | } 129 | Poll::Ready(Ok(None)) => Poll::Ready(None), 130 | Poll::Ready(Err(err)) => Poll::Ready(Some(Err(crate::Error::new(err)))), 131 | } 132 | } 133 | } 134 | 135 | // ===== impl Part ===== 136 | 137 | impl Part { 138 | /// Get the name of this part. 139 | pub fn name(&self) -> &str { 140 | self.part 141 | .name() 142 | .unwrap_or_else(|| self.part.file_name().expect("checked for name previously")) 143 | } 144 | 145 | /// Get the filename of this part, if present. 146 | pub fn filename(&self) -> Option<&str> { 147 | self.part.file_name() 148 | } 149 | 150 | /// Get the content-type of this part, if present. 151 | pub fn content_type(&self) -> Option<&str> { 152 | let content_type = self.part.content_type(); 153 | content_type.map(|t| t.as_ref()) 154 | } 155 | 156 | /// Asynchronously get some of the data for this `Part`. 157 | pub async fn data(&mut self) -> Option> { 158 | future::poll_fn(|cx| self.poll_next(cx)).await 159 | } 160 | 161 | /// Convert this `Part` into a `Stream` of `Buf`s. 162 | pub fn stream(self) -> impl Stream> { 163 | PartStream(self) 164 | } 165 | 166 | fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { 167 | match Pin::new(&mut self.part).poll_next(cx) { 168 | Poll::Pending => Poll::Pending, 169 | Poll::Ready(Some(Ok(bytes))) => Poll::Ready(Some(Ok(bytes))), 170 | Poll::Ready(None) => Poll::Ready(None), 171 | Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(crate::Error::new(err)))), 172 | } 173 | } 174 | } 175 | 176 | impl fmt::Debug for Part { 177 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 178 | let mut builder = f.debug_struct("Part"); 179 | builder.field("name", &self.name()); 180 | 181 | if let Some(ref filename) = self.part.file_name() { 182 | builder.field("filename", filename); 183 | } 184 | 185 | if let Some(ref mime) = self.part.content_type() { 186 | builder.field("content_type", mime); 187 | } 188 | 189 | builder.finish() 190 | } 191 | } 192 | 193 | struct PartStream(Part); 194 | 195 | impl Stream for PartStream { 196 | type Item = Result; 197 | 198 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 199 | self.0.poll_next(cx) 200 | } 201 | } 202 | 203 | struct BodyIoError(Body); 204 | 205 | impl Stream for BodyIoError { 206 | type Item = io::Result; 207 | 208 | fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 209 | match Pin::new(&mut self.0).poll_next(cx) { 210 | Poll::Pending => Poll::Pending, 211 | Poll::Ready(Some(Ok(bytes))) => Poll::Ready(Some(Ok(bytes))), 212 | Poll::Ready(None) => Poll::Ready(None), 213 | Poll::Ready(Some(Err(err))) => { 214 | Poll::Ready(Some(Err(io::Error::new(io::ErrorKind::Other, err)))) 215 | } 216 | } 217 | } 218 | } 219 | 220 | /// An error used when a multipart field is missing a name. 221 | #[derive(Debug)] 222 | struct MultipartFieldMissingName; 223 | 224 | impl Display for MultipartFieldMissingName { 225 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 226 | write!(f, "Multipart field is missing a name") 227 | } 228 | } 229 | 230 | impl StdError for MultipartFieldMissingName {} 231 | -------------------------------------------------------------------------------- /src/filters/query.rs: -------------------------------------------------------------------------------- 1 | //! Query Filters 2 | 3 | use futures_util::future; 4 | use serde::de::DeserializeOwned; 5 | 6 | use crate::filter::{filter_fn_one, Filter, One}; 7 | use crate::reject::{self, Rejection}; 8 | 9 | /// Creates a `Filter` that decodes query parameters to the type `T`. 10 | /// 11 | /// If cannot decode into a `T`, the request is rejected with a `400 Bad Request`. 12 | /// 13 | /// # Example 14 | /// 15 | /// ``` 16 | /// use std::collections::HashMap; 17 | /// use warp::{ 18 | /// http::Response, 19 | /// Filter, 20 | /// }; 21 | /// 22 | /// let route = warp::any() 23 | /// .and(warp::query::>()) 24 | /// .map(|map: HashMap| { 25 | /// let mut response: Vec = Vec::new(); 26 | /// for (key, value) in map.into_iter() { 27 | /// response.push(format!("{}={}", key, value)) 28 | /// } 29 | /// Response::builder().body(response.join(";")) 30 | /// }); 31 | /// ``` 32 | /// 33 | /// You can define your custom query object and deserialize with [Serde][Serde]. Ensure to include 34 | /// the crate in your dependencies before usage. 35 | /// 36 | /// ``` 37 | /// use serde_derive::{Deserialize, Serialize}; 38 | /// use std::collections::HashMap; 39 | /// use warp::{ 40 | /// http::Response, 41 | /// Filter, 42 | /// }; 43 | /// 44 | /// #[derive(Serialize, Deserialize)] 45 | /// struct FooQuery { 46 | /// foo: Option, 47 | /// bar: u8, 48 | /// } 49 | /// 50 | /// let route = warp::any() 51 | /// .and(warp::query::()) 52 | /// .map(|q: FooQuery| { 53 | /// if let Some(foo) = q.foo { 54 | /// Response::builder().body(format!("foo={}", foo)) 55 | /// } else { 56 | /// Response::builder().body(format!("bar={}", q.bar)) 57 | /// } 58 | /// }); 59 | /// ``` 60 | /// 61 | /// For more examples, please take a look at [examples/query_string.rs](https://github.com/seanmonstar/warp/blob/master/examples/query_string.rs). 62 | /// 63 | /// [Serde]: https://docs.rs/serde 64 | pub fn query( 65 | ) -> impl Filter, Error = Rejection> + Copy { 66 | filter_fn_one(|route| { 67 | let query_string = route.query().unwrap_or_else(|| { 68 | tracing::debug!("route was called without a query string, defaulting to empty"); 69 | "" 70 | }); 71 | 72 | let query_encoded = serde_urlencoded::from_str(query_string).map_err(|e| { 73 | tracing::debug!("failed to decode query string '{}': {:?}", query_string, e); 74 | reject::invalid_query() 75 | }); 76 | future::ready(query_encoded) 77 | }) 78 | } 79 | 80 | /// Creates a `Filter` that returns the raw query string as type String. 81 | pub fn raw() -> impl Filter, Error = Rejection> + Copy { 82 | filter_fn_one(|route| { 83 | let route = route 84 | .query() 85 | .map(|q| q.to_owned()) 86 | .map(Ok) 87 | .unwrap_or_else(|| Err(reject::invalid_query())); 88 | future::ready(route) 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /src/filters/reply.rs: -------------------------------------------------------------------------------- 1 | //! Reply Filters 2 | //! 3 | //! These "filters" behave a little differently than the rest. Instead of 4 | //! being used directly on requests, these filters "wrap" other filters. 5 | //! 6 | //! 7 | //! ## Wrapping a `Filter` (`with`) 8 | //! 9 | //! ``` 10 | //! use warp::Filter; 11 | //! 12 | //! let with_server = warp::reply::with::header("server", "warp"); 13 | //! 14 | //! let route = warp::any() 15 | //! .map(warp::reply) 16 | //! .with(with_server); 17 | //! ``` 18 | //! 19 | //! Wrapping allows adding in conditional logic *before* the request enters 20 | //! the inner filter (though the `with::header` wrapper does not). 21 | 22 | use std::convert::TryFrom; 23 | use std::sync::Arc; 24 | 25 | use http::header::{HeaderMap, HeaderName, HeaderValue}; 26 | 27 | use self::sealed::{WithDefaultHeader_, WithHeader_, WithHeaders_}; 28 | use crate::filter::{Filter, Map, WrapSealed}; 29 | use crate::reply::Reply; 30 | 31 | /// Wrap a [`Filter`] that adds a header to the reply. 32 | /// 33 | /// # Note 34 | /// 35 | /// This **only** adds a header if the underlying filter is successful, and 36 | /// returns a [`Reply`] If the underlying filter was rejected, the 37 | /// header is not added. 38 | /// 39 | /// # Example 40 | /// 41 | /// ``` 42 | /// use warp::Filter; 43 | /// 44 | /// // Always set `foo: bar` header. 45 | /// let route = warp::any() 46 | /// .map(warp::reply) 47 | /// .with(warp::reply::with::header("foo", "bar")); 48 | /// ``` 49 | pub fn header(name: K, value: V) -> WithHeader 50 | where 51 | HeaderName: TryFrom, 52 | >::Error: Into, 53 | HeaderValue: TryFrom, 54 | >::Error: Into, 55 | { 56 | let (name, value) = assert_name_and_value(name, value); 57 | WithHeader { name, value } 58 | } 59 | 60 | /// Wrap a [`Filter`] that adds multiple headers to the reply. 61 | /// 62 | /// # Note 63 | /// 64 | /// This **only** adds a header if the underlying filter is successful, and 65 | /// returns a [`Reply`] If the underlying filter was rejected, the 66 | /// header is not added. 67 | /// 68 | /// # Example 69 | /// 70 | /// ``` 71 | /// use warp::http::header::{HeaderMap, HeaderValue}; 72 | /// use warp::Filter; 73 | /// 74 | /// let mut headers = HeaderMap::new(); 75 | /// headers.insert("server", HeaderValue::from_static("wee/0")); 76 | /// headers.insert("foo", HeaderValue::from_static("bar")); 77 | /// 78 | /// // Always set `server: wee/0` and `foo: bar` headers. 79 | /// let route = warp::any() 80 | /// .map(warp::reply) 81 | /// .with(warp::reply::with::headers(headers)); 82 | /// ``` 83 | pub fn headers(headers: HeaderMap) -> WithHeaders { 84 | WithHeaders { 85 | headers: Arc::new(headers), 86 | } 87 | } 88 | 89 | // pub fn headers? 90 | 91 | /// Wrap a [`Filter`] that adds a header to the reply, if they 92 | /// aren't already set. 93 | /// 94 | /// # Note 95 | /// 96 | /// This **only** adds a header if the underlying filter is successful, and 97 | /// returns a [`Reply`] If the underlying filter was rejected, the 98 | /// header is not added. 99 | /// 100 | /// # Example 101 | /// 102 | /// ``` 103 | /// use warp::Filter; 104 | /// 105 | /// // Set `server: warp` if not already set. 106 | /// let route = warp::any() 107 | /// .map(warp::reply) 108 | /// .with(warp::reply::with::default_header("server", "warp")); 109 | /// ``` 110 | pub fn default_header(name: K, value: V) -> WithDefaultHeader 111 | where 112 | HeaderName: TryFrom, 113 | >::Error: Into, 114 | HeaderValue: TryFrom, 115 | >::Error: Into, 116 | { 117 | let (name, value) = assert_name_and_value(name, value); 118 | WithDefaultHeader { name, value } 119 | } 120 | 121 | /// Wrap a `Filter` to always set a header. 122 | #[derive(Clone, Debug)] 123 | pub struct WithHeader { 124 | name: HeaderName, 125 | value: HeaderValue, 126 | } 127 | 128 | impl WrapSealed for WithHeader 129 | where 130 | F: Filter, 131 | R: Reply, 132 | { 133 | type Wrapped = Map; 134 | 135 | fn wrap(&self, filter: F) -> Self::Wrapped { 136 | let with = WithHeader_ { with: self.clone() }; 137 | filter.map(with) 138 | } 139 | } 140 | 141 | /// Wrap a `Filter` to always set multiple headers. 142 | #[derive(Clone, Debug)] 143 | pub struct WithHeaders { 144 | headers: Arc, 145 | } 146 | 147 | impl WrapSealed for WithHeaders 148 | where 149 | F: Filter, 150 | R: Reply, 151 | { 152 | type Wrapped = Map; 153 | 154 | fn wrap(&self, filter: F) -> Self::Wrapped { 155 | let with = WithHeaders_ { with: self.clone() }; 156 | filter.map(with) 157 | } 158 | } 159 | 160 | /// Wrap a `Filter` to set a header if it is not already set. 161 | #[derive(Clone, Debug)] 162 | pub struct WithDefaultHeader { 163 | name: HeaderName, 164 | value: HeaderValue, 165 | } 166 | 167 | impl WrapSealed for WithDefaultHeader 168 | where 169 | F: Filter, 170 | R: Reply, 171 | { 172 | type Wrapped = Map; 173 | 174 | fn wrap(&self, filter: F) -> Self::Wrapped { 175 | let with = WithDefaultHeader_ { with: self.clone() }; 176 | filter.map(with) 177 | } 178 | } 179 | 180 | fn assert_name_and_value(name: K, value: V) -> (HeaderName, HeaderValue) 181 | where 182 | HeaderName: TryFrom, 183 | >::Error: Into, 184 | HeaderValue: TryFrom, 185 | >::Error: Into, 186 | { 187 | let name = >::try_from(name) 188 | .map_err(Into::into) 189 | .unwrap_or_else(|_| panic!("invalid header name")); 190 | 191 | let value = >::try_from(value) 192 | .map_err(Into::into) 193 | .unwrap_or_else(|_| panic!("invalid header value")); 194 | 195 | (name, value) 196 | } 197 | 198 | mod sealed { 199 | use super::{WithDefaultHeader, WithHeader, WithHeaders}; 200 | use crate::generic::{Func, One}; 201 | use crate::reply::{Reply, Reply_}; 202 | 203 | #[derive(Clone)] 204 | #[allow(missing_debug_implementations)] 205 | pub struct WithHeader_ { 206 | pub(super) with: WithHeader, 207 | } 208 | 209 | impl Func> for WithHeader_ { 210 | type Output = Reply_; 211 | 212 | fn call(&self, args: One) -> Self::Output { 213 | let mut resp = args.0.into_response(); 214 | // Use "insert" to replace any set header... 215 | resp.headers_mut() 216 | .insert(&self.with.name, self.with.value.clone()); 217 | Reply_(resp) 218 | } 219 | } 220 | 221 | #[derive(Clone)] 222 | #[allow(missing_debug_implementations)] 223 | pub struct WithHeaders_ { 224 | pub(super) with: WithHeaders, 225 | } 226 | 227 | impl Func> for WithHeaders_ { 228 | type Output = Reply_; 229 | 230 | fn call(&self, args: One) -> Self::Output { 231 | let mut resp = args.0.into_response(); 232 | for (name, value) in &*self.with.headers { 233 | resp.headers_mut().insert(name, value.clone()); 234 | } 235 | Reply_(resp) 236 | } 237 | } 238 | 239 | #[derive(Clone)] 240 | #[allow(missing_debug_implementations)] 241 | pub struct WithDefaultHeader_ { 242 | pub(super) with: WithDefaultHeader, 243 | } 244 | 245 | impl Func> for WithDefaultHeader_ { 246 | type Output = Reply_; 247 | 248 | fn call(&self, args: One) -> Self::Output { 249 | let mut resp = args.0.into_response(); 250 | resp.headers_mut() 251 | .entry(&self.with.name) 252 | .or_insert_with(|| self.with.value.clone()); 253 | 254 | Reply_(resp) 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/generic.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub struct Product(pub(crate) H, pub(crate) T); 3 | 4 | pub type One = (T,); 5 | 6 | #[inline] 7 | pub(crate) fn one(val: T) -> One { 8 | (val,) 9 | } 10 | 11 | #[derive(Debug)] 12 | pub enum Either { 13 | A(T), 14 | B(U), 15 | } 16 | 17 | // Converts Product (and ()) into tuples. 18 | pub trait HList: Sized { 19 | type Tuple: Tuple; 20 | 21 | fn flatten(self) -> Self::Tuple; 22 | } 23 | 24 | // Typeclass that tuples can be converted into a Product (or unit ()). 25 | pub trait Tuple: Sized { 26 | type HList: HList; 27 | 28 | fn hlist(self) -> Self::HList; 29 | 30 | #[inline] 31 | fn combine(self, other: T) -> CombinedTuples 32 | where 33 | Self: Sized, 34 | T: Tuple, 35 | Self::HList: Combine, 36 | { 37 | self.hlist().combine(other.hlist()).flatten() 38 | } 39 | } 40 | 41 | pub type CombinedTuples = 42 | <<::HList as Combine<::HList>>::Output as HList>::Tuple; 43 | 44 | // Combines Product together. 45 | pub trait Combine { 46 | type Output: HList; 47 | 48 | fn combine(self, other: T) -> Self::Output; 49 | } 50 | 51 | pub trait Func { 52 | type Output; 53 | 54 | fn call(&self, args: Args) -> Self::Output; 55 | } 56 | 57 | // ===== impl Combine ===== 58 | 59 | impl Combine for () { 60 | type Output = T; 61 | #[inline] 62 | fn combine(self, other: T) -> Self::Output { 63 | other 64 | } 65 | } 66 | 67 | impl Combine for Product 68 | where 69 | T: Combine, 70 | Product>::Output>: HList, 71 | { 72 | type Output = Product>::Output>; 73 | 74 | #[inline] 75 | fn combine(self, other: U) -> Self::Output { 76 | Product(self.0, self.1.combine(other)) 77 | } 78 | } 79 | 80 | impl HList for () { 81 | type Tuple = (); 82 | #[inline] 83 | fn flatten(self) -> Self::Tuple {} 84 | } 85 | 86 | impl Tuple for () { 87 | type HList = (); 88 | 89 | #[inline] 90 | fn hlist(self) -> Self::HList {} 91 | } 92 | 93 | impl Func<()> for F 94 | where 95 | F: Fn() -> R, 96 | { 97 | type Output = R; 98 | 99 | #[inline] 100 | fn call(&self, _args: ()) -> Self::Output { 101 | (*self)() 102 | } 103 | } 104 | 105 | impl Func for F 106 | where 107 | F: Fn(crate::Rejection) -> R, 108 | { 109 | type Output = R; 110 | 111 | #[inline] 112 | fn call(&self, arg: crate::Rejection) -> Self::Output { 113 | (*self)(arg) 114 | } 115 | } 116 | 117 | macro_rules! product { 118 | ($H:expr) => { Product($H, ()) }; 119 | ($H:expr, $($T:expr),*) => { Product($H, product!($($T),*)) }; 120 | } 121 | 122 | macro_rules! Product { 123 | ($H:ty) => { Product<$H, ()> }; 124 | ($H:ty, $($T:ty),*) => { Product<$H, Product!($($T),*)> }; 125 | } 126 | 127 | macro_rules! product_pat { 128 | ($H:pat) => { Product($H, ()) }; 129 | ($H:pat, $($T:pat),*) => { Product($H, product_pat!($($T),*)) }; 130 | } 131 | 132 | macro_rules! generics { 133 | ($type:ident) => { 134 | impl<$type> HList for Product!($type) { 135 | type Tuple = ($type,); 136 | 137 | #[inline] 138 | fn flatten(self) -> Self::Tuple { 139 | (self.0,) 140 | } 141 | } 142 | 143 | impl<$type> Tuple for ($type,) { 144 | type HList = Product!($type); 145 | #[inline] 146 | fn hlist(self) -> Self::HList { 147 | product!(self.0) 148 | } 149 | } 150 | 151 | impl Func for F 152 | where 153 | F: Fn($type) -> R, 154 | { 155 | type Output = R; 156 | 157 | #[inline] 158 | fn call(&self, args: Product!($type)) -> Self::Output { 159 | (*self)(args.0) 160 | } 161 | 162 | } 163 | 164 | impl Func<($type,)> for F 165 | where 166 | F: Fn($type) -> R, 167 | { 168 | type Output = R; 169 | 170 | #[inline] 171 | fn call(&self, args: ($type,)) -> Self::Output { 172 | (*self)(args.0) 173 | } 174 | } 175 | 176 | }; 177 | 178 | ($type1:ident, $( $type:ident ),*) => { 179 | generics!($( $type ),*); 180 | 181 | impl<$type1, $( $type ),*> HList for Product!($type1, $($type),*) { 182 | type Tuple = ($type1, $( $type ),*); 183 | 184 | #[inline] 185 | fn flatten(self) -> Self::Tuple { 186 | #[allow(non_snake_case)] 187 | let product_pat!($type1, $( $type ),*) = self; 188 | ($type1, $( $type ),*) 189 | } 190 | } 191 | 192 | impl<$type1, $( $type ),*> Tuple for ($type1, $($type),*) { 193 | type HList = Product!($type1, $( $type ),*); 194 | 195 | #[inline] 196 | fn hlist(self) -> Self::HList { 197 | #[allow(non_snake_case)] 198 | let ($type1, $( $type ),*) = self; 199 | product!($type1, $( $type ),*) 200 | } 201 | } 202 | 203 | impl Func for F 204 | where 205 | F: Fn($type1, $( $type ),*) -> R, 206 | { 207 | type Output = R; 208 | 209 | #[inline] 210 | fn call(&self, args: Product!($type1, $($type),*)) -> Self::Output { 211 | #[allow(non_snake_case)] 212 | let product_pat!($type1, $( $type ),*) = args; 213 | (*self)($type1, $( $type ),*) 214 | } 215 | } 216 | 217 | impl Func<($type1, $($type),*)> for F 218 | where 219 | F: Fn($type1, $( $type ),*) -> R, 220 | { 221 | type Output = R; 222 | 223 | #[inline] 224 | fn call(&self, args: ($type1, $($type),*)) -> Self::Output { 225 | #[allow(non_snake_case)] 226 | let ($type1, $( $type ),*) = args; 227 | (*self)($type1, $( $type ),*) 228 | } 229 | } 230 | }; 231 | } 232 | 233 | generics! { 234 | T1, 235 | T2, 236 | T3, 237 | T4, 238 | T5, 239 | T6, 240 | T7, 241 | T8, 242 | T9, 243 | T10, 244 | T11, 245 | T12, 246 | T13, 247 | T14, 248 | T15, 249 | T16 250 | } 251 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![deny(missing_debug_implementations)] 3 | #![deny(rust_2018_idioms)] 4 | #![cfg_attr(test, deny(warnings))] 5 | 6 | //! # warp 7 | //! 8 | //! warp is a super-easy, composable, web server framework for warp speeds. 9 | //! 10 | //! Thanks to its [`Filter`][Filter] system, warp provides these out of the box: 11 | //! 12 | //! - Path routing and parameter extraction 13 | //! - Header requirements and extraction 14 | //! - Query string deserialization 15 | //! - JSON and Form bodies 16 | //! - Multipart form data 17 | //! - Static Files and Directories 18 | //! - Websockets 19 | //! - Access logging 20 | //! - Etc 21 | //! 22 | //! Since it builds on top of [hyper](https://hyper.rs), you automatically get: 23 | //! 24 | //! - HTTP/1 25 | //! - HTTP/2 26 | //! - Asynchronous 27 | //! - One of the fastest HTTP implementations 28 | //! - Tested and **correct** 29 | //! 30 | //! ## Filters 31 | //! 32 | //! The main concept in warp is the [`Filter`][Filter], which allows composition 33 | //! to describe various endpoints in your web service. Besides this powerful 34 | //! trait, warp comes with several built in [filters](filters/index.html), which 35 | //! can be combined for your specific needs. 36 | //! 37 | //! As a small example, consider an endpoint that has path and header requirements: 38 | //! 39 | //! ``` 40 | //! use warp::Filter; 41 | //! 42 | //! let hi = warp::path("hello") 43 | //! .and(warp::path::param()) 44 | //! .and(warp::header("user-agent")) 45 | //! .map(|param: String, agent: String| { 46 | //! format!("Hello {}, whose agent is {}", param, agent) 47 | //! }); 48 | //! ``` 49 | //! 50 | //! This example composes several [`Filter`s][Filter] together using `and`: 51 | //! 52 | //! - A path prefix of "hello" 53 | //! - A path parameter of a `String` 54 | //! - The `user-agent` header parsed as a `String` 55 | //! 56 | //! These specific filters will [`reject`][reject] requests that don't match 57 | //! their requirements. 58 | //! 59 | //! This ends up matching requests like: 60 | //! 61 | //! ```notrust 62 | //! GET /hello/sean HTTP/1.1 63 | //! Host: hyper.rs 64 | //! User-Agent: reqwest/v0.8.6 65 | //! 66 | //! ``` 67 | //! And it returns a response similar to this: 68 | //! 69 | //! ```notrust 70 | //! HTTP/1.1 200 OK 71 | //! Content-Length: 41 72 | //! Date: ... 73 | //! 74 | //! Hello sean, whose agent is reqwest/v0.8.6 75 | //! ``` 76 | //! 77 | //! Take a look at the full list of [`filters`](filters/index.html) to see what 78 | //! you can build. 79 | //! 80 | //! ## Testing 81 | //! 82 | //! Testing your web services easily is extremely important, and warp provides 83 | //! a [`test`](mod@self::test) module to help send mocked requests through your service. 84 | //! 85 | //! [Filter]: trait.Filter.html 86 | //! [reject]: reject/index.html 87 | 88 | #[macro_use] 89 | mod error; 90 | mod filter; 91 | pub mod filters; 92 | mod generic; 93 | pub mod redirect; 94 | pub mod reject; 95 | pub mod reply; 96 | mod route; 97 | mod server; 98 | mod service; 99 | pub mod test; 100 | #[cfg(feature = "tls")] 101 | mod tls; 102 | mod transport; 103 | 104 | pub use self::error::Error; 105 | pub use self::filter::Filter; 106 | // This otherwise shows a big dump of re-exports in the doc homepage, 107 | // with zero context, so just hide it from the docs. Doc examples 108 | // on each can show that a convenient import exists. 109 | #[cfg(feature = "compression")] 110 | #[doc(hidden)] 111 | pub use self::filters::compression; 112 | #[cfg(feature = "multipart")] 113 | #[doc(hidden)] 114 | pub use self::filters::multipart; 115 | #[cfg(feature = "websocket")] 116 | #[doc(hidden)] 117 | pub use self::filters::ws; 118 | #[doc(hidden)] 119 | pub use self::filters::{ 120 | addr, 121 | // any() function 122 | any::any, 123 | body, 124 | cookie, 125 | // cookie() function 126 | cookie::cookie, 127 | cors, 128 | // cors() function 129 | cors::cors, 130 | ext, 131 | fs, 132 | header, 133 | // header() function 134 | header::header, 135 | host, 136 | log, 137 | // log() function 138 | log::log, 139 | method::{delete, get, head, method, options, patch, post, put}, 140 | path, 141 | // path() function and macro 142 | path::path, 143 | query, 144 | // query() function 145 | query::query, 146 | sse, 147 | trace, 148 | // trace() function 149 | trace::trace, 150 | }; 151 | // ws() function 152 | pub use self::filter::wrap_fn; 153 | #[cfg(feature = "websocket")] 154 | #[doc(hidden)] 155 | pub use self::filters::ws::ws; 156 | #[doc(hidden)] 157 | pub use self::redirect::redirect; 158 | #[doc(hidden)] 159 | #[allow(deprecated)] 160 | pub use self::reject::{reject, Rejection}; 161 | #[doc(hidden)] 162 | pub use self::reply::{reply, Reply}; 163 | #[cfg(feature = "tls")] 164 | pub use self::server::TlsServer; 165 | pub use self::server::{serve, Server}; 166 | pub use self::service::service; 167 | #[doc(hidden)] 168 | pub use http; 169 | #[doc(hidden)] 170 | pub use hyper; 171 | 172 | #[doc(hidden)] 173 | pub use bytes::Buf; 174 | #[doc(hidden)] 175 | pub use futures_util::{Future, Sink, Stream}; 176 | #[doc(hidden)] 177 | 178 | pub(crate) type Request = http::Request; 179 | -------------------------------------------------------------------------------- /src/redirect.rs: -------------------------------------------------------------------------------- 1 | //! Redirect requests to a new location. 2 | //! 3 | //! The types in this module are helpers that implement [`Reply`], and easy 4 | //! to use in order to setup redirects. 5 | 6 | use http::{header, StatusCode}; 7 | 8 | pub use self::sealed::AsLocation; 9 | use crate::reply::{self, Reply}; 10 | 11 | /// HTTP 301 Moved Permanently 12 | /// Description: The requested resource has been permanently moved to a new URL. 13 | /// Usage: It is used when a URL has permanently moved to a new location. Search engines will update their index to the new URL. Browsers and clients will automatically cache this redirect, so subsequent requests for the old URL will automatically go to the new URL without making a request to the old URL. 14 | /// Common Use Case: Changing domain names, restructuring website URLs. 15 | /// 16 | /// # Example 17 | /// 18 | /// ``` 19 | /// use warp::{http::Uri, Filter}; 20 | /// 21 | /// let route = warp::path("v1") 22 | /// .map(|| { 23 | /// warp::redirect(Uri::from_static("/v2")) 24 | /// }); 25 | /// ``` 26 | pub fn redirect(uri: impl AsLocation) -> impl Reply { 27 | reply::with_header( 28 | StatusCode::MOVED_PERMANENTLY, 29 | header::LOCATION, 30 | uri.header_value(), 31 | ) 32 | } 33 | 34 | /// HTTP 302 Found (or Temporary Redirect) 35 | /// Description: The requested resource can be found at a different URL temporarily. 36 | /// Usage: Historically, this status code was used for temporary redirects. However, its meaning was often misunderstood, and different clients treated it differently. As a result, it is recommended to use 307 (or 303) for temporary redirects instead. 37 | /// Common Use Case: Rarely used directly due to ambiguity; replaced by 307 or 303. 38 | /// 39 | /// # Example 40 | /// 41 | /// ``` 42 | /// use warp::{http::Uri, Filter}; 43 | /// 44 | /// let route = warp::path("v1") 45 | /// .map(|| { 46 | /// warp::redirect::found(Uri::from_static("/v2")) 47 | /// }); 48 | /// ``` 49 | pub fn found(uri: impl AsLocation) -> impl Reply { 50 | reply::with_header(StatusCode::FOUND, header::LOCATION, uri.header_value()) 51 | } 52 | 53 | /// HTTP 303 See Other 54 | /// Description: The response to the request can be found at a different URL, and the client should retrieve it using the GET method. 55 | /// Usage: It is typically used to redirect the client to another URL using a GET request after processing a POST request. It ensures that the client doesn't repeat the POST request if they refresh the page. 56 | /// Common Use Case: After form submissions or any non-idempotent request. 57 | /// 58 | /// The HTTP method of the request to the new location will always be `GET`. 59 | /// 60 | /// # Example 61 | /// 62 | /// ``` 63 | /// use warp::{http::Uri, Filter}; 64 | /// 65 | /// let route = warp::path("v1") 66 | /// .map(|| { 67 | /// warp::redirect::see_other(Uri::from_static("/v2")) 68 | /// }); 69 | /// ``` 70 | pub fn see_other(uri: impl AsLocation) -> impl Reply { 71 | reply::with_header(StatusCode::SEE_OTHER, header::LOCATION, uri.header_value()) 72 | } 73 | 74 | /// HTTP 307 Temporary Redirect: 75 | /// Description: The requested resource can be found at a different URL temporarily. 76 | /// Usage: Similar to 302, but explicitly defined as a temporary redirect. The main difference between 307 and 302 is that 307 preserves the method of the original request when redirecting. If the original request was a POST, the subsequent request to the new URL will also be a POST. 77 | /// Common Use Case: Temporary redirects that should preserve the original request method. 78 | /// 79 | /// This is similar to [`see_other`](fn@see_other) but the HTTP method and the body of the request 80 | /// to the new location will be the same as the method and body of the current request. 81 | /// 82 | /// # Example 83 | /// 84 | /// ``` 85 | /// use warp::{http::Uri, Filter}; 86 | /// 87 | /// let route = warp::path("v1") 88 | /// .map(|| { 89 | /// warp::redirect::temporary(Uri::from_static("/v2")) 90 | /// }); 91 | /// ``` 92 | pub fn temporary(uri: impl AsLocation) -> impl Reply { 93 | reply::with_header( 94 | StatusCode::TEMPORARY_REDIRECT, 95 | header::LOCATION, 96 | uri.header_value(), 97 | ) 98 | } 99 | 100 | /// HTTP 308 Permanent Redirect 101 | /// Description: The requested resource has been permanently moved to a new URL, and future requests should use the new URL. 102 | /// Usage: Similar to 301, but like 307, it preserves the original request method when redirecting. It indicates that the redirection is permanent, and browsers and clients will cache this redirect like they do for 301. 103 | // Common Use Case: Permanently moving resources to a new URL while maintaining the original request method. 104 | /// 105 | /// This is similar to [`redirect`](fn@redirect) but the HTTP method of the request to the new 106 | /// location will be the same as the method of the current request. 107 | /// 108 | /// # Example 109 | /// 110 | /// ``` 111 | /// use warp::{http::Uri, Filter}; 112 | /// 113 | /// let route = warp::path("v1") 114 | /// .map(|| { 115 | /// warp::redirect::permanent(Uri::from_static("/v2")) 116 | /// }); 117 | /// ``` 118 | pub fn permanent(uri: impl AsLocation) -> impl Reply { 119 | reply::with_header( 120 | StatusCode::PERMANENT_REDIRECT, 121 | header::LOCATION, 122 | uri.header_value(), 123 | ) 124 | } 125 | 126 | mod sealed { 127 | use bytes::Bytes; 128 | use http::{header::HeaderValue, Uri}; 129 | 130 | /// Trait for redirect locations. Currently only a `Uri` can be used in 131 | /// redirect. 132 | /// This sealed trait exists to allow adding possibly new impls so other 133 | /// arguments could be accepted, like maybe just `warp::redirect("/v2")`. 134 | pub trait AsLocation: Sealed {} 135 | pub trait Sealed { 136 | fn header_value(self) -> HeaderValue; 137 | } 138 | 139 | impl AsLocation for Uri {} 140 | 141 | impl Sealed for Uri { 142 | fn header_value(self) -> HeaderValue { 143 | let bytes = Bytes::from(self.to_string()); 144 | HeaderValue::from_maybe_shared(bytes).expect("Uri is a valid HeaderValue") 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/route.rs: -------------------------------------------------------------------------------- 1 | use scoped_tls::scoped_thread_local; 2 | use std::cell::RefCell; 3 | use std::mem; 4 | use std::net::SocketAddr; 5 | 6 | use hyper::Body; 7 | 8 | use crate::Request; 9 | 10 | scoped_thread_local!(static ROUTE: RefCell); 11 | 12 | pub(crate) fn set(r: &RefCell, func: F) -> U 13 | where 14 | F: FnOnce() -> U, 15 | { 16 | ROUTE.set(r, func) 17 | } 18 | 19 | pub(crate) fn is_set() -> bool { 20 | ROUTE.is_set() 21 | } 22 | 23 | pub(crate) fn with(func: F) -> R 24 | where 25 | F: FnOnce(&mut Route) -> R, 26 | { 27 | ROUTE.with(move |route| func(&mut *route.borrow_mut())) 28 | } 29 | 30 | #[derive(Debug)] 31 | pub(crate) struct Route { 32 | body: BodyState, 33 | remote_addr: Option, 34 | req: Request, 35 | segments_index: usize, 36 | } 37 | 38 | #[derive(Debug)] 39 | enum BodyState { 40 | Ready, 41 | Taken, 42 | } 43 | 44 | impl Route { 45 | pub(crate) fn new(req: Request, remote_addr: Option) -> RefCell { 46 | let segments_index = if req.uri().path().starts_with('/') { 47 | // Skip the beginning slash. 48 | 1 49 | } else { 50 | 0 51 | }; 52 | 53 | RefCell::new(Route { 54 | body: BodyState::Ready, 55 | remote_addr, 56 | req, 57 | segments_index, 58 | }) 59 | } 60 | 61 | pub(crate) fn method(&self) -> &http::Method { 62 | self.req.method() 63 | } 64 | 65 | pub(crate) fn headers(&self) -> &http::HeaderMap { 66 | self.req.headers() 67 | } 68 | 69 | pub(crate) fn version(&self) -> http::Version { 70 | self.req.version() 71 | } 72 | 73 | pub(crate) fn extensions(&self) -> &http::Extensions { 74 | self.req.extensions() 75 | } 76 | 77 | #[cfg(feature = "websocket")] 78 | pub(crate) fn extensions_mut(&mut self) -> &mut http::Extensions { 79 | self.req.extensions_mut() 80 | } 81 | 82 | pub(crate) fn uri(&self) -> &http::Uri { 83 | self.req.uri() 84 | } 85 | 86 | pub(crate) fn path(&self) -> &str { 87 | &self.req.uri().path()[self.segments_index..] 88 | } 89 | 90 | pub(crate) fn full_path(&self) -> &str { 91 | self.req.uri().path() 92 | } 93 | 94 | pub(crate) fn set_unmatched_path(&mut self, index: usize) { 95 | let index = self.segments_index + index; 96 | let path = self.req.uri().path(); 97 | if path.is_empty() { 98 | // malformed path 99 | return; 100 | } else if path.len() == index { 101 | self.segments_index = index; 102 | } else { 103 | debug_assert_eq!(path.as_bytes()[index], b'/'); 104 | self.segments_index = index + 1; 105 | } 106 | } 107 | 108 | pub(crate) fn query(&self) -> Option<&str> { 109 | self.req.uri().query() 110 | } 111 | 112 | pub(crate) fn matched_path_index(&self) -> usize { 113 | self.segments_index 114 | } 115 | 116 | pub(crate) fn reset_matched_path_index(&mut self, index: usize) { 117 | debug_assert!( 118 | index <= self.segments_index, 119 | "reset_match_path_index should not be bigger: current={}, arg={}", 120 | self.segments_index, 121 | index, 122 | ); 123 | self.segments_index = index; 124 | } 125 | 126 | pub(crate) fn remote_addr(&self) -> Option { 127 | self.remote_addr 128 | } 129 | 130 | pub(crate) fn take_body(&mut self) -> Option { 131 | match self.body { 132 | BodyState::Ready => { 133 | let body = mem::replace(self.req.body_mut(), Body::empty()); 134 | self.body = BodyState::Taken; 135 | Some(body) 136 | } 137 | BodyState::Taken => None, 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/service.rs: -------------------------------------------------------------------------------- 1 | //! Convert `Filter`s into `Service`s 2 | 3 | pub use crate::filter::service::service; 4 | -------------------------------------------------------------------------------- /src/transport.rs: -------------------------------------------------------------------------------- 1 | use std::io; 2 | use std::net::SocketAddr; 3 | use std::pin::Pin; 4 | use std::task::{Context, Poll}; 5 | 6 | use hyper::server::conn::AddrStream; 7 | use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; 8 | 9 | pub trait Transport: AsyncRead + AsyncWrite { 10 | fn remote_addr(&self) -> Option; 11 | } 12 | 13 | impl Transport for AddrStream { 14 | fn remote_addr(&self) -> Option { 15 | Some(self.remote_addr()) 16 | } 17 | } 18 | 19 | pub(crate) struct LiftIo(pub(crate) T); 20 | 21 | impl AsyncRead for LiftIo { 22 | fn poll_read( 23 | self: Pin<&mut Self>, 24 | cx: &mut Context<'_>, 25 | buf: &mut ReadBuf<'_>, 26 | ) -> Poll> { 27 | Pin::new(&mut self.get_mut().0).poll_read(cx, buf) 28 | } 29 | } 30 | 31 | impl AsyncWrite for LiftIo { 32 | fn poll_write( 33 | self: Pin<&mut Self>, 34 | cx: &mut Context<'_>, 35 | buf: &[u8], 36 | ) -> Poll> { 37 | Pin::new(&mut self.get_mut().0).poll_write(cx, buf) 38 | } 39 | 40 | fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 41 | Pin::new(&mut self.get_mut().0).poll_flush(cx) 42 | } 43 | 44 | fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { 45 | Pin::new(&mut self.get_mut().0).poll_shutdown(cx) 46 | } 47 | } 48 | 49 | impl Transport for LiftIo { 50 | fn remote_addr(&self) -> Option { 51 | None 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/addr.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use std::net::{IpAddr, Ipv4Addr, SocketAddr}; 4 | 5 | #[tokio::test] 6 | async fn remote_addr_missing() { 7 | let extract_remote_addr = warp::addr::remote(); 8 | 9 | let req = warp::test::request(); 10 | let resp = req.filter(&extract_remote_addr).await.unwrap(); 11 | assert_eq!(resp, None) 12 | } 13 | 14 | #[tokio::test] 15 | async fn remote_addr_present() { 16 | let extract_remote_addr = warp::addr::remote(); 17 | 18 | let req = warp::test::request().remote_addr("1.2.3.4:5678".parse().unwrap()); 19 | let resp = req.filter(&extract_remote_addr).await.unwrap(); 20 | assert_eq!( 21 | resp, 22 | Some(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 5678)) 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /tests/body.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use bytes::Buf; 4 | use futures_util::TryStreamExt; 5 | use warp::Filter; 6 | 7 | #[tokio::test] 8 | async fn matches() { 9 | let _ = pretty_env_logger::try_init(); 10 | 11 | let concat = warp::body::bytes(); 12 | 13 | let req = warp::test::request().path("/nothing-matches-me"); 14 | 15 | assert!(req.matches(&concat).await); 16 | 17 | let p = warp::path("body"); 18 | let req = warp::test::request().path("/body"); 19 | 20 | let and = p.and(concat); 21 | 22 | assert!(req.matches(&and).await); 23 | } 24 | 25 | #[tokio::test] 26 | async fn server_error_if_taking_body_multiple_times() { 27 | let _ = pretty_env_logger::try_init(); 28 | 29 | let concat = warp::body::bytes(); 30 | let double = concat.and(concat).map(|_, _| warp::reply()); 31 | 32 | let res = warp::test::request().reply(&double).await; 33 | 34 | assert_eq!(res.status(), 500); 35 | assert_eq!(res.body(), "Request body consumed multiple times"); 36 | } 37 | 38 | #[tokio::test] 39 | async fn content_length_limit() { 40 | let _ = pretty_env_logger::try_init(); 41 | 42 | let limit = warp::body::content_length_limit(30).map(warp::reply); 43 | 44 | let res = warp::test::request().reply(&limit).await; 45 | assert_eq!(res.status(), 411, "missing content-length returns 411"); 46 | 47 | let res = warp::test::request() 48 | .header("content-length", "999") 49 | .reply(&limit) 50 | .await; 51 | assert_eq!(res.status(), 413, "over limit returns 413"); 52 | 53 | let res = warp::test::request() 54 | .header("content-length", "2") 55 | .reply(&limit) 56 | .await; 57 | assert_eq!(res.status(), 200, "under limit succeeds"); 58 | } 59 | 60 | #[tokio::test] 61 | async fn json() { 62 | let _ = pretty_env_logger::try_init(); 63 | 64 | let json = warp::body::json::>(); 65 | 66 | let req = warp::test::request().body("[1, 2, 3]"); 67 | 68 | let vec = req.filter(&json).await.unwrap(); 69 | assert_eq!(vec, &[1, 2, 3]); 70 | 71 | let req = warp::test::request() 72 | .header("content-type", "application/json") 73 | .body("[3, 2, 1]"); 74 | 75 | let vec = req.filter(&json).await.unwrap(); 76 | assert_eq!(vec, &[3, 2, 1], "matches content-type"); 77 | } 78 | 79 | #[tokio::test] 80 | async fn json_rejects_bad_content_type() { 81 | let _ = pretty_env_logger::try_init(); 82 | 83 | let json = warp::body::json::>().map(|_| warp::reply()); 84 | 85 | let req = warp::test::request() 86 | .header("content-type", "text/xml") 87 | .body("[3, 2, 1]"); 88 | 89 | let res = req.reply(&json).await; 90 | assert_eq!( 91 | res.status(), 92 | 415, 93 | "bad content-type should be 415 Unsupported Media Type" 94 | ); 95 | } 96 | 97 | #[tokio::test] 98 | async fn json_invalid() { 99 | let _ = pretty_env_logger::try_init(); 100 | 101 | let json = warp::body::json::>().map(|vec| warp::reply::json(&vec)); 102 | 103 | let res = warp::test::request().body("lol#wat").reply(&json).await; 104 | assert_eq!(res.status(), 400); 105 | let prefix = b"Request body deserialize error: "; 106 | assert_eq!(&res.body()[..prefix.len()], prefix); 107 | } 108 | 109 | #[test] 110 | fn json_size_of() { 111 | let json = warp::body::json::>(); 112 | assert_eq!(std::mem::size_of_val(&json), 0); 113 | } 114 | 115 | #[tokio::test] 116 | async fn form() { 117 | let _ = pretty_env_logger::try_init(); 118 | 119 | let form = warp::body::form::>(); 120 | 121 | let req = warp::test::request().body("foo=bar&baz=quux"); 122 | 123 | let vec = req.filter(&form).await.unwrap(); 124 | let expected = vec![ 125 | ("foo".to_owned(), "bar".to_owned()), 126 | ("baz".to_owned(), "quux".to_owned()), 127 | ]; 128 | assert_eq!(vec, expected); 129 | } 130 | 131 | #[tokio::test] 132 | async fn form_rejects_bad_content_type() { 133 | let _ = pretty_env_logger::try_init(); 134 | 135 | let form = warp::body::form::>().map(|_| warp::reply()); 136 | 137 | let req = warp::test::request() 138 | .header("content-type", "application/x-www-form-urlencoded") 139 | .body("foo=bar"); 140 | 141 | let res = req.reply(&form).await; 142 | assert_eq!(res.status(), 200); 143 | 144 | let req = warp::test::request() 145 | .header("content-type", "text/xml") 146 | .body("foo=bar"); 147 | let res = req.reply(&form).await; 148 | assert_eq!( 149 | res.status(), 150 | 415, 151 | "bad content-type should be 415 Unsupported Media Type" 152 | ); 153 | } 154 | 155 | #[tokio::test] 156 | async fn form_allows_charset() { 157 | let _ = pretty_env_logger::try_init(); 158 | 159 | let form = warp::body::form::>(); 160 | 161 | let req = warp::test::request() 162 | .header( 163 | "content-type", 164 | "application/x-www-form-urlencoded; charset=utf-8", 165 | ) 166 | .body("foo=bar"); 167 | 168 | let vec = req.filter(&form).await.unwrap(); 169 | let expected = vec![("foo".to_owned(), "bar".to_owned())]; 170 | assert_eq!(vec, expected); 171 | } 172 | 173 | #[tokio::test] 174 | async fn form_invalid() { 175 | let _ = pretty_env_logger::try_init(); 176 | 177 | let form = warp::body::form::>().map(|vec| warp::reply::json(&vec)); 178 | 179 | let res = warp::test::request().body("nope").reply(&form).await; 180 | assert_eq!(res.status(), 400); 181 | let prefix = b"Request body deserialize error: "; 182 | assert_eq!(&res.body()[..prefix.len()], prefix); 183 | } 184 | 185 | #[tokio::test] 186 | async fn stream() { 187 | let _ = pretty_env_logger::try_init(); 188 | 189 | let stream = warp::body::stream(); 190 | 191 | let body = warp::test::request() 192 | .body("foo=bar") 193 | .filter(&stream) 194 | .await 195 | .expect("filter() stream"); 196 | 197 | let bufs: Result, warp::Error> = body.try_collect().await; 198 | let bufs = bufs.unwrap(); 199 | 200 | assert_eq!(bufs.len(), 1); 201 | assert_eq!(bufs[0].chunk(), b"foo=bar"); 202 | } 203 | -------------------------------------------------------------------------------- /tests/cookie.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | #[tokio::test] 4 | async fn cookie() { 5 | let foo = warp::cookie::("foo"); 6 | 7 | let req = warp::test::request().header("cookie", "foo=bar"); 8 | assert_eq!(req.filter(&foo).await.unwrap(), "bar"); 9 | 10 | let req = warp::test::request().header("cookie", "abc=def; foo=baz"); 11 | assert_eq!(req.filter(&foo).await.unwrap(), "baz"); 12 | 13 | let req = warp::test::request().header("cookie", "abc=def"); 14 | assert!(!req.matches(&foo).await); 15 | 16 | let req = warp::test::request().header("cookie", "foobar=quux"); 17 | assert!(!req.matches(&foo).await); 18 | } 19 | 20 | #[tokio::test] 21 | async fn optional() { 22 | let foo = warp::cookie::optional::("foo"); 23 | 24 | let req = warp::test::request().header("cookie", "foo=bar"); 25 | assert_eq!(req.filter(&foo).await.unwrap().unwrap(), "bar"); 26 | 27 | let req = warp::test::request().header("cookie", "abc=def; foo=baz"); 28 | assert_eq!(req.filter(&foo).await.unwrap().unwrap(), "baz"); 29 | 30 | let req = warp::test::request().header("cookie", "abc=def"); 31 | assert!(req.matches(&foo).await); 32 | 33 | let req = warp::test::request().header("cookie", "foobar=quux"); 34 | assert!(req.matches(&foo).await); 35 | } 36 | 37 | #[tokio::test] 38 | async fn missing() { 39 | let _ = pretty_env_logger::try_init(); 40 | 41 | let cookie = warp::cookie::("foo"); 42 | 43 | let res = warp::test::request() 44 | .header("cookie", "not=here") 45 | .reply(&cookie) 46 | .await; 47 | 48 | assert_eq!(res.status(), 400); 49 | assert_eq!(res.body(), "Missing request cookie \"foo\""); 50 | } 51 | -------------------------------------------------------------------------------- /tests/cors.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::{http::Method, Filter}; 3 | 4 | #[tokio::test] 5 | async fn allow_methods() { 6 | let cors = warp::cors().allow_methods(&[Method::GET, Method::POST, Method::DELETE]); 7 | 8 | let route = warp::any().map(warp::reply).with(cors); 9 | 10 | let res = warp::test::request() 11 | .method("OPTIONS") 12 | .header("origin", "warp") 13 | .header("access-control-request-method", "DELETE") 14 | .reply(&route) 15 | .await; 16 | 17 | assert_eq!(res.status(), 200); 18 | 19 | let res = warp::test::request() 20 | .method("OPTIONS") 21 | .header("origin", "warp") 22 | .header("access-control-request-method", "PUT") 23 | .reply(&route) 24 | .await; 25 | 26 | assert_eq!(res.status(), 403); 27 | } 28 | 29 | #[tokio::test] 30 | async fn origin_not_allowed() { 31 | let cors = warp::cors() 32 | .allow_methods(&[Method::DELETE]) 33 | .allow_origin("https://hyper.rs"); 34 | 35 | let route = warp::any().map(warp::reply).with(cors); 36 | 37 | let res = warp::test::request() 38 | .method("OPTIONS") 39 | .header("origin", "https://warp.rs") 40 | .header("access-control-request-method", "DELETE") 41 | .reply(&route) 42 | .await; 43 | 44 | assert_eq!(res.status(), 403); 45 | 46 | let res = warp::test::request() 47 | .header("origin", "https://warp.rs") 48 | .header("access-control-request-method", "DELETE") 49 | .reply(&route) 50 | .await; 51 | 52 | assert_eq!(res.status(), 403); 53 | } 54 | 55 | #[tokio::test] 56 | async fn headers_not_exposed() { 57 | let cors = warp::cors() 58 | .allow_any_origin() 59 | .allow_methods(&[Method::GET]); 60 | 61 | let route = warp::any().map(warp::reply).with(cors); 62 | 63 | let res = warp::test::request() 64 | .method("OPTIONS") 65 | .header("origin", "https://warp.rs") 66 | .header("access-control-request-method", "GET") 67 | .reply(&route) 68 | .await; 69 | 70 | assert_eq!( 71 | res.headers().contains_key("access-control-expose-headers"), 72 | false 73 | ); 74 | 75 | let res = warp::test::request() 76 | .method("GET") 77 | .header("origin", "https://warp.rs") 78 | .reply(&route) 79 | .await; 80 | 81 | assert_eq!( 82 | res.headers().contains_key("access-control-expose-headers"), 83 | false 84 | ); 85 | } 86 | 87 | #[tokio::test] 88 | async fn headers_not_allowed() { 89 | let cors = warp::cors() 90 | .allow_methods(&[Method::DELETE]) 91 | .allow_headers(vec!["x-foo"]); 92 | 93 | let route = warp::any().map(warp::reply).with(cors); 94 | 95 | let res = warp::test::request() 96 | .method("OPTIONS") 97 | .header("origin", "https://warp.rs") 98 | .header("access-control-request-headers", "x-bar") 99 | .header("access-control-request-method", "DELETE") 100 | .reply(&route) 101 | .await; 102 | 103 | assert_eq!(res.status(), 403); 104 | } 105 | 106 | #[tokio::test] 107 | async fn success() { 108 | let cors = warp::cors() 109 | .allow_credentials(true) 110 | .allow_headers(vec!["x-foo", "x-bar"]) 111 | .allow_methods(&[Method::POST, Method::DELETE]) 112 | .expose_header("x-header1") 113 | .expose_headers(vec!["x-header2"]) 114 | .max_age(30); 115 | 116 | let route = warp::any().map(warp::reply).with(cors); 117 | 118 | // preflight 119 | let res = warp::test::request() 120 | .method("OPTIONS") 121 | .header("origin", "https://hyper.rs") 122 | .header("access-control-request-headers", "x-bar, x-foo") 123 | .header("access-control-request-method", "DELETE") 124 | .reply(&route) 125 | .await; 126 | assert_eq!(res.status(), 200); 127 | assert_eq!( 128 | res.headers()["access-control-allow-origin"], 129 | "https://hyper.rs" 130 | ); 131 | assert_eq!(res.headers()["access-control-allow-credentials"], "true"); 132 | let allowed_headers = &res.headers()["access-control-allow-headers"]; 133 | assert!(allowed_headers == "x-bar, x-foo" || allowed_headers == "x-foo, x-bar"); 134 | let exposed_headers = &res.headers()["access-control-expose-headers"]; 135 | assert!(exposed_headers == "x-header1, x-header2" || exposed_headers == "x-header2, x-header1"); 136 | assert_eq!(res.headers()["access-control-max-age"], "30"); 137 | let methods = &res.headers()["access-control-allow-methods"]; 138 | assert!( 139 | // HashSet randomly orders these... 140 | methods == "DELETE, POST" || methods == "POST, DELETE", 141 | "access-control-allow-methods: {:?}", 142 | methods, 143 | ); 144 | 145 | // cors request 146 | let res = warp::test::request() 147 | .method("DELETE") 148 | .header("origin", "https://hyper.rs") 149 | .header("x-foo", "hello") 150 | .header("x-bar", "world") 151 | .reply(&route) 152 | .await; 153 | assert_eq!(res.status(), 200); 154 | assert_eq!( 155 | res.headers()["access-control-allow-origin"], 156 | "https://hyper.rs" 157 | ); 158 | assert_eq!(res.headers()["access-control-allow-credentials"], "true"); 159 | assert_eq!(res.headers().get("access-control-max-age"), None); 160 | assert_eq!(res.headers().get("access-control-allow-methods"), None); 161 | let exposed_headers = &res.headers()["access-control-expose-headers"]; 162 | assert!(exposed_headers == "x-header1, x-header2" || exposed_headers == "x-header2, x-header1"); 163 | } 164 | 165 | #[tokio::test] 166 | async fn with_log() { 167 | let cors = warp::cors() 168 | .allow_any_origin() 169 | .allow_methods(&[Method::GET]); 170 | 171 | let route = warp::any() 172 | .map(warp::reply) 173 | .with(cors) 174 | .with(warp::log("cors test")); 175 | 176 | let res = warp::test::request() 177 | .method("OPTIONS") 178 | .header("origin", "warp") 179 | .header("access-control-request-method", "GET") 180 | .reply(&route) 181 | .await; 182 | 183 | assert_eq!(res.status(), 200); 184 | } 185 | -------------------------------------------------------------------------------- /tests/ext.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::Filter; 3 | 4 | #[derive(Clone, Debug, PartialEq)] 5 | struct Ext1(i32); 6 | 7 | #[tokio::test] 8 | async fn set_and_get() { 9 | let ext = warp::ext::get::(); 10 | 11 | let extracted = warp::test::request() 12 | .extension(Ext1(55)) 13 | .filter(&ext) 14 | .await 15 | .unwrap(); 16 | 17 | assert_eq!(extracted, Ext1(55)); 18 | } 19 | 20 | #[tokio::test] 21 | async fn get_missing() { 22 | let ext = warp::ext::get().map(|e: Ext1| e.0.to_string()); 23 | 24 | let res = warp::test::request().reply(&ext).await; 25 | 26 | assert_eq!(res.status(), 500); 27 | assert_eq!(res.body(), "Missing request extension"); 28 | } 29 | -------------------------------------------------------------------------------- /tests/filter.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::convert::Infallible; 3 | use warp::Filter; 4 | 5 | #[tokio::test] 6 | async fn flattens_tuples() { 7 | let _ = pretty_env_logger::try_init(); 8 | 9 | let str1 = warp::any().map(|| "warp"); 10 | let true1 = warp::any().map(|| true); 11 | let unit1 = warp::any(); 12 | 13 | // just 1 value 14 | let ext = warp::test::request().filter(&str1).await.unwrap(); 15 | assert_eq!(ext, "warp"); 16 | 17 | // just 1 unit 18 | let ext = warp::test::request().filter(&unit1).await.unwrap(); 19 | assert_eq!(ext, ()); 20 | 21 | // combine 2 values 22 | let and = str1.and(true1); 23 | let ext = warp::test::request().filter(&and).await.unwrap(); 24 | assert_eq!(ext, ("warp", true)); 25 | 26 | // combine 2 reversed 27 | let and = true1.and(str1); 28 | let ext = warp::test::request().filter(&and).await.unwrap(); 29 | assert_eq!(ext, (true, "warp")); 30 | 31 | // combine 1 with unit 32 | let and = str1.and(unit1); 33 | let ext = warp::test::request().filter(&and).await.unwrap(); 34 | assert_eq!(ext, "warp"); 35 | 36 | let and = unit1.and(str1); 37 | let ext = warp::test::request().filter(&and).await.unwrap(); 38 | assert_eq!(ext, "warp"); 39 | 40 | // combine 3 values 41 | let and = str1.and(str1).and(true1); 42 | let ext = warp::test::request().filter(&and).await.unwrap(); 43 | assert_eq!(ext, ("warp", "warp", true)); 44 | 45 | // combine 2 with unit 46 | let and = str1.and(unit1).and(true1); 47 | let ext = warp::test::request().filter(&and).await.unwrap(); 48 | assert_eq!(ext, ("warp", true)); 49 | 50 | let and = unit1.and(str1).and(true1); 51 | let ext = warp::test::request().filter(&and).await.unwrap(); 52 | assert_eq!(ext, ("warp", true)); 53 | 54 | let and = str1.and(true1).and(unit1); 55 | let ext = warp::test::request().filter(&and).await.unwrap(); 56 | assert_eq!(ext, ("warp", true)); 57 | 58 | // nested tuples 59 | let str_true_unit = str1.and(true1).and(unit1); 60 | let unit_str_true = unit1.and(str1).and(true1); 61 | 62 | let and = str_true_unit.and(unit_str_true); 63 | let ext = warp::test::request().filter(&and).await.unwrap(); 64 | assert_eq!(ext, ("warp", true, "warp", true)); 65 | 66 | let and = unit_str_true.and(unit1).and(str1).and(str_true_unit); 67 | let ext = warp::test::request().filter(&and).await.unwrap(); 68 | assert_eq!(ext, ("warp", true, "warp", "warp", true)); 69 | } 70 | 71 | #[tokio::test] 72 | async fn map() { 73 | let _ = pretty_env_logger::try_init(); 74 | 75 | let ok = warp::any().map(warp::reply); 76 | 77 | let req = warp::test::request(); 78 | let resp = req.reply(&ok).await; 79 | assert_eq!(resp.status(), 200); 80 | } 81 | 82 | #[tokio::test] 83 | async fn or() { 84 | let _ = pretty_env_logger::try_init(); 85 | 86 | // Or can be combined with an infallible filter 87 | let a = warp::path::param::(); 88 | let b = warp::any().map(|| 41i32); 89 | let f = a.or(b); 90 | 91 | let _: Result<_, Infallible> = warp::test::request().filter(&f).await; 92 | } 93 | 94 | #[tokio::test] 95 | async fn or_else() { 96 | let _ = pretty_env_logger::try_init(); 97 | 98 | let a = warp::path::param::(); 99 | let f = a.or_else(|_| async { Ok::<_, warp::Rejection>((44u32,)) }); 100 | 101 | assert_eq!( 102 | warp::test::request().path("/33").filter(&f).await.unwrap(), 103 | 33, 104 | ); 105 | assert_eq!(warp::test::request().filter(&f).await.unwrap(), 44,); 106 | 107 | // OrElse can be combined with an infallible filter 108 | let a = warp::path::param::(); 109 | let f = a.or_else(|_| async { Ok::<_, Infallible>((44u32,)) }); 110 | 111 | let _: Result<_, Infallible> = warp::test::request().filter(&f).await; 112 | } 113 | 114 | #[tokio::test] 115 | async fn recover() { 116 | let _ = pretty_env_logger::try_init(); 117 | 118 | let a = warp::path::param::(); 119 | let f = a.recover(|err| async move { Err::(err) }); 120 | 121 | // not rejected 122 | let resp = warp::test::request().path("/hi").reply(&f).await; 123 | assert_eq!(resp.status(), 200); 124 | assert_eq!(resp.body(), "hi"); 125 | 126 | // rejected, recovered, re-rejected 127 | let resp = warp::test::request().reply(&f).await; 128 | assert_eq!(resp.status(), 404); 129 | 130 | // Recover can be infallible 131 | let f = a.recover(|_| async move { Ok::<_, Infallible>("shh") }); 132 | 133 | let _: Result<_, Infallible> = warp::test::request().filter(&f).await; 134 | } 135 | 136 | #[tokio::test] 137 | async fn unify() { 138 | let _ = pretty_env_logger::try_init(); 139 | 140 | let a = warp::path::param::(); 141 | let b = warp::path::param::(); 142 | let f = a.or(b).unify(); 143 | 144 | let ex = warp::test::request().path("/1").filter(&f).await.unwrap(); 145 | 146 | assert_eq!(ex, 1); 147 | } 148 | 149 | #[should_panic] 150 | #[tokio::test] 151 | async fn nested() { 152 | let f = warp::any().and_then(|| async { 153 | let p = warp::path::param::(); 154 | warp::test::request().filter(&p).await 155 | }); 156 | 157 | let _ = warp::test::request().filter(&f).await; 158 | } 159 | -------------------------------------------------------------------------------- /tests/fs.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use std::fs; 3 | 4 | #[tokio::test] 5 | async fn file() { 6 | let _ = pretty_env_logger::try_init(); 7 | 8 | let file = warp::fs::file("README.md"); 9 | 10 | let req = warp::test::request(); 11 | let res = req.reply(&file).await; 12 | 13 | assert_eq!(res.status(), 200); 14 | 15 | let contents = fs::read("README.md").expect("fs::read README.md"); 16 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 17 | assert_eq!(res.headers()["accept-ranges"], "bytes"); 18 | 19 | let ct = &res.headers()["content-type"]; 20 | assert!( 21 | ct == "text/x-markdown" || ct == "text/markdown", 22 | "content-type is not markdown: {:?}", 23 | ct, 24 | ); 25 | 26 | assert_eq!(res.body(), &*contents); 27 | } 28 | 29 | #[tokio::test] 30 | async fn dir() { 31 | let _ = pretty_env_logger::try_init(); 32 | 33 | let file = warp::fs::dir("examples"); 34 | 35 | let req = warp::test::request().path("/todos.rs"); 36 | let res = req.reply(&file).await; 37 | 38 | assert_eq!(res.status(), 200); 39 | 40 | let contents = fs::read("examples/todos.rs").expect("fs::read"); 41 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 42 | assert_eq!(res.headers()["content-type"], "text/x-rust"); 43 | assert_eq!(res.headers()["accept-ranges"], "bytes"); 44 | 45 | assert_eq!(res.body(), &*contents); 46 | 47 | let malformed_req = warp::test::request().path("todos.rs"); 48 | assert_eq!(malformed_req.reply(&file).await.status(), 404); 49 | } 50 | 51 | #[tokio::test] 52 | async fn dir_encoded() { 53 | let _ = pretty_env_logger::try_init(); 54 | 55 | let file = warp::fs::dir("examples"); 56 | 57 | let req = warp::test::request().path("/todos%2ers"); 58 | let res = req.reply(&file).await; 59 | 60 | assert_eq!(res.status(), 200); 61 | 62 | let contents = fs::read("examples/todos.rs").expect("fs::read"); 63 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 64 | 65 | assert_eq!(res.body(), &*contents); 66 | } 67 | 68 | #[tokio::test] 69 | async fn dir_not_found() { 70 | let _ = pretty_env_logger::try_init(); 71 | 72 | let file = warp::fs::dir("examples"); 73 | 74 | let req = warp::test::request().path("/definitely-not-found"); 75 | let res = req.reply(&file).await; 76 | 77 | assert_eq!(res.status(), 404); 78 | } 79 | 80 | #[tokio::test] 81 | async fn dir_bad_path() { 82 | let _ = pretty_env_logger::try_init(); 83 | 84 | let file = warp::fs::dir("examples"); 85 | 86 | let req = warp::test::request().path("/../README.md"); 87 | let res = req.reply(&file).await; 88 | 89 | assert_eq!(res.status(), 404); 90 | } 91 | 92 | #[tokio::test] 93 | async fn dir_bad_encoded_path() { 94 | let _ = pretty_env_logger::try_init(); 95 | 96 | let file = warp::fs::dir("examples"); 97 | 98 | let req = warp::test::request().path("/%2E%2e/README.md"); 99 | let res = req.reply(&file).await; 100 | 101 | assert_eq!(res.status(), 404); 102 | } 103 | 104 | #[tokio::test] 105 | async fn dir_fallback_index_on_dir() { 106 | let _ = pretty_env_logger::try_init(); 107 | 108 | let file = warp::fs::dir("examples"); 109 | let req = warp::test::request().path("/dir"); 110 | let res = req.reply(&file).await; 111 | let contents = fs::read("examples/dir/index.html").expect("fs::read"); 112 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 113 | assert_eq!(res.status(), 200); 114 | let req = warp::test::request().path("/dir/"); 115 | let res = req.reply(&file).await; 116 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 117 | assert_eq!(res.status(), 200); 118 | } 119 | 120 | #[tokio::test] 121 | async fn not_modified() { 122 | let _ = pretty_env_logger::try_init(); 123 | 124 | let file = warp::fs::file("README.md"); 125 | 126 | let req = warp::test::request(); 127 | let body = fs::read("README.md").unwrap(); 128 | let res1 = req.reply(&file).await; 129 | assert_eq!(res1.status(), 200); 130 | assert_eq!(res1.headers()["content-length"], body.len().to_string()); 131 | 132 | // if-modified-since 133 | let res = warp::test::request() 134 | .header("if-modified-since", &res1.headers()["last-modified"]) 135 | .reply(&file) 136 | .await; 137 | assert_eq!(res.headers().get("content-length"), None); 138 | assert_eq!(res.status(), 304); 139 | assert_eq!(res.body(), ""); 140 | 141 | // clearly too old 142 | let res = warp::test::request() 143 | .header("if-modified-since", "Mon, 07 Nov 1994 01:00:00 GMT") 144 | .reply(&file) 145 | .await; 146 | assert_eq!(res.status(), 200); 147 | assert_eq!(res.body(), &body); 148 | assert_eq!(res1.headers()["content-length"], body.len().to_string()); 149 | } 150 | 151 | #[tokio::test] 152 | async fn precondition() { 153 | let _ = pretty_env_logger::try_init(); 154 | 155 | let file = warp::fs::file("README.md"); 156 | 157 | let req = warp::test::request(); 158 | let res1 = req.reply(&file).await; 159 | assert_eq!(res1.status(), 200); 160 | 161 | // if-unmodified-since 162 | let res = warp::test::request() 163 | .header("if-unmodified-since", &res1.headers()["last-modified"]) 164 | .reply(&file) 165 | .await; 166 | assert_eq!(res.status(), 200); 167 | 168 | // clearly too old 169 | let res = warp::test::request() 170 | .header("if-unmodified-since", "Mon, 07 Nov 1994 01:00:00 GMT") 171 | .reply(&file) 172 | .await; 173 | assert_eq!(res.status(), 412); 174 | assert_eq!(res.body(), ""); 175 | } 176 | 177 | #[tokio::test] 178 | async fn byte_ranges() { 179 | let _ = pretty_env_logger::try_init(); 180 | 181 | let contents = fs::read("README.md").expect("fs::read README.md"); 182 | let file = warp::fs::file("README.md"); 183 | 184 | let res = warp::test::request() 185 | .header("range", "bytes=100-200") 186 | .reply(&file) 187 | .await; 188 | assert_eq!(res.status(), 206); 189 | assert_eq!( 190 | res.headers()["content-range"], 191 | format!("bytes 100-200/{}", contents.len()) 192 | ); 193 | assert_eq!(res.headers()["content-length"], "101"); 194 | assert_eq!(res.body(), &contents[100..=200]); 195 | 196 | // bad range 197 | let res = warp::test::request() 198 | .header("range", "bytes=100-10") 199 | .reply(&file) 200 | .await; 201 | assert_eq!(res.status(), 416); 202 | assert_eq!( 203 | res.headers()["content-range"], 204 | format!("bytes */{}", contents.len()) 205 | ); 206 | assert_eq!(res.headers().get("content-length"), None); 207 | assert_eq!(res.body(), ""); 208 | 209 | // out of range 210 | let res = warp::test::request() 211 | .header("range", "bytes=100-100000") 212 | .reply(&file) 213 | .await; 214 | assert_eq!(res.status(), 416); 215 | assert_eq!( 216 | res.headers()["content-range"], 217 | format!("bytes */{}", contents.len()) 218 | ); 219 | assert_eq!(res.headers().get("content-length"), None); 220 | assert_eq!(res.body(), ""); 221 | 222 | // if-range too old 223 | let res = warp::test::request() 224 | .header("range", "bytes=100-200") 225 | .header("if-range", "Mon, 07 Nov 1994 01:00:00 GMT") 226 | .reply(&file) 227 | .await; 228 | assert_eq!(res.status(), 200); 229 | assert_eq!(res.headers()["content-length"], contents.len().to_string()); 230 | assert_eq!(res.headers().get("content-range"), None); 231 | } 232 | 233 | #[tokio::test] 234 | async fn byte_ranges_with_excluded_file_size() { 235 | let _ = pretty_env_logger::try_init(); 236 | 237 | let contents = fs::read("README.md").expect("fs::read README.md"); 238 | let file = warp::fs::file("README.md"); 239 | 240 | // range including end of file (non-inclusive result) 241 | let res = warp::test::request() 242 | .header("range", format!("bytes=100-{}", contents.len())) 243 | .reply(&file) 244 | .await; 245 | assert_eq!(res.status(), 206); 246 | assert_eq!( 247 | res.headers()["content-range"], 248 | format!("bytes 100-{}/{}", contents.len() - 1, contents.len()) 249 | ); 250 | assert_eq!( 251 | res.headers()["content-length"], 252 | format!("{}", contents.len() - 100) 253 | ); 254 | assert_eq!(res.body(), &contents[100..=contents.len() - 1]); 255 | 256 | // range with 1 byte to end yields same result as above. (inclusive result) 257 | let res = warp::test::request() 258 | .header("range", format!("bytes=100-{}", contents.len() - 1)) 259 | .reply(&file) 260 | .await; 261 | assert_eq!(res.status(), 206); 262 | assert_eq!( 263 | res.headers()["content-range"], 264 | format!("bytes 100-{}/{}", contents.len() - 1, contents.len()) 265 | ); 266 | assert_eq!( 267 | res.headers()["content-length"], 268 | format!("{}", contents.len() - 100) 269 | ); 270 | assert_eq!(res.body(), &contents[100..=contents.len() - 1]); 271 | } 272 | -------------------------------------------------------------------------------- /tests/header.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::Filter; 3 | 4 | #[tokio::test] 5 | async fn exact() { 6 | let _ = pretty_env_logger::try_init(); 7 | 8 | let host = warp::header::exact("host", "localhost"); 9 | 10 | let req = warp::test::request().header("host", "localhost"); 11 | 12 | assert!(req.matches(&host).await); 13 | 14 | let req = warp::test::request(); 15 | assert!(!req.matches(&host).await, "header missing"); 16 | 17 | let req = warp::test::request().header("host", "hyper.rs"); 18 | assert!(!req.matches(&host).await, "header value different"); 19 | } 20 | 21 | #[tokio::test] 22 | async fn exact_rejections() { 23 | let _ = pretty_env_logger::try_init(); 24 | 25 | let host = warp::header::exact("host", "localhost").map(warp::reply); 26 | 27 | let res = warp::test::request() 28 | .header("host", "nope") 29 | .reply(&host) 30 | .await; 31 | 32 | assert_eq!(res.status(), 400); 33 | assert_eq!(res.body(), "Invalid request header \"host\""); 34 | 35 | let res = warp::test::request() 36 | .header("not-even-a-host", "localhost") 37 | .reply(&host) 38 | .await; 39 | 40 | assert_eq!(res.status(), 400); 41 | assert_eq!(res.body(), "Missing request header \"host\""); 42 | } 43 | 44 | #[tokio::test] 45 | async fn optional() { 46 | let _ = pretty_env_logger::try_init(); 47 | 48 | let con_len = warp::header::optional::("content-length"); 49 | 50 | let val = warp::test::request() 51 | .filter(&con_len) 52 | .await 53 | .expect("missing header matches"); 54 | assert_eq!(val, None); 55 | 56 | let val = warp::test::request() 57 | .header("content-length", "5") 58 | .filter(&con_len) 59 | .await 60 | .expect("existing header matches"); 61 | 62 | assert_eq!(val, Some(5)); 63 | 64 | assert!( 65 | !warp::test::request() 66 | .header("content-length", "boom") 67 | .matches(&con_len) 68 | .await, 69 | "invalid optional header still rejects", 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /tests/host.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::host::Authority; 3 | 4 | #[tokio::test] 5 | async fn exact() { 6 | let filter = warp::host::exact("known.com"); 7 | 8 | // no authority 9 | let req = warp::test::request(); 10 | assert!(req.filter(&filter).await.unwrap_err().is_not_found()); 11 | 12 | // specified in URI 13 | let req = warp::test::request().path("http://known.com/about-us"); 14 | assert!(req.filter(&filter).await.is_ok()); 15 | 16 | let req = warp::test::request().path("http://unknown.com/about-us"); 17 | assert!(req.filter(&filter).await.unwrap_err().is_not_found()); 18 | 19 | // specified in Host header 20 | let req = warp::test::request() 21 | .header("host", "known.com") 22 | .path("/about-us"); 23 | assert!(req.filter(&filter).await.is_ok()); 24 | 25 | let req = warp::test::request() 26 | .header("host", "unknown.com") 27 | .path("/about-us"); 28 | assert!(req.filter(&filter).await.unwrap_err().is_not_found()); 29 | 30 | // specified in both - matching 31 | let req = warp::test::request() 32 | .header("host", "known.com") 33 | .path("http://known.com/about-us"); 34 | assert!(req.filter(&filter).await.is_ok()); 35 | 36 | let req = warp::test::request() 37 | .header("host", "unknown.com") 38 | .path("http://unknown.com/about-us"); 39 | assert!(req.filter(&filter).await.unwrap_err().is_not_found()); 40 | 41 | // specified in both - mismatch 42 | let req = warp::test::request() 43 | .header("host", "known.com") 44 | .path("http://known2.com/about-us"); 45 | assert!(req 46 | .filter(&filter) 47 | .await 48 | .unwrap_err() 49 | .find::() 50 | .is_some()); 51 | 52 | // bad host header - invalid chars 53 | let req = warp::test::request() 54 | .header("host", "😭") 55 | .path("http://known.com/about-us"); 56 | assert!(req 57 | .filter(&filter) 58 | .await 59 | .unwrap_err() 60 | .find::() 61 | .is_some()); 62 | 63 | // bad host header - invalid format 64 | let req = warp::test::request() 65 | .header("host", "hello space.com") 66 | .path("http://known.com//about-us"); 67 | assert!(req 68 | .filter(&filter) 69 | .await 70 | .unwrap_err() 71 | .find::() 72 | .is_some()); 73 | } 74 | 75 | #[tokio::test] 76 | async fn optional() { 77 | let filter = warp::host::optional(); 78 | 79 | let req = warp::test::request().header("host", "example.com"); 80 | assert_eq!( 81 | req.filter(&filter).await.unwrap(), 82 | Some(Authority::from_static("example.com")) 83 | ); 84 | 85 | let req = warp::test::request(); 86 | assert_eq!(req.filter(&filter).await.unwrap(), None); 87 | } 88 | -------------------------------------------------------------------------------- /tests/method.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::Filter; 3 | 4 | #[tokio::test] 5 | async fn method() { 6 | let _ = pretty_env_logger::try_init(); 7 | let get = warp::get().map(warp::reply); 8 | 9 | let req = warp::test::request(); 10 | assert!(req.matches(&get).await); 11 | 12 | let req = warp::test::request().method("POST"); 13 | assert!(!req.matches(&get).await); 14 | 15 | let req = warp::test::request().method("POST"); 16 | let resp = req.reply(&get).await; 17 | assert_eq!(resp.status(), 405); 18 | } 19 | 20 | #[tokio::test] 21 | async fn method_not_allowed_trumps_not_found() { 22 | let _ = pretty_env_logger::try_init(); 23 | let get = warp::get().and(warp::path("hello").map(warp::reply)); 24 | let post = warp::post().and(warp::path("bye").map(warp::reply)); 25 | 26 | let routes = get.or(post); 27 | 28 | let req = warp::test::request().method("GET").path("/bye"); 29 | 30 | let resp = req.reply(&routes).await; 31 | // GET was allowed, but only for /hello, so POST returning 405 is fine. 32 | assert_eq!(resp.status(), 405); 33 | } 34 | 35 | #[tokio::test] 36 | async fn bad_request_trumps_method_not_allowed() { 37 | let _ = pretty_env_logger::try_init(); 38 | let get = warp::get() 39 | .and(warp::path("hello")) 40 | .and(warp::header::exact("foo", "bar")) 41 | .map(warp::reply); 42 | let post = warp::post().and(warp::path("bye")).map(warp::reply); 43 | 44 | let routes = get.or(post); 45 | 46 | let req = warp::test::request().method("GET").path("/hello"); 47 | 48 | let resp = req.reply(&routes).await; 49 | // GET was allowed, but header rejects with 400, should not 50 | // assume POST was the appropriate method. 51 | assert_eq!(resp.status(), 400); 52 | } 53 | -------------------------------------------------------------------------------- /tests/multipart.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use bytes::BufMut; 3 | use futures_util::{TryFutureExt, TryStreamExt}; 4 | use warp::{multipart, Filter}; 5 | 6 | #[tokio::test] 7 | async fn form_fields() { 8 | let _ = pretty_env_logger::try_init(); 9 | 10 | let route = multipart::form().and_then(|form: multipart::FormData| { 11 | async { 12 | // Collect the fields into (name, value): (String, Vec) 13 | let part: Result)>, warp::Rejection> = form 14 | .and_then(|part| { 15 | let name = part.name().to_string(); 16 | let value = part.stream().try_fold(Vec::new(), |mut vec, data| { 17 | vec.put(data); 18 | async move { Ok(vec) } 19 | }); 20 | value.map_ok(move |vec| (name, vec)) 21 | }) 22 | .try_collect() 23 | .await 24 | .map_err(|e| { 25 | panic!("multipart error: {:?}", e); 26 | }); 27 | part 28 | } 29 | }); 30 | 31 | let boundary = "--abcdef1234--"; 32 | let body = format!( 33 | "\ 34 | --{0}\r\n\ 35 | content-disposition: form-data; name=\"foo\"\r\n\r\n\ 36 | bar\r\n\ 37 | --{0}--\r\n\ 38 | ", 39 | boundary 40 | ); 41 | 42 | let req = warp::test::request() 43 | .method("POST") 44 | .header("content-length", body.len()) 45 | .header( 46 | "content-type", 47 | format!("multipart/form-data; boundary={}", boundary), 48 | ) 49 | .body(body); 50 | 51 | let vec = req.filter(&route).await.unwrap(); 52 | assert_eq!(&vec[0].0, "foo"); 53 | assert_eq!(&vec[0].1, b"bar"); 54 | } 55 | 56 | #[tokio::test] 57 | async fn max_length_is_enforced() { 58 | let _ = pretty_env_logger::try_init(); 59 | 60 | let route = multipart::form() 61 | .and_then(|_: multipart::FormData| async { Ok::<(), warp::Rejection>(()) }); 62 | 63 | let boundary = "--abcdef1234--"; 64 | 65 | let req = warp::test::request() 66 | .method("POST") 67 | // Note no content-length header 68 | .header("transfer-encoding", "chunked") 69 | .header( 70 | "content-type", 71 | format!("multipart/form-data; boundary={}", boundary), 72 | ); 73 | 74 | // Intentionally don't add body, as it automatically also adds 75 | // content-length header 76 | let resp = req.filter(&route).await; 77 | assert!(resp.is_err()); 78 | } 79 | 80 | #[tokio::test] 81 | async fn max_length_can_be_disabled() { 82 | let _ = pretty_env_logger::try_init(); 83 | 84 | let route = multipart::form() 85 | .max_length(None) 86 | .and_then(|_: multipart::FormData| async { Ok::<(), warp::Rejection>(()) }); 87 | 88 | let boundary = "--abcdef1234--"; 89 | 90 | let req = warp::test::request() 91 | .method("POST") 92 | .header("transfer-encoding", "chunked") 93 | .header( 94 | "content-type", 95 | format!("multipart/form-data; boundary={}", boundary), 96 | ); 97 | 98 | // Intentionally don't add body, as it automatically also adds 99 | // content-length header 100 | let resp = req.filter(&route).await; 101 | assert!(resp.is_ok()); 102 | } 103 | -------------------------------------------------------------------------------- /tests/query.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | use serde_derive::Deserialize; 4 | use std::collections::HashMap; 5 | use warp::Filter; 6 | 7 | #[tokio::test] 8 | async fn query() { 9 | let as_map = warp::query::>(); 10 | 11 | let req = warp::test::request().path("/?foo=bar&baz=quux"); 12 | 13 | let extracted = req.filter(&as_map).await.unwrap(); 14 | assert_eq!(extracted["foo"], "bar"); 15 | assert_eq!(extracted["baz"], "quux"); 16 | } 17 | 18 | #[tokio::test] 19 | async fn query_struct() { 20 | let as_struct = warp::query::(); 21 | 22 | let req = warp::test::request().path("/?foo=bar&baz=quux"); 23 | 24 | let extracted = req.filter(&as_struct).await.unwrap(); 25 | assert_eq!( 26 | extracted, 27 | MyArgs { 28 | foo: Some("bar".into()), 29 | baz: Some("quux".into()) 30 | } 31 | ); 32 | } 33 | 34 | #[tokio::test] 35 | async fn empty_query_struct() { 36 | let as_struct = warp::query::(); 37 | 38 | let req = warp::test::request().path("/?"); 39 | 40 | let extracted = req.filter(&as_struct).await.unwrap(); 41 | assert_eq!( 42 | extracted, 43 | MyArgs { 44 | foo: None, 45 | baz: None 46 | } 47 | ); 48 | } 49 | 50 | #[tokio::test] 51 | async fn query_struct_no_values() { 52 | let as_struct = warp::query::(); 53 | 54 | let req = warp::test::request().path("/?foo&baz"); 55 | 56 | let extracted = req.filter(&as_struct).await.unwrap(); 57 | assert_eq!( 58 | extracted, 59 | MyArgs { 60 | foo: Some("".into()), 61 | baz: Some("".into()) 62 | } 63 | ); 64 | } 65 | 66 | #[tokio::test] 67 | async fn missing_query_struct() { 68 | let as_struct = warp::query::(); 69 | 70 | let req = warp::test::request().path("/"); 71 | 72 | let extracted = req.filter(&as_struct).await.unwrap(); 73 | assert_eq!( 74 | extracted, 75 | MyArgs { 76 | foo: None, 77 | baz: None 78 | } 79 | ); 80 | } 81 | 82 | #[derive(Deserialize, Debug, Eq, PartialEq)] 83 | struct MyArgs { 84 | foo: Option, 85 | baz: Option, 86 | } 87 | 88 | #[tokio::test] 89 | async fn required_query_struct() { 90 | let as_struct = warp::query::(); 91 | 92 | let req = warp::test::request().path("/?foo=bar&baz=quux"); 93 | 94 | let extracted = req.filter(&as_struct).await.unwrap(); 95 | assert_eq!( 96 | extracted, 97 | MyRequiredArgs { 98 | foo: "bar".into(), 99 | baz: "quux".into() 100 | } 101 | ); 102 | } 103 | 104 | #[tokio::test] 105 | async fn missing_required_query_struct_partial() { 106 | let as_struct = warp::query::(); 107 | 108 | let req = warp::test::request().path("/?foo=something"); 109 | 110 | let extracted = req.filter(&as_struct).await; 111 | assert!(extracted.is_err()) 112 | } 113 | 114 | #[tokio::test] 115 | async fn missing_required_query_struct_no_query() { 116 | let as_struct = warp::query::().map(|_| warp::reply()); 117 | 118 | let req = warp::test::request().path("/"); 119 | 120 | let res = req.reply(&as_struct).await; 121 | assert_eq!(res.status(), 400); 122 | assert_eq!(res.body(), "Invalid query string"); 123 | } 124 | 125 | #[derive(Deserialize, Debug, Eq, PartialEq)] 126 | struct MyRequiredArgs { 127 | foo: String, 128 | baz: String, 129 | } 130 | 131 | #[tokio::test] 132 | async fn raw_query() { 133 | let as_raw = warp::query::raw(); 134 | 135 | let req = warp::test::request().path("/?foo=bar&baz=quux"); 136 | 137 | let extracted = req.filter(&as_raw).await.unwrap(); 138 | assert_eq!(extracted, "foo=bar&baz=quux".to_owned()); 139 | } 140 | -------------------------------------------------------------------------------- /tests/redirect.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::{http::Uri, Filter}; 3 | 4 | #[tokio::test] 5 | async fn redirect_uri() { 6 | let over_there = warp::any().map(|| warp::redirect(Uri::from_static("/over-there"))); 7 | 8 | let req = warp::test::request(); 9 | let resp = req.reply(&over_there).await; 10 | 11 | assert_eq!(resp.status(), 301); 12 | assert_eq!(resp.headers()["location"], "/over-there"); 13 | } 14 | 15 | #[tokio::test] 16 | async fn redirect_found_uri() { 17 | let over_there = warp::any().map(|| warp::redirect::found(Uri::from_static("/over-there"))); 18 | 19 | let req = warp::test::request(); 20 | let resp = req.reply(&over_there).await; 21 | 22 | assert_eq!(resp.status(), 302); 23 | assert_eq!(resp.headers()["location"], "/over-there"); 24 | } 25 | 26 | #[tokio::test] 27 | async fn redirect_see_other_uri() { 28 | let over_there = warp::any().map(|| warp::redirect::see_other(Uri::from_static("/over-there"))); 29 | 30 | let req = warp::test::request(); 31 | let resp = req.reply(&over_there).await; 32 | 33 | assert_eq!(resp.status(), 303); 34 | assert_eq!(resp.headers()["location"], "/over-there"); 35 | } 36 | 37 | #[tokio::test] 38 | async fn redirect_temporary_uri() { 39 | let over_there = warp::any().map(|| warp::redirect::temporary(Uri::from_static("/over-there"))); 40 | 41 | let req = warp::test::request(); 42 | let resp = req.reply(&over_there).await; 43 | 44 | assert_eq!(resp.status(), 307); 45 | assert_eq!(resp.headers()["location"], "/over-there"); 46 | } 47 | 48 | #[tokio::test] 49 | async fn redirect_permanent_uri() { 50 | let over_there = warp::any().map(|| warp::redirect::permanent(Uri::from_static("/over-there"))); 51 | 52 | let req = warp::test::request(); 53 | let resp = req.reply(&over_there).await; 54 | 55 | assert_eq!(resp.status(), 308); 56 | assert_eq!(resp.headers()["location"], "/over-there"); 57 | } 58 | -------------------------------------------------------------------------------- /tests/reply_with.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | use warp::http::header::{HeaderMap, HeaderValue}; 3 | use warp::Filter; 4 | 5 | #[tokio::test] 6 | async fn header() { 7 | let header = warp::reply::with::header("foo", "bar"); 8 | 9 | let no_header = warp::any().map(warp::reply).with(&header); 10 | 11 | let req = warp::test::request(); 12 | let resp = req.reply(&no_header).await; 13 | assert_eq!(resp.headers()["foo"], "bar"); 14 | 15 | let prev_header = warp::reply::with::header("foo", "sean"); 16 | let yes_header = warp::any().map(warp::reply).with(prev_header).with(header); 17 | 18 | let req = warp::test::request(); 19 | let resp = req.reply(&yes_header).await; 20 | assert_eq!(resp.headers()["foo"], "bar", "replaces header"); 21 | } 22 | 23 | #[tokio::test] 24 | async fn headers() { 25 | let mut headers = HeaderMap::new(); 26 | headers.insert("server", HeaderValue::from_static("warp")); 27 | headers.insert("foo", HeaderValue::from_static("bar")); 28 | 29 | let headers = warp::reply::with::headers(headers); 30 | 31 | let no_header = warp::any().map(warp::reply).with(&headers); 32 | 33 | let req = warp::test::request(); 34 | let resp = req.reply(&no_header).await; 35 | assert_eq!(resp.headers()["foo"], "bar"); 36 | assert_eq!(resp.headers()["server"], "warp"); 37 | 38 | let prev_header = warp::reply::with::header("foo", "sean"); 39 | let yes_header = warp::any().map(warp::reply).with(prev_header).with(headers); 40 | 41 | let req = warp::test::request(); 42 | let resp = req.reply(&yes_header).await; 43 | assert_eq!(resp.headers()["foo"], "bar", "replaces header"); 44 | } 45 | 46 | #[tokio::test] 47 | async fn default_header() { 48 | let def_header = warp::reply::with::default_header("foo", "bar"); 49 | 50 | let no_header = warp::any().map(warp::reply).with(&def_header); 51 | 52 | let req = warp::test::request(); 53 | let resp = req.reply(&no_header).await; 54 | 55 | assert_eq!(resp.headers()["foo"], "bar"); 56 | 57 | let header = warp::reply::with::header("foo", "sean"); 58 | let yes_header = warp::any().map(warp::reply).with(header).with(def_header); 59 | 60 | let req = warp::test::request(); 61 | let resp = req.reply(&yes_header).await; 62 | 63 | assert_eq!(resp.headers()["foo"], "sean", "doesn't replace header"); 64 | } 65 | -------------------------------------------------------------------------------- /tests/tracing.rs: -------------------------------------------------------------------------------- 1 | use warp::Filter; 2 | 3 | #[tokio::test] 4 | async fn uses_tracing() { 5 | // Setup a log subscriber (responsible to print to output) 6 | let subscriber = tracing_subscriber::fmt() 7 | .with_env_filter("trace") 8 | .without_time() 9 | .finish(); 10 | 11 | // Set the previously created subscriber as the global subscriber 12 | tracing::subscriber::set_global_default(subscriber).unwrap(); 13 | // Redirect normal log messages to the tracing subscriber 14 | tracing_log::LogTracer::init().unwrap(); 15 | 16 | // Start a span with some metadata (fields) 17 | let span = tracing::info_span!("app", domain = "www.example.org"); 18 | let _guard = span.enter(); 19 | 20 | log::info!("logged using log macro"); 21 | 22 | let ok = warp::any() 23 | .map(|| { 24 | tracing::info!("printed for every request"); 25 | }) 26 | .untuple_one() 27 | .and(warp::path("aa")) 28 | .map(|| { 29 | tracing::info!("only printed when path '/aa' matches"); 30 | }) 31 | .untuple_one() 32 | .map(warp::reply) 33 | // Here we add the tracing logger which will ensure that all requests has a span with 34 | // useful information about the request (method, url, version, remote_addr, etc.) 35 | .with(warp::trace::request()); 36 | 37 | tracing::info!("logged using tracing macro"); 38 | 39 | // Send a request for / 40 | let req = warp::test::request(); 41 | let resp = req.reply(&ok); 42 | assert_eq!(resp.await.status(), 404); 43 | 44 | // Send a request for /aa 45 | let req = warp::test::request().path("/aa"); 46 | let resp = req.reply(&ok); 47 | assert_eq!(resp.await.status(), 200); 48 | } 49 | --------------------------------------------------------------------------------