1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-06-27 07:19:04 +02:00

move actix-web to own dir

This commit is contained in:
Rob Ede
2022-02-01 00:30:41 +00:00
parent 30aa64ea32
commit bcdde1d4ea
109 changed files with 1287 additions and 1280 deletions

1032
actix-web/CHANGES.md Normal file

File diff suppressed because it is too large Load Diff

144
actix-web/Cargo.toml Normal file
View File

@ -0,0 +1,144 @@
[package]
name = "actix-web"
version = "4.0.0-rc.1"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"]
categories = [
"network-programming",
"asynchronous",
"web-programming::http-server",
"web-programming::websocket"
]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0"
edition = "2018"
[package.metadata.docs.rs]
# features that docs.rs will build with
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
rustdoc-args = ["--cfg", "docsrs"]
[lib]
name = "actix_web"
path = "src/lib.rs"
[features]
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
# Brotli algorithm content-encoding support
compress-brotli = ["actix-http/compress-brotli", "__compress"]
# Gzip and deflate algorithms content-encoding support
compress-gzip = ["actix-http/compress-gzip", "__compress"]
# Zstd algorithm content-encoding support
compress-zstd = ["actix-http/compress-zstd", "__compress"]
# support for cookies
cookies = ["cookie"]
# secure cookies feature
secure-cookies = ["cookie/secure"]
# openssl
openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
# Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime.
__compress = []
# io-uring feature only avaiable for Linux OSes.
experimental-io-uring = ["actix-server/io-uring"]
[dependencies]
actix-codec = "0.4.1"
actix-macros = "0.2.3"
actix-rt = "2.6"
actix-server = "2"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-tls = { version = "3.0.0", default-features = false, optional = true }
actix-http = { version = "3.0.0-rc.1", features = ["http2", "ws"] }
actix-router = "0.5.0-rc.3"
actix-web-codegen = "0.5.0-rc.2"
ahash = "0.7"
bytes = "1"
cfg-if = "1"
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
itoa = "1"
language-tags = "0.3"
once_cell = "1.5"
log = "0.4"
mime = "0.3"
pin-project-lite = "0.2.7"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6.1"
socket2 = "0.4.0"
time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1"
[dev-dependencies]
actix-files = "0.6.0-beta.16"
actix-test = { version = "0.1.0-beta.12", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.20", features = ["openssl"] }
brotli = "3.3.3"
const-str = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9"
flate2 = "1.0.13"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
rand = "0.8"
rcgen = "0.8"
rustls-pemfile = "0.2"
static_assertions = "1"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.9"
[[test]]
name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
[[test]]
name = "compression"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
[[example]]
name = "basic"
required-features = ["compress-gzip"]
[[example]]
name = "uds"
required-features = ["compress-gzip"]
[[example]]
name = "on-connect"
required-features = []
[[bench]]
name = "server"
harness = false
[[bench]]
name = "service"
harness = false
[[bench]]
name = "responder"
harness = false

1
actix-web/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-web/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

677
actix-web/MIGRATION.md Normal file
View File

@ -0,0 +1,677 @@
## Unreleased
- The default `NormalizePath` behavior now strips trailing slashes by default. This was
previously documented to be the case in v3 but the behavior now matches. The effect is that
routes defined with trailing slashes will become inaccessible when
using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
It is advised that the `new` method be used instead.
Before: `#[get("/test/")]`
After: `#[get("/test")]`
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
- The `type Config` of `FromRequest` was removed.
- Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
By default all compression algorithms are enabled.
To select algorithm you want to include with `middleware::Compress` use following flags:
- `compress-brotli`
- `compress-gzip`
- `compress-zstd`
If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want
to have compression enabled. Please change features selection like bellow:
Before: `"compress"`
After: `"compress-brotli", "compress-gzip", "compress-zstd"`
## 3.0.0
- The return type for `ServiceRequest::app_data::<T>()` was changed from returning a `Data<T>` to
simply a `T`. To access a `Data<T>` use `ServiceRequest::app_data::<Data<T>>()`.
- Cookie handling has been offloaded to the `cookie` crate:
* `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs.
* Some types now require lifetime parameters.
- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects
any `actix-web` method previously expecting a time v0.1 input.
- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now
result in `SameSite=None` being sent with the response Set-Cookie header.
To create a cookie without a SameSite attribute, remove any calls setting same_site.
- actix-http support for Actors messages was moved to actix-http crate and is enabled
with feature `actors`
- content_length function is removed from actix-http.
You can set Content-Length by normally setting the response body or calling no_chunking function.
- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`.
- Code that was using `path.<index>` to access a `web::Path<(A, B, C)>`s elements now needs to use
destructuring or `.into_inner()`. For example:
```rust
// Previously:
async fn some_route(path: web::Path<(String, String)>) -> String {
format!("Hello, {} {}", path.0, path.1)
}
// Now (this also worked before):
async fn some_route(path: web::Path<(String, String)>) -> String {
let (first_name, last_name) = path.into_inner();
format!("Hello, {} {}", first_name, last_name)
}
// Or (this wasn't previously supported):
async fn some_route(web::Path((first_name, last_name)): web::Path<(String, String)>) -> String {
format!("Hello, {} {}", first_name, last_name)
}
```
- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one.
It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`,
or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`.
- `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`.
- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`.
## 2.0.0
- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to
`.await` on `run` method result, in that case it awaits server exit.
- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`.
Stored data is available via `HttpRequest::app_data()` method at runtime.
- Extractor configuration must be registered with `App::app_data()` instead of `App::data()`
- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()`
replace `fn` with `async fn` to convert sync handler to async
- `actix_http_test::TestServer` moved to `actix_web::test` module. To start
test server use `test::start()` or `test_start_with_config()` methods
- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders
http response.
- Feature `rust-tls` renamed to `rustls`
instead of
```rust
actix-web = { version = "2.0.0", features = ["rust-tls"] }
```
use
```rust
actix-web = { version = "2.0.0", features = ["rustls"] }
```
- Feature `ssl` renamed to `openssl`
instead of
```rust
actix-web = { version = "2.0.0", features = ["ssl"] }
```
use
```rust
actix-web = { version = "2.0.0", features = ["openssl"] }
```
- `Cors` builder now requires that you call `.finish()` to construct the middleware
## 1.0.1
- Cors middleware has been moved to `actix-cors` crate
instead of
```rust
use actix_web::middleware::cors::Cors;
```
use
```rust
use actix_cors::Cors;
```
- Identity middleware has been moved to `actix-identity` crate
instead of
```rust
use actix_web::middleware::identity::{Identity, CookieIdentityPolicy, IdentityService};
```
use
```rust
use actix_identity::{Identity, CookieIdentityPolicy, IdentityService};
```
## 1.0.0
- Extractor configuration. In version 1.0 this is handled with the new `Data` mechanism for both setting and retrieving the configuration
instead of
```rust
#[derive(Default)]
struct ExtractorConfig {
config: String,
}
impl FromRequest for YourExtractor {
type Config = ExtractorConfig;
type Result = Result<YourExtractor, Error>;
fn from_request(req: &HttpRequest, cfg: &Self::Config) -> Self::Result {
println!("use the config: {:?}", cfg.config);
...
}
}
App::new().resource("/route_with_config", |r| {
r.post().with_config(handler_fn, |cfg| {
cfg.0.config = "test".to_string();
})
})
```
use the HttpRequest to get the configuration like any other `Data` with `req.app_data::<C>()` and set it with the `data()` method on the `resource`
```rust
#[derive(Default)]
struct ExtractorConfig {
config: String,
}
impl FromRequest for YourExtractor {
type Error = Error;
type Future = Result<Self, Self::Error>;
type Config = ExtractorConfig;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let cfg = req.app_data::<ExtractorConfig>();
println!("config data?: {:?}", cfg.unwrap().role);
...
}
}
App::new().service(
resource("/route_with_config")
.data(ExtractorConfig {
config: "test".to_string(),
})
.route(post().to(handler_fn)),
)
```
- Resource registration. 1.0 version uses generalized resource
registration via `.service()` method.
instead of
```rust
App.new().resource("/welcome", |r| r.f(welcome))
```
use App's or Scope's `.service()` method. `.service()` method accepts
object that implements `HttpServiceFactory` trait. By default
actix-web provides `Resource` and `Scope` services.
```rust
App.new().service(
web::resource("/welcome")
.route(web::get().to(welcome))
.route(web::post().to(post_handler))
```
- Scope registration.
instead of
```rust
let app = App::new().scope("/{project_id}", |scope| {
scope
.resource("/path1", |r| r.f(|_| HttpResponse::Ok()))
.resource("/path2", |r| r.f(|_| HttpResponse::Ok()))
.resource("/path3", |r| r.f(|_| HttpResponse::MethodNotAllowed()))
});
```
use `.service()` for registration and `web::scope()` as scope object factory.
```rust
let app = App::new().service(
web::scope("/{project_id}")
.service(web::resource("/path1").to(|| HttpResponse::Ok()))
.service(web::resource("/path2").to(|| HttpResponse::Ok()))
.service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
);
```
- `.with()`, `.with_async()` registration methods have been renamed to `.to()` and `.to_async()`.
instead of
```rust
App.new().resource("/welcome", |r| r.with(welcome))
```
use `.to()` or `.to_async()` methods
```rust
App.new().service(web::resource("/welcome").to(welcome))
```
- Passing arguments to handler with extractors, multiple arguments are allowed
instead of
```rust
fn welcome((body, req): (Bytes, HttpRequest)) -> ... {
...
}
```
use multiple arguments
```rust
fn welcome(body: Bytes, req: HttpRequest) -> ... {
...
}
```
- `.f()`, `.a()` and `.h()` handler registration methods have been removed.
Use `.to()` for handlers and `.to_async()` for async handlers. Handler function
must use extractors.
instead of
```rust
App.new().resource("/welcome", |r| r.f(welcome))
```
use App's `to()` or `to_async()` methods
```rust
App.new().service(web::resource("/welcome").to(welcome))
```
- `HttpRequest` does not provide access to request's payload stream.
instead of
```rust
fn index(req: &HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
req
.payload()
.from_err()
.fold((), |_, chunk| {
...
})
.map(|_| HttpResponse::Ok().finish())
.responder()
}
```
use `Payload` extractor
```rust
fn index(stream: web::Payload) -> impl Future<Item=HttpResponse, Error=Error> {
stream
.from_err()
.fold((), |_, chunk| {
...
})
.map(|_| HttpResponse::Ok().finish())
}
```
- `State` is now `Data`. You register Data during the App initialization process
and then access it from handlers either using a Data extractor or using
HttpRequest's api.
instead of
```rust
App.with_state(T)
```
use App's `data` method
```rust
App.new()
.data(T)
```
and either use the Data extractor within your handler
```rust
use actix_web::web::Data;
fn endpoint_handler(Data<T>)){
...
}
```
.. or access your Data element from the HttpRequest
```rust
fn endpoint_handler(req: HttpRequest) {
let data: Option<Data<T>> = req.app_data::<T>();
}
```
- AsyncResponder is removed, use `.to_async()` registration method and `impl Future<>` as result type.
instead of
```rust
use actix_web::AsyncResponder;
fn endpoint_handler(...) -> impl Future<Item=HttpResponse, Error=Error>{
...
.responder()
}
```
.. simply omit AsyncResponder and the corresponding responder() finish method
- Middleware
instead of
```rust
let app = App::new()
.middleware(middleware::Logger::default())
```
use `.wrap()` method
```rust
let app = App::new()
.wrap(middleware::Logger::default())
.route("/index.html", web::get().to(index));
```
- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()`
method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead.
instead of
```rust
fn index(req: &HttpRequest) -> Responder {
req.body()
.and_then(|body| {
...
})
}
```
use
```rust
fn index(body: Bytes) -> Responder {
...
}
```
- `actix_web::server` module has been removed. To start http server use `actix_web::HttpServer` type
- StaticFiles and NamedFile have been moved to a separate crate.
instead of `use actix_web::fs::StaticFile`
use `use actix_files::Files`
instead of `use actix_web::fs::Namedfile`
use `use actix_files::NamedFile`
- Multipart has been moved to a separate crate.
instead of `use actix_web::multipart::Multipart`
use `use actix_multipart::Multipart`
- Response compression is not enabled by default.
To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`.
- Session middleware moved to actix-session crate
- Actors support have been moved to `actix-web-actors` crate
- Custom Error
Instead of error_response method alone, ResponseError now provides two methods: error_response and render_response respectively. Where, error_response creates the error response and render_response returns the error response to the caller.
Simplest migration from 0.7 to 1.0 shall include below method to the custom implementation of ResponseError:
```rust
fn render_response(&self) -> HttpResponse {
self.error_response()
}
```
## 0.7.15
- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in
your routes, you should use `%20`.
instead of
```rust
fn main() {
let app = App::new().resource("/my index", |r| {
r.method(http::Method::GET)
.with(index);
});
}
```
use
```rust
fn main() {
let app = App::new().resource("/my%20index", |r| {
r.method(http::Method::GET)
.with(index);
});
}
```
- If you used `AsyncResult::async` you need to replace it with `AsyncResult::future`
## 0.7.4
- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple
even for handler with one parameter.
## 0.7
- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload
use `HttpMessage::payload()` method.
instead of
```rust
fn index(req: HttpRequest) -> impl Responder {
req
.from_err()
.fold(...)
....
}
```
use `.payload()`
```rust
fn index(req: HttpRequest) -> impl Responder {
req
.payload() // <- get request payload stream
.from_err()
.fold(...)
....
}
```
- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html)
trait uses `&HttpRequest` instead of `&mut HttpRequest`.
- Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead.
instead of
```rust
fn index(query: Query<..>, info: Json<MyStruct) -> impl Responder {}
```
use tuple of extractors and use `.with()` for registration:
```rust
fn index((query, json): (Query<..>, Json<MyStruct)) -> impl Responder {}
```
- `Handler::handle()` uses `&self` instead of `&mut self`
- `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value
- Removed deprecated `HttpServer::threads()`, use
[HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead.
- Renamed `client::ClientConnectorError::Connector` to
`client::ClientConnectorError::Resolver`
- `Route::with()` does not return `ExtractorConfig`, to configure
extractor use `Route::with_config()`
instead of
```rust
fn main() {
let app = App::new().resource("/index.html", |r| {
r.method(http::Method::GET)
.with(index)
.limit(4096); // <- limit size of the payload
});
}
```
use
```rust
fn main() {
let app = App::new().resource("/index.html", |r| {
r.method(http::Method::GET)
.with_config(index, |cfg| { // <- register handler
cfg.limit(4096); // <- limit size of the payload
})
});
}
```
- `Route::with_async()` does not return `ExtractorConfig`, to configure
extractor use `Route::with_async_config()`
## 0.6
- `Path<T>` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest`
- `ws::Message::Close` now includes optional close reason.
`ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed.
- `HttpServer::threads()` renamed to `HttpServer::workers()`.
- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated.
Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead.
- `HttpRequest::extensions()` returns read only reference to the request's Extension
`HttpRequest::extensions_mut()` returns mutable reference.
- Instead of
`use actix_web::middleware::{
CookieSessionBackend, CookieSessionError, RequestSession,
Session, SessionBackend, SessionImpl, SessionStorage};`
use `actix_web::middleware::session`
`use actix_web::middleware::session{CookieSessionBackend, CookieSessionError,
RequestSession, Session, SessionBackend, SessionImpl, SessionStorage};`
- `FromRequest::from_request()` accepts mutable reference to a request
- `FromRequest::Result` has to implement `Into<Reply<Self>>`
- [`Responder::respond_to()`](
https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to)
is generic over `S`
- Use `Query` extractor instead of HttpRequest::query()`.
```rust
fn index(q: Query<HashMap<String, String>>) -> Result<..> {
...
}
```
or
```rust
let q = Query::<HashMap<String, String>>::extract(req);
```
- Websocket operations are implemented as `WsWriter` trait.
you need to use `use actix_web::ws::WsWriter`
## 0.5
- `HttpResponseBuilder::body()`, `.finish()`, `.json()`
methods return `HttpResponse` instead of `Result<HttpResponse>`
- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version`
moved to `actix_web::http` module
- `actix_web::header` moved to `actix_web::http::header`
- `NormalizePath` moved to `actix_web::http` module
- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function,
shortcut for `actix_web::server::HttpServer::new()`
- `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself
- `StaticFiles::new()`'s show_index parameter removed, use `show_files_listing()` method instead.
- `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type
- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()`
functions should be used instead
- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>`
instead of `Result<_, http::Error>`
- `Application` renamed to a `App`
- `actix_web::Reply`, `actix_web::Resource` moved to `actix_web::dev`

105
actix-web/README.md Normal file
View File

@ -0,0 +1,105 @@
<div align="center">
<h1>Actix Web</h1>
<p>
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p>
<p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-rc.1)](https://docs.rs/actix-web/4.0.0-rc.1)
![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-rc.1/status.svg)](https://deps.rs/crate/actix-web/4.0.0-rc.1)
<br />
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p>
</div>
## Features
- Supports _HTTP/1.x_ and _HTTP/2_
- Streaming and pipelining
- Powerful [request routing](https://actix.rs/docs/url-dispatch/) with optional macros
- Full [Tokio](https://tokio.rs) compatibility
- Keep-alive and slow requests handling
- Client/server [WebSockets](https://actix.rs/docs/websockets/) support
- Transparent content compression/decompression (br, gzip, deflate, zstd)
- Multipart streams
- Static assets
- SSL support using OpenSSL or Rustls
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
- Includes an async [HTTP client](https://docs.rs/awc/)
- Runs on stable Rust 1.54+
## Documentation
- [Website & User Guide](https://actix.rs)
- [Examples Repository](https://github.com/actix/examples)
- [API Documentation](https://docs.rs/actix-web)
- [API Documentation (master branch)](https://actix.rs/actix-web/actix_web)
## Example
Dependencies:
```toml
[dependencies]
actix-web = "4.0.0-rc.1"
```
Code:
```rust
use actix_web::{get, web, App, HttpServer, Responder};
#[get("/{id}/{name}/index.html")]
async fn index(params: web::Path<(u32, String)>) -> impl Responder {
let (id, name) = params.into_inner();
format!("Hello {}! id:{}", name, id)
}
#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
```
### More examples
- [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
- [Application State](https://github.com/actix/examples/tree/master/basics/state/)
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
- [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
- [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
- [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
- [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
- [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
- [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
- [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
- [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples.
## Benchmarks
One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License
This project is licensed under either of the following licenses, at your option:
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0])
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT])
## Code of Conduct
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
The Actix team promises to intervene to uphold that code of conduct.

View File

@ -0,0 +1,117 @@
use std::{future::Future, time::Instant};
use actix_http::body::BoxBody;
use actix_utils::future::{ready, Ready};
use actix_web::{
error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder,
};
use criterion::{criterion_group, criterion_main, Criterion};
use futures_util::future::{join_all, Either};
// responder simulate the old responder trait.
trait FutureResponder {
type Error;
type Future: Future<Output = Result<HttpResponse, Self::Error>>;
fn future_respond_to(self, req: &HttpRequest) -> Self::Future;
}
// a simple option responder type.
struct OptionResponder<T>(Option<T>);
// a simple wrapper type around string
struct StringResponder(String);
impl FutureResponder for StringResponder {
type Error = Error;
type Future = Ready<Result<HttpResponse, Self::Error>>;
fn future_respond_to(self, _: &HttpRequest) -> Self::Future {
// this is default builder for string response in both new and old responder trait.
ready(Ok(HttpResponse::build(StatusCode::OK)
.content_type("text/plain; charset=utf-8")
.body(self.0)))
}
}
impl<T> FutureResponder for OptionResponder<T>
where
T: FutureResponder,
T::Future: Future<Output = Result<HttpResponse, Error>>,
{
type Error = Error;
type Future = Either<T::Future, Ready<Result<HttpResponse, Self::Error>>>;
fn future_respond_to(self, req: &HttpRequest) -> Self::Future {
match self.0 {
Some(t) => Either::Left(t.future_respond_to(req)),
None => Either::Right(ready(Err(error::ErrorInternalServerError("err")))),
}
}
}
impl Responder for StringResponder {
type Body = BoxBody;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::build(StatusCode::OK)
.content_type("text/plain; charset=utf-8")
.body(self.0)
}
}
impl<T: Responder> Responder for OptionResponder<T> {
type Body = BoxBody;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
match self.0 {
Some(t) => t.respond_to(req).map_into_boxed_body(),
None => HttpResponse::from_error(error::ErrorInternalServerError("err")),
}
}
}
fn future_responder(c: &mut Criterion) {
let rt = actix_rt::System::new();
let req = TestRequest::default().to_http_request();
c.bench_function("future_responder", move |b| {
b.iter_custom(|_| {
let futs = (0..100_000).map(|_| async {
StringResponder(String::from("Hello World!!"))
.future_respond_to(&req)
.await
});
let futs = join_all(futs);
let start = Instant::now();
let _res = rt.block_on(async { futs.await });
start.elapsed()
})
});
}
fn responder(c: &mut Criterion) {
let rt = actix_rt::System::new();
let req = TestRequest::default().to_http_request();
c.bench_function("responder", move |b| {
b.iter_custom(|_| {
let responders =
(0..100_000).map(|_| StringResponder(String::from("Hello World!!")));
let start = Instant::now();
let _res = rt.block_on(async {
// don't need runtime block on but to be fair.
responders.map(|r| r.respond_to(&req)).collect::<Vec<_>>()
});
start.elapsed()
})
});
}
criterion_group!(responder_bench, future_responder, responder);
criterion_main!(responder_bench);

View File

@ -0,0 +1,70 @@
use actix_web::{web, App, HttpResponse};
use awc::Client;
use criterion::{criterion_group, criterion_main, Criterion};
use futures_util::future::join_all;
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World \
Hello World Hello World Hello World Hello World Hello World";
// benchmark sending all requests at the same time
fn bench_async_burst(c: &mut Criterion) {
// We are using System here, since Runtime requires preinitialized tokio
// Maybe add to actix_rt docs
let rt = actix_rt::System::new();
let srv = rt.block_on(async {
actix_test::start(|| {
App::new().service(
web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })),
)
})
});
let url = srv.url("/");
c.bench_function("get_body_async_burst", move |b| {
b.iter_custom(|iters| {
rt.block_on(async {
let client = Client::new().get(url.clone()).freeze().unwrap();
let start = std::time::Instant::now();
// benchmark body
let burst = (0..iters).map(|_| client.send());
let resps = join_all(burst).await;
let elapsed = start.elapsed();
// if there are failed requests that might be an issue
let failed = resps.iter().filter(|r| r.is_err()).count();
if failed > 0 {
eprintln!("failed {} requests (might be bench timeout)", failed);
};
elapsed
})
})
});
}
criterion_group!(server_benches, bench_async_burst);
criterion_main!(server_benches);

View File

@ -0,0 +1,107 @@
use actix_service::Service;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::{web, App, Error, HttpResponse};
use criterion::{criterion_main, Criterion};
use std::cell::RefCell;
use std::rc::Rc;
use actix_web::test::{init_service, ok_service, TestRequest};
/// Criterion Benchmark for async Service
/// Should be used from within criterion group:
/// ```ignore
/// let mut criterion: ::criterion::Criterion<_> =
/// ::criterion::Criterion::default().configure_from_args();
/// bench_async_service(&mut criterion, ok_service(), "async_service_direct");
/// ```
///
/// Usable for benching Service wrappers:
/// Using minimum service code implementation we first measure
/// time to run minimum service, then measure time with wrapper.
///
/// Sample output
/// async_service_direct time: [1.0908 us 1.1656 us 1.2613 us]
pub fn bench_async_service<S>(c: &mut Criterion, srv: S, name: &str)
where
S: Service<ServiceRequest, Response = ServiceResponse, Error = Error> + 'static,
{
let rt = actix_rt::System::new();
let srv = Rc::new(RefCell::new(srv));
let req = TestRequest::default().to_srv_request();
assert!(rt
.block_on(srv.borrow_mut().call(req))
.unwrap()
.status()
.is_success());
// start benchmark loops
c.bench_function(name, move |b| {
b.iter_custom(|iters| {
let srv = srv.clone();
// exclude request generation, it appears it takes significant time vs call (3us vs 1us)
let futs = (0..iters)
.map(|_| TestRequest::default().to_srv_request())
.map(|req| srv.borrow_mut().call(req));
let start = std::time::Instant::now();
// benchmark body
rt.block_on(async move {
for fut in futs {
fut.await.unwrap();
}
});
// check that at least first request succeeded
start.elapsed()
})
});
}
async fn index(req: ServiceRequest) -> Result<ServiceResponse, Error> {
Ok(req.into_response(HttpResponse::Ok().finish()))
}
// Benchmark basic WebService directly
// this approach is usable for benching WebService, though it adds some time to direct service call:
// Sample results on MacBook Pro '14
// time: [2.0724 us 2.1345 us 2.2074 us]
fn async_web_service(c: &mut Criterion) {
let rt = actix_rt::System::new();
let srv = Rc::new(RefCell::new(rt.block_on(init_service(
App::new().service(web::service("/").finish(index)),
))));
let req = TestRequest::get().uri("/").to_request();
assert!(rt
.block_on(srv.borrow_mut().call(req))
.unwrap()
.status()
.is_success());
// start benchmark loops
c.bench_function("async_web_service_direct", move |b| {
b.iter_custom(|iters| {
let srv = srv.clone();
let futs = (0..iters)
.map(|_| TestRequest::get().uri("/").to_request())
.map(|req| srv.borrow_mut().call(req));
let start = std::time::Instant::now();
// benchmark body
rt.block_on(async move {
for fut in futs {
fut.await.unwrap();
}
});
// check that at least first request succeeded
start.elapsed()
})
});
}
pub fn service_benches() {
let mut criterion: ::criterion::Criterion<_> =
::criterion::Criterion::default().configure_from_args();
bench_async_service(&mut criterion, ok_service(), "async_service_direct");
async_web_service(&mut criterion);
}
criterion_main!(service_benches);

View File

@ -0,0 +1,3 @@
# Actix Web Examples
This folder contain just a few standalone code samples. There is a much larger registry of example projects [in the examples repo](https://github.com/actix/examples).

View File

@ -0,0 +1,42 @@
use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer};
#[get("/resource1/{name}/index.html")]
async fn index(req: HttpRequest, name: web::Path<String>) -> String {
println!("REQ: {:?}", req);
format!("Hello: {}!\r\n", name)
}
async fn index_async(req: HttpRequest) -> &'static str {
println!("REQ: {:?}", req);
"Hello world!\r\n"
}
#[get("/")]
async fn no_params() -> &'static str {
"Hello world!\r\n"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(|| {
App::new()
.wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default().log_target("http_log"))
.service(index)
.service(no_params)
.service(
web::resource("/resource2/index.html")
.wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3")))
.default_service(web::route().to(HttpResponse::MethodNotAllowed))
.route(web::get().to(index_async)),
)
.service(web::resource("/test1.html").to(|| async { "Test\r\n" }))
})
.bind(("127.0.0.1", 8080))?
.workers(1)
.run()
.await
}

View File

@ -0,0 +1,59 @@
//! This example shows how to use `actix_web::HttpServer::on_connect` to access a lower-level socket
//! properties and pass them to a handler through request-local data.
//!
//! For an example of extracting a client TLS certificate, see:
//! <https://github.com/actix/examples/tree/HEAD/security/rustls-client-cert>
use std::{any::Any, io, net::SocketAddr};
use actix_web::{
dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer,
Responder,
};
#[allow(dead_code)]
#[derive(Debug, Clone)]
struct ConnectionInfo {
bind: SocketAddr,
peer: SocketAddr,
ttl: Option<u32>,
}
async fn route_whoami(req: HttpRequest) -> impl Responder {
match req.conn_data::<ConnectionInfo>() {
Some(info) => HttpResponse::Ok().body(format!(
"Here is some info about your connection:\n\n{:#?}",
info
)),
None => {
HttpResponse::InternalServerError().body("Missing expected request extension data")
}
}
}
fn get_conn_info(connection: &dyn Any, data: &mut Extensions) {
if let Some(sock) = connection.downcast_ref::<TcpStream>() {
data.insert(ConnectionInfo {
bind: sock.local_addr().unwrap(),
peer: sock.peer_addr().unwrap(),
ttl: sock.ttl().ok(),
});
} else {
unreachable!("connection should only be plaintext since no TLS is set up");
}
}
#[actix_web::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
let bind = ("127.0.0.1", 8080);
log::info!("staring server at http://{}:{}", &bind.0, &bind.1);
HttpServer::new(|| App::new().default_service(web::to(route_whoami)))
.on_connect(get_conn_info)
.bind(bind)?
.workers(1)
.run()
.await
}

49
actix-web/examples/uds.rs Normal file
View File

@ -0,0 +1,49 @@
use actix_web::{get, web, HttpRequest};
#[cfg(unix)]
use actix_web::{middleware, App, Error, HttpResponse, HttpServer};
#[get("/resource1/{name}/index.html")]
async fn index(req: HttpRequest, name: web::Path<String>) -> String {
println!("REQ: {:?}", req);
format!("Hello: {}!\r\n", name)
}
#[cfg(unix)]
async fn index_async(req: HttpRequest) -> Result<&'static str, Error> {
println!("REQ: {:?}", req);
Ok("Hello world!\r\n")
}
#[get("/")]
async fn no_params() -> &'static str {
"Hello world!\r\n"
}
#[cfg(unix)]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(|| {
App::new()
.wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
.service(index)
.service(no_params)
.service(
web::resource("/resource2/index.html")
.wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3")))
.default_service(web::route().to(HttpResponse::MethodNotAllowed))
.route(web::get().to(index_async)),
)
.service(web::resource("/test1.html").to(|| async { "Test\r\n" }))
})
.bind_uds("/Users/fafhrd91/uds-test")?
.workers(1)
.run()
.await
}
#[cfg(not(unix))]
fn main() {}

714
actix-web/src/app.rs Normal file
View File

@ -0,0 +1,714 @@
use std::{cell::RefCell, fmt, future::Future, rc::Rc};
use actix_http::{body::MessageBody, Extensions, Request};
use actix_service::{
apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt,
Transform,
};
use futures_util::future::FutureExt as _;
use crate::{
app_service::{AppEntry, AppInit, AppRoutingFactory},
config::ServiceConfig,
data::{Data, DataFactory, FnDataFactory},
dev::ResourceDef,
error::Error,
resource::Resource,
route::Route,
service::{
AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper,
ServiceRequest, ServiceResponse,
},
};
/// Application builder - structure that follows the builder pattern
/// for building application instances.
pub struct App<T> {
endpoint: T,
services: Vec<Box<dyn AppServiceFactory>>,
default: Option<Rc<BoxedHttpServiceFactory>>,
factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
data_factories: Vec<FnDataFactory>,
external: Vec<ResourceDef>,
extensions: Extensions,
}
impl App<AppEntry> {
/// Create application builder. Application can be configured with a builder-like pattern.
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
let factory_ref = Rc::new(RefCell::new(None));
App {
endpoint: AppEntry::new(factory_ref.clone()),
data_factories: Vec::new(),
services: Vec::new(),
default: None,
factory_ref,
external: Vec::new(),
extensions: Extensions::new(),
}
}
}
impl<T> App<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set application (root level) data.
///
/// Application data stored with `App::app_data()` method is available through the
/// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime.
///
/// # [`Data<T>`]
/// Any [`Data<T>`] type added here can utilize it's extractor implementation in handlers.
/// Types not wrapped in `Data<T>` cannot use this extractor. See [its docs](Data<T>) for more
/// about its usage and patterns.
///
/// ```
/// use std::cell::Cell;
/// use actix_web::{web, App, HttpRequest, HttpResponse, Responder};
///
/// struct MyData {
/// count: std::cell::Cell<usize>,
/// }
///
/// async fn handler(req: HttpRequest, counter: web::Data<MyData>) -> impl Responder {
/// // note this cannot use the Data<T> extractor because it was not added with it
/// let incr = *req.app_data::<usize>().unwrap();
/// assert_eq!(incr, 3);
///
/// // update counter using other value from app data
/// counter.count.set(counter.count.get() + incr);
///
/// HttpResponse::Ok().body(counter.count.get().to_string())
/// }
///
/// let app = App::new().service(
/// web::resource("/")
/// .app_data(3usize)
/// .app_data(web::Data::new(MyData { count: Default::default() }))
/// .route(web::get().to(handler))
/// );
/// ```
///
/// # Shared Mutable State
/// [`HttpServer::new`](crate::HttpServer::new) accepts an application factory rather than an
/// application instance; the factory closure is called on each worker thread independently.
/// Therefore, if you want to share a data object between different workers, a shareable object
/// needs to be created first, outside the `HttpServer::new` closure and cloned into it.
/// [`Data<T>`] is an example of such a sharable object.
///
/// ```ignore
/// let counter = web::Data::new(AppStateWithCounter {
/// counter: Mutex::new(0),
/// });
///
/// HttpServer::new(move || {
/// // move counter object into the closure and clone for each worker
///
/// App::new()
/// .app_data(counter.clone())
/// .route("/", web::get().to(handler))
/// })
/// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, ext: U) -> Self {
self.extensions.insert(ext);
self
}
/// Add application (root) data after wrapping in `Data<T>`.
///
/// Deprecated in favor of [`app_data`](Self::app_data).
#[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")]
pub fn data<U: 'static>(self, data: U) -> Self {
self.app_data(Data::new(data))
}
/// Add application data factory that resolves asynchronously.
///
/// Data items are constructed during application initialization, before the server starts
/// accepting requests.
pub fn data_factory<F, Out, D, E>(mut self, data: F) -> Self
where
F: Fn() -> Out + 'static,
Out: Future<Output = Result<D, E>> + 'static,
D: 'static,
E: std::fmt::Debug,
{
self.data_factories.push(Box::new(move || {
{
let fut = data();
async move {
match fut.await {
Err(e) => {
log::error!("Can not construct data instance: {:?}", e);
Err(())
}
Ok(data) => {
let data: Box<dyn DataFactory> = Box::new(Data::new(data));
Ok(data)
}
}
}
}
.boxed_local()
}));
self
}
/// Run external configuration as part of the application building
/// process
///
/// This function is useful for moving parts of configuration to a
/// different module or even library. For example,
/// some of the resource's configuration could be moved to different module.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// // this function could be located in different module
/// fn config(cfg: &mut web::ServiceConfig) {
/// cfg.service(web::resource("/test")
/// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
/// );
/// }
///
/// App::new()
/// .configure(config) // <- register resources
/// .route("/index.html", web::get().to(|| HttpResponse::Ok()));
/// ```
pub fn configure<F>(mut self, f: F) -> Self
where
F: FnOnce(&mut ServiceConfig),
{
let mut cfg = ServiceConfig::new();
f(&mut cfg);
self.services.extend(cfg.services);
self.external.extend(cfg.external);
self.extensions.extend(cfg.app_data);
self
}
/// Configure route for a specific path.
///
/// This is a simplified version of the `App::service()` method.
/// This method can be used multiple times with same path, in that case
/// multiple resources with one route would be registered for same resource path.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// async fn index(data: web::Path<(String, String)>) -> &'static str {
/// "Welcome!"
/// }
///
/// let app = App::new()
/// .route("/test1", web::get().to(index))
/// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed()));
/// ```
pub fn route(self, path: &str, mut route: Route) -> Self {
self.service(
Resource::new(path)
.add_guards(route.take_guards())
.route(route),
)
}
/// Register HTTP service.
///
/// Http service is any type that implements `HttpServiceFactory` trait.
///
/// Actix Web provides several services implementations:
///
/// * *Resource* is an entry in resource table which corresponds to requested URL.
/// * *Scope* is a set of resources with common root path.
/// * "StaticFiles" is a service for static files support
pub fn service<F>(mut self, factory: F) -> Self
where
F: HttpServiceFactory + 'static,
{
self.services
.push(Box::new(ServiceFactoryWrapper::new(factory)));
self
}
/// Default service that is invoked when no matching resource could be found.
///
/// You can use a [`Route`] as default service.
///
/// If a default service is not registered, an empty `404 Not Found` response will be sent to
/// the client instead.
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// let app = App::new()
/// .service(web::resource("/index.html").route(web::get().to(index)))
/// .default_service(web::to(|| HttpResponse::NotFound()));
/// ```
pub fn default_service<F, U>(mut self, svc: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
U::InitError: fmt::Debug,
{
let svc = svc
.into_factory()
.map(|res| res.map_into_boxed_body())
.map_init_err(|e| log::error!("Can not construct default service: {:?}", e));
self.default = Some(Rc::new(boxed::factory(svc)));
self
}
/// Register an external resource.
///
/// External resources are useful for URL generation purposes only
/// and are never considered for matching at request time. Calls to
/// `HttpRequest::url_for()` will work as expected.
///
/// ```
/// use actix_web::{web, App, HttpRequest, HttpResponse, Result};
///
/// async fn index(req: HttpRequest) -> Result<HttpResponse> {
/// let url = req.url_for("youtube", &["asdlkjqme"])?;
/// assert_eq!(url.as_str(), "https://youtube.com/watch/asdlkjqme");
/// Ok(HttpResponse::Ok().into())
/// }
///
/// fn main() {
/// let app = App::new()
/// .service(web::resource("/index.html").route(
/// web::get().to(index)))
/// .external_resource("youtube", "https://youtube.com/watch/{video_id}");
/// }
/// ```
pub fn external_resource<N, U>(mut self, name: N, url: U) -> Self
where
N: AsRef<str>,
U: AsRef<str>,
{
let mut rdef = ResourceDef::new(url.as_ref());
rdef.set_name(name.as_ref());
self.external.push(rdef);
self
}
/// Registers an app-wide middleware.
///
/// Registers middleware, in the form of a middleware compo nen t (type), that runs during
/// inbound and/or outbound processing in the request life-cycle (request -> response),
/// modifying request/response as necessary, across all requests managed by the `App`.
///
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
///
/// # Middleware Order
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
/// the *last* to start executing during request processing.
///
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
/// wrapped service.
///
/// # Examples
/// ```
/// use actix_web::{middleware, web, App};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// let app = App::new()
/// .wrap(middleware::Logger::default())
/// .route("/index.html", web::get().to(index));
/// ```
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
App {
endpoint: apply(mw, self.endpoint),
data_factories: self.data_factories,
services: self.services,
default: self.default,
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
}
}
/// Registers an app-wide function middleware.
///
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `App`.
///
/// Use middleware when you need to read or modify *every* request or response in some way.
///
/// Middleware can also be applied to individual `Scope`s and `Resource`s.
///
/// See [`App::wrap`] for details on how middlewares compose with each other.
///
/// # Examples
/// ```
/// use actix_web::{dev::Service as _, middleware, web, App};
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
///
/// async fn index() -> &'static str {
/// "Welcome!"
/// }
///
/// let app = App::new()
/// .wrap_fn(|req, srv| {
/// let fut = srv.call(req);
/// async {
/// let mut res = fut.await?;
/// res.headers_mut()
/// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
/// Ok(res)
/// }
/// })
/// .route("/index.html", web::get().to(index));
/// ```
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
{
App {
endpoint: apply_fn_factory(self.endpoint, mw),
data_factories: self.data_factories,
services: self.services,
default: self.default,
factory_ref: self.factory_ref,
external: self.external,
extensions: self.extensions,
}
}
}
impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
fn into_factory(self) -> AppInit<T, B> {
AppInit {
async_data_factories: self.data_factories.into_boxed_slice().into(),
endpoint: self.endpoint,
services: Rc::new(RefCell::new(self.services)),
external: RefCell::new(self.external),
default: self.default,
factory_ref: self.factory_ref,
extensions: RefCell::new(Some(self.extensions)),
}
}
}
#[cfg(test)]
mod tests {
use actix_service::Service as _;
use actix_utils::future::{err, ok};
use bytes::Bytes;
use super::*;
use crate::{
http::{
header::{self, HeaderValue},
Method, StatusCode,
},
middleware::DefaultHeaders,
service::ServiceRequest,
test::{call_service, init_service, read_body, try_init_service, TestRequest},
web, HttpRequest, HttpResponse,
};
#[actix_rt::test]
async fn test_default_resource() {
let srv =
init_service(App::new().service(web::resource("/test").to(HttpResponse::Ok))).await;
let req = TestRequest::with_uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/blah").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = init_service(
App::new()
.service(web::resource("/test").to(HttpResponse::Ok))
.service(
web::resource("/test2")
.default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::Created()))
})
.route(web::get().to(HttpResponse::Ok)),
)
.default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::MethodNotAllowed()))
}),
)
.await;
let req = TestRequest::with_uri("/blah").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let req = TestRequest::with_uri("/test2").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test2")
.method(Method::POST)
.to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::CREATED);
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data_factory() {
let srv = init_service(
App::new()
.data_factory(|| ok::<_, ()>(10usize))
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let srv = init_service(
App::new()
.data_factory(|| ok::<_, ()>(10u32))
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data_factory_errors() {
let srv = try_init_service(
App::new()
.data_factory(|| err::<u32, _>(()))
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
assert!(srv.is_err());
}
#[actix_rt::test]
async fn test_extension() {
let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to(
|req: HttpRequest| {
assert_eq!(*req.app_data::<usize>().unwrap(), 10);
HttpResponse::Ok()
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_wrap() {
let srv = init_service(
App::new()
.wrap(
DefaultHeaders::new()
.add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
)
.route("/test", web::get().to(HttpResponse::Ok)),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_router_wrap() {
let srv = init_service(
App::new()
.route("/test", web::get().to(HttpResponse::Ok))
.wrap(
DefaultHeaders::new()
.add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_wrap_fn() {
let srv = init_service(
App::new()
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async move {
let mut res = fut.await?;
res.headers_mut()
.insert(header::CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(res)
}
})
.service(web::resource("/test").to(HttpResponse::Ok)),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_router_wrap_fn() {
let srv = init_service(
App::new()
.route("/test", web::get().to(HttpResponse::Ok))
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async {
let mut res = fut.await?;
res.headers_mut()
.insert(header::CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(res)
}
}),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_external_resource() {
let srv = init_service(
App::new()
.external_resource("youtube", "https://youtube.com/watch/{video_id}")
.route(
"/test",
web::get().to(|req: HttpRequest| {
HttpResponse::Ok()
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
}),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
}
#[test]
fn can_be_returned_from_fn() {
/// compile-only test for returning app type from function
pub fn my_app() -> App<
impl ServiceFactory<
ServiceRequest,
Response = ServiceResponse<impl MessageBody>,
Config = (),
InitError = (),
Error = Error,
>,
> {
App::new()
// logger can be removed without affecting the return type
.wrap(crate::middleware::Logger::default())
.route("/", web::to(|| async { "hello" }))
}
let _ = init_service(my_app());
}
}

View File

@ -0,0 +1,381 @@
use std::{cell::RefCell, mem, rc::Rc};
use actix_http::Request;
use actix_router::{Path, ResourceDef, Router, Url};
use actix_service::{boxed, fn_service, Service, ServiceFactory};
use futures_core::future::LocalBoxFuture;
use futures_util::future::join_all;
use crate::{
body::BoxBody,
config::{AppConfig, AppService},
data::FnDataFactory,
dev::Extensions,
guard::Guard,
request::{HttpRequest, HttpRequestPool},
rmap::ResourceMap,
service::{
AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, ServiceRequest,
ServiceResponse,
},
Error, HttpResponse,
};
/// Service factory to convert `Request` to a `ServiceRequest<S>`.
///
/// It also executes data factories.
pub struct AppInit<T, B>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
{
pub(crate) endpoint: T,
pub(crate) extensions: RefCell<Option<Extensions>>,
pub(crate) async_data_factories: Rc<[FnDataFactory]>,
pub(crate) services: Rc<RefCell<Vec<Box<dyn AppServiceFactory>>>>,
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
pub(crate) factory_ref: Rc<RefCell<Option<AppRoutingFactory>>>,
pub(crate) external: RefCell<Vec<ResourceDef>>,
}
impl<T, B> ServiceFactory<Request> for AppInit<T, B>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
T::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = T::Error;
type Config = AppConfig;
type Service = AppInitService<T::Service, B>;
type InitError = T::InitError;
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, config: AppConfig) -> Self::Future {
// set AppService's default service to 404 NotFound
// if no user defined default service exists.
let default = self.default.clone().unwrap_or_else(|| {
Rc::new(boxed::factory(fn_service(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::NotFound()))
})))
});
// create App config to pass to child services
let mut config = AppService::new(config, default.clone());
// register services
mem::take(&mut *self.services.borrow_mut())
.into_iter()
.for_each(|mut srv| srv.register(&mut config));
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
let (config, services) = config.into_services();
// complete pipeline creation.
*self.factory_ref.borrow_mut() = Some(AppRoutingFactory {
default,
services: services
.into_iter()
.map(|(mut rdef, srv, guards, nested)| {
rmap.add(&mut rdef, nested);
(rdef, srv, RefCell::new(guards))
})
.collect::<Vec<_>>()
.into_boxed_slice()
.into(),
});
// external resources
for mut rdef in mem::take(&mut *self.external.borrow_mut()) {
rmap.add(&mut rdef, None);
}
// complete ResourceMap tree creation
let rmap = Rc::new(rmap);
ResourceMap::finish(&rmap);
// construct all async data factory futures
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));
// construct app service and middleware service factory future.
let endpoint_fut = self.endpoint.new_service(());
// take extensions or create new one as app data container.
let mut app_data = self
.extensions
.borrow_mut()
.take()
.unwrap_or_else(Extensions::new);
Box::pin(async move {
// async data factories
let async_data_factories = factory_futs
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()
.map_err(|_| ())?;
// app service and middleware
let service = endpoint_fut.await?;
// populate app data container from (async) data factories.
for factory in &async_data_factories {
factory.create(&mut app_data);
}
Ok(AppInitService {
service,
app_data: Rc::new(app_data),
app_state: AppInitServiceState::new(rmap, config),
})
})
}
}
/// The [`Service`] that is passed to `actix-http`'s server builder.
///
/// Wraps a service receiving a [`ServiceRequest`] into one receiving a [`Request`].
pub struct AppInitService<T, B>
where
T: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
service: T,
app_data: Rc<Extensions>,
app_state: Rc<AppInitServiceState>,
}
/// A collection of [`AppInitService`] state that shared across `HttpRequest`s.
pub(crate) struct AppInitServiceState {
rmap: Rc<ResourceMap>,
config: AppConfig,
pool: HttpRequestPool,
}
impl AppInitServiceState {
pub(crate) fn new(rmap: Rc<ResourceMap>, config: AppConfig) -> Rc<Self> {
Rc::new(AppInitServiceState {
rmap,
config,
pool: HttpRequestPool::default(),
})
}
#[inline]
pub(crate) fn rmap(&self) -> &ResourceMap {
&*self.rmap
}
#[inline]
pub(crate) fn config(&self) -> &AppConfig {
&self.config
}
#[inline]
pub(crate) fn pool(&self) -> &HttpRequestPool {
&self.pool
}
}
impl<T, B> Service<Request> for AppInitService<T, B>
where
T: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Response = ServiceResponse<B>;
type Error = T::Error;
type Future = T::Future;
actix_service::forward_ready!(service);
fn call(&self, mut req: Request) -> Self::Future {
let extensions = Rc::new(RefCell::new(req.take_req_data()));
let conn_data = req.take_conn_data();
let (head, payload) = req.into_parts();
let req = match self.app_state.pool().pop() {
Some(mut req) => {
let inner = Rc::get_mut(&mut req.inner).unwrap();
inner.path.get_mut().update(&head.uri);
inner.path.reset();
inner.head = head;
inner.conn_data = conn_data;
inner.extensions = extensions;
req
}
None => HttpRequest::new(
Path::new(Url::new(head.uri.clone())),
head,
Rc::clone(&self.app_state),
Rc::clone(&self.app_data),
conn_data,
extensions,
),
};
self.service.call(ServiceRequest::new(req, payload))
}
}
impl<T, B> Drop for AppInitService<T, B>
where
T: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
fn drop(&mut self) {
self.app_state.pool().clear();
}
}
pub struct AppRoutingFactory {
#[allow(clippy::type_complexity)]
services: Rc<
[(
ResourceDef,
BoxedHttpServiceFactory,
RefCell<Option<Vec<Box<dyn Guard>>>>,
)],
>,
default: Rc<BoxedHttpServiceFactory>,
}
impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = AppRouting;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
// construct all services factory future with it's resource def and guards.
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
let path = path.clone();
let guards = guards.borrow_mut().take().unwrap_or_default();
let factory_fut = factory.new_service(());
async move {
let service = factory_fut.await?;
Ok((path, guards, service))
}
}));
// construct default service factory future
let default_fut = self.default.new_service(());
Box::pin(async move {
let default = default_fut.await?;
// build router from the factory future result.
let router = factory_fut
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?
.drain(..)
.fold(Router::build(), |mut router, (path, guards, service)| {
router.push(path, service, guards);
router
})
.finish();
Ok(AppRouting { router, default })
})
}
}
/// The Actix Web router default entry point.
pub struct AppRouting {
router: Router<BoxedHttpService, Vec<Box<dyn Guard>>>,
default: BoxedHttpService,
}
impl Service<ServiceRequest> for AppRouting {
type Response = ServiceResponse<BoxBody>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let res = self.router.recognize_fn(&mut req, |req, guards| {
let guard_ctx = req.guard_ctx();
guards.iter().all(|guard| guard.check(&guard_ctx))
});
if let Some((srv, _info)) = res {
srv.call(req)
} else {
self.default.call(req)
}
}
}
/// Wrapper service for routing
pub struct AppEntry {
factory: Rc<RefCell<Option<AppRoutingFactory>>>,
}
impl AppEntry {
pub fn new(factory: Rc<RefCell<Option<AppRoutingFactory>>>) -> Self {
AppEntry { factory }
}
}
impl ServiceFactory<ServiceRequest> for AppEntry {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = AppRouting;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
self.factory.borrow_mut().as_mut().unwrap().new_service(())
}
}
#[cfg(test)]
mod tests {
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use actix_service::Service;
use crate::test::{init_service, TestRequest};
use crate::{web, App, HttpResponse};
struct DropData(Arc<AtomicBool>);
impl Drop for DropData {
fn drop(&mut self) {
self.0.store(true, Ordering::Relaxed);
}
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_drop_data() {
let data = Arc::new(AtomicBool::new(false));
{
let app = init_service(
App::new()
.data(DropData(data.clone()))
.service(web::resource("/test").to(HttpResponse::Ok)),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let _ = app.call(req).await.unwrap();
}
assert!(data.load(Ordering::Relaxed));
}
}

363
actix-web/src/config.rs Normal file
View File

@ -0,0 +1,363 @@
use std::net::SocketAddr;
use std::rc::Rc;
use actix_http::Extensions;
use actix_router::ResourceDef;
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
use crate::data::Data;
use crate::error::Error;
use crate::guard::Guard;
use crate::resource::Resource;
use crate::rmap::ResourceMap;
use crate::route::Route;
use crate::service::{
AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest,
ServiceResponse,
};
type Guards = Vec<Box<dyn Guard>>;
type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
/// Application configuration
pub struct AppService {
config: AppConfig,
root: bool,
default: Rc<HttpNewService>,
#[allow(clippy::type_complexity)]
services: Vec<(
ResourceDef,
HttpNewService,
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
}
impl AppService {
/// Crate server settings instance.
pub(crate) fn new(config: AppConfig, default: Rc<HttpNewService>) -> Self {
AppService {
config,
default,
root: true,
services: Vec::new(),
}
}
/// Check if root is being configured
pub fn is_root(&self) -> bool {
self.root
}
#[allow(clippy::type_complexity)]
pub(crate) fn into_services(
self,
) -> (
AppConfig,
Vec<(
ResourceDef,
HttpNewService,
Option<Guards>,
Option<Rc<ResourceMap>>,
)>,
) {
(self.config, self.services)
}
/// Clones inner config and default service, returning new `AppService` with empty service list
/// marked as non-root.
pub(crate) fn clone_config(&self) -> Self {
AppService {
config: self.config.clone(),
default: self.default.clone(),
services: Vec::new(),
root: false,
}
}
/// Returns reference to configuration.
pub fn config(&self) -> &AppConfig {
&self.config
}
/// Returns default handler factory.
pub fn default_service(&self) -> Rc<HttpNewService> {
self.default.clone()
}
/// Register HTTP service.
pub fn register_service<F, S>(
&mut self,
rdef: ResourceDef,
guards: Option<Vec<Box<dyn Guard>>>,
factory: F,
nested: Option<Rc<ResourceMap>>,
) where
F: IntoServiceFactory<S, ServiceRequest>,
S: ServiceFactory<
ServiceRequest,
Response = ServiceResponse,
Error = Error,
Config = (),
InitError = (),
> + 'static,
{
self.services
.push((rdef, boxed::factory(factory.into_factory()), guards, nested));
}
}
/// Application connection config.
#[derive(Debug, Clone)]
pub struct AppConfig {
secure: bool,
host: String,
addr: SocketAddr,
}
impl AppConfig {
pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self {
AppConfig { secure, host, addr }
}
/// Needed in actix-test crate. Semver exempt.
#[doc(hidden)]
pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self {
AppConfig::new(secure, host, addr)
}
/// Server host name.
///
/// Host name is used by application router as a hostname for URL generation.
/// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
/// documentation for more information.
///
/// By default host name is set to a "localhost" value.
pub fn host(&self) -> &str {
&self.host
}
/// Returns true if connection is secure (i.e., running over `https:`).
pub fn secure(&self) -> bool {
self.secure
}
/// Returns the socket address of the local half of this TCP connection
pub fn local_addr(&self) -> SocketAddr {
self.addr
}
#[cfg(test)]
pub(crate) fn set_host(&mut self, host: &str) {
self.host = host.to_owned();
}
}
impl Default for AppConfig {
fn default() -> Self {
AppConfig::new(
false,
"localhost:8080".to_owned(),
"127.0.0.1:8080".parse().unwrap(),
)
}
}
/// Enables parts of app configuration to be declared separately from the app itself. Helpful for
/// modularizing large applications.
///
/// Merge a `ServiceConfig` into an app using [`App::configure`](crate::App::configure). Scope and
/// resources services have similar methods.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// // this function could be located in different module
/// fn config(cfg: &mut web::ServiceConfig) {
/// cfg.service(web::resource("/test")
/// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
/// );
/// }
///
/// // merge `/test` routes from config function to App
/// App::new().configure(config);
/// ```
pub struct ServiceConfig {
pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
pub(crate) external: Vec<ResourceDef>,
pub(crate) app_data: Extensions,
}
impl ServiceConfig {
pub(crate) fn new() -> Self {
Self {
services: Vec::new(),
external: Vec::new(),
app_data: Extensions::new(),
}
}
/// Add shared app data item.
///
/// Counterpart to [`App::data()`](crate::App::data).
#[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")]
pub fn data<U: 'static>(&mut self, data: U) -> &mut Self {
self.app_data(Data::new(data));
self
}
/// Add arbitrary app data item.
///
/// Counterpart to [`App::app_data()`](crate::App::app_data).
pub fn app_data<U: 'static>(&mut self, ext: U) -> &mut Self {
self.app_data.insert(ext);
self
}
/// Run external configuration as part of the application building process
///
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
pub fn configure<F>(&mut self, f: F) -> &mut Self
where
F: FnOnce(&mut ServiceConfig),
{
f(self);
self
}
/// Configure route for a specific path.
///
/// Counterpart to [`App::route()`](crate::App::route).
pub fn route(&mut self, path: &str, mut route: Route) -> &mut Self {
self.service(
Resource::new(path)
.add_guards(route.take_guards())
.route(route),
)
}
/// Register HTTP service factory.
///
/// Counterpart to [`App::service()`](crate::App::service).
pub fn service<F>(&mut self, factory: F) -> &mut Self
where
F: HttpServiceFactory + 'static,
{
self.services
.push(Box::new(ServiceFactoryWrapper::new(factory)));
self
}
/// Register an external resource.
///
/// External resources are useful for URL generation purposes only and are never considered for
/// matching at request time. Calls to [`HttpRequest::url_for()`](crate::HttpRequest::url_for)
/// will work as expected.
///
/// Counterpart to [`App::external_resource()`](crate::App::external_resource).
pub fn external_resource<N, U>(&mut self, name: N, url: U) -> &mut Self
where
N: AsRef<str>,
U: AsRef<str>,
{
let mut rdef = ResourceDef::new(url.as_ref());
rdef.set_name(name.as_ref());
self.external.push(rdef);
self
}
}
#[cfg(test)]
mod tests {
use actix_service::Service;
use bytes::Bytes;
use super::*;
use crate::http::{Method, StatusCode};
use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest};
use crate::{web, App, HttpRequest, HttpResponse};
// allow deprecated `ServiceConfig::data`
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data() {
let cfg = |cfg: &mut ServiceConfig| {
cfg.data(10usize);
cfg.app_data(15u8);
};
let srv = init_service(App::new().configure(cfg).service(web::resource("/").to(
|_: web::Data<usize>, req: HttpRequest| {
assert_eq!(*req.app_data::<u8>().unwrap(), 15u8);
HttpResponse::Ok()
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_external_resource() {
let srv = init_service(
App::new()
.configure(|cfg| {
cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}");
})
.route(
"/test",
web::get().to(|req: HttpRequest| {
HttpResponse::Ok()
.body(req.url_for("youtube", &["12345"]).unwrap().to_string())
}),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
}
#[actix_rt::test]
async fn test_service() {
let srv = init_service(App::new().configure(|cfg| {
cfg.service(web::resource("/test").route(web::get().to(HttpResponse::Created)))
.route("/index.html", web::get().to(HttpResponse::Ok));
}))
.await;
let req = TestRequest::with_uri("/test")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/index.html")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn nested_service_configure() {
fn cfg_root(cfg: &mut ServiceConfig) {
cfg.configure(cfg_sub);
}
fn cfg_sub(cfg: &mut ServiceConfig) {
cfg.route("/", web::get().to(|| async { "hello world" }));
}
let srv = init_service(App::new().configure(cfg_root)).await;
let req = TestRequest::with_uri("/").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_body_eq!(res, b"hello world");
}
}

368
actix-web/src/data.rs Normal file
View File

@ -0,0 +1,368 @@
use std::{any::type_name, ops::Deref, sync::Arc};
use actix_http::Extensions;
use actix_utils::future::{err, ok, Ready};
use futures_core::future::LocalBoxFuture;
use serde::Serialize;
use crate::{
dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest,
Error,
};
/// Data factory.
pub(crate) trait DataFactory {
/// Return true if modifications were made to extensions map.
fn create(&self, extensions: &mut Extensions) -> bool;
}
pub(crate) type FnDataFactory =
Box<dyn Fn() -> LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>;
/// Application data wrapper and extractor.
///
/// # Setting Data
/// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped
/// in this `Data` type for those calls, it can be used as an extractor.
///
/// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared,
/// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`.
///
/// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data),
/// and [`Resource::app_data`](crate::Resource::app_data).
///
/// # Extracting `Data`
/// Since the Actix Web router layers application data, the returned object will reference the
/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
/// extracting a `web::<Data<u32>>` for that handler will return the `Scope`'s instance.
/// However, using the same router set up and a request that does not get captured by the `Scope`,
/// `web::<Data<u32>>` would return the `App`'s instance.
///
/// If route data is not set for a handler, using `Data<T>` extractor would cause a `500 Internal
/// Server Error` response.
///
/// See also [`HttpRequest::app_data`]
/// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data).
///
/// # Unsized Data
/// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first
/// constructing an `Arc<dyn T>` and using the `From` implementation to convert it.
///
/// ```
/// # use std::{fmt::Display, sync::Arc};
/// # use actix_web::web::Data;
/// let displayable_arc: Arc<dyn Display> = Arc::new(42usize);
/// let displayable_data: Data<dyn Display> = Data::from(displayable_arc);
/// ```
///
/// # Examples
/// ```
/// use std::sync::Mutex;
/// use actix_web::{App, HttpRequest, HttpResponse, Responder, web::{self, Data}};
///
/// struct MyData {
/// counter: usize,
/// }
///
/// /// Use the `Data<T>` extractor to access data in a handler.
/// async fn index(data: Data<Mutex<MyData>>) -> impl Responder {
/// let mut my_data = data.lock().unwrap();
/// my_data.counter += 1;
/// HttpResponse::Ok()
/// }
///
/// /// Alteratively, use the `HttpRequest::app_data` method to access data in a handler.
/// async fn index_alt(req: HttpRequest) -> impl Responder {
/// let data = req.app_data::<Data<Mutex<MyData>>>().unwrap();
/// let mut my_data = data.lock().unwrap();
/// my_data.counter += 1;
/// HttpResponse::Ok()
/// }
///
/// let data = Data::new(Mutex::new(MyData { counter: 0 }));
///
/// let app = App::new()
/// // Store `MyData` in application storage.
/// .app_data(Data::clone(&data))
/// .route("/index.html", web::get().to(index))
/// .route("/index-alt.html", web::get().to(index_alt));
/// ```
#[doc(alias = "state")]
#[derive(Debug)]
pub struct Data<T: ?Sized>(Arc<T>);
impl<T> Data<T> {
/// Create new `Data` instance.
pub fn new(state: T) -> Data<T> {
Data(Arc::new(state))
}
}
impl<T: ?Sized> Data<T> {
/// Returns reference to inner `T`.
pub fn get_ref(&self) -> &T {
self.0.as_ref()
}
/// Unwraps to the internal `Arc<T>`
pub fn into_inner(self) -> Arc<T> {
self.0
}
}
impl<T: ?Sized> Deref for Data<T> {
type Target = Arc<T>;
fn deref(&self) -> &Arc<T> {
&self.0
}
}
impl<T: ?Sized> Clone for Data<T> {
fn clone(&self) -> Data<T> {
Data(self.0.clone())
}
}
impl<T: ?Sized> From<Arc<T>> for Data<T> {
fn from(arc: Arc<T>) -> Self {
Data(arc)
}
}
impl<T> Serialize for Data<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
impl<T: ?Sized + 'static> FromRequest for Data<T> {
type Error = Error;
type Future = Ready<Result<Self, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.app_data::<Data<T>>() {
ok(st.clone())
} else {
log::debug!(
"Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \
correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \
Ensure that types align in both the set and retrieve calls.",
type_name::<T>(),
req.match_name().unwrap_or_else(|| req.path())
);
err(ErrorInternalServerError(
"Requested application data is not configured correctly. \
View/enable debug logs for more details.",
))
}
}
}
impl<T: ?Sized + 'static> DataFactory for Data<T> {
fn create(&self, extensions: &mut Extensions) -> bool {
extensions.insert(Data(self.0.clone()));
true
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
dev::Service,
http::StatusCode,
test::{init_service, TestRequest},
web, App, HttpResponse,
};
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data_extractor() {
let srv = init_service(App::new().data("TEST".to_string()).service(
web::resource("/").to(|data: web::Data<String>| {
assert_eq!(data.to_lowercase(), "test");
HttpResponse::Ok()
}),
))
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let srv = init_service(
App::new()
.data(10u32)
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let srv = init_service(
App::new()
.data(10u32)
.data(13u32)
.app_data(12u64)
.app_data(15u64)
.default_service(web::to(|n: web::Data<u32>, req: HttpRequest| {
// in each case, the latter insertion should be preserved
assert_eq!(*req.app_data::<u64>().unwrap(), 15);
assert_eq!(*n.into_inner(), 13);
HttpResponse::Ok()
})),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_app_data_extractor() {
let srv = init_service(
App::new()
.app_data(Data::new(10usize))
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let srv = init_service(
App::new()
.app_data(Data::new(10u32))
.service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok())),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_route_data_extractor() {
let srv = init_service(
App::new().service(
web::resource("/")
.data(10usize)
.route(web::get().to(|_data: web::Data<usize>| HttpResponse::Ok())),
),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
// different type
let srv = init_service(
App::new().service(
web::resource("/")
.data(10u32)
.route(web::get().to(|_: web::Data<usize>| HttpResponse::Ok())),
),
)
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_override_data() {
let srv =
init_service(App::new().data(1usize).service(
web::resource("/").data(10usize).route(web::get().to(
|data: web::Data<usize>| {
assert_eq!(**data, 10);
HttpResponse::Ok()
},
)),
))
.await;
let req = TestRequest::default().to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_data_from_arc() {
let data_new = Data::new(String::from("test-123"));
let data_from_arc = Data::from(Arc::new(String::from("test-123")));
assert_eq!(data_new.0, data_from_arc.0);
}
#[actix_rt::test]
async fn test_data_from_dyn_arc() {
trait TestTrait {
fn get_num(&self) -> i32;
}
struct A {}
impl TestTrait for A {
fn get_num(&self) -> i32 {
42
}
}
// This works when Sized is required
let dyn_arc_box: Arc<Box<dyn TestTrait>> = Arc::new(Box::new(A {}));
let data_arc_box = Data::from(dyn_arc_box);
// This works when Data Sized Bound is removed
let dyn_arc: Arc<dyn TestTrait> = Arc::new(A {});
let data_arc = Data::from(dyn_arc);
assert_eq!(data_arc_box.get_num(), data_arc.get_num())
}
#[actix_rt::test]
async fn test_dyn_data_into_arc() {
trait TestTrait {
fn get_num(&self) -> i32;
}
struct A {}
impl TestTrait for A {
fn get_num(&self) -> i32 {
42
}
}
let dyn_arc: Arc<dyn TestTrait> = Arc::new(A {});
let data_arc = Data::from(dyn_arc);
let arc_from_data = data_arc.clone().into_inner();
assert_eq!(data_arc.get_num(), arc_from_data.get_num())
}
#[actix_rt::test]
async fn test_get_ref_from_dyn_data() {
trait TestTrait {
fn get_num(&self) -> i32;
}
struct A {}
impl TestTrait for A {
fn get_num(&self) -> i32 {
42
}
}
let dyn_arc: Arc<dyn TestTrait> = Arc::new(A {});
let data_arc = Data::from(dyn_arc);
let ref_data = data_arc.get_ref();
assert_eq!(data_arc.get_num(), ref_data.get_num())
}
}

44
actix-web/src/dev.rs Normal file
View File

@ -0,0 +1,44 @@
//! Lower-level types and re-exports.
//!
//! Most users will not have to interact with the types in this module, but it is useful for those
//! writing extractors, middleware, libraries, or interacting with the service API directly.
pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead};
pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
pub use actix_server::{Server, ServerHandle};
pub use actix_service::{
always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform,
};
#[cfg(feature = "__compress")]
pub use actix_http::encoding::Decoder as Decompress;
pub use crate::config::{AppConfig, AppService};
#[doc(hidden)]
pub use crate::handler::Handler;
pub use crate::info::{ConnectionInfo, PeerAddr};
pub use crate::rmap::ResourceMap;
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
pub use crate::types::{JsonBody, Readlines, UrlEncoded};
use actix_router::Patterns;
pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
match &mut patterns {
Patterns::Single(pat) => {
if !pat.is_empty() && !pat.starts_with('/') {
pat.insert(0, '/');
};
}
Patterns::List(pats) => {
for pat in pats {
if !pat.is_empty() && !pat.starts_with('/') {
pat.insert(0, '/');
};
}
}
}
patterns
}

View File

@ -0,0 +1,74 @@
use std::{error::Error as StdError, fmt};
use actix_http::{body::BoxBody, Response};
use crate::{HttpResponse, ResponseError};
/// General purpose Actix Web error.
///
/// An Actix Web error is used to carry errors from `std::error` through actix in a convenient way.
/// It can be created through converting errors with `into()`.
///
/// Whenever it is created from an external object a response error is created for it that can be
/// used to create an HTTP response from it this means that if you have access to an actix `Error`
/// you can always get a `ResponseError` reference from it.
pub struct Error {
cause: Box<dyn ResponseError>,
}
impl Error {
/// Returns the reference to the underlying `ResponseError`.
pub fn as_response_error(&self) -> &dyn ResponseError {
self.cause.as_ref()
}
/// Similar to `as_response_error` but downcasts.
pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> {
<dyn ResponseError>::downcast_ref(self.cause.as_ref())
}
/// Shortcut for creating an `HttpResponse`.
pub fn error_response(&self) -> HttpResponse {
self.cause.error_response()
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.cause, f)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", &self.cause)
}
}
impl StdError for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
// TODO: populate if replacement for Box<dyn Error> is found
None
}
}
impl From<std::convert::Infallible> for Error {
fn from(val: std::convert::Infallible) -> Self {
match val {}
}
}
/// `Error` for any error that implements `ResponseError`
impl<T: ResponseError + 'static> From<T> for Error {
fn from(err: T) -> Error {
Error {
cause: Box::new(err),
}
}
}
impl From<Error> for Response<BoxBody> {
fn from(err: Error) -> Response<BoxBody> {
err.error_response().into()
}
}

View File

@ -0,0 +1,316 @@
use std::{cell::RefCell, fmt, io::Write as _};
use actix_http::{
body::BoxBody,
header::{self, TryIntoHeaderValue as _},
StatusCode,
};
use bytes::{BufMut as _, BytesMut};
use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError};
/// Wraps errors to alter the generated response status code.
///
/// In following example, the `io::Error` is wrapped into `ErrorBadRequest` which will generate a
/// response with the 400 Bad Request status code instead of the usual status code generated by
/// an `io::Error`.
///
/// # Examples
/// ```
/// # use std::io;
/// # use actix_web::{error, HttpRequest};
/// async fn handler_error() -> Result<String, actix_web::Error> {
/// let err = io::Error::new(io::ErrorKind::Other, "error");
/// Err(error::ErrorBadRequest(err))
/// }
/// ```
pub struct InternalError<T> {
cause: T,
status: InternalErrorType,
}
enum InternalErrorType {
Status(StatusCode),
Response(RefCell<Option<HttpResponse>>),
}
impl<T> InternalError<T> {
/// Constructs an `InternalError` with given status code.
pub fn new(cause: T, status: StatusCode) -> Self {
InternalError {
cause,
status: InternalErrorType::Status(status),
}
}
/// Constructs an `InternalError` with pre-defined response.
pub fn from_response(cause: T, response: HttpResponse) -> Self {
InternalError {
cause,
status: InternalErrorType::Response(RefCell::new(Some(response))),
}
}
}
impl<T: fmt::Debug> fmt::Debug for InternalError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.cause.fmt(f)
}
}
impl<T: fmt::Display> fmt::Display for InternalError<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.cause.fmt(f)
}
}
impl<T> ResponseError for InternalError<T>
where
T: fmt::Debug + fmt::Display,
{
fn status_code(&self) -> StatusCode {
match self.status {
InternalErrorType::Status(st) => st,
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow().as_ref() {
resp.head().status
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
}
}
fn error_response(&self) -> HttpResponse {
match self.status {
InternalErrorType::Status(status) => {
let mut res = HttpResponse::new(status);
let mut buf = BytesMut::new().writer();
let _ = write!(buf, "{}", self);
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res.set_body(BoxBody::new(buf.into_inner()))
}
InternalErrorType::Response(ref resp) => {
if let Some(resp) = resp.borrow_mut().take() {
resp
} else {
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
}
}
impl<T> Responder for InternalError<T>
where
T: fmt::Debug + fmt::Display + 'static,
{
type Body = BoxBody;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::from_error(self)
}
}
macro_rules! error_helper {
($name:ident, $status:ident) => {
#[doc = concat!("Helper function that wraps any error and generates a `", stringify!($status), "` response.")]
#[allow(non_snake_case)]
pub fn $name<T>(err: T) -> Error
where
T: fmt::Debug + fmt::Display + 'static,
{
InternalError::new(err, StatusCode::$status).into()
}
};
}
error_helper!(ErrorBadRequest, BAD_REQUEST);
error_helper!(ErrorUnauthorized, UNAUTHORIZED);
error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED);
error_helper!(ErrorForbidden, FORBIDDEN);
error_helper!(ErrorNotFound, NOT_FOUND);
error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED);
error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE);
error_helper!(
ErrorProxyAuthenticationRequired,
PROXY_AUTHENTICATION_REQUIRED
);
error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT);
error_helper!(ErrorConflict, CONFLICT);
error_helper!(ErrorGone, GONE);
error_helper!(ErrorLengthRequired, LENGTH_REQUIRED);
error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE);
error_helper!(ErrorUriTooLong, URI_TOO_LONG);
error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE);
error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE);
error_helper!(ErrorImATeapot, IM_A_TEAPOT);
error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST);
error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY);
error_helper!(ErrorLocked, LOCKED);
error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY);
error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED);
error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED);
error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED);
error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS);
error_helper!(
ErrorRequestHeaderFieldsTooLarge,
REQUEST_HEADER_FIELDS_TOO_LARGE
);
error_helper!(
ErrorUnavailableForLegalReasons,
UNAVAILABLE_FOR_LEGAL_REASONS
);
error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED);
error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR);
error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED);
error_helper!(ErrorBadGateway, BAD_GATEWAY);
error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE);
error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT);
error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED);
error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES);
error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE);
error_helper!(ErrorLoopDetected, LOOP_DETECTED);
error_helper!(ErrorNotExtended, NOT_EXTENDED);
error_helper!(
ErrorNetworkAuthenticationRequired,
NETWORK_AUTHENTICATION_REQUIRED
);
#[cfg(test)]
mod tests {
use actix_http::error::ParseError;
use super::*;
#[test]
fn test_internal_error() {
let err = InternalError::from_response(ParseError::Method, HttpResponse::Ok().finish());
let resp: HttpResponse = err.error_response();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_error_helpers() {
let res: HttpResponse = ErrorBadRequest("err").into();
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
let res: HttpResponse = ErrorUnauthorized("err").into();
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
let res: HttpResponse = ErrorPaymentRequired("err").into();
assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED);
let res: HttpResponse = ErrorForbidden("err").into();
assert_eq!(res.status(), StatusCode::FORBIDDEN);
let res: HttpResponse = ErrorNotFound("err").into();
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let res: HttpResponse = ErrorMethodNotAllowed("err").into();
assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED);
let res: HttpResponse = ErrorNotAcceptable("err").into();
assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE);
let res: HttpResponse = ErrorProxyAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED);
let res: HttpResponse = ErrorRequestTimeout("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT);
let res: HttpResponse = ErrorConflict("err").into();
assert_eq!(res.status(), StatusCode::CONFLICT);
let res: HttpResponse = ErrorGone("err").into();
assert_eq!(res.status(), StatusCode::GONE);
let res: HttpResponse = ErrorLengthRequired("err").into();
assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED);
let res: HttpResponse = ErrorPreconditionFailed("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED);
let res: HttpResponse = ErrorPayloadTooLarge("err").into();
assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE);
let res: HttpResponse = ErrorUriTooLong("err").into();
assert_eq!(res.status(), StatusCode::URI_TOO_LONG);
let res: HttpResponse = ErrorUnsupportedMediaType("err").into();
assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
let res: HttpResponse = ErrorRangeNotSatisfiable("err").into();
assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE);
let res: HttpResponse = ErrorExpectationFailed("err").into();
assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED);
let res: HttpResponse = ErrorImATeapot("err").into();
assert_eq!(res.status(), StatusCode::IM_A_TEAPOT);
let res: HttpResponse = ErrorMisdirectedRequest("err").into();
assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST);
let res: HttpResponse = ErrorUnprocessableEntity("err").into();
assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY);
let res: HttpResponse = ErrorLocked("err").into();
assert_eq!(res.status(), StatusCode::LOCKED);
let res: HttpResponse = ErrorFailedDependency("err").into();
assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY);
let res: HttpResponse = ErrorUpgradeRequired("err").into();
assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED);
let res: HttpResponse = ErrorPreconditionRequired("err").into();
assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED);
let res: HttpResponse = ErrorTooManyRequests("err").into();
assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS);
let res: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into();
assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE);
let res: HttpResponse = ErrorUnavailableForLegalReasons("err").into();
assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS);
let res: HttpResponse = ErrorInternalServerError("err").into();
assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR);
let res: HttpResponse = ErrorNotImplemented("err").into();
assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED);
let res: HttpResponse = ErrorBadGateway("err").into();
assert_eq!(res.status(), StatusCode::BAD_GATEWAY);
let res: HttpResponse = ErrorServiceUnavailable("err").into();
assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE);
let res: HttpResponse = ErrorGatewayTimeout("err").into();
assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT);
let res: HttpResponse = ErrorHttpVersionNotSupported("err").into();
assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED);
let res: HttpResponse = ErrorVariantAlsoNegotiates("err").into();
assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES);
let res: HttpResponse = ErrorInsufficientStorage("err").into();
assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE);
let res: HttpResponse = ErrorLoopDetected("err").into();
assert_eq!(res.status(), StatusCode::LOOP_DETECTED);
let res: HttpResponse = ErrorNotExtended("err").into();
assert_eq!(res.status(), StatusCode::NOT_EXTENDED);
let res: HttpResponse = ErrorNetworkAuthenticationRequired("err").into();
assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED);
}
}

View File

@ -0,0 +1,107 @@
macro_rules! downcast_get_type_id {
() => {
/// A helper method to get the type ID of the type
/// this trait is implemented on.
/// This method is unsafe to *implement*, since `downcast_ref` relies
/// on the returned `TypeId` to perform a cast.
///
/// Unfortunately, Rust has no notion of a trait method that is
/// unsafe to implement (marking it as `unsafe` makes it unsafe
/// to *call*). As a workaround, we require this method
/// to return a private type along with the `TypeId`. This
/// private type (`PrivateHelper`) has a private constructor,
/// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method.
///
/// We also take `PrivateHelper` as a parameter, to ensure that
/// safe code cannot obtain a `PrivateHelper` instance by
/// delegating to an existing implementation of `__private_get_type_id__`
#[doc(hidden)]
#[allow(dead_code)]
fn __private_get_type_id__(&self, _: PrivateHelper) -> (std::any::TypeId, PrivateHelper)
where
Self: 'static,
{
(std::any::TypeId::of::<Self>(), PrivateHelper(()))
}
};
}
// Generate implementation for dyn $name
macro_rules! downcast_dyn {
($name:ident) => {
/// A struct with a private constructor, for use with
/// `__private_get_type_id__`. Its single field is private,
/// ensuring that it can only be constructed from this module
#[doc(hidden)]
#[allow(dead_code)]
pub struct PrivateHelper(());
impl dyn $name + 'static {
/// Downcasts generic body to a specific type.
#[allow(dead_code)]
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore
// rely on the returned `TypeId`, which ensures that this
// case is correct.
unsafe { Some(&*(self as *const dyn $name as *const T)) }
} else {
None
}
}
/// Downcasts a generic body to a mutable specific type.
#[allow(dead_code)]
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore
// rely on the returned `TypeId`, which ensures that this
// case is correct.
unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) }
} else {
None
}
}
}
};
}
pub(crate) use {downcast_dyn, downcast_get_type_id};
#[cfg(test)]
mod tests {
#![allow(clippy::upper_case_acronyms)]
trait MB {
downcast_get_type_id!();
}
downcast_dyn!(MB);
impl MB for String {}
impl MB for () {}
#[actix_rt::test]
async fn test_any_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MB = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

266
actix-web/src/error/mod.rs Normal file
View File

@ -0,0 +1,266 @@
//! Error and Result module
// This is meant to be a glob import of the whole error module except for `Error`. Rustdoc can't yet
// correctly resolve the conflicting `Error` type defined in this module, so these re-exports are
// expanded manually.
//
// See <https://github.com/rust-lang/rust/issues/83375>
pub use actix_http::error::{
BlockingError, ContentTypeError, DispatchError, HttpError, ParseError, PayloadError,
};
use derive_more::{Display, Error, From};
use serde_json::error::Error as JsonError;
use serde_urlencoded::de::Error as FormDeError;
use serde_urlencoded::ser::Error as FormError;
use url::ParseError as UrlParseError;
use crate::http::StatusCode;
#[allow(clippy::module_inception)]
mod error;
mod internal;
mod macros;
mod response_error;
pub use self::error::Error;
pub use self::internal::*;
pub use self::response_error::ResponseError;
pub(crate) use macros::{downcast_dyn, downcast_get_type_id};
/// A convenience [`Result`](std::result::Result) for Actix Web operations.
///
/// This type alias is generally used to avoid writing out `actix_http::Error` directly.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/// Errors which can occur when attempting to generate resource uri.
#[derive(Debug, PartialEq, Display, Error, From)]
#[non_exhaustive]
pub enum UrlGenerationError {
/// Resource not found.
#[display(fmt = "Resource not found")]
ResourceNotFound,
/// Not all URL parameters covered.
#[display(fmt = "Not all URL parameters covered")]
NotEnoughElements,
/// URL parse error.
#[display(fmt = "{}", _0)]
ParseError(UrlParseError),
}
impl ResponseError for UrlGenerationError {}
/// A set of errors that can occur during parsing urlencoded payloads
#[derive(Debug, Display, Error, From)]
#[non_exhaustive]
pub enum UrlencodedError {
/// Can not decode chunked transfer encoding.
#[display(fmt = "Can not decode chunked transfer encoding.")]
Chunked,
/// Payload size is larger than allowed. (default limit: 256kB).
#[display(
fmt = "URL encoded payload is larger ({} bytes) than allowed (limit: {} bytes).",
size,
limit
)]
Overflow { size: usize, limit: usize },
/// Payload size is now known.
#[display(fmt = "Payload size is now known.")]
UnknownLength,
/// Content type error.
#[display(fmt = "Content type error.")]
ContentType,
/// Parse error.
#[display(fmt = "Parse error: {}.", _0)]
Parse(FormDeError),
/// Encoding error.
#[display(fmt = "Encoding error.")]
Encoding,
/// Serialize error.
#[display(fmt = "Serialize error: {}.", _0)]
Serialize(FormError),
/// Payload error.
#[display(fmt = "Error that occur during reading payload: {}.", _0)]
Payload(PayloadError),
}
impl ResponseError for UrlencodedError {
fn status_code(&self) -> StatusCode {
match self {
Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE,
Self::UnknownLength => StatusCode::LENGTH_REQUIRED,
Self::Payload(err) => err.status_code(),
_ => StatusCode::BAD_REQUEST,
}
}
}
/// A set of errors that can occur during parsing json payloads
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum JsonPayloadError {
/// Payload size is bigger than allowed & content length header set. (default: 2MB)
#[display(
fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).",
length,
limit
)]
OverflowKnownLength { length: usize, limit: usize },
/// Payload size is bigger than allowed but no content length header set. (default: 2MB)
#[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)]
Overflow { limit: usize },
/// Content type error
#[display(fmt = "Content type error")]
ContentType,
/// Deserialize error
#[display(fmt = "Json deserialize error: {}", _0)]
Deserialize(JsonError),
/// Serialize error
#[display(fmt = "Json serialize error: {}", _0)]
Serialize(JsonError),
/// Payload error
#[display(fmt = "Error that occur during reading payload: {}", _0)]
Payload(PayloadError),
}
impl From<PayloadError> for JsonPayloadError {
fn from(err: PayloadError) -> Self {
Self::Payload(err)
}
}
impl ResponseError for JsonPayloadError {
fn status_code(&self) -> StatusCode {
match self {
Self::OverflowKnownLength {
length: _,
limit: _,
} => StatusCode::PAYLOAD_TOO_LARGE,
Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE,
Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Payload(err) => err.status_code(),
_ => StatusCode::BAD_REQUEST,
}
}
}
/// A set of errors that can occur during parsing request paths
#[derive(Debug, Display, Error)]
#[non_exhaustive]
pub enum PathError {
/// Deserialize error
#[display(fmt = "Path deserialize error: {}", _0)]
Deserialize(serde::de::value::Error),
}
/// Return `BadRequest` for `PathError`
impl ResponseError for PathError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// A set of errors that can occur during parsing query strings.
#[derive(Debug, Display, Error, From)]
#[non_exhaustive]
pub enum QueryPayloadError {
/// Query deserialize error.
#[display(fmt = "Query deserialize error: {}", _0)]
Deserialize(serde::de::value::Error),
}
impl ResponseError for QueryPayloadError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Error type returned when reading body as lines.
#[derive(Debug, Display, Error, From)]
#[non_exhaustive]
pub enum ReadlinesError {
#[display(fmt = "Encoding error")]
/// Payload size is bigger than allowed. (default: 256kB)
EncodingError,
/// Payload error.
#[display(fmt = "Error that occur during reading payload: {}", _0)]
Payload(PayloadError),
/// Line limit exceeded.
#[display(fmt = "Line limit exceeded")]
LimitOverflow,
/// ContentType error.
#[display(fmt = "Content-type error")]
ContentTypeError(ContentTypeError),
}
impl ResponseError for ReadlinesError {
fn status_code(&self) -> StatusCode {
match *self {
ReadlinesError::LimitOverflow => StatusCode::PAYLOAD_TOO_LARGE,
_ => StatusCode::BAD_REQUEST,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_urlencoded_error() {
let resp = UrlencodedError::Overflow { size: 0, limit: 0 }.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = UrlencodedError::UnknownLength.error_response();
assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED);
let resp = UrlencodedError::ContentType.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_json_payload_error() {
let resp = JsonPayloadError::OverflowKnownLength {
length: 0,
limit: 0,
}
.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::Overflow { limit: 0 }.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = JsonPayloadError::ContentType.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_query_payload_error() {
let resp = QueryPayloadError::Deserialize(
serde_urlencoded::from_str::<i32>("bad query").unwrap_err(),
)
.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_readlines_error() {
let resp = ReadlinesError::LimitOverflow.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp = ReadlinesError::EncodingError.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
}

View File

@ -0,0 +1,152 @@
//! `ResponseError` trait and foreign impls.
use std::{
error::Error as StdError,
fmt,
io::{self, Write as _},
};
use actix_http::{
body::BoxBody,
header::{self, TryIntoHeaderValue},
Response, StatusCode,
};
use bytes::BytesMut;
use crate::{
error::{downcast_dyn, downcast_get_type_id},
helpers, HttpResponse,
};
/// Errors that can generate responses.
// TODO: add std::error::Error bound when replacement for Box<dyn Error> is found
pub trait ResponseError: fmt::Debug + fmt::Display {
/// Returns appropriate status code for error.
///
/// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is
/// also implemented and does not call `self.status_code()`, then this will not be used.
fn status_code(&self) -> StatusCode {
StatusCode::INTERNAL_SERVER_ERROR
}
/// Creates full response for error.
///
/// By default, the generated response uses a 500 Internal Server Error status code, a
/// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl.
fn error_response(&self) -> HttpResponse<BoxBody> {
let mut res = HttpResponse::new(self.status_code());
let mut buf = BytesMut::new();
let _ = write!(helpers::MutWriter(&mut buf), "{}", self);
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res.set_body(BoxBody::new(buf))
}
downcast_get_type_id!();
}
downcast_dyn!(ResponseError);
impl ResponseError for Box<dyn StdError + 'static> {}
#[cfg(feature = "openssl")]
impl ResponseError for actix_tls::accept::openssl::reexports::Error {}
impl ResponseError for serde::de::value::Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for serde_json::Error {}
impl ResponseError for serde_urlencoded::ser::Error {}
impl ResponseError for std::str::Utf8Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for std::io::Error {
fn status_code(&self) -> StatusCode {
// TODO: decide if these errors should consider not found or permission errors
match self.kind() {
io::ErrorKind::NotFound => StatusCode::NOT_FOUND,
io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
impl ResponseError for actix_http::error::HttpError {}
impl ResponseError for actix_http::Error {
fn status_code(&self) -> StatusCode {
// TODO: map error kinds to status code better
StatusCode::INTERNAL_SERVER_ERROR
}
fn error_response(&self) -> HttpResponse<BoxBody> {
HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body()
}
}
impl ResponseError for actix_http::header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::error::ParseError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::error::BlockingError {}
impl ResponseError for actix_http::error::PayloadError {
fn status_code(&self) -> StatusCode {
match *self {
actix_http::error::PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE,
_ => StatusCode::BAD_REQUEST,
}
}
}
impl ResponseError for actix_http::ws::ProtocolError {}
impl ResponseError for actix_http::error::ContentTypeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
impl ResponseError for actix_http::ws::HandshakeError {
fn error_response(&self) -> HttpResponse<BoxBody> {
Response::from(self).map_into_boxed_body().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_casting() {
use actix_http::error::{ContentTypeError, PayloadError};
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "Payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}
}

534
actix-web/src/extract.rs Normal file
View File

@ -0,0 +1,534 @@
//! Request extractors
use std::{
convert::Infallible,
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use actix_http::{Method, Uri};
use actix_utils::future::{ok, Ready};
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::{dev::Payload, Error, HttpRequest};
/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from
/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`].
///
/// # Configuration
/// An extractor can be customized by injecting the corresponding configuration with one of:
///
/// - [`App::app_data()`][crate::App::app_data]
/// - [`Scope::app_data()`][crate::Scope::app_data]
/// - [`Resource::app_data()`][crate::Resource::app_data]
///
/// Here are some built-in extractors and their corresponding configuration.
/// Please refer to the respective documentation for details.
///
/// | Extractor | Configuration |
/// |-------------|-------------------|
/// | [`Header`] | _None_ |
/// | [`Path`] | [`PathConfig`] |
/// | [`Json`] | [`JsonConfig`] |
/// | [`Form`] | [`FormConfig`] |
/// | [`Query`] | [`QueryConfig`] |
/// | [`Bytes`] | [`PayloadConfig`] |
/// | [`String`] | [`PayloadConfig`] |
/// | [`Payload`] | [`PayloadConfig`] |
///
/// # Implementing An Extractor
/// To reduce duplicate code in handlers where extracting certain parts of a request has a common
/// structure, you can implement `FromRequest` for your own types.
///
/// Note that the request payload can only be consumed by one extractor.
///
/// [`Header`]: crate::web::Header
/// [`Json`]: crate::web::Json
/// [`JsonConfig`]: crate::web::JsonConfig
/// [`Form`]: crate::web::Form
/// [`FormConfig`]: crate::web::FormConfig
/// [`Path`]: crate::web::Path
/// [`PathConfig`]: crate::web::PathConfig
/// [`Query`]: crate::web::Query
/// [`QueryConfig`]: crate::web::QueryConfig
/// [`Payload`]: crate::web::Payload
/// [`PayloadConfig`]: crate::web::PayloadConfig
/// [`String`]: FromRequest#impl-FromRequest-for-String
/// [`Bytes`]: crate::web::Bytes#impl-FromRequest
/// [`Either`]: crate::web::Either
#[doc(alias = "extract", alias = "extractor")]
pub trait FromRequest: Sized {
/// The associated error which can be returned.
type Error: Into<Error>;
/// Future that resolves to a Self.
type Future: Future<Output = Result<Self, Self::Error>>;
/// Create a Self from request parts asynchronously.
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future;
/// Create a Self from request head asynchronously.
///
/// This method is short for `T::from_request(req, &mut Payload::None)`.
fn extract(req: &HttpRequest) -> Self::Future {
Self::from_request(req, &mut Payload::None)
}
}
/// Optionally extract a field from the request
///
/// If the FromRequest for T fails, return None rather than returning an error response
///
/// # Examples
/// ```
/// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest;
/// use futures_util::future::{ok, err, Ready};
/// use serde::Deserialize;
/// use rand;
///
/// #[derive(Debug, Deserialize)]
/// struct Thing {
/// name: String
/// }
///
/// impl FromRequest for Thing {
/// type Error = Error;
/// type Future = Ready<Result<Self, Self::Error>>;
///
/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
/// if rand::random() {
/// ok(Thing { name: "thingy".into() })
/// } else {
/// err(ErrorBadRequest("no luck"))
/// }
///
/// }
/// }
///
/// /// extract `Thing` from request
/// async fn index(supplied_thing: Option<Thing>) -> String {
/// match supplied_thing {
/// // Puns not intended
/// Some(thing) => format!("Got something: {:?}", thing),
/// None => format!("No thing!")
/// }
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/users/:first").route(
/// web::post().to(index))
/// );
/// }
/// ```
impl<T> FromRequest for Option<T>
where
T: FromRequest,
{
type Error = Infallible;
type Future = FromRequestOptFuture<T::Future>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
FromRequestOptFuture {
fut: T::from_request(req, payload),
}
}
}
pin_project! {
pub struct FromRequestOptFuture<Fut> {
#[pin]
fut: Fut,
}
}
impl<Fut, T, E> Future for FromRequestOptFuture<Fut>
where
Fut: Future<Output = Result<T, E>>,
E: Into<Error>,
{
type Output = Result<Option<T>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
match res {
Ok(t) => Poll::Ready(Ok(Some(t))),
Err(e) => {
log::debug!("Error for Option<T> extractor: {}", e.into());
Poll::Ready(Ok(None))
}
}
}
}
/// Optionally extract a field from the request or extract the Error if unsuccessful
///
/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response
///
/// # Examples
/// ```
/// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest;
/// use futures_util::future::{ok, err, Ready};
/// use serde::Deserialize;
/// use rand;
///
/// #[derive(Debug, Deserialize)]
/// struct Thing {
/// name: String
/// }
///
/// impl FromRequest for Thing {
/// type Error = Error;
/// type Future = Ready<Result<Thing, Error>>;
///
/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
/// if rand::random() {
/// ok(Thing { name: "thingy".into() })
/// } else {
/// err(ErrorBadRequest("no luck"))
/// }
/// }
/// }
///
/// /// extract `Thing` from request
/// async fn index(supplied_thing: Result<Thing>) -> String {
/// match supplied_thing {
/// Ok(thing) => format!("Got thing: {:?}", thing),
/// Err(e) => format!("Error extracting thing: {}", e)
/// }
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/users/:first").route(web::post().to(index))
/// );
/// }
/// ```
impl<T, E> FromRequest for Result<T, E>
where
T: FromRequest,
T::Error: Into<E>,
{
type Error = Infallible;
type Future = FromRequestResFuture<T::Future, E>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
FromRequestResFuture {
fut: T::from_request(req, payload),
_phantom: PhantomData,
}
}
}
pin_project! {
pub struct FromRequestResFuture<Fut, E> {
#[pin]
fut: Fut,
_phantom: PhantomData<E>,
}
}
impl<Fut, T, Ei, E> Future for FromRequestResFuture<Fut, E>
where
Fut: Future<Output = Result<T, Ei>>,
Ei: Into<E>,
{
type Output = Result<Result<T, E>, Infallible>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = ready!(this.fut.poll(cx));
Poll::Ready(Ok(res.map_err(Into::into)))
}
}
/// Extract the request's URI.
///
/// # Examples
/// ```
/// use actix_web::{http::Uri, web, App, Responder};
///
/// async fn handler(uri: Uri) -> impl Responder {
/// format!("Requested path: {}", uri.path())
/// }
///
/// let app = App::new().default_service(web::to(handler));
/// ```
impl FromRequest for Uri {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.uri().clone())
}
}
/// Extract the request's method.
///
/// # Examples
/// ```
/// use actix_web::{http::Method, web, App, Responder};
///
/// async fn handler(method: Method) -> impl Responder {
/// format!("Request method: {}", method)
/// }
///
/// let app = App::new().default_service(web::to(handler));
/// ```
impl FromRequest for Method {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.method().clone())
}
}
#[doc(hidden)]
#[allow(non_snake_case)]
mod tuple_from_req {
use super::*;
macro_rules! tuple_from_req {
($fut: ident; $($T: ident),*) => {
/// FromRequest implementation for tuple
#[allow(unused_parens)]
impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+)
{
type Error = Error;
type Future = $fut<$($T),+>;
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
$fut {
$(
$T: ExtractFuture::Future {
fut: $T::from_request(req, payload)
},
)+
}
}
}
pin_project! {
pub struct $fut<$($T: FromRequest),+> {
$(
#[pin]
$T: ExtractFuture<$T::Future, $T>,
)+
}
}
impl<$($T: FromRequest),+> Future for $fut<$($T),+>
{
type Output = Result<($($T,)+), Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
let mut ready = true;
$(
match this.$T.as_mut().project() {
ExtractProj::Future { fut } => match fut.poll(cx) {
Poll::Ready(Ok(output)) => {
let _ = this.$T.as_mut().project_replace(ExtractFuture::Done { output });
},
Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())),
Poll::Pending => ready = false,
},
ExtractProj::Done { .. } => {},
ExtractProj::Empty => unreachable!("FromRequest polled after finished"),
}
)+
if ready {
Poll::Ready(Ok(
($(
match this.$T.project_replace(ExtractFuture::Empty) {
ExtractReplaceProj::Done { output } => output,
_ => unreachable!("FromRequest polled after finished"),
},
)+)
))
} else {
Poll::Pending
}
}
}
};
}
pin_project! {
#[project = ExtractProj]
#[project_replace = ExtractReplaceProj]
enum ExtractFuture<Fut, Res> {
Future {
#[pin]
fut: Fut
},
Done {
output: Res,
},
Empty
}
}
impl FromRequest for () {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(())
}
}
tuple_from_req! { TupleFromRequest1; A }
tuple_from_req! { TupleFromRequest2; A, B }
tuple_from_req! { TupleFromRequest3; A, B, C }
tuple_from_req! { TupleFromRequest4; A, B, C, D }
tuple_from_req! { TupleFromRequest5; A, B, C, D, E }
tuple_from_req! { TupleFromRequest6; A, B, C, D, E, F }
tuple_from_req! { TupleFromRequest7; A, B, C, D, E, F, G }
tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H }
tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I }
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
}
#[cfg(test)]
mod tests {
use actix_http::header;
use bytes::Bytes;
use serde::Deserialize;
use super::*;
use crate::test::TestRequest;
use crate::types::{Form, FormConfig};
#[derive(Deserialize, Debug, PartialEq)]
struct Info {
hello: String,
}
#[actix_rt::test]
async fn test_option() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.data(FormConfig::default().limit(4096))
.to_http_parts();
let r = Option::<Form<Info>>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(r, None);
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((header::CONTENT_LENGTH, "9"))
.set_payload(Bytes::from_static(b"hello=world"))
.to_http_parts();
let r = Option::<Form<Info>>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(
r,
Some(Form(Info {
hello: "world".into()
}))
);
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((header::CONTENT_LENGTH, "9"))
.set_payload(Bytes::from_static(b"bye=world"))
.to_http_parts();
let r = Option::<Form<Info>>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(r, None);
}
#[actix_rt::test]
async fn test_result() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((header::CONTENT_LENGTH, "11"))
.set_payload(Bytes::from_static(b"hello=world"))
.to_http_parts();
let r = Result::<Form<Info>, Error>::from_request(&req, &mut pl)
.await
.unwrap()
.unwrap();
assert_eq!(
r,
Form(Info {
hello: "world".into()
})
);
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((header::CONTENT_LENGTH, 9))
.set_payload(Bytes::from_static(b"bye=world"))
.to_http_parts();
struct MyError;
impl From<Error> for MyError {
fn from(_: Error) -> Self {
Self
}
}
let r = Result::<Form<Info>, MyError>::from_request(&req, &mut pl)
.await
.unwrap();
assert!(r.is_err());
}
#[actix_rt::test]
async fn test_uri() {
let req = TestRequest::default().uri("/foo/bar").to_http_request();
let uri = Uri::extract(&req).await.unwrap();
assert_eq!(uri.path(), "/foo/bar");
}
#[actix_rt::test]
async fn test_method() {
let req = TestRequest::default().method(Method::GET).to_http_request();
let method = Method::extract(&req).await.unwrap();
assert_eq!(method, Method::GET);
}
#[actix_rt::test]
async fn test_concurrent() {
let (req, mut pl) = TestRequest::default()
.uri("/foo/bar")
.method(Method::GET)
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((header::CONTENT_LENGTH, "11"))
.set_payload(Bytes::from_static(b"hello=world"))
.to_http_parts();
let (method, uri, form) = <(Method, Uri, Form<Info>)>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(method, Method::GET);
assert_eq!(uri.path(), "/foo/bar");
assert_eq!(
form,
Form(Info {
hello: "world".into()
})
);
}
}

689
actix-web/src/guard.rs Normal file
View File

@ -0,0 +1,689 @@
//! Route guards.
//!
//! Guards are used during routing to help select a matching service or handler using some aspect of
//! the request; though guards should not be used for path matching since it is a built-in function
//! of the Actix Web router.
//!
//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services.
//!
//! Fundamentally, a guard is a predicate function that receives a reference to a request context
//! object and returns a boolean; true if the request _should_ be handled by the guarded service
//! or handler. This interface is defined by the [`Guard`] trait.
//!
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a
//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
//! services multiple times (which might have different combining behavior than you want).
//!
//! There are shortcuts for routes with method guards in the [`web`](crate::web) module:
//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by
//! the following calls are equivalent:
//! - `web::get()` (recommended form)
//! - `web::route().guard(guard::Get())`
//!
//! Guards can not modify anything about the request. However, it is possible to store extra
//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`].
//!
//! Guards can prevent resource definitions from overlapping which, when only considering paths,
//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting.
//!
//! # Examples
//! In the following code, the `/guarded` resource has one defined route whose handler will only be
//! called if the request method is `POST` and there is a request header with name and value equal
//! to `x-guarded` and `secret`, respectively.
//! ```
//! use actix_web::{web, http::Method, guard, HttpResponse};
//!
//! web::resource("/guarded").route(
//! web::route()
//! .guard(guard::Any(guard::Get()).or(guard::Post()))
//! .guard(guard::Header("x-guarded", "secret"))
//! .to(|| HttpResponse::Ok())
//! );
//! ```
//!
//! [`Scope`]: crate::Scope::guard()
//! [`Resource`]: crate::Resource::guard()
//! [`Route`]: crate::Route::guard()
use std::{
cell::{Ref, RefMut},
convert::TryFrom,
rc::Rc,
};
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _};
/// Provides access to request parts that are useful during routing.
#[derive(Debug)]
pub struct GuardContext<'a> {
pub(crate) req: &'a ServiceRequest,
}
impl<'a> GuardContext<'a> {
/// Returns reference to the request head.
#[inline]
pub fn head(&self) -> &RequestHead {
self.req.head()
}
/// Returns reference to the request-local data/extensions container.
#[inline]
pub fn req_data(&self) -> Ref<'a, Extensions> {
self.req.extensions()
}
/// Returns mutable reference to the request-local data/extensions container.
#[inline]
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
self.req.extensions_mut()
}
/// Extracts a typed header from the request.
///
/// Returns `None` if parsing `H` fails.
///
/// # Examples
/// ```
/// use actix_web::{guard::fn_guard, http::header};
///
/// let image_accept_guard = fn_guard(|ctx| {
/// match ctx.header::<header::Accept>() {
/// Some(hdr) => hdr.preference() == "image/*",
/// None => false,
/// }
/// });
/// ```
#[inline]
pub fn header<H: Header>(&self) -> Option<H> {
H::parse(self.req).ok()
}
}
/// Interface for routing guards.
///
/// See [module level documentation](self) for more.
pub trait Guard {
/// Returns true if predicate condition is met for a given request.
fn check(&self, ctx: &GuardContext<'_>) -> bool;
}
impl Guard for Rc<dyn Guard> {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
(**self).check(ctx)
}
}
/// Creates a guard using the given function.
///
/// # Examples
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
/// .guard(guard::fn_guard(|ctx| {
/// ctx.head().headers().contains_key("content-type")
/// }))
/// .to(|| HttpResponse::Ok());
/// ```
pub fn fn_guard<F>(f: F) -> impl Guard
where
F: Fn(&GuardContext<'_>) -> bool,
{
FnGuard(f)
}
struct FnGuard<F: Fn(&GuardContext<'_>) -> bool>(F);
impl<F> Guard for FnGuard<F>
where
F: Fn(&GuardContext<'_>) -> bool,
{
fn check(&self, ctx: &GuardContext<'_>) -> bool {
(self.0)(ctx)
}
}
impl<F> Guard for F
where
F: Fn(&GuardContext<'_>) -> bool,
{
fn check(&self, ctx: &GuardContext<'_>) -> bool {
(self)(ctx)
}
}
/// Creates a guard that matches if any added guards match.
///
/// # Examples
/// The handler below will be called for either request method `GET` or `POST`.
/// ```
/// use actix_web::{web, guard, HttpResponse};
///
/// web::route()
/// .guard(
/// guard::Any(guard::Get())
/// .or(guard::Post()))
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
AnyGuard {
guards: vec![Box::new(guard)],
}
}
/// A collection of guards that match if the disjunction of their `check` outcomes is true.
///
/// That is, only one contained guard needs to match in order for the aggregate guard to match.
///
/// Construct an `AnyGuard` using [`Any`].
pub struct AnyGuard {
guards: Vec<Box<dyn Guard>>,
}
impl AnyGuard {
/// Adds new guard to the collection of guards to check.
pub fn or<F: Guard + 'static>(mut self, guard: F) -> Self {
self.guards.push(Box::new(guard));
self
}
}
impl Guard for AnyGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
for guard in &self.guards {
if guard.check(ctx) {
return true;
}
}
false
}
}
/// Creates a guard that matches if all added guards match.
///
/// # Examples
/// The handler below will only be called if the request method is `GET` **and** the specified
/// header name and value match exactly.
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
/// .guard(
/// guard::All(guard::Get())
/// .and(guard::Header("accept", "text/plain"))
/// )
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
AllGuard {
guards: vec![Box::new(guard)],
}
}
/// A collection of guards that match if the conjunction of their `check` outcomes is true.
///
/// That is, **all** contained guard needs to match in order for the aggregate guard to match.
///
/// Construct an `AllGuard` using [`All`].
pub struct AllGuard {
guards: Vec<Box<dyn Guard>>,
}
impl AllGuard {
/// Adds new guard to the collection of guards to check.
pub fn and<F: Guard + 'static>(mut self, guard: F) -> Self {
self.guards.push(Box::new(guard));
self
}
}
impl Guard for AllGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
for guard in &self.guards {
if !guard.check(ctx) {
return false;
}
}
true
}
}
/// Wraps a guard and inverts the outcome of it's `Guard` implementation.
///
/// # Examples
/// The handler below will be called for any request method apart from `GET`.
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
/// .guard(guard::Not(guard::Get()))
/// .to(|| HttpResponse::Ok());
/// ```
pub struct Not<G>(pub G);
impl<G: Guard> Guard for Not<G> {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
!self.0.check(ctx)
}
}
/// Creates a guard that matches a specified HTTP method.
#[allow(non_snake_case)]
pub fn Method(method: HttpMethod) -> impl Guard {
MethodGuard(method)
}
/// HTTP method guard.
struct MethodGuard(HttpMethod);
impl Guard for MethodGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
ctx.head().method == self.0
}
}
macro_rules! method_guard {
($method_fn:ident, $method_const:ident) => {
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
///
/// # Examples
#[doc = concat!("The route in this example will only respond to `", stringify!($method_const), "` requests.")]
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
#[doc = concat!(" .guard(guard::", stringify!($method_fn), "())")]
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn $method_fn() -> impl Guard {
MethodGuard(HttpMethod::$method_const)
}
};
}
method_guard!(Get, GET);
method_guard!(Post, POST);
method_guard!(Put, PUT);
method_guard!(Delete, DELETE);
method_guard!(Head, HEAD);
method_guard!(Options, OPTIONS);
method_guard!(Connect, CONNECT);
method_guard!(Patch, PATCH);
method_guard!(Trace, TRACE);
/// Creates a guard that matches if request contains given header name and value.
///
/// # Examples
/// The handler below will be called when the request contains an `x-guarded` header with value
/// equal to `secret`.
/// ```
/// use actix_web::{guard, web, HttpResponse};
///
/// web::route()
/// .guard(guard::Header("x-guarded", "secret"))
/// .to(|| HttpResponse::Ok());
/// ```
#[allow(non_snake_case)]
pub fn Header(name: &'static str, value: &'static str) -> impl Guard {
HeaderGuard(
header::HeaderName::try_from(name).unwrap(),
header::HeaderValue::from_static(value),
)
}
struct HeaderGuard(header::HeaderName, header::HeaderValue);
impl Guard for HeaderGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
if let Some(val) = ctx.head().headers.get(&self.0) {
return val == self.1;
}
false
}
}
/// Creates a guard that matches requests targetting a specific host.
///
/// # Matching Host
/// This guard will:
/// - match against the `Host` header, if present;
/// - fall-back to matching against the request target's host, if present;
/// - return false if host cannot be determined;
///
/// # Matching Scheme
/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
/// the guard from matching successfully.
///
/// # Examples
/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards.
///
/// The example below additionally guards on the host URI's scheme. This could allow routing to
/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
/// ```
/// use actix_web::{web, guard::Host, HttpResponse};
///
/// web::scope("/admin")
/// .guard(Host("admin.rust-lang.org").scheme("https"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("admin connection is secure")
/// }));
/// ```
///
/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app.
/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
/// definitions they become safe to use in this way. Without these host guards, only routes under
/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
/// and `localhost` as the `Host` guards.
/// ```
/// use actix_web::{web, http::Method, guard, App, HttpResponse};
///
/// App::new()
/// .service(
/// web::scope("")
/// .guard(guard::Host("www.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("marketing site")
/// })),
/// )
/// .service(
/// web::scope("")
/// .guard(guard::Host("play.rust-lang.org"))
/// .default_service(web::to(|| async {
/// HttpResponse::Ok().body("playground frontend")
/// })),
/// );
/// ```
///
/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
#[allow(non_snake_case)]
pub fn Host(host: impl AsRef<str>) -> HostGuard {
HostGuard {
host: host.as_ref().to_string(),
scheme: None,
}
}
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
req.headers
.get(header::HOST)
.and_then(|host_value| host_value.to_str().ok())
.or_else(|| req.uri.host())
.and_then(|host| host.parse().ok())
}
#[doc(hidden)]
pub struct HostGuard {
host: String,
scheme: Option<String>,
}
impl HostGuard {
/// Set request scheme to match
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
self.scheme = Some(scheme.as_ref().to_string());
self
}
}
impl Guard for HostGuard {
fn check(&self, ctx: &GuardContext<'_>) -> bool {
// parse host URI from header or request target
let req_host_uri = match get_host_uri(ctx.head()) {
Some(uri) => uri,
// no match if host cannot be determined
None => return false,
};
match req_host_uri.host() {
// fall through to scheme checks
Some(uri_host) if self.host == uri_host => {}
// Either:
// - request's host does not match guard's host;
// - It was possible that the parsed URI from request target did not contain a host.
_ => return false,
}
if let Some(ref scheme) = self.scheme {
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
return scheme == req_host_uri_scheme;
}
// TODO: is the the correct behavior?
// falls through if scheme cannot be determined
}
// all conditions passed
true
}
}
#[cfg(test)]
mod tests {
use actix_http::{header, Method};
use super::*;
use crate::test::TestRequest;
#[test]
fn header_match() {
let req = TestRequest::default()
.insert_header((header::TRANSFER_ENCODING, "chunked"))
.to_srv_request();
let hdr = Header("transfer-encoding", "chunked");
assert!(hdr.check(&req.guard_ctx()));
let hdr = Header("transfer-encoding", "other");
assert!(!hdr.check(&req.guard_ctx()));
let hdr = Header("content-type", "chunked");
assert!(!hdr.check(&req.guard_ctx()));
let hdr = Header("content-type", "other");
assert!(!hdr.check(&req.guard_ctx()));
}
#[test]
fn host_from_header() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_without_header() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.to_srv_request();
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn host_scheme() {
let req = TestRequest::default()
.insert_header((
header::HOST,
header::HeaderValue::from_static("https://www.rust-lang.org"),
))
.to_srv_request();
let host = Host("www.rust-lang.org").scheme("https");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org");
assert!(host.check(&req.guard_ctx()));
let host = Host("www.rust-lang.org").scheme("http");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org");
assert!(!host.check(&req.guard_ctx()));
let host = Host("blog.rust-lang.org").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("crates.io").scheme("https");
assert!(!host.check(&req.guard_ctx()));
let host = Host("localhost");
assert!(!host.check(&req.guard_ctx()));
}
#[test]
fn method_guards() {
let get_req = TestRequest::get().to_srv_request();
let post_req = TestRequest::post().to_srv_request();
assert!(Get().check(&get_req.guard_ctx()));
assert!(!Get().check(&post_req.guard_ctx()));
assert!(Post().check(&post_req.guard_ctx()));
assert!(!Post().check(&get_req.guard_ctx()));
let req = TestRequest::put().to_srv_request();
assert!(Put().check(&req.guard_ctx()));
assert!(!Put().check(&get_req.guard_ctx()));
let req = TestRequest::patch().to_srv_request();
assert!(Patch().check(&req.guard_ctx()));
assert!(!Patch().check(&get_req.guard_ctx()));
let r = TestRequest::delete().to_srv_request();
assert!(Delete().check(&r.guard_ctx()));
assert!(!Delete().check(&get_req.guard_ctx()));
let req = TestRequest::default().method(Method::HEAD).to_srv_request();
assert!(Head().check(&req.guard_ctx()));
assert!(!Head().check(&get_req.guard_ctx()));
let req = TestRequest::default()
.method(Method::OPTIONS)
.to_srv_request();
assert!(Options().check(&req.guard_ctx()));
assert!(!Options().check(&get_req.guard_ctx()));
let req = TestRequest::default()
.method(Method::CONNECT)
.to_srv_request();
assert!(Connect().check(&req.guard_ctx()));
assert!(!Connect().check(&get_req.guard_ctx()));
let req = TestRequest::default()
.method(Method::TRACE)
.to_srv_request();
assert!(Trace().check(&req.guard_ctx()));
assert!(!Trace().check(&get_req.guard_ctx()));
}
#[test]
fn aggregate_any() {
let req = TestRequest::default()
.method(Method::TRACE)
.to_srv_request();
assert!(Any(Trace()).check(&req.guard_ctx()));
assert!(Any(Trace()).or(Get()).check(&req.guard_ctx()));
assert!(!Any(Get()).or(Get()).check(&req.guard_ctx()));
}
#[test]
fn aggregate_all() {
let req = TestRequest::default()
.method(Method::TRACE)
.to_srv_request();
assert!(All(Trace()).check(&req.guard_ctx()));
assert!(All(Trace()).and(Trace()).check(&req.guard_ctx()));
assert!(!All(Trace()).and(Get()).check(&req.guard_ctx()));
}
#[test]
fn nested_not() {
let req = TestRequest::default().to_srv_request();
let get = Get();
assert!(get.check(&req.guard_ctx()));
let not_get = Not(get);
assert!(!not_get.check(&req.guard_ctx()));
let not_not_get = Not(not_get);
assert!(not_not_get.check(&req.guard_ctx()));
}
#[test]
fn function_guard() {
let domain = "rust-lang.org".to_owned();
let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
let req = TestRequest::default()
.uri("blog.rust-lang.org")
.to_srv_request();
assert!(guard.check(&req.guard_ctx()));
let req = TestRequest::default().uri("crates.io").to_srv_request();
assert!(!guard.check(&req.guard_ctx()));
}
#[test]
fn mega_nesting() {
let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx));
let req = TestRequest::default().to_srv_request();
assert!(!guard.check(&req.guard_ctx()));
let req = TestRequest::default()
.method(Method::TRACE)
.to_srv_request();
assert!(guard.check(&req.guard_ctx()));
}
}

171
actix-web/src/handler.rs Normal file
View File

@ -0,0 +1,171 @@
use std::future::Future;
use actix_service::{boxed, fn_service};
use crate::{
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
FromRequest, HttpResponse, Responder,
};
/// The interface for request handlers.
///
/// # What Is A Request Handler
/// A request handler has three requirements:
/// 1. It is an async function (or a function/closure that returns an appropriate future);
/// 1. The function parameters (up to 12) implement [`FromRequest`];
/// 1. The async function (or future) resolves to a type that can be converted into an
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
///
/// # Compiler Errors
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on
/// implementing [`FromRequest`] and [`Responder`], respectively.
///
/// # How Do Handlers Receive Variable Numbers Of Arguments
/// Rest assured there is no macro magic here; it's just traits.
///
/// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length).
///
/// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way
/// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args`
/// type parameter for this trait).
///
/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the
/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on
/// that tuple, and the `Handler::call` implementation for that particular function arity
/// destructures the tuple into it's component types and calls your handler function with them.
///
/// In pseudo-code the process looks something like this:
/// ```ignore
/// async fn my_handler(body: String, state: web::Data<MyState>) -> impl Responder {
/// ...
/// }
///
/// // the function params above described as a tuple, names do not matter, only position
/// type InferredMyHandlerArgs = (String, web::Data<MyState>);
///
/// // create tuple of arguments to be passed to handler
/// let args = InferredMyHandlerArgs::from_request(&request, &payload).await;
///
/// // call handler with argument tuple
/// let response = Handler::call(&my_handler, args).await;
///
/// // which is effectively...
///
/// let (body, state) = args;
/// let response = my_handler(body, state).await;
/// ```
///
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
/// bounds of the handler call after argument extraction:
/// ```ignore
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
/// where
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
/// Fut: Future,
/// {
/// type Output = Fut::Output;
/// type Future = Fut;
///
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {
/// (self)(arg1, arg2)
/// }
/// }
/// ```
///
/// [arity]: https://en.wikipedia.org/wiki/Arity
/// [`from_request`]: FromRequest::from_request
pub trait Handler<Args>: Clone + 'static {
type Output;
type Future: Future<Output = Self::Output>;
fn call(&self, args: Args) -> Self::Future;
}
pub(crate) fn handler_service<F, Args>(handler: F) -> BoxedHttpServiceFactory
where
F: Handler<Args>,
Args: FromRequest,
F::Output: Responder,
{
boxed::factory(fn_service(move |req: ServiceRequest| {
let handler = handler.clone();
async move {
let (req, mut payload) = req.into_parts();
let res = match Args::from_request(&req, &mut payload).await {
Err(err) => HttpResponse::from_error(err),
Ok(data) => handler
.call(data)
.await
.respond_to(&req)
.map_into_boxed_body(),
};
Ok(ServiceResponse::new(req, res))
}
}))
}
/// Generates a [`Handler`] trait impl for N-ary functions where N is specified with a sequence of
/// space separated type parameters.
///
/// # Examples
/// ```ignore
/// factory_tuple! {} // implements Handler for types: fn() -> R
/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R
/// ```
macro_rules! factory_tuple ({ $($param:ident)* } => {
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
where
Func: Fn($($param),*) -> Fut + Clone + 'static,
Fut: Future,
{
type Output = Fut::Output;
type Future = Fut;
#[inline]
#[allow(non_snake_case)]
fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future {
(self)($($param,)*)
}
}
});
factory_tuple! {}
factory_tuple! { A }
factory_tuple! { A B }
factory_tuple! { A B C }
factory_tuple! { A B C D }
factory_tuple! { A B C D E }
factory_tuple! { A B C D E F }
factory_tuple! { A B C D E F G }
factory_tuple! { A B C D E F G H }
factory_tuple! { A B C D E F G H I }
factory_tuple! { A B C D E F G H I J }
factory_tuple! { A B C D E F G H I J K }
factory_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
fn assert_impl_handler<T: FromRequest>(_: impl Handler<T>) {}
#[test]
fn arg_number() {
async fn handler_min() {}
#[rustfmt::skip]
#[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)]
async fn handler_max(
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
) {}
assert_impl_handler(handler_min);
assert_impl_handler(handler_max);
}
}

25
actix-web/src/helpers.rs Normal file
View File

@ -0,0 +1,25 @@
use std::io;
use bytes::BufMut;
/// An `io::Write`r that only requires mutable reference and assumes that there is space available
/// in the buffer for every write operation or that it can be extended implicitly (like
/// `bytes::BytesMut`, for example).
///
/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<'a, B> io::Write for MutWriter<'a, B>
where
B: BufMut,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.put_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@ -0,0 +1,293 @@
use std::cmp::Ordering;
use mime::Mime;
use super::{common_header, QualityItem};
use crate::http::header;
common_header! {
/// `Accept` header, defined
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
///
/// The `Accept` header field can be used by user agents to specify
/// response media types that are acceptable. Accept header fields can
/// be used to indicate that the request is specifically limited to a
/// small set of desired types, as in the case of a request for an
/// in-line image
///
/// # ABNF
/// ```plain
/// Accept = #( media-range [ accept-params ] )
///
/// media-range = ( "*/*"
/// / ( type "/" "*" )
/// / ( type "/" subtype )
/// ) *( OWS ";" OWS parameter )
/// accept-params = weight *( accept-ext )
/// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
/// ```
///
/// # Example Values
/// * `audio/*; q=0.2, audio/basic`
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::TEXT_HTML),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::APPLICATION_JSON),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{Accept, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Accept(vec![
/// QualityItem::max(mime::TEXT_HTML),
/// QualityItem::max("application/xhtml+xml".parse().unwrap()),
/// QualityItem::new(mime::TEXT_XML, q(0.9)),
/// QualityItem::max("image/webp".parse().unwrap()),
/// QualityItem::new(mime::STAR_STAR, q(0.8)),
/// ])
/// );
/// ```
(Accept, header::ACCEPT) => (QualityItem<Mime>)*
test_parse_and_format {
// Tests from the RFC
crate::http::header::common_header_test!(
test1,
vec![b"audio/*; q=0.2, audio/basic"],
Some(Accept(vec![
QualityItem::new("audio/*".parse().unwrap(), q(0.2)),
QualityItem::max("audio/basic".parse().unwrap()),
])));
crate::http::header::common_header_test!(
test2,
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN, q(0.5)),
QualityItem::max(mime::TEXT_HTML),
QualityItem::new(
"text/x-dvi".parse().unwrap(),
q(0.8)),
QualityItem::max("text/x-c".parse().unwrap()),
])));
// Custom tests
crate::http::header::common_header_test!(
test3,
vec![b"text/plain; charset=utf-8"],
Some(Accept(vec![
QualityItem::max(mime::TEXT_PLAIN_UTF_8),
])));
crate::http::header::common_header_test!(
test4,
vec![b"text/plain; charset=utf-8; q=0.5"],
Some(Accept(vec![
QualityItem::new(mime::TEXT_PLAIN_UTF_8,
q(0.5)),
])));
#[test]
fn test_fuzzing1() {
let req = test::TestRequest::default()
.insert_header((header::ACCEPT, "chunk#;e"))
.finish();
let header = Accept::parse(&req);
assert!(header.is_ok());
}
}
}
impl Accept {
/// Construct `Accept: */*`.
pub fn star() -> Accept {
Accept(vec![QualityItem::max(mime::STAR_STAR)])
}
/// Construct `Accept: application/json`.
pub fn json() -> Accept {
Accept(vec![QualityItem::max(mime::APPLICATION_JSON)])
}
/// Construct `Accept: text/*`.
pub fn text() -> Accept {
Accept(vec![QualityItem::max(mime::TEXT_STAR)])
}
/// Construct `Accept: image/*`.
pub fn image() -> Accept {
Accept(vec![QualityItem::max(mime::IMAGE_STAR)])
}
/// Construct `Accept: text/html`.
pub fn html() -> Accept {
Accept(vec![QualityItem::max(mime::TEXT_HTML)])
}
// TODO: method for getting best content encoding based on q-factors, available from server side
// and if none are acceptable return None
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first mime type is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
/// list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Mime {
use actix_http::header::Quality;
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(mime::STAR_STAR)
}
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
/// [q-factor weighting] and specificity.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Mime> {
if self.is_empty() {
return vec![];
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor and specificity retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality).then_with(|| {
// use specificity rules on mime types with
// same q-factor (eg. text/html > text/* > */*)
// subtypes are not comparable if main type is star, so return
match (a.item.type_(), b.item.type_()) {
(mime::STAR, mime::STAR) => return Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => return Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => return Ordering::Less,
_ => {}
}
// in both these match expressions, the returned ordering appears
// inverted because sort is high-to-low ("descending") precedence
match (a.item.subtype(), b.item.subtype()) {
(mime::STAR, mime::STAR) => Ordering::Equal,
// a is sorted after b
(mime::STAR, _) => Ordering::Greater,
// a is sorted before b
(_, mime::STAR) => Ordering::Less,
_ => Ordering::Equal,
}
})
});
types.into_iter().map(|qitem| qitem.item).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::q;
#[test]
fn ranking_precedence() {
let test = Accept(vec![]);
assert!(test.ranked().is_empty());
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(
test.ranked(),
vec![
mime::TEXT_HTML,
"application/xhtml+xml".parse().unwrap(),
"application/xml".parse().unwrap(),
mime::STAR_STAR,
]
);
let test = Accept(vec![
QualityItem::max(mime::STAR_STAR),
QualityItem::max(mime::IMAGE_STAR),
QualityItem::max(mime::IMAGE_PNG),
]);
assert_eq!(
test.ranked(),
vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR]
);
}
#[test]
fn preference_selection() {
let test = Accept(vec![
QualityItem::max(mime::TEXT_HTML),
"application/xhtml+xml".parse().unwrap(),
QualityItem::new("application/xml".parse().unwrap(), q(0.9)),
QualityItem::new(mime::STAR_STAR, q(0.8)),
]);
assert_eq!(test.preference(), mime::TEXT_HTML);
let test = Accept(vec![
QualityItem::new("video/*".parse().unwrap(), q(0.8)),
QualityItem::max(mime::IMAGE_PNG),
QualityItem::new(mime::STAR_STAR, q(0.5)),
QualityItem::max(mime::IMAGE_SVG),
QualityItem::new(mime::IMAGE_STAR, q(0.8)),
]);
assert_eq!(test.preference(), mime::IMAGE_PNG);
}
}

View File

@ -0,0 +1,62 @@
use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET};
common_header! {
/// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
///
/// The `Accept-Charset` header field can be sent by a user agent to
/// indicate what charsets are acceptable in textual response content.
/// This field allows user agents capable of understanding more
/// comprehensive or special-purpose charsets to signal that capability
/// to an origin server that is capable of representing information in
/// those charsets.
///
/// # ABNF
/// ```plain
/// Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
/// ```
///
/// # Example Values
/// * `iso-8859-5, unicode-1-1;q=0.8`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptCharset(vec![QualityItem::max(Charset::Us_Ascii)])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptCharset(vec![
/// QualityItem::new(Charset::Us_Ascii, q(0.9)),
/// QualityItem::new(Charset::Iso_8859_10, q(0.2)),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptCharset, Charset, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
/// );
/// ```
///
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
test_parse_and_format {
// Test case from RFC
common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
}
}

View File

@ -0,0 +1,429 @@
use std::collections::HashSet;
use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
use crate::http::header;
common_header! {
/// `Accept-Encoding` header, defined
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
///
/// The `Accept-Encoding` header field can be used by user agents to indicate what response
/// content-codings are acceptable in the response. An `identity` token is used as a synonym
/// for "no encoding" in order to communicate when no encoding is preferred.
///
/// # ABNF
/// ```plain
/// Accept-Encoding = #( codings [ weight ] )
/// codings = content-coding / "identity" / "*"
/// ```
///
/// # Example Values
/// * `compress, gzip`
/// * ``
/// * `*`
/// * `compress;q=0.5, gzip;q=1`
/// * `gzip;q=1.0, identity; q=0.5, *;q=0`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptEncoding(vec![
/// "gzip".parse().unwrap(),
/// "br".parse().unwrap(),
/// ])
/// );
/// ```
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
test_parse_and_format {
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
common_header_test!(
order_of_appearance,
vec![b"br, gzip"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::brotli())),
QualityItem::max(Preference::Specific(Encoding::gzip())),
]))
);
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
QualityItem::max(Preference::Any),
])));
// Note: Removed quality 1 from gzip
common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
// Note: Removed quality 1 from gzip
common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
common_header_test!(
only_gzip_no_identity,
vec![b"gzip, *; q=0"],
Some(AcceptEncoding(vec![
QualityItem::max(Preference::Specific(Encoding::gzip())),
QualityItem::zero(Preference::Any),
]))
);
}
}
impl AcceptEncoding {
/// Selects the most acceptable encoding according to client preference and supported types.
///
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
/// if a non-encoded representation can be selected.
///
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
/// includes the server's supported encodings in the body plus a [`Vary`] header.
///
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
pub fn negotiate<'a>(
&self,
supported: impl Iterator<Item = &'a Encoding>,
) -> Option<Encoding> {
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
// acceptable by the user agent.
let supported_set = supported.collect::<HashSet<_>>();
if supported_set.is_empty() {
return None;
}
if self.0.is_empty() {
// though it is not recommended to encode in this case, return identity encoding
return Some(Encoding::identity());
}
// 2. If the representation has no content-coding, then it is acceptable by default unless
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
// "*;q=0" without a more specific entry for "identity".
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
let identity_acceptable = is_identity_acceptable(&acceptable_items);
let identity_supported = supported_set.contains(&Encoding::identity());
if identity_acceptable && identity_supported && supported_set.len() == 1 {
return Some(Encoding::identity());
}
// 3. If the representation's content-coding is one of the content-codings listed in the
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
// the highest non-zero qvalue is preferred.
let matched = acceptable_items
.into_iter()
.filter(|q| q.quality > Quality::ZERO)
// search relies on item list being in descending order of quality
.find(|q| {
let enc = &q.item;
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
})
.map(|q| q.item);
match matched {
Some(Preference::Specific(enc)) => Some(enc),
_ if identity_acceptable => Some(Encoding::identity()),
_ => None,
}
}
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first encoding is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
/// returned, it is recommended to use an un-encoded representation.
///
/// If `None` is returned, it means that the client has signalled that no representations
/// are acceptable. This should never occur for a well behaved user-agent.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Option<Preference<Encoding>> {
// empty header indicates no preference
if self.0.is_empty() {
return Some(Preference::Any);
}
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
// Return max_item if any items were above 0 quality...
max_item.or_else(|| {
// ...or else check for "*" or "identity". We can elide quality checks since
// entering this block means all items had "q=0".
match self.0.iter().find(|pref| {
matches!(
pref.item,
Preference::Any
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
)
}) {
// "identity" or "*" found so no representation is acceptable
Some(_) => None,
// implicit "identity" is acceptable
None => Some(Preference::Specific(Encoding::identity())),
}
})
}
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
self.ranked_items().map(|q| q.item).collect()
}
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
if self.0.is_empty() {
return vec![].into_iter();
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality)
});
types.into_iter()
}
}
/// Returns true if "identity" is an acceptable encoding.
///
/// Internal algorithm relies on item list being in descending order of quality.
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
if items.is_empty() {
return true;
}
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
for q in items {
match (q.quality, &q.item) {
// occurrence of "identity;q=n"; return true if quality is non-zero
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
return q > Quality::ZERO
}
// occurrence of "*;q=n"; return true if quality is non-zero
(q, Preference::Any) => return q > Quality::ZERO,
_ => {}
}
}
// implicit acceptable identity
true
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::*;
macro_rules! accept_encoding {
() => { AcceptEncoding(vec![]) };
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
}
/// Parses an encoding string.
fn enc(enc: &str) -> Preference<Encoding> {
enc.parse().unwrap()
}
#[test]
fn detect_identity_acceptable() {
macro_rules! accept_encoding_ranked {
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
}
let test = accept_encoding_ranked!();
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "br");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
assert!(is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
assert!(!is_identity_acceptable(&test));
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
assert!(!is_identity_acceptable(&test));
}
#[test]
fn encoding_negotiation() {
// no preference
let test = accept_encoding!();
assert_eq!(test.negotiate([].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("identity;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!("*;q=0");
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
let test = accept_encoding!();
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
let test = accept_encoding!("gzip");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::identity()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
let test = accept_encoding!("gzip", "identity;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "*;q=0");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
None
);
let test = accept_encoding!("gzip", "deflate", "br");
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip()),
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::brotli())
);
assert_eq!(
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
Some(Encoding::deflate())
);
assert_eq!(
test.negotiate(
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
assert_eq!(
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
Some(Encoding::gzip())
);
}
#[test]
fn ranking_precedence() {
let test = accept_encoding!();
assert!(test.ranked().is_empty());
let test = accept_encoding!("gzip");
assert_eq!(test.ranked(), vec![enc("gzip")]);
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
}
#[test]
fn preference_selection() {
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
assert_eq!(accept_encoding!("*;q=0").preference(), None);
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
let test = accept_encoding!("*;q=0.5");
assert_eq!(test.preference().unwrap(), enc("*"));
let test = accept_encoding!("br;q=0");
assert_eq!(test.preference().unwrap(), enc("identity"));
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
assert_eq!(test.preference().unwrap(), enc("gzip"));
let test = accept_encoding!("br", "gzip", "*");
assert_eq!(test.preference().unwrap(), enc("br"));
}
}

View File

@ -0,0 +1,223 @@
use language_tags::LanguageTag;
use super::{common_header, Preference, Quality, QualityItem};
use crate::http::header;
common_header! {
/// `Accept-Language` header, defined
/// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5)
///
/// The `Accept-Language` header field can be used by user agents to indicate the set of natural
/// languages that are preferred in the response.
///
/// The `Accept-Language` header is defined in
/// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language
/// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1).
///
/// # ABNF
/// ```plain
/// Accept-Language = 1#( language-range [ weight ] )
/// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*"
/// alphanum = ALPHA / DIGIT
/// weight = OWS ";" OWS "q=" qvalue
/// qvalue = ( "0" [ "." 0*3DIGIT ] )
/// / ( "1" [ "." 0*3("0") ] )
/// ```
///
/// # Example Values
/// - `da, en-gb;q=0.8, en;q=0.7`
/// - `en-us;q=1.0, en;q=0.5, fr`
/// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptLanguage(vec![
/// "en-US".parse().unwrap(),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptLanguage(vec![
/// "da".parse().unwrap(),
/// "en-GB;q=0.8".parse().unwrap(),
/// "en;q=0.7".parse().unwrap(),
/// ])
/// );
/// ```
(AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem<Preference<LanguageTag>>)*
test_parse_and_format {
common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![])));
common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![])));
common_header_test!(
example_from_rfc,
vec![b"da, en-gb;q=0.8, en;q=0.7"]
);
common_header_test!(
not_ordered_by_weight,
vec![b"en-US, en; q=0.5, fr"],
Some(AcceptLanguage(vec![
QualityItem::max("en-US".parse().unwrap()),
QualityItem::new("en".parse().unwrap(), q(0.5)),
QualityItem::max("fr".parse().unwrap()),
]))
);
common_header_test!(
has_wildcard,
vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"],
Some(AcceptLanguage(vec![
QualityItem::max("fr-CH".parse().unwrap()),
QualityItem::new("fr".parse().unwrap(), q(0.9)),
QualityItem::new("en".parse().unwrap(), q(0.8)),
QualityItem::new("de".parse().unwrap(), q(0.7)),
QualityItem::new("*".parse().unwrap(), q(0.5)),
]))
);
}
}
impl AcceptLanguage {
/// Extracts the most preferable language, accounting for [q-factor weighting].
///
/// If no q-factors are provided, the first language is chosen. Note that items without
/// q-factors are given the maximum preference value.
///
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn preference(&self) -> Preference<LanguageTag> {
let mut max_item = None;
let mut max_pref = Quality::ZERO;
// uses manual max lookup loop since we want the first occurrence in the case of same
// preference but `Iterator::max_by_key` would give us the last occurrence
for pref in &self.0 {
// only change if strictly greater
// equal items, even while unsorted, still have higher preference if they appear first
if pref.quality > max_pref {
max_pref = pref.quality;
max_item = Some(pref.item.clone());
}
}
max_item.unwrap_or(Preference::Any)
}
/// Returns a sorted list of languages from highest to lowest precedence, accounting
/// for [q-factor weighting].
///
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
pub fn ranked(&self) -> Vec<Preference<LanguageTag>> {
if self.0.is_empty() {
return vec![];
}
let mut types = self.0.clone();
// use stable sort so items with equal q-factor retain listed order
types.sort_by(|a, b| {
// sort by q-factor descending
b.quality.cmp(&a.quality)
});
types.into_iter().map(|qitem| qitem.item).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::header::*;
#[test]
fn ranking_precedence() {
let test = AcceptLanguage(vec![]);
assert!(test.ranked().is_empty());
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
let test = AcceptLanguage(vec![
QualityItem::new("fr".parse().unwrap(), q(0.900)),
QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
QualityItem::new("en".parse().unwrap(), q(0.800)),
QualityItem::new("*".parse().unwrap(), q(0.500)),
QualityItem::new("de".parse().unwrap(), q(0.700)),
]);
assert_eq!(
test.ranked(),
vec![
"fr-CH".parse().unwrap(),
"fr".parse().unwrap(),
"en".parse().unwrap(),
"de".parse().unwrap(),
"*".parse().unwrap(),
]
);
let test = AcceptLanguage(vec![
QualityItem::max("fr".parse().unwrap()),
QualityItem::max("fr-CH".parse().unwrap()),
QualityItem::max("en".parse().unwrap()),
QualityItem::max("*".parse().unwrap()),
QualityItem::max("de".parse().unwrap()),
]);
assert_eq!(
test.ranked(),
vec![
"fr".parse().unwrap(),
"fr-CH".parse().unwrap(),
"en".parse().unwrap(),
"*".parse().unwrap(),
"de".parse().unwrap(),
]
);
}
#[test]
fn preference_selection() {
let test = AcceptLanguage(vec![
QualityItem::new("fr".parse().unwrap(), q(0.900)),
QualityItem::new("fr-CH".parse().unwrap(), q(1.0)),
QualityItem::new("en".parse().unwrap(), q(0.800)),
QualityItem::new("*".parse().unwrap(), q(0.500)),
QualityItem::new("de".parse().unwrap(), q(0.700)),
]);
assert_eq!(
test.preference(),
Preference::Specific("fr-CH".parse().unwrap())
);
let test = AcceptLanguage(vec![
QualityItem::max("fr".parse().unwrap()),
QualityItem::max("fr-CH".parse().unwrap()),
QualityItem::max("en".parse().unwrap()),
QualityItem::max("*".parse().unwrap()),
QualityItem::max("de".parse().unwrap()),
]);
assert_eq!(
test.preference(),
Preference::Specific("fr".parse().unwrap())
);
let test = AcceptLanguage(vec![]);
assert_eq!(test.preference(), Preference::Any);
}
}

View File

@ -0,0 +1,75 @@
use actix_http::Method;
use crate::http::header;
crate::http::header::common_header! {
/// `Allow` header, defined
/// in [RFC 7231 §7.4.1](https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1)
///
/// The `Allow` header field lists the set of methods advertised as
/// supported by the target resource. The purpose of this field is
/// strictly to inform the recipient of valid request methods associated
/// with the resource.
///
/// # ABNF
/// ```plain
/// Allow = #method
/// ```
///
/// # Example Values
/// * `GET, HEAD, PUT`
/// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr`
/// * ``
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::{header::Allow, Method};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Allow(vec![Method::GET])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::{header::Allow, Method};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Allow(vec![
/// Method::GET,
/// Method::POST,
/// Method::PATCH,
/// ])
/// );
/// ```
(Allow, header::ALLOW) => (Method)*
test_parse_and_format {
// From the RFC
crate::http::header::common_header_test!(
test1,
vec![b"GET, HEAD, PUT"],
Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT])));
// Own tests
crate::http::header::common_header_test!(
test2,
vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"],
Some(HeaderField(vec![
Method::OPTIONS,
Method::GET,
Method::PUT,
Method::POST,
Method::DELETE,
Method::HEAD,
Method::TRACE,
Method::CONNECT,
Method::PATCH])));
crate::http::header::common_header_test!(
test3,
vec![b""],
Some(HeaderField(Vec::<Method>::new())));
}
}

View File

@ -0,0 +1,70 @@
use std::{
fmt::{self, Write as _},
str,
};
/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the
/// underlying type does not support them.
///
/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage)
/// typed header but it does parse `*` successfully. On the other hand, the `mime` crate, used for
/// [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not
/// used in those header types.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)]
pub enum AnyOrSome<T> {
/// A wildcard value.
Any,
/// A valid `T`.
Item(T),
}
impl<T> AnyOrSome<T> {
/// Returns true if item is wildcard (`*`) variant.
pub fn is_any(&self) -> bool {
matches!(self, Self::Any)
}
/// Returns true if item is a valid item (`T`) variant.
pub fn is_item(&self) -> bool {
matches!(self, Self::Item(_))
}
/// Returns reference to value in `Item` variant, if it is set.
pub fn item(&self) -> Option<&T> {
match self {
AnyOrSome::Item(ref item) => Some(item),
AnyOrSome::Any => None,
}
}
/// Consumes the container, returning the value in the `Item` variant, if it is set.
pub fn into_item(self) -> Option<T> {
match self {
AnyOrSome::Item(item) => Some(item),
AnyOrSome::Any => None,
}
}
}
impl<T: fmt::Display> fmt::Display for AnyOrSome<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AnyOrSome::Any => f.write_char('*'),
AnyOrSome::Item(item) => fmt::Display::fmt(item, f),
}
}
}
impl<T: str::FromStr> str::FromStr for AnyOrSome<T> {
type Err = T::Err;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"*" => Ok(Self::Any),
other => other.parse().map(AnyOrSome::Item),
}
}
}

View File

@ -0,0 +1,192 @@
use std::{fmt, str};
use super::common_header;
use crate::http::header;
common_header! {
/// `Cache-Control` header, defined
/// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2).
///
/// The `Cache-Control` header field is used to specify directives for
/// caches along the request/response chain. Such cache directives are
/// unidirectional in that the presence of a directive in a request does
/// not imply that the same directive is to be given in the response.
///
/// # ABNF
/// ```text
/// Cache-Control = 1#cache-directive
/// cache-directive = token [ "=" ( token / quoted-string ) ]
/// ```
///
/// # Example Values
/// * `no-cache`
/// * `private, community="UCI"`
/// * `max-age=30`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(CacheControl(vec![
/// CacheDirective::NoCache,
/// CacheDirective::Private,
/// CacheDirective::MaxAge(360u32),
/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())),
/// ]));
/// ```
(CacheControl, header::CACHE_CONTROL) => (CacheDirective)+
test_parse_and_format {
common_header_test!(no_headers, vec![b""; 0], None);
common_header_test!(empty_header, vec![b""; 1], None);
common_header_test!(bad_syntax, vec![b"foo="], None);
common_header_test!(
multiple_headers,
vec![&b"no-cache"[..], &b"private"[..]],
Some(CacheControl(vec![
CacheDirective::NoCache,
CacheDirective::Private,
]))
);
common_header_test!(
argument,
vec![b"max-age=100, private"],
Some(CacheControl(vec![
CacheDirective::MaxAge(100),
CacheDirective::Private,
]))
);
common_header_test!(
extension,
vec![b"foo, bar=baz"],
Some(CacheControl(vec![
CacheDirective::Extension("foo".to_owned(), None),
CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())),
]))
);
#[test]
fn parse_quote_form() {
let req = test::TestRequest::default()
.insert_header((header::CACHE_CONTROL, "max-age=\"200\""))
.finish();
assert_eq!(
Header::parse(&req).ok(),
Some(CacheControl(vec![CacheDirective::MaxAge(200)]))
)
}
}
}
/// `CacheControl` contains a list of these directives.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CacheDirective {
/// "no-cache"
NoCache,
/// "no-store"
NoStore,
/// "no-transform"
NoTransform,
/// "only-if-cached"
OnlyIfCached,
// request directives
/// "max-age=delta"
MaxAge(u32),
/// "max-stale=delta"
MaxStale(u32),
/// "min-fresh=delta"
MinFresh(u32),
// response directives
/// "must-revalidate"
MustRevalidate,
/// "public"
Public,
/// "private"
Private,
/// "proxy-revalidate"
ProxyRevalidate,
/// "s-maxage=delta"
SMaxAge(u32),
/// Extension directives. Optionally include an argument.
Extension(String, Option<String>),
}
impl fmt::Display for CacheDirective {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use self::CacheDirective::*;
let dir_str = match self {
NoCache => "no-cache",
NoStore => "no-store",
NoTransform => "no-transform",
OnlyIfCached => "only-if-cached",
MaxAge(secs) => return write!(f, "max-age={}", secs),
MaxStale(secs) => return write!(f, "max-stale={}", secs),
MinFresh(secs) => return write!(f, "min-fresh={}", secs),
MustRevalidate => "must-revalidate",
Public => "public",
Private => "private",
ProxyRevalidate => "proxy-revalidate",
SMaxAge(secs) => return write!(f, "s-maxage={}", secs),
Extension(name, None) => name.as_str(),
Extension(name, Some(arg)) => return write!(f, "{}={}", name, arg),
};
f.write_str(dir_str)
}
}
impl str::FromStr for CacheDirective {
type Err = Option<<u32 as str::FromStr>::Err>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
use self::CacheDirective::*;
match s {
"" => Err(None),
"no-cache" => Ok(NoCache),
"no-store" => Ok(NoStore),
"no-transform" => Ok(NoTransform),
"only-if-cached" => Ok(OnlyIfCached),
"must-revalidate" => Ok(MustRevalidate),
"public" => Ok(Public),
"private" => Ok(Private),
"proxy-revalidate" => Ok(ProxyRevalidate),
_ => match s.find('=') {
Some(idx) if idx + 1 < s.len() => {
match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) {
("max-age", secs) => secs.parse().map(MaxAge).map_err(Some),
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some),
(left, right) => Ok(Extension(left.to_owned(), Some(right.to_owned()))),
}
}
Some(_) => Err(None),
None => Ok(Extension(s.to_owned(), None)),
},
}
}
}

View File

@ -0,0 +1,988 @@
//! The `Content-Disposition` header and associated types.
//!
//! # References
//! - "The Content-Disposition Header Field":
//! <https://datatracker.ietf.org/doc/html/rfc2183>
//! - "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)":
//! <https://datatracker.ietf.org/doc/html/rfc6266>
//! - "Returning Values from Forms: multipart/form-data":
//! <https://datatracker.ietf.org/doc/html/rfc7578>
//! - Browser conformance tests at: <http://greenbytes.de/tech/tc2231/>
//! - IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml>
use once_cell::sync::Lazy;
use regex::Regex;
use std::fmt::{self, Write};
use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header;
/// Split at the index of the first `needle` if it exists or at the end.
fn split_once(haystack: &str, needle: char) -> (&str, &str) {
haystack.find(needle).map_or_else(
|| (haystack, ""),
|sc| {
let (first, last) = haystack.split_at(sc);
(first, last.split_at(1).1)
},
)
}
/// Split at the index of the first `needle` if it exists or at the end, trim the right of the
/// first part and the left of the last part.
fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) {
let (first, last) = split_once(haystack, needle);
(first.trim_end(), last.trim_start())
}
/// The implied disposition of the content of the HTTP body.
#[derive(Clone, Debug, PartialEq)]
pub enum DispositionType {
/// Inline implies default processing.
Inline,
/// Attachment implies that the recipient should prompt the user to save the response locally,
/// rather than process it normally (as per its media type).
Attachment,
/// Used in *multipart/form-data* as defined in
/// [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) to carry the field name and
/// optional filename.
FormData,
/// Extension type. Should be handled by recipients the same way as Attachment.
Ext(String),
}
impl<'a> From<&'a str> for DispositionType {
fn from(origin: &'a str) -> DispositionType {
if origin.eq_ignore_ascii_case("inline") {
DispositionType::Inline
} else if origin.eq_ignore_ascii_case("attachment") {
DispositionType::Attachment
} else if origin.eq_ignore_ascii_case("form-data") {
DispositionType::FormData
} else {
DispositionType::Ext(origin.to_owned())
}
}
}
/// Parameter in [`ContentDisposition`].
///
/// # Examples
/// ```
/// use actix_web::http::header::DispositionParam;
///
/// let param = DispositionParam::Filename(String::from("sample.txt"));
/// assert!(param.is_filename());
/// assert_eq!(param.as_filename().unwrap(), "sample.txt");
/// ```
#[derive(Clone, Debug, PartialEq)]
#[allow(clippy::large_enum_variant)]
pub enum DispositionParam {
/// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from
/// the form.
Name(String),
/// A plain file name.
///
/// It is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to contain
/// any non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
/// in case there are Unicode characters in file names.
Filename(String),
/// An extended file name. It must not exist for `ContentType::Formdata` according to
/// [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2).
FilenameExt(ExtendedValue),
/// An unrecognized regular parameter as defined in
/// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as
/// `reg-parameter`, in
/// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as
/// `token "=" value`. Recipients should ignore unrecognizable parameters.
Unknown(String, String),
/// An unrecognized extended parameter as defined in
/// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as
/// `ext-parameter`, in
/// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as
/// `ext-token "=" ext-value`. The single trailing asterisk is not included. Recipients should
/// ignore unrecognizable parameters.
UnknownExt(String, ExtendedValue),
}
impl DispositionParam {
/// Returns `true` if the parameter is [`Name`](DispositionParam::Name).
#[inline]
pub fn is_name(&self) -> bool {
self.as_name().is_some()
}
/// Returns `true` if the parameter is [`Filename`](DispositionParam::Filename).
#[inline]
pub fn is_filename(&self) -> bool {
self.as_filename().is_some()
}
/// Returns `true` if the parameter is [`FilenameExt`](DispositionParam::FilenameExt).
#[inline]
pub fn is_filename_ext(&self) -> bool {
self.as_filename_ext().is_some()
}
/// Returns `true` if the parameter is [`Unknown`](DispositionParam::Unknown) and the `name`
#[inline]
/// matches.
pub fn is_unknown<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown(name).is_some()
}
/// Returns `true` if the parameter is [`UnknownExt`](DispositionParam::UnknownExt) and the
/// `name` matches.
#[inline]
pub fn is_unknown_ext<T: AsRef<str>>(&self, name: T) -> bool {
self.as_unknown_ext(name).is_some()
}
/// Returns the name if applicable.
#[inline]
pub fn as_name(&self) -> Option<&str> {
match self {
DispositionParam::Name(ref name) => Some(name.as_str()),
_ => None,
}
}
/// Returns the filename if applicable.
#[inline]
pub fn as_filename(&self) -> Option<&str> {
match self {
DispositionParam::Filename(ref filename) => Some(filename.as_str()),
_ => None,
}
}
/// Returns the filename* if applicable.
#[inline]
pub fn as_filename_ext(&self) -> Option<&ExtendedValue> {
match self {
DispositionParam::FilenameExt(ref value) => Some(value),
_ => None,
}
}
/// Returns the value of the unrecognized regular parameter if it is
/// [`Unknown`](DispositionParam::Unknown) and the `name` matches.
#[inline]
pub fn as_unknown<T: AsRef<str>>(&self, name: T) -> Option<&str> {
match self {
DispositionParam::Unknown(ref ext_name, ref value)
if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
{
Some(value.as_str())
}
_ => None,
}
}
/// Returns the value of the unrecognized extended parameter if it is
/// [`Unknown`](DispositionParam::Unknown) and the `name` matches.
#[inline]
pub fn as_unknown_ext<T: AsRef<str>>(&self, name: T) -> Option<&ExtendedValue> {
match self {
DispositionParam::UnknownExt(ref ext_name, ref value)
if ext_name.eq_ignore_ascii_case(name.as_ref()) =>
{
Some(value)
}
_ => None,
}
}
}
/// A *Content-Disposition* header. It is compatible to be used either as
/// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body)
/// as (re)defined in [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266), or as
/// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body)
/// as (re)defined in [RFC 7587](https://datatracker.ietf.org/doc/html/rfc7578).
///
/// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if
/// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as
/// part of a Web page, or as an attachment, that is downloaded and saved locally, and also can be
/// used to attach additional metadata, such as the filename to use when saving the response payload
/// locally.
///
/// In a *multipart/form-data* body, the HTTP *Content-Disposition* general header is a header that
/// can be used on the subpart of a multipart body to give information about the field it applies to.
/// The subpart is delimited by the boundary defined in the *Content-Type* header. Used on the body
/// itself, *Content-Disposition* has no effect.
///
/// # ABNF
/// ```plain
/// content-disposition = "Content-Disposition" ":"
/// disposition-type *( ";" disposition-parm )
///
/// disposition-type = "inline" | "attachment" | disp-ext-type
/// ; case-insensitive
///
/// disp-ext-type = token
///
/// disposition-parm = filename-parm | disp-ext-parm
///
/// filename-parm = "filename" "=" value
/// | "filename*" "=" ext-value
///
/// disp-ext-parm = token "=" value
/// | ext-token "=" ext-value
///
/// ext-token = <the characters in token, followed by "*">
/// ```
///
/// # Note
/// *filename* is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to
/// contain any non-ASCII characters when used in a *Content-Disposition* HTTP response header,
/// where filename* with charset UTF-8 may be used instead in case there are Unicode characters in
/// file names. Filename is [acceptable](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2)
/// to be UTF-8 encoded directly in a *Content-Disposition* header for
/// *multipart/form-data*, though.
///
/// *filename* [must not](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) be used within
/// *multipart/form-data*.
///
/// # Examples
/// ```
/// use actix_web::http::header::{
/// Charset, ContentDisposition, DispositionParam, DispositionType,
/// ExtendedValue,
/// };
///
/// let cd1 = ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Iso_8859_1, // The character set for the bytes of the filename
/// language_tag: None, // The optional language tag (see `language-tag` crate)
/// value: b"\xa9 Copyright 1989.txt".to_vec(), // the actual bytes of the filename
/// })],
/// };
/// assert!(cd1.is_attachment());
/// assert!(cd1.get_filename_ext().is_some());
///
/// let cd2 = ContentDisposition {
/// disposition: DispositionType::FormData,
/// parameters: vec![
/// DispositionParam::Name(String::from("file")),
/// DispositionParam::Filename(String::from("bill.odt")),
/// ],
/// };
/// assert_eq!(cd2.get_name(), Some("file")); // field name
/// assert_eq!(cd2.get_filename(), Some("bill.odt"));
///
/// // HTTP response header with Unicode characters in file names
/// let cd3 = ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![
/// DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Ext(String::from("UTF-8")),
/// language_tag: None,
/// value: String::from("\u{1f600}.svg").into_bytes(),
/// }),
/// // fallback for better compatibility
/// DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg"))
/// ],
/// };
/// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()),
/// Some("\u{1f600}.svg".as_bytes()));
/// ```
///
/// # Security Note
/// If "filename" parameter is supplied, do not use the file name blindly, check and possibly
/// change to match local file system conventions if applicable, and do not use directory path
/// information that may be present.
/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3).
#[derive(Clone, Debug, PartialEq)]
pub struct ContentDisposition {
/// The disposition type
pub disposition: DispositionType,
/// Disposition parameters
pub parameters: Vec<DispositionParam>,
}
impl ContentDisposition {
/// Parse a raw Content-Disposition header value.
pub fn from_raw(hv: &header::HeaderValue) -> Result<Self, crate::error::ParseError> {
// `header::from_one_raw_str` invokes `hv.to_str` which assumes `hv` contains only visible
// ASCII characters. So `hv.as_bytes` is necessary here.
let hv = String::from_utf8(hv.as_bytes().to_vec())
.map_err(|_| crate::error::ParseError::Header)?;
let (disp_type, mut left) = split_once_and_trim(hv.as_str().trim(), ';');
if disp_type.is_empty() {
return Err(crate::error::ParseError::Header);
}
let mut cd = ContentDisposition {
disposition: disp_type.into(),
parameters: Vec::new(),
};
while !left.is_empty() {
let (param_name, new_left) = split_once_and_trim(left, '=');
if param_name.is_empty() || param_name == "*" || new_left.is_empty() {
return Err(crate::error::ParseError::Header);
}
left = new_left;
if let Some(param_name) = param_name.strip_suffix('*') {
// extended parameters
let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left;
let ext_value = header::parse_extended_value(ext_value)?;
let param = if param_name.eq_ignore_ascii_case("filename") {
DispositionParam::FilenameExt(ext_value)
} else {
DispositionParam::UnknownExt(param_name.to_owned(), ext_value)
};
cd.parameters.push(param);
} else {
// regular parameters
let value = if left.starts_with('\"') {
// quoted-string: defined in RFC 6266 -> RFC 2616 Section 3.6
let mut escaping = false;
let mut quoted_string = vec![];
let mut end = None;
// search for closing quote
for (i, &c) in left.as_bytes().iter().skip(1).enumerate() {
if escaping {
escaping = false;
quoted_string.push(c);
} else if c == 0x5c {
// backslash
escaping = true;
} else if c == 0x22 {
// double quote
end = Some(i + 1); // cuz skipped 1 for the leading quote
break;
} else {
quoted_string.push(c);
}
}
left = &left[end.ok_or(crate::error::ParseError::Header)? + 1..];
left = split_once(left, ';').1.trim_start();
// In fact, it should not be Err if the above code is correct.
String::from_utf8(quoted_string)
.map_err(|_| crate::error::ParseError::Header)?
} else {
// token: won't contains semicolon according to RFC 2616 Section 2.2
let (token, new_left) = split_once_and_trim(left, ';');
left = new_left;
if token.is_empty() {
// quoted-string can be empty, but token cannot be empty
return Err(crate::error::ParseError::Header);
}
token.to_owned()
};
let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") {
// See also comments in test_from_raw_unnecessary_percent_decode.
DispositionParam::Filename(value)
} else {
DispositionParam::Unknown(param_name.to_owned(), value)
};
cd.parameters.push(param);
}
}
Ok(cd)
}
/// Returns `true` if type is [`Inline`](DispositionType::Inline).
pub fn is_inline(&self) -> bool {
matches!(self.disposition, DispositionType::Inline)
}
/// Returns `true` if type is [`Attachment`](DispositionType::Attachment).
pub fn is_attachment(&self) -> bool {
matches!(self.disposition, DispositionType::Attachment)
}
/// Returns `true` if type is [`FormData`](DispositionType::FormData).
pub fn is_form_data(&self) -> bool {
matches!(self.disposition, DispositionType::FormData)
}
/// Returns `true` if type is [`Ext`](DispositionType::Ext) and the `disp_type` matches.
pub fn is_ext(&self, disp_type: impl AsRef<str>) -> bool {
matches!(
self.disposition,
DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref())
)
}
/// Return the value of *name* if exists.
pub fn get_name(&self) -> Option<&str> {
self.parameters.iter().find_map(DispositionParam::as_name)
}
/// Return the value of *filename* if exists.
pub fn get_filename(&self) -> Option<&str> {
self.parameters
.iter()
.find_map(DispositionParam::as_filename)
}
/// Return the value of *filename\** if exists.
pub fn get_filename_ext(&self) -> Option<&ExtendedValue> {
self.parameters
.iter()
.find_map(DispositionParam::as_filename_ext)
}
/// Return the value of the parameter which the `name` matches.
pub fn get_unknown(&self, name: impl AsRef<str>) -> Option<&str> {
let name = name.as_ref();
self.parameters.iter().find_map(|p| p.as_unknown(name))
}
/// Return the value of the extended parameter which the `name` matches.
pub fn get_unknown_ext(&self, name: impl AsRef<str>) -> Option<&ExtendedValue> {
let name = name.as_ref();
self.parameters.iter().find_map(|p| p.as_unknown_ext(name))
}
}
impl TryIntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValue;
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take())
}
}
impl Header for ContentDisposition {
fn name() -> header::HeaderName {
header::CONTENT_DISPOSITION
}
fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
if let Some(h) = msg.headers().get(&Self::name()) {
Self::from_raw(h)
} else {
Err(crate::error::ParseError::Header)
}
}
}
impl fmt::Display for DispositionType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
DispositionType::Inline => write!(f, "inline"),
DispositionType::Attachment => write!(f, "attachment"),
DispositionType::FormData => write!(f, "form-data"),
DispositionType::Ext(ref s) => write!(f, "{}", s),
}
}
}
impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// All ASCII control characters (0-30, 127) including horizontal tab, double quote, and
// backslash should be escaped in quoted-string (i.e. "foobar").
//
// Ref: RFC 6266 §4.1 -> RFC 2616 §3.6
//
// filename-parm = "filename" "=" value
// value = token | quoted-string
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
// qdtext = <any TEXT except <">>
// quoted-pair = "\" CHAR
// TEXT = <any OCTET except CTLs,
// but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
//
// Ref: RFC 7578 S4.2 -> RFC 2183 S2 -> RFC 2045 S5.1
// parameter := attribute "=" value
// attribute := token
// ; Matching of attributes
// ; is ALWAYS case-insensitive.
// value := token / quoted-string
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
// or tspecials>
// tspecials := "(" / ")" / "<" / ">" / "@" /
// "," / ";" / ":" / "\" / <">
// "/" / "[" / "]" / "?" / "="
// ; Must be in quoted-string,
// ; to use within parameter values
//
//
// See also comments in test_from_raw_unnecessary_percent_decode.
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => {
write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref())
}
DispositionParam::Unknown(ref name, ref value) => write!(
f,
"{}=\"{}\"",
name,
&RE.replace_all(value, "\\$0").as_ref()
),
DispositionParam::FilenameExt(ref ext_value) => {
write!(f, "filename*={}", ext_value)
}
DispositionParam::UnknownExt(ref name, ref ext_value) => {
write!(f, "{}*={}", name, ext_value)
}
}
}
}
impl fmt::Display for ContentDisposition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.disposition)?;
self.parameters
.iter()
.try_for_each(|param| write!(f, "; {}", param))
}
}
#[cfg(test)]
mod tests {
use super::{ContentDisposition, DispositionParam, DispositionType};
use crate::http::header::{Charset, ExtendedValue, HeaderValue};
#[test]
fn test_from_raw_basic() {
assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err());
let a = HeaderValue::from_static(
"form-data; dummy=3; name=upload; filename=\"sample.png\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
],
};
assert_eq!(a, b);
let a = HeaderValue::from_static("attachment; filename=\"image.jpg\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
};
assert_eq!(a, b);
let a = HeaderValue::from_static("inline; filename=image.jpg");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename("image.jpg".to_owned())],
};
assert_eq!(a, b);
let a = HeaderValue::from_static(
"attachment; creation-date=\"Wed, 12 Feb 1997 16:29:51 -0500\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Unknown(
String::from("creation-date"),
"Wed, 12 Feb 1997 16:29:51 -0500".to_owned(),
)],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_extended() {
let a = HeaderValue::from_static(
"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: vec![
0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r',
b'a', b't', b'e', b's',
],
})],
};
assert_eq!(a, b);
let a = HeaderValue::from_static(
"attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: vec![
0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r',
b'a', b't', b'e', b's',
],
})],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_extra_whitespace() {
let a = HeaderValue::from_static(
"form-data ; du-mmy= 3 ; name =upload ; filename = \"sample.png\" ; ",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("du-mmy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_unordered() {
let a = HeaderValue::from_static(
"form-data; dummy=3; filename=\"sample.png\" ; name=upload;",
// Actually, a trailing semicolon is not compliant. But it is fine to accept.
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
DispositionParam::Name("upload".to_owned()),
],
};
assert_eq!(a, b);
let a = HeaderValue::from_str(
"attachment; filename*=iso-8859-1''foo-%E4.html; filename=\"foo-ä.html\"",
)
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![
DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: None,
value: b"foo-\xe4.html".to_vec(),
}),
DispositionParam::Filename("foo-ä.html".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_only_disp() {
let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Attachment,
parameters: vec![],
};
assert_eq!(a, b);
let a = ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![],
};
assert_eq!(a, b);
let a = ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param"))
.unwrap();
let b = ContentDisposition {
disposition: DispositionType::Ext(String::from("unknown-disp-param")),
parameters: vec![],
};
assert_eq!(a, b);
}
#[test]
fn from_raw_with_mixed_case() {
let a = HeaderValue::from_str(
"InLInE; fIlenAME*=iso-8859-1''foo-%E4.html; filEName=\"foo-ä.html\"",
)
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![
DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: None,
value: b"foo-\xe4.html".to_vec(),
}),
DispositionParam::Filename("foo-ä.html".to_owned()),
],
};
assert_eq!(a, b);
}
#[test]
fn from_raw_with_unicode() {
/* RFC 7578 Section 4.2:
Some commonly deployed systems use multipart/form-data with file names directly encoded
including octets outside the US-ASCII range. The encoding used for the file names is
typically UTF-8, although HTML forms will use the charset associated with the form.
Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above.
(And now, only UTF-8 is handled by this implementation.)
*/
let a =
HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name(String::from("upload")),
DispositionParam::Filename(String::from("文件.webp")),
],
};
assert_eq!(a, b);
let a = HeaderValue::from_str(
"form-data; name=upload; filename=\"余固知謇謇之為患兮,忍而不能舍也.pptx\"",
)
.unwrap();
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name(String::from("upload")),
DispositionParam::Filename(String::from(
"余固知謇謇之為患兮,忍而不能舍也.pptx",
)),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_escape() {
let a = HeaderValue::from_static(
"form-data; dummy=3; name=upload; filename=\"s\\amp\\\"le.png\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename(
['s', 'a', 'm', 'p', '\"', 'l', 'e', '.', 'p', 'n', 'g']
.iter()
.collect(),
),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_semicolon() {
let a = HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![DispositionParam::Filename(String::from(
"A semicolon here;.pdf",
))],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_unnecessary_percent_decode() {
// In fact, RFC 7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with
// non-ASCII characters MAY be percent-encoded.
// On the contrary, RFC 6266 or other RFCs related to Content-Disposition response header
// do not mention such percent-encoding.
// So, it appears to be undecidable whether to percent-decode or not without
// knowing the usage scenario (multipart/form-data v.s. HTTP response header) and
// inevitable to unnecessarily percent-decode filename with %XX in the former scenario.
// Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file
// names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without
// percent-encoding. So we do not bother to attempt to percent-decode.
let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name("photo".to_owned()),
DispositionParam::Filename(String::from("%74%65%73%74%2e%70%6e%67")),
],
};
assert_eq!(a, b);
let a =
HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\"");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Name("photo".to_owned()),
DispositionParam::Filename(String::from("%74%65%73%74.png")),
],
};
assert_eq!(a, b);
}
#[test]
fn test_from_raw_param_value_missing() {
let a = HeaderValue::from_static("form-data; name=upload ; filename=");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("attachment; dummy=; filename=invoice.pdf");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename= ");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename=\"\"");
assert!(ContentDisposition::from_raw(&a)
.expect("parse cd")
.get_filename()
.expect("filename")
.is_empty());
}
#[test]
fn test_from_raw_param_name_missing() {
let a = HeaderValue::from_static("inline; =\"test.txt\"");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; =diary.odt");
assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; =");
assert!(ContentDisposition::from_raw(&a).is_err());
}
#[test]
fn test_display_extended() {
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a = HeaderValue::from_static("attachment; filename=colourful.csv");
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(
"attachment; filename=\"colourful.csv\"".to_owned(),
display_rendered
);
}
#[test]
fn test_display_quote() {
let as_string = "form-data; name=upload; filename=\"Quote\\\"here.png\"";
as_string
.find(['\\', '\"'].iter().collect::<String>().as_str())
.unwrap(); // ensure `\"` is there
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
}
#[test]
fn test_display_space_tab() {
let as_string = "form-data; name=upload; filename=\"Space here.png\"";
let a = HeaderValue::from_static(as_string);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(as_string, display_rendered);
let a: ContentDisposition = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename(String::from("Tab\there.png"))],
};
let display_rendered = format!("{}", a);
assert_eq!("inline; filename=\"Tab\x09here.png\"", display_rendered);
}
#[test]
fn test_display_control_characters() {
/* let a = "attachment; filename=\"carriage\rreturn.png\"";
let a = HeaderValue::from_static(a);
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let display_rendered = format!("{}", a);
assert_eq!(
"attachment; filename=\"carriage\\\rreturn.png\"",
display_rendered
);*/
// No way to create a HeaderValue containing a carriage return.
let a: ContentDisposition = ContentDisposition {
disposition: DispositionType::Inline,
parameters: vec![DispositionParam::Filename(String::from("bell\x07.png"))],
};
let display_rendered = format!("{}", a);
assert_eq!("inline; filename=\"bell\\\x07.png\"", display_rendered);
}
#[test]
fn test_param_methods() {
let param = DispositionParam::Filename(String::from("sample.txt"));
assert!(param.is_filename());
assert_eq!(param.as_filename().unwrap(), "sample.txt");
let param = DispositionParam::Unknown(String::from("foo"), String::from("bar"));
assert!(param.is_unknown("foo"));
assert_eq!(param.as_unknown("fOo"), Some("bar"));
}
#[test]
fn test_disposition_methods() {
let cd = ContentDisposition {
disposition: DispositionType::FormData,
parameters: vec![
DispositionParam::Unknown("dummy".to_owned(), "3".to_owned()),
DispositionParam::Name("upload".to_owned()),
DispositionParam::Filename("sample.png".to_owned()),
],
};
assert_eq!(cd.get_name(), Some("upload"));
assert_eq!(cd.get_unknown("dummy"), Some("3"));
assert_eq!(cd.get_filename(), Some("sample.png"));
assert_eq!(cd.get_unknown_ext("dummy"), None);
assert_eq!(cd.get_unknown("duMMy"), Some("3"));
}
}

View File

@ -0,0 +1,54 @@
use language_tags::LanguageTag;
use super::{common_header, QualityItem, CONTENT_LANGUAGE};
common_header! {
/// `Content-Language` header, defined
/// in [RFC 7231 §3.1.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.3.2)
///
/// The `Content-Language` header field describes the natural language(s)
/// of the intended audience for the representation. Note that this
/// might not be equivalent to all the languages used within the
/// representation.
///
/// # ABNF
/// ```plain
/// Content-Language = 1#language-tag
/// ```
///
/// # Example Values
/// * `da`
/// * `mi, en`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ContentLanguage(vec![
/// QualityItem::max(LanguageTag::parse("en").unwrap()),
/// ])
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ContentLanguage, LanguageTag, QualityItem};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ContentLanguage(vec![
/// QualityItem::max(LanguageTag::parse("da").unwrap()),
/// QualityItem::max(LanguageTag::parse("en-GB").unwrap()),
/// ])
/// );
/// ```
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+
test_parse_and_format {
crate::http::header::common_header_test!(test1, vec![b"da"]);
crate::http::header::common_header_test!(test2, vec![b"mi, en"]);
}
}

View File

@ -0,0 +1,207 @@
use std::{
fmt::{self, Display, Write},
str::FromStr,
};
use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer, CONTENT_RANGE};
use crate::error::ParseError;
crate::http::header::common_header! {
/// `Content-Range` header, defined
/// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2)
(ContentRange, CONTENT_RANGE) => [ContentRangeSpec]
test_parse_and_format {
crate::http::header::common_header_test!(test_bytes,
vec![b"bytes 0-499/500"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: Some((0, 499)),
instance_length: Some(500)
})));
crate::http::header::common_header_test!(test_bytes_unknown_len,
vec![b"bytes 0-499/*"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: Some((0, 499)),
instance_length: None
})));
crate::http::header::common_header_test!(test_bytes_unknown_range,
vec![b"bytes */500"],
Some(ContentRange(ContentRangeSpec::Bytes {
range: None,
instance_length: Some(500)
})));
crate::http::header::common_header_test!(test_unregistered,
vec![b"seconds 1-2"],
Some(ContentRange(ContentRangeSpec::Unregistered {
unit: "seconds".to_owned(),
resp: "1-2".to_owned()
})));
crate::http::header::common_header_test!(test_no_len,
vec![b"bytes 0-499"],
None::<ContentRange>);
crate::http::header::common_header_test!(test_only_unit,
vec![b"bytes"],
None::<ContentRange>);
crate::http::header::common_header_test!(test_end_less_than_start,
vec![b"bytes 499-0/500"],
None::<ContentRange>);
crate::http::header::common_header_test!(test_blank,
vec![b""],
None::<ContentRange>);
crate::http::header::common_header_test!(test_bytes_many_spaces,
vec![b"bytes 1-2/500 3"],
None::<ContentRange>);
crate::http::header::common_header_test!(test_bytes_many_slashes,
vec![b"bytes 1-2/500/600"],
None::<ContentRange>);
crate::http::header::common_header_test!(test_bytes_many_dashes,
vec![b"bytes 1-2-3/500"],
None::<ContentRange>);
}
}
/// Content-Range header, defined
/// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2)
///
/// # ABNF
/// ```plain
/// Content-Range = byte-content-range
/// / other-content-range
///
/// byte-content-range = bytes-unit SP
/// ( byte-range-resp / unsatisfied-range )
///
/// byte-range-resp = byte-range "/" ( complete-length / "*" )
/// byte-range = first-byte-pos "-" last-byte-pos
/// unsatisfied-range = "*/" complete-length
///
/// complete-length = 1*DIGIT
///
/// other-content-range = other-range-unit SP other-range-resp
/// other-range-resp = *CHAR
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ContentRangeSpec {
/// Byte range
Bytes {
/// First and last bytes of the range, omitted if request could not be
/// satisfied
range: Option<(u64, u64)>,
/// Total length of the instance, can be omitted if unknown
instance_length: Option<u64>,
},
/// Custom range, with unit not registered at IANA
Unregistered {
/// other-range-unit
unit: String,
/// other-range-resp
resp: String,
},
}
fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> {
let mut iter = s.splitn(2, separator);
match (iter.next(), iter.next()) {
(Some(a), Some(b)) => Some((a, b)),
_ => None,
}
}
impl FromStr for ContentRangeSpec {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, ParseError> {
let res = match split_in_two(s, ' ') {
Some(("bytes", resp)) => {
let (range, instance_length) =
split_in_two(resp, '/').ok_or(ParseError::Header)?;
let instance_length = if instance_length == "*" {
None
} else {
Some(instance_length.parse().map_err(|_| ParseError::Header)?)
};
let range = if range == "*" {
None
} else {
let (first_byte, last_byte) =
split_in_two(range, '-').ok_or(ParseError::Header)?;
let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?;
let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?;
if last_byte < first_byte {
return Err(ParseError::Header);
}
Some((first_byte, last_byte))
};
ContentRangeSpec::Bytes {
range,
instance_length,
}
}
Some((unit, resp)) => ContentRangeSpec::Unregistered {
unit: unit.to_owned(),
resp: resp.to_owned(),
},
_ => return Err(ParseError::Header),
};
Ok(res)
}
}
impl Display for ContentRangeSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ContentRangeSpec::Bytes {
range,
instance_length,
} => {
f.write_str("bytes ")?;
match range {
Some((first_byte, last_byte)) => {
write!(f, "{}-{}", first_byte, last_byte)?;
}
None => {
f.write_str("*")?;
}
};
f.write_str("/")?;
if let Some(v) = instance_length {
write!(f, "{}", v)
} else {
f.write_str("*")
}
}
ContentRangeSpec::Unregistered { ref unit, ref resp } => {
f.write_str(unit)?;
f.write_str(" ")?;
f.write_str(resp)
}
}
}
}
impl TryIntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())
}
}

View File

@ -0,0 +1,112 @@
use super::CONTENT_TYPE;
use mime::Mime;
crate::http::header::common_header! {
/// `Content-Type` header, defined
/// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5)
///
/// The `Content-Type` header field indicates the media type of the
/// associated representation: either the representation enclosed in the
/// message payload or the selected representation, as determined by the
/// message semantics. The indicated media type defines both the data
/// format and how that data is intended to be processed by a recipient,
/// within the scope of the received message semantics, after any content
/// codings indicated by Content-Encoding are decoded.
///
/// Although the `mime` crate allows the mime options to be any slice, this crate
/// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If
/// this is an issue, it's possible to implement `Header` on a custom struct.
///
/// # ABNF
/// ```plain
/// Content-Type = media-type
/// ```
///
/// # Example Values
/// * `text/html; charset=utf-8`
/// * `application/json`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::ContentType;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ContentType::json()
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::ContentType;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ContentType(mime::TEXT_HTML)
/// );
/// ```
(ContentType, CONTENT_TYPE) => [Mime]
test_parse_and_format {
crate::http::header::common_header_test!(
test1,
vec![b"text/html"],
Some(HeaderField(mime::TEXT_HTML)));
}
}
impl ContentType {
/// A constructor to easily create a `Content-Type: application/json`
/// header.
#[inline]
pub fn json() -> ContentType {
ContentType(mime::APPLICATION_JSON)
}
/// A constructor to easily create a `Content-Type: text/plain;
/// charset=utf-8` header.
#[inline]
pub fn plaintext() -> ContentType {
ContentType(mime::TEXT_PLAIN_UTF_8)
}
/// A constructor to easily create a `Content-Type: text/html; charset=utf-8`
/// header.
#[inline]
pub fn html() -> ContentType {
ContentType(mime::TEXT_HTML_UTF_8)
}
/// A constructor to easily create a `Content-Type: text/xml` header.
#[inline]
pub fn xml() -> ContentType {
ContentType(mime::TEXT_XML)
}
/// A constructor to easily create a `Content-Type:
/// application/www-form-url-encoded` header.
#[inline]
pub fn form_url_encoded() -> ContentType {
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
}
/// A constructor to easily create a `Content-Type: image/jpeg` header.
#[inline]
pub fn jpeg() -> ContentType {
ContentType(mime::IMAGE_JPEG)
}
/// A constructor to easily create a `Content-Type: image/png` header.
#[inline]
pub fn png() -> ContentType {
ContentType(mime::IMAGE_PNG)
}
/// A constructor to easily create a `Content-Type:
/// application/octet-stream` header.
#[inline]
pub fn octet_stream() -> ContentType {
ContentType(mime::APPLICATION_OCTET_STREAM)
}
}

View File

@ -0,0 +1,43 @@
use super::{HttpDate, DATE};
use std::time::SystemTime;
crate::http::header::common_header! {
/// `Date` header, defined
/// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2)
///
/// The `Date` header field represents the date and time at which the
/// message was originated.
///
/// # ABNF
/// ```plain
/// Date = HTTP-date
/// ```
///
/// # Example Values
/// * `Tue, 15 Nov 1994 08:12:31 GMT`
///
/// # Examples
///
/// ```
/// use std::time::SystemTime;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Date;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// Date(SystemTime::now().into())
/// );
/// ```
(Date, DATE) => [HttpDate]
test_parse_and_format {
crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]);
}
}
impl Date {
/// Create a date instance set to the current system time
pub fn now() -> Date {
Date(SystemTime::now().into())
}
}

View File

@ -0,0 +1,55 @@
use std::{fmt, str};
use actix_http::ContentEncoding;
/// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Encoding {
/// A supported content encoding. See [`ContentEncoding`] for variants.
Known(ContentEncoding),
/// Some other encoding that is less common, can be any string.
Unknown(String),
}
impl Encoding {
pub const fn identity() -> Self {
Self::Known(ContentEncoding::Identity)
}
pub const fn brotli() -> Self {
Self::Known(ContentEncoding::Brotli)
}
pub const fn deflate() -> Self {
Self::Known(ContentEncoding::Deflate)
}
pub const fn gzip() -> Self {
Self::Known(ContentEncoding::Gzip)
}
pub const fn zstd() -> Self {
Self::Known(ContentEncoding::Zstd)
}
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Encoding::Known(enc) => enc.as_str(),
Encoding::Unknown(enc) => enc.as_str(),
})
}
}
impl str::FromStr for Encoding {
type Err = crate::error::ParseError;
fn from_str(enc: &str) -> Result<Self, crate::error::ParseError> {
match enc.parse::<ContentEncoding>() {
Ok(enc) => Ok(Self::Known(enc)),
Err(_) => Ok(Self::Unknown(enc.to_owned())),
}
}
}

View File

@ -0,0 +1,284 @@
use std::{
fmt::{self, Display, Write},
str::FromStr,
};
use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer};
/// check that each char in the slice is either:
/// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80`
fn entity_validate_char(c: u8) -> bool {
c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
}
fn check_slice_validity(slice: &str) -> bool {
slice.bytes().all(entity_validate_char)
}
/// An entity tag, defined in [RFC 7232 §2.3].
///
/// An entity tag consists of a string enclosed by two literal double quotes.
/// Preceding the first double quote is an optional weakness indicator,
/// which always looks like `W/`. Examples for valid tags are `"xyzzy"` and
/// `W/"xyzzy"`.
///
/// # ABNF
/// ```plain
/// entity-tag = [ weak ] opaque-tag
/// weak = %x57.2F ; "W/", case-sensitive
/// opaque-tag = DQUOTE *etagc DQUOTE
/// etagc = %x21 / %x23-7E / obs-text
/// ; VCHAR except double quotes, plus obs-text
/// ```
///
/// # Comparison
/// To check if two entity tags are equivalent in an application always use the
/// `strong_eq` or `weak_eq` methods based on the context of the Tag. Only use
/// `==` to check if two tags are identical.
///
/// The example below shows the results for a set of entity-tag pairs and
/// both the weak and strong comparison function results:
///
/// | `ETag 1`| `ETag 2`| Strong Comparison | Weak Comparison |
/// |---------|---------|-------------------|-----------------|
/// | `W/"1"` | `W/"1"` | no match | match |
/// | `W/"1"` | `W/"2"` | no match | no match |
/// | `W/"1"` | `"1"` | no match | match |
/// | `"1"` | `"1"` | match | match |
///
/// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntityTag {
/// Weakness indicator for the tag
pub weak: bool,
/// The opaque string in between the DQUOTEs
tag: String,
}
impl EntityTag {
/// Constructs a new `EntityTag`.
///
/// # Panics
/// If the tag contains invalid characters.
pub fn new(weak: bool, tag: String) -> EntityTag {
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
EntityTag { weak, tag }
}
/// Constructs a new weak EntityTag.
///
/// # Panics
/// If the tag contains invalid characters.
pub fn new_weak(tag: String) -> EntityTag {
EntityTag::new(true, tag)
}
#[deprecated(since = "3.0.0", note = "Renamed to `new_weak`.")]
pub fn weak(tag: String) -> EntityTag {
Self::new_weak(tag)
}
/// Constructs a new strong EntityTag.
///
/// # Panics
/// If the tag contains invalid characters.
pub fn new_strong(tag: String) -> EntityTag {
EntityTag::new(false, tag)
}
#[deprecated(since = "3.0.0", note = "Renamed to `new_strong`.")]
pub fn strong(tag: String) -> EntityTag {
Self::new_strong(tag)
}
/// Returns tag.
pub fn tag(&self) -> &str {
self.tag.as_ref()
}
/// Sets tag.
///
/// # Panics
/// If the tag contains invalid characters.
pub fn set_tag(&mut self, tag: impl Into<String>) {
let tag = tag.into();
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
self.tag = tag
}
/// For strong comparison two entity-tags are equivalent if both are not weak and their
/// opaque-tags match character-by-character.
pub fn strong_eq(&self, other: &EntityTag) -> bool {
!self.weak && !other.weak && self.tag == other.tag
}
/// For weak comparison two entity-tags are equivalent if their opaque-tags match
/// character-by-character, regardless of either or both being tagged as "weak".
pub fn weak_eq(&self, other: &EntityTag) -> bool {
self.tag == other.tag
}
/// Returns the inverse of `strong_eq()`.
pub fn strong_ne(&self, other: &EntityTag) -> bool {
!self.strong_eq(other)
}
/// Returns inverse of `weak_eq()`.
pub fn weak_ne(&self, other: &EntityTag) -> bool {
!self.weak_eq(other)
}
}
impl Display for EntityTag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.weak {
write!(f, "W/\"{}\"", self.tag)
} else {
write!(f, "\"{}\"", self.tag)
}
}
}
impl FromStr for EntityTag {
type Err = crate::error::ParseError;
fn from_str(slice: &str) -> Result<EntityTag, crate::error::ParseError> {
let length = slice.len();
// Early exits if it doesn't terminate in a DQUOTE.
if !slice.ends_with('"') || slice.len() < 2 {
return Err(crate::error::ParseError::Header);
}
// The etag is weak if its first char is not a DQUOTE.
if slice.len() >= 2
&& slice.starts_with('"')
&& check_slice_validity(&slice[1..length - 1])
{
// No need to check if the last char is a DQUOTE,
// we already did that above.
return Ok(EntityTag {
weak: false,
tag: slice[1..length - 1].to_owned(),
});
} else if slice.len() >= 4
&& slice.starts_with("W/\"")
&& check_slice_validity(&slice[3..length - 1])
{
return Ok(EntityTag {
weak: true,
tag: slice[3..length - 1].to_owned(),
});
}
Err(crate::error::ParseError::Header)
}
}
impl TryIntoHeaderValue for EntityTag {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = Writer::new();
write!(wrt, "{}", self).unwrap();
HeaderValue::from_maybe_shared(wrt.take())
}
}
#[cfg(test)]
mod tests {
use super::EntityTag;
#[test]
fn test_etag_parse_success() {
// Expected success
assert_eq!(
"\"foobar\"".parse::<EntityTag>().unwrap(),
EntityTag::new_strong("foobar".to_owned())
);
assert_eq!(
"\"\"".parse::<EntityTag>().unwrap(),
EntityTag::new_strong("".to_owned())
);
assert_eq!(
"W/\"weaktag\"".parse::<EntityTag>().unwrap(),
EntityTag::new_weak("weaktag".to_owned())
);
assert_eq!(
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
EntityTag::new_weak("\x65\x62".to_owned())
);
assert_eq!(
"W/\"\"".parse::<EntityTag>().unwrap(),
EntityTag::new_weak("".to_owned())
);
}
#[test]
fn test_etag_parse_failures() {
// Expected failures
assert!("no-dquotes".parse::<EntityTag>().is_err());
assert!("w/\"the-first-w-is-case-sensitive\""
.parse::<EntityTag>()
.is_err());
assert!("".parse::<EntityTag>().is_err());
assert!("\"unmatched-dquotes1".parse::<EntityTag>().is_err());
assert!("unmatched-dquotes2\"".parse::<EntityTag>().is_err());
assert!("matched-\"dquotes\"".parse::<EntityTag>().is_err());
}
#[test]
fn test_etag_fmt() {
assert_eq!(
format!("{}", EntityTag::new_strong("foobar".to_owned())),
"\"foobar\""
);
assert_eq!(format!("{}", EntityTag::new_strong("".to_owned())), "\"\"");
assert_eq!(
format!("{}", EntityTag::new_weak("weak-etag".to_owned())),
"W/\"weak-etag\""
);
assert_eq!(
format!("{}", EntityTag::new_weak("\u{0065}".to_owned())),
"W/\"\x65\""
);
assert_eq!(format!("{}", EntityTag::new_weak("".to_owned())), "W/\"\"");
}
#[test]
fn test_cmp() {
// | ETag 1 | ETag 2 | Strong Comparison | Weak Comparison |
// |---------|---------|-------------------|-----------------|
// | `W/"1"` | `W/"1"` | no match | match |
// | `W/"1"` | `W/"2"` | no match | no match |
// | `W/"1"` | `"1"` | no match | match |
// | `"1"` | `"1"` | match | match |
let mut etag1 = EntityTag::new_weak("1".to_owned());
let mut etag2 = EntityTag::new_weak("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::new_weak("1".to_owned());
etag2 = EntityTag::new_weak("2".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(!etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(etag1.weak_ne(&etag2));
etag1 = EntityTag::new_weak("1".to_owned());
etag2 = EntityTag::new_strong("1".to_owned());
assert!(!etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
etag1 = EntityTag::new_strong("1".to_owned());
etag2 = EntityTag::new_strong("1".to_owned());
assert!(etag1.strong_eq(&etag2));
assert!(etag1.weak_eq(&etag2));
assert!(!etag1.strong_ne(&etag2));
assert!(!etag1.weak_ne(&etag2));
}
}

View File

@ -0,0 +1,98 @@
use super::{EntityTag, ETAG};
crate::http::header::common_header! {
/// `ETag` header, defined in
/// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
///
/// The `ETag` header field in a response provides the current entity-tag
/// for the selected representation, as determined at the conclusion of
/// handling the request. An entity-tag is an opaque validator for
/// differentiating between multiple representations of the same
/// resource, regardless of whether those multiple representations are
/// due to resource state changes over time, content negotiation
/// resulting in multiple representations being valid at the same time,
/// or both. An entity-tag consists of an opaque quoted string, possibly
/// prefixed by a weakness indicator.
///
/// # ABNF
/// ```plain
/// ETag = entity-tag
/// ```
///
/// # Example Values
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `""`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ETag, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ETag(EntityTag::new_strong("xyzzy".to_owned()))
/// );
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{ETag, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// ETag(EntityTag::new_weak("xyzzy".to_owned()))
/// );
/// ```
(ETag, ETAG) => [EntityTag]
test_parse_and_format {
// From the RFC
crate::http::header::common_header_test!(test1,
vec![b"\"xyzzy\""],
Some(ETag(EntityTag::new_strong("xyzzy".to_owned()))));
crate::http::header::common_header_test!(test2,
vec![b"W/\"xyzzy\""],
Some(ETag(EntityTag::new_weak("xyzzy".to_owned()))));
crate::http::header::common_header_test!(test3,
vec![b"\"\""],
Some(ETag(EntityTag::new_strong("".to_owned()))));
// Own tests
crate::http::header::common_header_test!(test4,
vec![b"\"foobar\""],
Some(ETag(EntityTag::new_strong("foobar".to_owned()))));
crate::http::header::common_header_test!(test5,
vec![b"\"\""],
Some(ETag(EntityTag::new_strong("".to_owned()))));
crate::http::header::common_header_test!(test6,
vec![b"W/\"weak-etag\""],
Some(ETag(EntityTag::new_weak("weak-etag".to_owned()))));
crate::http::header::common_header_test!(test7,
vec![b"W/\"\x65\x62\""],
Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned()))));
crate::http::header::common_header_test!(test8,
vec![b"W/\"\""],
Some(ETag(EntityTag::new_weak("".to_owned()))));
crate::http::header::common_header_test!(test9,
vec![b"no-dquotes"],
None::<ETag>);
crate::http::header::common_header_test!(test10,
vec![b"w/\"the-first-w-is-case-sensitive\""],
None::<ETag>);
crate::http::header::common_header_test!(test11,
vec![b""],
None::<ETag>);
crate::http::header::common_header_test!(test12,
vec![b"\"unmatched-dquotes1"],
None::<ETag>);
crate::http::header::common_header_test!(test13,
vec![b"unmatched-dquotes2\""],
None::<ETag>);
crate::http::header::common_header_test!(test14,
vec![b"matched-\"dquotes\""],
None::<ETag>);
crate::http::header::common_header_test!(test15,
vec![b"\""],
None::<ETag>);
}
}

View File

@ -0,0 +1,41 @@
use super::{HttpDate, EXPIRES};
crate::http::header::common_header! {
/// `Expires` header, defined
/// in [RFC 7234 §5.3](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3)
///
/// The `Expires` header field gives the date/time after which the
/// response is considered stale.
///
/// The presence of an Expires field does not imply that the original
/// resource will change or cease to exist at, before, or after that
/// time.
///
/// # ABNF
/// ```plain
/// Expires = HTTP-date
/// ```
///
/// # Example Values
/// * `Thu, 01 Dec 1994 16:00:00 GMT`
///
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Expires;
///
/// let mut builder = HttpResponse::Ok();
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// Expires(expiration.into())
/// );
/// ```
(Expires, EXPIRES) => [HttpDate]
test_parse_and_format {
// Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]);
}
}

View File

@ -0,0 +1,68 @@
use super::{common_header, EntityTag, IF_MATCH};
common_header! {
/// `If-Match` header, defined
/// in [RFC 7232 §3.1](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1)
///
/// The `If-Match` header field makes the request method conditional on
/// the recipient origin server either having at least one current
/// representation of the target resource, when the field-value is "*",
/// or having a current representation of the target resource that has an
/// entity-tag matching a member of the list of entity-tags provided in
/// the field-value.
///
/// An origin server MUST use the strong comparison function when
/// comparing entity-tags for `If-Match`, since the client
/// intends this precondition to prevent the method from being applied if
/// there have been any changes to the representation data.
///
/// # ABNF
/// ```plain
/// If-Match = "*" / 1#entity-tag
/// ```
///
/// # Example Values
/// * `"xyzzy"`
/// * "xyzzy", "r2d2xxxx", "c3piozzzz"
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfMatch;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(IfMatch::Any);
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{IfMatch, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// IfMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
/// EntityTag::new(false, "bazquux".to_owned()),
/// ])
/// );
/// ```
(IfMatch, IF_MATCH) => {Any / (EntityTag)+}
test_parse_and_format {
crate::http::header::common_header_test!(
test1,
vec![b"\"xyzzy\""],
Some(HeaderField::Items(
vec![EntityTag::new_strong("xyzzy".to_owned())])));
crate::http::header::common_header_test!(
test2,
vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""],
Some(HeaderField::Items(
vec![EntityTag::new_strong("xyzzy".to_owned()),
EntityTag::new_strong("r2d2xxxx".to_owned()),
EntityTag::new_strong("c3piozzzz".to_owned())])));
crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any));
}
}

View File

@ -0,0 +1,40 @@
use super::{HttpDate, IF_MODIFIED_SINCE};
crate::http::header::common_header! {
/// `If-Modified-Since` header, defined
/// in [RFC 7232 §3.3](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3)
///
/// The `If-Modified-Since` header field makes a GET or HEAD request
/// method conditional on the selected representation's modification date
/// being more recent than the date provided in the field-value.
/// Transfer of the selected representation's data is avoided if that
/// data has not changed.
///
/// # ABNF
/// ```plain
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfModifiedSince;
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfModifiedSince(modified.into())
/// );
/// ```
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]
test_parse_and_format {
// Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
}
}

View File

@ -0,0 +1,91 @@
use super::{EntityTag, IF_NONE_MATCH};
crate::http::header::common_header! {
/// `If-None-Match` header, defined
/// in [RFC 7232 §3.2](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2)
///
/// The `If-None-Match` header field makes the request method conditional
/// on a recipient cache or origin server either not having any current
/// representation of the target resource, when the field-value is "*",
/// or having a selected representation with an entity-tag that does not
/// match any of those listed in the field-value.
///
/// A recipient MUST use the weak comparison function when comparing
/// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags
/// can be used for cache validation even if there have been changes to
/// the representation data.
///
/// # ABNF
/// ```plain
/// If-None-Match = "*" / 1#entity-tag
/// ```
///
/// # Example Values
/// * `"xyzzy"`
/// * `W/"xyzzy"`
/// * `"xyzzy", "r2d2xxxx", "c3piozzzz"`
/// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"`
/// * `*`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfNoneMatch;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(IfNoneMatch::Any);
/// ```
///
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{IfNoneMatch, EntityTag};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// IfNoneMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
/// EntityTag::new(false, "bazquux".to_owned()),
/// ])
/// );
/// ```
(IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+}
test_parse_and_format {
crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]);
crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]);
crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]);
crate::http::header::common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]);
crate::http::header::common_header_test!(test5, vec![b"*"]);
}
}
#[cfg(test)]
mod tests {
use super::IfNoneMatch;
use crate::http::header::{EntityTag, Header, IF_NONE_MATCH};
use actix_http::test::TestRequest;
#[test]
fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, "*"))
.finish();
if_none_match = Header::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]))
.finish();
if_none_match = Header::parse(&req);
let mut entities: Vec<EntityTag> = Vec::new();
let foobar_etag = EntityTag::new_strong("foobar".to_owned());
let weak_etag = EntityTag::new_weak("weak-etag".to_owned());
entities.push(foobar_etag);
entities.push(weak_etag);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));
}
}

View File

@ -0,0 +1,119 @@
use std::fmt::{self, Display, Write};
use super::{
from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue,
TryIntoHeaderValue, Writer,
};
use crate::error::ParseError;
use crate::http::header;
use crate::HttpMessage;
/// `If-Range` header, defined
/// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2)
///
/// If a client has a partial copy of a representation and wishes to have
/// an up-to-date copy of the entire representation, it could use the
/// Range header field with a conditional GET (using either or both of
/// If-Unmodified-Since and If-Match.) However, if the precondition
/// fails because the representation has been modified, the client would
/// then have to make a second request to obtain the entire current
/// representation.
///
/// The `If-Range` header field allows a client to \"short-circuit\" the
/// second request. Informally, its meaning is as follows: if the
/// representation is unchanged, send me the part(s) that I am requesting
/// in Range; otherwise, send me the entire representation.
///
/// # ABNF
/// ```plain
/// If-Range = entity-tag / HTTP-date
/// ```
///
/// # Example Values
///
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
/// * `\"xyzzy\"`
///
/// # Examples
/// ```
/// use actix_web::HttpResponse;
/// use actix_web::http::header::{EntityTag, IfRange};
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// IfRange::EntityTag(
/// EntityTag::new(false, "abc".to_owned())
/// )
/// );
/// ```
///
/// ```
/// use std::time::{Duration, SystemTime};
/// use actix_web::{http::header::IfRange, HttpResponse};
///
/// let mut builder = HttpResponse::Ok();
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfRange::Date(fetched.into())
/// );
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum IfRange {
/// The entity-tag the client has of the resource.
EntityTag(EntityTag),
/// The date when the client retrieved the resource.
Date(HttpDate),
}
impl Header for IfRange {
fn name() -> HeaderName {
header::IF_RANGE
}
#[inline]
fn parse<T>(msg: &T) -> Result<Self, ParseError>
where
T: HttpMessage,
{
let etag: Result<EntityTag, _> = from_one_raw_str(msg.headers().get(&header::IF_RANGE));
if let Ok(etag) = etag {
return Ok(IfRange::EntityTag(etag));
}
let date: Result<HttpDate, _> = from_one_raw_str(msg.headers().get(&header::IF_RANGE));
if let Ok(date) = date {
return Ok(IfRange::Date(date));
}
Err(ParseError::Header)
}
}
impl Display for IfRange {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
IfRange::EntityTag(ref x) => Display::fmt(x, f),
IfRange::Date(ref x) => Display::fmt(x, f),
}
}
}
impl TryIntoHeaderValue for IfRange {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())
}
}
#[cfg(test)]
mod test_parse_and_format {
use std::str;
use super::IfRange as HeaderField;
use crate::http::header::*;
crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
crate::http::header::common_header_test!(test2, vec![b"\"abc\""]);
crate::http::header::common_header_test!(test3, vec![b"this-is-invalid"], None::<IfRange>);
}

View File

@ -0,0 +1,40 @@
use super::{HttpDate, IF_UNMODIFIED_SINCE};
crate::http::header::common_header! {
/// `If-Unmodified-Since` header, defined
/// in [RFC 7232 §3.4](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4)
///
/// The `If-Unmodified-Since` header field makes the request method
/// conditional on the selected representation's last modification date
/// being earlier than or equal to the date provided in the field-value.
/// This field accomplishes the same purpose as If-Match for cases where
/// the user agent does not have an entity-tag for the representation.
///
/// # ABNF
/// ```plain
/// If-Unmodified-Since = HTTP-date
/// ```
///
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfUnmodifiedSince;
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfUnmodifiedSince(modified.into())
/// );
/// ```
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]
test_parse_and_format {
// Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
}
}

View File

@ -0,0 +1,39 @@
use super::{HttpDate, LAST_MODIFIED};
crate::http::header::common_header! {
/// `Last-Modified` header, defined
/// in [RFC 7232 §2.2](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2)
///
/// The `Last-Modified` header field in a response provides a timestamp
/// indicating the date and time at which the origin server believes the
/// selected representation was last modified, as determined at the
/// conclusion of handling the request.
///
/// # ABNF
/// ```plain
/// Expires = HTTP-date
/// ```
///
/// # Example Values
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
///
/// # Examples
///
/// ```
/// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::LastModified;
///
/// let mut builder = HttpResponse::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// LastModified(modified.into())
/// );
/// ```
(LastModified, LAST_MODIFIED) => [HttpDate]
test_parse_and_format {
// Test case from RFC
crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
}
}

View File

@ -0,0 +1,319 @@
macro_rules! common_header_test_module {
($id:ident, $tm:ident{$($tf:item)*}) => {
#[cfg(test)]
mod $tm {
#![allow(unused_imports)]
use ::core::str;
use ::actix_http::{Method, test};
use ::mime::*;
use $crate::http::header::{self, *};
use super::{$id as HeaderField, *};
$($tf)*
}
}
}
#[cfg(test)]
macro_rules! common_header_test {
($id:ident, $raw:expr) => {
#[test]
fn $id() {
use ::actix_http::test;
let raw = $raw;
let headers = raw.iter().map(|x| x.to_vec()).collect::<Vec<_>>();
let mut req = test::TestRequest::default();
for item in headers {
req = req.append_header((HeaderField::name(), item)).take();
}
let req = req.finish();
let value = HeaderField::parse(&req);
let result = format!("{}", value.unwrap());
let expected = ::std::string::String::from_utf8(raw[0].to_vec()).unwrap();
let result_cmp: Vec<String> = result
.to_ascii_lowercase()
.split(' ')
.map(|x| x.to_owned())
.collect();
let expected_cmp: Vec<String> = expected
.to_ascii_lowercase()
.split(' ')
.map(|x| x.to_owned())
.collect();
assert_eq!(result_cmp.concat(), expected_cmp.concat());
}
};
($id:ident, $raw:expr, $exp:expr) => {
#[test]
fn $id() {
use actix_http::test;
let headers = $raw.iter().map(|x| x.to_vec()).collect::<Vec<_>>();
let mut req = test::TestRequest::default();
for item in headers {
req.append_header((HeaderField::name(), item));
}
let req = req.finish();
let val = HeaderField::parse(&req);
let exp: ::core::option::Option<HeaderField> = $exp;
// test parsing
assert_eq!(val.ok(), exp);
// test formatting
if let Some(exp) = exp {
let raw = &($raw)[..];
let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap());
let mut joined = String::new();
if let Some(s) = iter.next() {
joined.push_str(s);
for s in iter {
joined.push_str(", ");
joined.push_str(s);
}
}
assert_eq!(format!("{}", exp), joined);
}
}
};
}
macro_rules! common_header {
// TODO: these docs are wrong, there's no $n or $nn
// $attrs:meta: Attributes associated with the header item (usually docs)
// $id:ident: Identifier of the header
// $n:expr: Lowercase name of the header
// $nn:expr: Nice name of the header
// List header, zero or more items
($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)*) => {
$(#[$attrs])*
#[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)]
pub struct $id(pub Vec<$item>);
impl $crate::http::header::Header for $id {
#[inline]
fn name() -> $crate::http::header::HeaderName {
$name
}
#[inline]
fn parse<M: $crate::HttpMessage>(msg: &M) -> Result<Self, $crate::error::ParseError> {
let headers = msg.headers().get_all(Self::name());
$crate::http::header::from_comma_delimited(headers).map($id)
}
}
impl ::core::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
$crate::http::header::fmt_comma_delimited(f, &self.0[..])
}
}
impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use ::core::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
}
}
};
// List header, one or more items
($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)+) => {
$(#[$attrs])*
#[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)]
pub struct $id(pub Vec<$item>);
impl $crate::http::header::Header for $id {
#[inline]
fn name() -> $crate::http::header::HeaderName {
$name
}
#[inline]
fn parse<M: $crate::HttpMessage>(msg: &M) -> Result<Self, $crate::error::ParseError>{
let headers = msg.headers().get_all(Self::name());
$crate::http::header::from_comma_delimited(headers)
.and_then(|items| {
if items.is_empty() {
Err($crate::error::ParseError::Header)
} else {
Ok($id(items))
}
})
}
}
impl ::core::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
$crate::http::header::fmt_comma_delimited(f, &self.0[..])
}
}
impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use ::core::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
}
}
};
// Single value header
($(#[$attrs:meta])*($id:ident, $name:expr) => [$value:ty]) => {
$(#[$attrs])*
#[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)]
pub struct $id(pub $value);
impl $crate::http::header::Header for $id {
#[inline]
fn name() -> $crate::http::header::HeaderName {
$name
}
#[inline]
fn parse<M: $crate::HttpMessage>(msg: &M) -> Result<Self, $crate::error::ParseError> {
let header = msg.headers().get(Self::name());
$crate::http::header::from_one_raw_str(header).map($id)
}
}
impl ::core::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
::core::fmt::Display::fmt(&self.0, f)
}
}
impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into_value()
}
}
};
// List header, one or more items with "*" option
($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => {
$(#[$attrs])*
#[derive(Clone, Debug, PartialEq)]
pub enum $id {
/// Any value is a match
Any,
/// Only the listed items are a match
Items(Vec<$item>),
}
impl $crate::http::header::Header for $id {
#[inline]
fn name() -> $crate::http::header::HeaderName {
$name
}
#[inline]
fn parse<M: $crate::HttpMessage>(msg: &M) -> Result<Self, $crate::error::ParseError> {
let is_any = msg
.headers()
.get(Self::name())
.and_then(|hdr| hdr.to_str().ok())
.map(|hdr| hdr.trim() == "*");
if let Some(true) = is_any {
Ok($id::Any)
} else {
let headers = msg.headers().get_all(Self::name());
Ok($id::Items($crate::http::header::from_comma_delimited(headers)?))
}
}
}
impl ::core::fmt::Display for $id {
#[inline]
fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
match *self {
$id::Any => f.write_str("*"),
$id::Items(ref fields) =>
$crate::http::header::fmt_comma_delimited(f, &fields[..])
}
}
}
impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use ::core::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
$crate::http::header::HeaderValue::from_maybe_shared(writer.take())
}
}
};
// optional test module
($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => {
crate::http::header::common_header! {
$(#[$attrs])*
($id, $name) => ($item)*
}
crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }}
};
($(#[$attrs:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => {
crate::http::header::common_header! {
$(#[$attrs])*
($id, $n) => ($item)+
}
crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }}
};
($(#[$attrs:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => {
crate::http::header::common_header! {
$(#[$attrs])* ($id, $name) => [$item]
}
crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }}
};
($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => {
crate::http::header::common_header! {
$(#[$attrs])*
($id, $name) => {Any / ($item)+}
}
crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }}
};
}
pub(crate) use {common_header, common_header_test_module};
#[cfg(test)]
pub(crate) use common_header_test;

View File

@ -0,0 +1,102 @@
//! A Collection of Header implementations for common HTTP Headers.
//!
//! ## Mime Types
//! Several header fields use MIME values for their contents. Keeping with the strongly-typed theme,
//! the [mime] crate is used in such headers as [`ContentType`] and [`Accept`].
use std::fmt;
use bytes::{Bytes, BytesMut};
// re-export from actix-http
// - header name / value types
// - relevant traits for converting to header name / value
// - all const header names
// - header map
// - the few typed headers from actix-http
// - header parsing utils
pub use actix_http::header::*;
mod accept;
mod accept_charset;
mod accept_encoding;
mod accept_language;
mod allow;
mod cache_control;
mod content_disposition;
mod content_language;
mod content_range;
mod content_type;
mod date;
mod encoding;
mod entity;
mod etag;
mod expires;
mod if_match;
mod if_modified_since;
mod if_none_match;
mod if_range;
mod if_unmodified_since;
mod last_modified;
mod macros;
mod preference;
mod range;
#[cfg(test)]
pub(crate) use macros::common_header_test;
pub(crate) use macros::{common_header, common_header_test_module};
pub use self::accept::Accept;
pub use self::accept_charset::AcceptCharset;
pub use self::accept_encoding::AcceptEncoding;
pub use self::accept_language::AcceptLanguage;
pub use self::allow::Allow;
pub use self::cache_control::{CacheControl, CacheDirective};
pub use self::content_disposition::{ContentDisposition, DispositionParam, DispositionType};
pub use self::content_language::ContentLanguage;
pub use self::content_range::{ContentRange, ContentRangeSpec};
pub use self::content_type::ContentType;
pub use self::date::Date;
pub use self::encoding::Encoding;
pub use self::entity::EntityTag;
pub use self::etag::ETag;
pub use self::expires::Expires;
pub use self::if_match::IfMatch;
pub use self::if_modified_since::IfModifiedSince;
pub use self::if_none_match::IfNoneMatch;
pub use self::if_range::IfRange;
pub use self::if_unmodified_since::IfUnmodifiedSince;
pub use self::last_modified::LastModified;
pub use self::preference::Preference;
pub use self::range::{ByteRangeSpec, Range};
/// Format writer ([`fmt::Write`]) for a [`BytesMut`].
#[derive(Debug, Default)]
struct Writer {
buf: BytesMut,
}
impl Writer {
/// Constructs new bytes writer.
pub fn new() -> Writer {
Writer::default()
}
/// Splits bytes out of writer, leaving writer buffer empty.
pub fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}

View File

@ -0,0 +1,70 @@
use std::{
fmt::{self, Write as _},
str,
};
/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the
/// underlying type does not support them.
///
/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage)
/// typed header but it does not parse `*` successfully. On the other hand, the `mime` crate, used
/// for [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not
/// used in those header types.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)]
pub enum Preference<T> {
/// A wildcard value.
Any,
/// A valid `T`.
Specific(T),
}
impl<T> Preference<T> {
/// Returns true if preference is the any/wildcard (`*`) value.
pub fn is_any(&self) -> bool {
matches!(self, Self::Any)
}
/// Returns true if preference is the specific item (`T`) variant.
pub fn is_specific(&self) -> bool {
matches!(self, Self::Specific(_))
}
/// Returns reference to value in `Specific` variant, if it is set.
pub fn item(&self) -> Option<&T> {
match self {
Preference::Specific(ref item) => Some(item),
Preference::Any => None,
}
}
/// Consumes the container, returning the value in the `Specific` variant, if it is set.
pub fn into_item(self) -> Option<T> {
match self {
Preference::Specific(item) => Some(item),
Preference::Any => None,
}
}
}
impl<T: fmt::Display> fmt::Display for Preference<T> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Preference::Any => f.write_char('*'),
Preference::Specific(item) => fmt::Display::fmt(item, f),
}
}
}
impl<T: str::FromStr> str::FromStr for Preference<T> {
type Err = T::Err;
#[inline]
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim() {
"*" => Ok(Self::Any),
other => other.parse().map(Preference::Specific),
}
}
}

View File

@ -0,0 +1,427 @@
use std::{
cmp,
fmt::{self, Display, Write},
str::FromStr,
};
use actix_http::{error::ParseError, header, HttpMessage};
use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer};
/// `Range` header, defined
/// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1)
///
/// The "Range" header field on a GET request modifies the method semantics to request transfer of
/// only one or more sub-ranges of the selected representation data, rather than the entire selected
/// representation data.
///
/// # ABNF
/// ```plain
/// Range = byte-ranges-specifier / other-ranges-specifier
/// other-ranges-specifier = other-range-unit "=" other-range-set
/// other-range-set = 1*VCHAR
///
/// bytes-unit = "bytes"
///
/// byte-ranges-specifier = bytes-unit "=" byte-range-set
/// byte-range-set = 1#(byte-range-spec / suffix-byte-range-spec)
/// byte-range-spec = first-byte-pos "-" [last-byte-pos]
/// suffix-byte-range-spec = "-" suffix-length
/// suffix-length = 1*DIGIT
/// first-byte-pos = 1*DIGIT
/// last-byte-pos = 1*DIGIT
/// ```
///
/// # Example Values
/// * `bytes=1000-`
/// * `bytes=-50`
/// * `bytes=0-1,30-40`
/// * `bytes=0-10,20-90,-100`
/// * `custom_unit=0-123`
/// * `custom_unit=xxx-yyy`
///
/// # Examples
/// ```
/// use actix_web::http::header::{Range, ByteRangeSpec};
/// use actix_web::HttpResponse;
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(Range::Bytes(
/// vec![ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::From(200)]
/// ));
/// builder.insert_header(Range::Unregistered("letters".to_owned(), "a-f".to_owned()));
/// builder.insert_header(Range::bytes(1, 100));
/// builder.insert_header(Range::bytes_multi(vec![(1, 100), (200, 300)]));
/// ```
#[derive(PartialEq, Clone, Debug)]
pub enum Range {
/// Byte range.
Bytes(Vec<ByteRangeSpec>),
/// Custom range, with unit not registered at IANA.
///
/// (`other-range-unit`: String , `other-range-set`: String)
Unregistered(String, String),
}
/// A range of bytes to fetch.
///
/// Each [`Range::Bytes`] header can contain one or more `ByteRangeSpec`s.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ByteRangeSpec {
/// All bytes from `x` to `y`, inclusive.
///
/// Serialized as `x-y`.
///
/// Example: `bytes=500-999` would represent the second 500 bytes.
FromTo(u64, u64),
/// All bytes starting from `x`, inclusive.
///
/// Serialized as `x-`.
///
/// Example: For a file of 1000 bytes, `bytes=950-` would represent bytes 950-999, inclusive.
From(u64),
/// The last `y` bytes, inclusive.
///
/// Using the spec terminology, this is `suffix-byte-range-spec`. Serialized as `-y`.
///
/// Example: For a file of 1000 bytes, `bytes=-50` is equivalent to `bytes=950-`.
Last(u64),
}
impl ByteRangeSpec {
/// Given the full length of the entity, attempt to normalize the byte range into an satisfiable
/// end-inclusive `(from, to)` range.
///
/// The resulting range is guaranteed to be a satisfiable range within the bounds
/// of `0 <= from <= to < full_length`.
///
/// If the byte range is deemed unsatisfiable, `None` is returned. An unsatisfiable range is
/// generally cause for a server to either reject the client request with a
/// `416 Range Not Satisfiable` status code, or to simply ignore the range header and serve the
/// full entity using a `200 OK` status code.
///
/// This function closely follows [RFC 7233 §2.1]. As such, it considers ranges to be
/// satisfiable if they meet the following conditions:
///
/// > If a valid byte-range-set includes at least one byte-range-spec with a first-byte-pos that
/// is less than the current length of the representation, or at least one
/// suffix-byte-range-spec with a non-zero suffix-length, then the byte-range-set
/// is satisfiable. Otherwise, the byte-range-set is unsatisfiable.
///
/// The function also computes remainder ranges based on the RFC:
///
/// > If the last-byte-pos value is absent, or if the value is greater than or equal to the
/// current length of the representation data, the byte range is interpreted as the remainder
/// of the representation (i.e., the server replaces the value of last-byte-pos with a value
/// that is one less than the current length of the selected representation).
///
/// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233
pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> {
// If the full length is zero, there is no satisfiable end-inclusive range.
if full_length == 0 {
return None;
}
match *self {
ByteRangeSpec::FromTo(from, to) => {
if from < full_length && from <= to {
Some((from, cmp::min(to, full_length - 1)))
} else {
None
}
}
ByteRangeSpec::From(from) => {
if from < full_length {
Some((from, full_length - 1))
} else {
None
}
}
ByteRangeSpec::Last(last) => {
if last > 0 {
// From the RFC: If the selected representation is shorter than the specified
// suffix-length, the entire representation is used.
if last > full_length {
Some((0, full_length - 1))
} else {
Some((full_length - last, full_length - 1))
}
} else {
None
}
}
}
}
}
impl Range {
/// Constructs a common byte range header.
///
/// Eg: `bytes=from-to`
pub fn bytes(from: u64, to: u64) -> Range {
Range::Bytes(vec![ByteRangeSpec::FromTo(from, to)])
}
/// Constructs a byte range header with multiple subranges.
///
/// Eg: `bytes=from1-to1,from2-to2,fromX-toX`
pub fn bytes_multi(ranges: Vec<(u64, u64)>) -> Range {
Range::Bytes(
ranges
.into_iter()
.map(|(from, to)| ByteRangeSpec::FromTo(from, to))
.collect(),
)
}
}
impl fmt::Display for ByteRangeSpec {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
ByteRangeSpec::FromTo(from, to) => write!(f, "{}-{}", from, to),
ByteRangeSpec::Last(pos) => write!(f, "-{}", pos),
ByteRangeSpec::From(pos) => write!(f, "{}-", pos),
}
}
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Range::Bytes(ranges) => {
write!(f, "bytes=")?;
for (i, range) in ranges.iter().enumerate() {
if i != 0 {
f.write_str(",")?;
}
Display::fmt(range, f)?;
}
Ok(())
}
Range::Unregistered(unit, range_str) => {
write!(f, "{}={}", unit, range_str)
}
}
}
}
impl FromStr for Range {
type Err = ParseError;
fn from_str(s: &str) -> Result<Range, ParseError> {
let (unit, val) = s.split_once('=').ok_or(ParseError::Header)?;
match (unit, val) {
("bytes", ranges) => {
let ranges = from_comma_delimited(ranges);
if ranges.is_empty() {
return Err(ParseError::Header);
}
Ok(Range::Bytes(ranges))
}
(_, "") => Err(ParseError::Header),
("", _) => Err(ParseError::Header),
(unit, range_str) => Ok(Range::Unregistered(unit.to_owned(), range_str.to_owned())),
}
}
}
impl FromStr for ByteRangeSpec {
type Err = ParseError;
fn from_str(s: &str) -> Result<ByteRangeSpec, ParseError> {
let (start, end) = s.split_once('-').ok_or(ParseError::Header)?;
match (start, end) {
("", end) => end
.parse()
.or(Err(ParseError::Header))
.map(ByteRangeSpec::Last),
(start, "") => start
.parse()
.or(Err(ParseError::Header))
.map(ByteRangeSpec::From),
(start, end) => match (start.parse(), end.parse()) {
(Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)),
_ => Err(ParseError::Header),
},
}
}
}
impl Header for Range {
fn name() -> HeaderName {
header::RANGE
}
#[inline]
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
header::from_one_raw_str(msg.headers().get(&Self::name()))
}
}
impl TryIntoHeaderValue for Range {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = Writer::new();
let _ = write!(wrt, "{}", self);
HeaderValue::from_maybe_shared(wrt.take())
}
}
/// Parses 0 or more items out of a comma delimited string, ignoring invalid items.
fn from_comma_delimited<T: FromStr>(s: &str) -> Vec<T> {
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.parse().ok())
.collect()
}
#[cfg(test)]
mod tests {
use actix_http::{test::TestRequest, Request};
use super::*;
fn req(s: &str) -> Request {
TestRequest::default()
.insert_header((header::RANGE, s))
.finish()
}
#[test]
fn test_parse_bytes_range_valid() {
let r: Range = Header::parse(&req("bytes=1-100")).unwrap();
let r2: Range = Header::parse(&req("bytes=1-100,-")).unwrap();
let r3 = Range::bytes(1, 100);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse(&req("bytes=1-100,200-")).unwrap();
let r2: Range = Header::parse(&req("bytes= 1-100 , 101-xxx, 200- ")).unwrap();
let r3 = Range::Bytes(vec![
ByteRangeSpec::FromTo(1, 100),
ByteRangeSpec::From(200),
]);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse(&req("bytes=1-100,-100")).unwrap();
let r2: Range = Header::parse(&req("bytes=1-100, ,,-100")).unwrap();
let r3 = Range::Bytes(vec![
ByteRangeSpec::FromTo(1, 100),
ByteRangeSpec::Last(100),
]);
assert_eq!(r, r2);
assert_eq!(r2, r3);
let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
assert_eq!(r, r2);
}
#[test]
fn test_parse_unregistered_range_valid() {
let r: Range = Header::parse(&req("custom=1-100,-100")).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "1-100,-100".to_owned());
assert_eq!(r, r2);
let r: Range = Header::parse(&req("custom=abcd")).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "abcd".to_owned());
assert_eq!(r, r2);
let r: Range = Header::parse(&req("custom=xxx-yyy")).unwrap();
let r2 = Range::Unregistered("custom".to_owned(), "xxx-yyy".to_owned());
assert_eq!(r, r2);
}
#[test]
fn test_parse_invalid() {
let r: Result<Range, ParseError> = Header::parse(&req("bytes=1-a,-"));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("bytes=1-2-3"));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("abc"));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("bytes=1-100="));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("bytes="));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("custom="));
assert_eq!(r.ok(), None);
let r: Result<Range, ParseError> = Header::parse(&req("=1-100"));
assert_eq!(r.ok(), None);
}
#[test]
fn test_fmt() {
let range = Range::Bytes(vec![
ByteRangeSpec::FromTo(0, 1000),
ByteRangeSpec::From(2000),
]);
assert_eq!(&range.to_string(), "bytes=0-1000,2000-");
let range = Range::Bytes(vec![]);
assert_eq!(&range.to_string(), "bytes=");
let range = Range::Unregistered("custom".to_owned(), "1-xxx".to_owned());
assert_eq!(&range.to_string(), "custom=1-xxx");
}
#[test]
fn test_byte_range_spec_to_satisfiable_range() {
assert_eq!(
Some((0, 0)),
ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(3)
);
assert_eq!(
Some((1, 2)),
ByteRangeSpec::FromTo(1, 2).to_satisfiable_range(3)
);
assert_eq!(
Some((1, 2)),
ByteRangeSpec::FromTo(1, 5).to_satisfiable_range(3)
);
assert_eq!(None, ByteRangeSpec::FromTo(3, 3).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::FromTo(2, 1).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::FromTo(0, 0).to_satisfiable_range(0));
assert_eq!(Some((0, 2)), ByteRangeSpec::From(0).to_satisfiable_range(3));
assert_eq!(Some((2, 2)), ByteRangeSpec::From(2).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::From(3).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::From(5).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::From(0).to_satisfiable_range(0));
assert_eq!(Some((1, 2)), ByteRangeSpec::Last(2).to_satisfiable_range(3));
assert_eq!(Some((2, 2)), ByteRangeSpec::Last(1).to_satisfiable_range(3));
assert_eq!(Some((0, 2)), ByteRangeSpec::Last(5).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::Last(0).to_satisfiable_range(3));
assert_eq!(None, ByteRangeSpec::Last(2).to_satisfiable_range(0));
}
}

View File

@ -0,0 +1,6 @@
//! Various HTTP related types.
pub mod header;
// TODO: figure out how best to expose http::Error vs actix_http::Error
pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version};

471
actix-web/src/info.rs Normal file
View File

@ -0,0 +1,471 @@
use std::{convert::Infallible, net::SocketAddr};
use actix_utils::future::{err, ok, Ready};
use derive_more::{Display, Error};
use once_cell::sync::Lazy;
use crate::{
dev::{AppConfig, Payload, RequestHead},
http::{
header::{self, HeaderName},
uri::{Authority, Scheme},
},
FromRequest, HttpRequest, ResponseError,
};
static X_FORWARDED_FOR: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-for"));
static X_FORWARDED_HOST: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-host"));
static X_FORWARDED_PROTO: Lazy<HeaderName> =
Lazy::new(|| HeaderName::from_static("x-forwarded-proto"));
/// Trim whitespace then any quote marks.
fn unquote(val: &str) -> &str {
val.trim().trim_start_matches('"').trim_end_matches('"')
}
/// Extracts and trims first value for given header name.
fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> {
let hdr = req.headers.get(name)?.to_str().ok()?;
let val = hdr.split(',').next()?.trim();
Some(val)
}
/// HTTP connection information.
///
/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers.
///
/// # Examples
/// ```
/// # use actix_web::{HttpResponse, Responder};
/// use actix_web::dev::ConnectionInfo;
///
/// async fn handler(conn: ConnectionInfo) -> impl Responder {
/// match conn.host() {
/// "actix.rs" => HttpResponse::Ok().body("Welcome!"),
/// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."),
/// _ => HttpResponse::NotFound().finish()
/// }
/// }
/// # let _svc = actix_web::web::to(handler);
/// ```
///
/// # Implementation Notes
/// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to
/// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return
/// strings instead of IP addresses or other types to acknowledge that they may be
/// [obfuscated][rfc7239-63] or [unknown][rfc7239-62].
///
/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded`
/// is preferred.
///
/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239
/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2
/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3
#[derive(Debug, Clone, Default)]
pub struct ConnectionInfo {
host: String,
scheme: String,
peer_addr: Option<String>,
realip_remote_addr: Option<String>,
}
impl ConnectionInfo {
pub(crate) fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo {
let mut host = None;
let mut scheme = None;
let mut realip_remote_addr = None;
for (name, val) in req
.headers
.get_all(&header::FORWARDED)
.into_iter()
.filter_map(|hdr| hdr.to_str().ok())
// "for=1.2.3.4, for=5.6.7.8; scheme=https"
.flat_map(|val| val.split(';'))
// ["for=1.2.3.4, for=5.6.7.8", " scheme=https"]
.flat_map(|vals| vals.split(','))
// ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"]
.flat_map(|pair| {
let mut items = pair.trim().splitn(2, '=');
Some((items.next()?, items.next()?))
})
{
// [(name , val ), ... ]
// [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")]
// taking the first value for each property is correct because spec states that first
// "for" value is client and rest are proxies; multiple values other properties have
// no defined semantics
//
// > In a chain of proxy servers where this is fully utilized, the first
// > "for" parameter will disclose the client where the request was first
// > made, followed by any subsequent proxy identifiers.
// --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2
match name.trim().to_lowercase().as_str() {
"for" => realip_remote_addr.get_or_insert_with(|| unquote(val)),
"proto" => scheme.get_or_insert_with(|| unquote(val)),
"host" => host.get_or_insert_with(|| unquote(val)),
"by" => {
// TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1
continue;
}
_ => continue,
};
}
let scheme = scheme
.or_else(|| first_header_value(req, &*X_FORWARDED_PROTO))
.or_else(|| req.uri.scheme().map(Scheme::as_str))
.or_else(|| Some("https").filter(|_| cfg.secure()))
.unwrap_or("http")
.to_owned();
let host = host
.or_else(|| first_header_value(req, &*X_FORWARDED_HOST))
.or_else(|| req.headers.get(&header::HOST)?.to_str().ok())
.or_else(|| req.uri.authority().map(Authority::as_str))
.unwrap_or_else(|| cfg.host())
.to_owned();
let realip_remote_addr = realip_remote_addr
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR))
.map(str::to_owned);
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());
ConnectionInfo {
host,
scheme,
peer_addr,
realip_remote_addr,
}
}
/// Real IP (remote address) of client that initiated request.
///
/// The address is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-For` header
/// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr))
///
/// # Security
/// Do not use this function for security purposes unless you can be sure that the `Forwarded`
/// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a
/// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate.
#[inline]
pub fn realip_remote_addr(&self) -> Option<&str> {
self.realip_remote_addr
.as_deref()
.or_else(|| self.peer_addr.as_deref())
}
/// Returns serialized IP address of the peer connection.
///
/// See [`HttpRequest::peer_addr`] for more details.
#[inline]
pub fn peer_addr(&self) -> Option<&str> {
self.peer_addr.as_deref()
}
/// Hostname of the request.
///
/// Hostname is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Host` header
/// - `Host` header
/// - request target / URI
/// - configured server hostname
#[inline]
pub fn host(&self) -> &str {
&self.host
}
/// Scheme of the request.
///
/// Scheme is resolved through the following, in order:
/// - `Forwarded` header
/// - `X-Forwarded-Proto` header
/// - request target / URI
#[inline]
pub fn scheme(&self) -> &str {
&self.scheme
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")]
pub fn remote_addr(&self) -> Option<&str> {
self.peer_addr()
}
}
impl FromRequest for ConnectionInfo {
type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.connection_info().clone())
}
}
/// Extractor for peer's socket address.
///
/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`].
///
/// # Examples
/// ```
/// # use actix_web::Responder;
/// use actix_web::dev::PeerAddr;
///
/// async fn handler(peer_addr: PeerAddr) -> impl Responder {
/// let socket_addr = peer_addr.0;
/// socket_addr.to_string()
/// }
/// # let _svc = actix_web::web::to(handler);
/// ```
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)]
#[display(fmt = "{}", _0)]
pub struct PeerAddr(pub SocketAddr);
impl PeerAddr {
/// Unwrap into inner `SocketAddr` value.
pub fn into_inner(self) -> SocketAddr {
self.0
}
}
#[derive(Debug, Display, Error)]
#[non_exhaustive]
#[display(fmt = "Missing peer address")]
pub struct MissingPeerAddr;
impl ResponseError for MissingPeerAddr {}
impl FromRequest for PeerAddr {
type Error = MissingPeerAddr;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
match req.peer_addr() {
Some(addr) => ok(PeerAddr(addr)),
None => {
log::error!("Missing peer address.");
err(MissingPeerAddr)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test::TestRequest;
const X_FORWARDED_FOR: &str = "x-forwarded-for";
const X_FORWARDED_HOST: &str = "x-forwarded-host";
const X_FORWARDED_PROTO: &str = "x-forwarded-proto";
#[test]
fn info_default() {
let req = TestRequest::default().to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "http");
assert_eq!(info.host(), "localhost:8080");
}
#[test]
fn host_header() {
let req = TestRequest::default()
.insert_header((header::HOST, "rust-lang.org"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "http");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.realip_remote_addr(), None);
}
#[test]
fn x_forwarded_for_header() {
let req = TestRequest::default()
.insert_header((X_FORWARDED_FOR, "192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
}
#[test]
fn x_forwarded_host_header() {
let req = TestRequest::default()
.insert_header((X_FORWARDED_HOST, "192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.host(), "192.0.2.60");
assert_eq!(info.realip_remote_addr(), None);
}
#[test]
fn x_forwarded_proto_header() {
let req = TestRequest::default()
.insert_header((X_FORWARDED_PROTO, "https"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "https");
}
#[test]
fn forwarded_header() {
let req = TestRequest::default()
.insert_header((
header::FORWARDED,
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "https");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
let req = TestRequest::default()
.insert_header((
header::FORWARDED,
"for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org",
))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "https");
assert_eq!(info.host(), "rust-lang.org");
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
}
#[test]
fn forwarded_case_sensitivity() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, "For=192.0.2.60"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
}
#[test]
fn forwarded_weird_whitespace() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https"))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("1.2.3.4"));
assert_eq!(info.scheme(), "https");
let req = TestRequest::default()
.insert_header((header::FORWARDED, " for = 1.2.3.4 "))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("1.2.3.4"));
}
#[test]
fn forwarded_for_quoted() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60:8080"));
}
#[test]
fn forwarded_for_ipv6() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#))
.to_http_request();
let info = req.connection_info();
assert_eq!(info.realip_remote_addr(), Some("[2001:db8:cafe::17]:4711"));
}
#[test]
fn forwarded_for_multiple() {
let req = TestRequest::default()
.insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17"))
.to_http_request();
let info = req.connection_info();
// takes the first value
assert_eq!(info.realip_remote_addr(), Some("192.0.2.60"));
}
#[test]
fn scheme_from_uri() {
let req = TestRequest::get()
.uri("https://actix.rs/test")
.to_http_request();
let info = req.connection_info();
assert_eq!(info.scheme(), "https");
}
#[test]
fn host_from_uri() {
let req = TestRequest::get()
.uri("https://actix.rs/test")
.to_http_request();
let info = req.connection_info();
assert_eq!(info.host(), "actix.rs");
}
#[test]
fn host_from_server_hostname() {
let mut req = TestRequest::get();
req.set_server_hostname("actix.rs");
let req = req.to_http_request();
let info = req.connection_info();
assert_eq!(info.host(), "actix.rs");
}
#[actix_rt::test]
async fn conn_info_extract() {
let req = TestRequest::default()
.uri("https://actix.rs/test")
.to_http_request();
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
assert_eq!(conn_info.scheme(), "https");
assert_eq!(conn_info.host(), "actix.rs");
}
#[actix_rt::test]
async fn peer_addr_extract() {
let req = TestRequest::default().to_http_request();
let res = PeerAddr::extract(&req).await;
assert!(res.is_err());
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default().peer_addr(addr).to_http_request();
let peer_addr = PeerAddr::extract(&req).await.unwrap();
assert_eq!(peer_addr, PeerAddr(addr));
}
#[actix_rt::test]
async fn remote_address() {
let req = TestRequest::default().to_http_request();
let res = ConnectionInfo::extract(&req).await.unwrap();
assert!(res.peer_addr().is_none());
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default().peer_addr(addr).to_http_request();
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1");
}
#[actix_rt::test]
async fn real_ip_from_socket_addr() {
let req = TestRequest::default().to_http_request();
let res = ConnectionInfo::extract(&req).await.unwrap();
assert!(res.realip_remote_addr().is_none());
let addr = "127.0.0.1:8080".parse().unwrap();
let req = TestRequest::default().peer_addr(addr).to_http_request();
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1");
}
}

117
actix-web/src/lib.rs Normal file
View File

@ -0,0 +1,117 @@
//! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust.
//!
//! # Examples
//! ```no_run
//! use actix_web::{get, web, App, HttpServer, Responder};
//!
//! #[get("/{id}/{name}/index.html")]
//! async fn index(path: web::Path<(u32, String)>) -> impl Responder {
//! let (id, name) = path.into_inner();
//! format!("Hello {}! id:{}", name, id)
//! }
//!
//! #[actix_web::main]
//! async fn main() -> std::io::Result<()> {
//! HttpServer::new(|| App::new().service(index))
//! .bind("127.0.0.1:8080")?
//! .run()
//! .await
//! }
//! ```
//!
//! # Documentation & Community Resources
//! In addition to this API documentation, several other resources are available:
//!
//! * [Website & User Guide](https://actix.rs/)
//! * [Examples Repository](https://github.com/actix/examples)
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
//!
//! To get started navigating the API docs, you may consider looking at the following pages first:
//!
//! * [`App`]: This struct represents an Actix Web application and is used to
//! configure routes and other common application settings.
//!
//! * [`HttpServer`]: This struct represents an HTTP server instance and is
//! used to instantiate and configure servers.
//!
//! * [`web`]: This module provides essential types for route registration as well as
//! common utilities for request handlers.
//!
//! * [`HttpRequest`] and [`HttpResponse`]: These
//! structs represent HTTP requests and responses and expose methods for creating, inspecting,
//! and otherwise utilizing them.
//!
//! # Features
//! * Supports *HTTP/1.x* and *HTTP/2*
//! * Streaming and pipelining
//! * Keep-alive and slow requests handling
//! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support
//! * Transparent content compression/decompression (br, gzip, deflate, zstd)
//! * Powerful [request routing](https://actix.rs/docs/url-dispatch/)
//! * Multipart streams
//! * Static assets
//! * SSL support using OpenSSL or Rustls
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
//! * Includes an async [HTTP client](https://docs.rs/awc/)
//! * Runs on stable Rust 1.54+
//!
//! # Crate Features
//! * `cookies` - cookies support (enabled by default)
//! * `compress-brotli` - brotli content encoding compression support (enabled by default)
//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default)
//! * `compress-zstd` - zstd content encoding compression support (enabled by default)
//! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2`
//! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2`
//! * `secure-cookies` - secure cookies support
#![deny(rust_2018_idioms, nonstandard_style)]
#![warn(future_incompatible)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
mod app;
mod app_service;
mod config;
mod data;
pub mod dev;
pub mod error;
mod extract;
pub mod guard;
mod handler;
mod helpers;
pub mod http;
mod info;
pub mod middleware;
mod request;
mod request_data;
mod resource;
mod response;
mod rmap;
mod route;
mod scope;
mod server;
mod service;
pub mod test;
pub(crate) mod types;
pub mod web;
pub use actix_http::{body, HttpMessage};
#[doc(inline)]
pub use actix_rt as rt;
pub use actix_web_codegen::*;
#[cfg(feature = "cookies")]
pub use cookie;
pub use crate::app::App;
pub use crate::error::{Error, ResponseError, Result};
pub use crate::extract::FromRequest;
pub use crate::handler::Handler;
pub use crate::request::HttpRequest;
pub use crate::resource::Resource;
pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder};
pub use crate::route::Route;
pub use crate::scope::Scope;
pub use crate::server::HttpServer;
pub use crate::types::Either;
pub(crate) type BoxError = Box<dyn std::error::Error>;

View File

@ -0,0 +1,237 @@
//! For middleware documentation, see [`Compat`].
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project;
use crate::{
body::{BoxBody, MessageBody},
dev::{Service, Transform},
error::Error,
service::ServiceResponse,
};
/// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap),
/// and [`Condition`](super::Condition).
///
/// # Examples
/// ```
/// use actix_web::middleware::{Logger, Compat};
/// use actix_web::{App, web};
///
/// let logger = Logger::default();
///
/// // this would not compile because of incompatible body types
/// // let app = App::new()
/// // .service(web::scope("scoped").wrap(logger));
///
/// // by using this middleware we can use the logger on a scope
/// let app = App::new()
/// .service(web::scope("scoped").wrap(Compat::new(logger)));
/// ```
pub struct Compat<T> {
transform: T,
}
#[cfg(test)]
impl Compat<super::Noop> {
pub(crate) fn noop() -> Self {
Self {
transform: super::Noop,
}
}
}
impl<T> Compat<T> {
/// Wrap a middleware to give it broader compatibility.
pub fn new(middleware: T) -> Self {
Self {
transform: middleware,
}
}
}
impl<S, T, Req> Transform<S, Req> for Compat<T>
where
S: Service<Req>,
T: Transform<S, Req>,
T::Future: 'static,
T::Response: MapServiceResponseBody,
T::Error: Into<Error>,
{
type Response = ServiceResponse<BoxBody>;
type Error = Error;
type Transform = CompatMiddleware<T::Transform>;
type InitError = T::InitError;
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let fut = self.transform.new_transform(service);
Box::pin(async move {
let service = fut.await?;
Ok(CompatMiddleware { service })
})
}
}
pub struct CompatMiddleware<S> {
service: S,
}
impl<S, Req> Service<Req> for CompatMiddleware<S>
where
S: Service<Req>,
S::Response: MapServiceResponseBody,
S::Error: Into<Error>,
{
type Response = ServiceResponse<BoxBody>;
type Error = Error;
type Future = CompatMiddlewareFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Req) -> Self::Future {
let fut = self.service.call(req);
CompatMiddlewareFuture { fut }
}
}
pin_project! {
pub struct CompatMiddlewareFuture<Fut> {
#[pin]
fut: Fut,
}
}
impl<Fut, T, E> Future for CompatMiddlewareFuture<Fut>
where
Fut: Future<Output = Result<T, E>>,
T: MapServiceResponseBody,
E: Into<Error>,
{
type Output = Result<ServiceResponse<BoxBody>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = match ready!(self.project().fut.poll(cx)) {
Ok(res) => res,
Err(err) => return Poll::Ready(Err(err.into())),
};
Poll::Ready(Ok(res.map_body()))
}
}
/// Convert `ServiceResponse`'s `ResponseBody<B>` generic type to `ResponseBody<Body>`.
pub trait MapServiceResponseBody {
fn map_body(self) -> ServiceResponse<BoxBody>;
}
impl<B> MapServiceResponseBody for ServiceResponse<B>
where
B: MessageBody + 'static,
{
#[inline]
fn map_body(self) -> ServiceResponse<BoxBody> {
self.map_into_boxed_body()
}
}
#[cfg(test)]
mod tests {
// easier to code when cookies feature is disabled
#![allow(unused_imports)]
use super::*;
use actix_service::IntoService;
use crate::{
dev::ServiceRequest,
http::StatusCode,
middleware::{self, Condition, Logger},
test::{self, call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test]
#[cfg(all(feature = "cookies", feature = "__compress"))]
async fn test_scope_middleware() {
use crate::middleware::Compress;
let logger = Logger::default();
let compress = Compress::default();
let srv = init_service(
App::new().service(
web::scope("app")
.wrap(logger)
.wrap(Compat::new(compress))
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok))),
),
)
.await;
let req = TestRequest::with_uri("/app/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
#[cfg(all(feature = "cookies", feature = "__compress"))]
async fn test_resource_scope_middleware() {
use crate::middleware::Compress;
let logger = Logger::default();
let compress = Compress::default();
let srv = init_service(
App::new().service(
web::resource("app/test")
.wrap(Compat::new(logger))
.wrap(Compat::new(compress))
.route(web::get().to(HttpResponse::Ok)),
),
)
.await;
let req = TestRequest::with_uri("/app/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_condition_scope_middleware() {
let srv = |req: ServiceRequest| {
Box::pin(async move {
Ok(req.into_response(HttpResponse::InternalServerError().finish()))
})
};
let logger = Logger::default();
let mw = Condition::new(true, Compat::new(logger))
.new_transform(srv.into_service())
.await
.unwrap();
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[actix_rt::test]
async fn compat_noop_is_noop() {
let srv = test::ok_service();
let mw = Compat::noop()
.new_transform(srv.into_service())
.await
.unwrap();
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -0,0 +1,308 @@
//! For middleware documentation, see [`Compress`].
use std::{
future::Future,
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use actix_http::encoding::Encoder;
use actix_service::{Service, Transform};
use actix_utils::future::{ok, Either, Ready};
use futures_core::ready;
use once_cell::sync::Lazy;
use pin_project_lite::pin_project;
use crate::{
body::{EitherBody, MessageBody},
http::{
header::{self, AcceptEncoding, Encoding, HeaderValue},
StatusCode,
},
service::{ServiceRequest, ServiceResponse},
Error, HttpMessage, HttpResponse,
};
/// Middleware for compressing response payloads.
///
/// # Encoding Negotiation
/// `Compress` will read the `Accept-Encoding` header to negotiate which compression codec to use.
/// Payloads are not compressed if the header is not sent. The `compress-*` [feature flags] are also
/// considered in this selection process.
///
/// # Pre-compressed Payload
/// If you are serving some data is already using a compressed representation (e.g., a gzip
/// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate
/// `Content-Encoding` header. In addition to preventing double compressing the payload, this header
/// is required by the spec when using compressed representations and will inform the client that
/// the content should be uncompressed.
///
/// However, it is not advised to unconditionally serve encoded representations of content because
/// the client may not support it. The [`AcceptEncoding`] typed header has some utilities to help
/// perform manual encoding negotiation, if required. When negotiating content encoding, it is also
/// required by the spec to send a `Vary: Accept-Encoding` header.
///
/// A (naïve) example serving an pre-compressed Gzip file is included below.
///
/// # Examples
/// To enable automatic payload compression just include `Compress` as a top-level middleware:
/// ```
/// use actix_web::{middleware, web, App, HttpResponse};
///
/// let app = App::new()
/// .wrap(middleware::Compress::default())
/// .default_service(web::to(|| async { HttpResponse::Ok().body("hello world") }));
/// ```
///
/// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
/// ```no_run
/// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder};
///
/// async fn index_handler() -> actix_web::Result<impl Responder> {
/// Ok(actix_files::NamedFile::open_async("./assets/index.html.gz").await?
/// .customize()
/// .insert_header(header::ContentEncoding::Gzip))
/// }
///
/// let app = App::new()
/// .wrap(middleware::Compress::default())
/// .default_service(web::to(index_handler));
/// ```
///
/// [feature flags]: ../index.html#crate-features
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct Compress;
impl<S, B> Transform<S, ServiceRequest> for Compress
where
B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Response = ServiceResponse<EitherBody<Encoder<B>>>;
type Error = Error;
type Transform = CompressMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(CompressMiddleware { service })
}
}
pub struct CompressMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
type Response = ServiceResponse<EitherBody<Encoder<B>>>;
type Error = Error;
#[allow(clippy::type_complexity)]
type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
actix_service::forward_ready!(service);
#[allow(clippy::borrow_interior_mutable_const)]
fn call(&self, req: ServiceRequest) -> Self::Future {
// negotiate content-encoding
let accept_encoding = req.get_header::<AcceptEncoding>();
let accept_encoding = match accept_encoding {
// missing header; fallback to identity
None => {
return Either::left(CompressResponse {
encoding: Encoding::identity(),
fut: self.service.call(req),
_phantom: PhantomData,
})
}
// valid accept-encoding header
Some(accept_encoding) => accept_encoding,
};
match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) {
None => {
let mut res = HttpResponse::with_body(
StatusCode::NOT_ACCEPTABLE,
SUPPORTED_ENCODINGS_STRING.as_str(),
);
res.headers_mut()
.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
Either::right(ok(req
.into_response(res)
.map_into_boxed_body()
.map_into_right_body()))
}
Some(encoding) => Either::left(CompressResponse {
fut: self.service.call(req),
encoding,
_phantom: PhantomData,
}),
}
}
}
pin_project! {
pub struct CompressResponse<S, B>
where
S: Service<ServiceRequest>,
{
#[pin]
fut: S::Future,
encoding: Encoding,
_phantom: PhantomData<B>,
}
}
impl<S, B> Future for CompressResponse<S, B>
where
B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Output = Result<ServiceResponse<EitherBody<Encoder<B>>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match ready!(this.fut.poll(cx)) {
Ok(resp) => {
let enc = match this.encoding {
Encoding::Known(enc) => *enc,
Encoding::Unknown(enc) => {
unimplemented!("encoding {} should not be here", enc);
}
};
Poll::Ready(Ok(resp.map_body(move |head, body| {
EitherBody::left(Encoder::response(enc, head, body))
})))
}
Err(err) => Poll::Ready(Err(err)),
}
}
}
static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
#[allow(unused_mut)] // only unused when no compress features enabled
let mut encoding: Vec<&str> = vec![];
#[cfg(feature = "compress-brotli")]
{
encoding.push("br");
}
#[cfg(feature = "compress-gzip")]
{
encoding.push("gzip");
encoding.push("deflate");
}
#[cfg(feature = "compress-zstd")]
{
encoding.push("zstd");
}
assert!(
!encoding.is_empty(),
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
);
encoding.join(", ")
});
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
let mut encodings = vec![Encoding::identity()];
#[cfg(feature = "compress-brotli")]
{
encodings.push(Encoding::brotli());
}
#[cfg(feature = "compress-gzip")]
{
encodings.push(Encoding::gzip());
encodings.push(Encoding::deflate());
}
#[cfg(feature = "compress-zstd")]
{
encodings.push(Encoding::zstd());
}
assert!(
!encodings.is_empty(),
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
);
encodings
});
// move cfg(feature) to prevents_double_compressing if more tests are added
#[cfg(feature = "compress-gzip")]
#[cfg(test)]
mod tests {
use super::*;
use crate::{middleware::DefaultHeaders, test, web, App};
pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
use std::io::Read as _;
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).unwrap();
buf
}
#[actix_rt::test]
async fn prevents_double_compressing() {
const D: &str = "hello world ";
const DATA: &str = const_str::repeat!(D, 100);
let app = test::init_service({
App::new()
.wrap(Compress::default())
.route(
"/single",
web::get().to(move || HttpResponse::Ok().body(DATA)),
)
.service(
web::resource("/double")
.wrap(Compress::default())
.wrap(DefaultHeaders::new().add(("x-double", "true")))
.route(web::get().to(move || HttpResponse::Ok().body(DATA))),
)
})
.await;
let req = test::TestRequest::default()
.uri("/single")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get("x-double"), None);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
let req = test::TestRequest::default()
.uri("/double")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.to_request();
let res = test::call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get("x-double").unwrap(), "true");
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = test::read_body(res).await;
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
}
}

View File

@ -0,0 +1,159 @@
//! For middleware documentation, see [`Condition`].
use std::task::{Context, Poll};
use actix_service::{Service, Transform};
use actix_utils::future::Either;
use futures_core::future::LocalBoxFuture;
use futures_util::future::FutureExt as _;
/// Middleware for conditionally enabling other middleware.
///
/// The controlled middleware must not change the `Service` interfaces. This means you cannot
/// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat)
/// middleware for a workaround.
///
/// # Examples
/// ```
/// use actix_web::middleware::{Condition, NormalizePath};
/// use actix_web::App;
///
/// let enable_normalize = std::env::var("NORMALIZE_PATH").is_ok();
/// let app = App::new()
/// .wrap(Condition::new(enable_normalize, NormalizePath::default()));
/// ```
pub struct Condition<T> {
transformer: T,
enable: bool,
}
impl<T> Condition<T> {
pub fn new(enable: bool, transformer: T) -> Self {
Self {
transformer,
enable,
}
}
}
impl<S, T, Req> Transform<S, Req> for Condition<T>
where
S: Service<Req> + 'static,
T: Transform<S, Req, Response = S::Response, Error = S::Error>,
T::Future: 'static,
T::InitError: 'static,
T::Transform: 'static,
{
type Response = S::Response;
type Error = S::Error;
type Transform = ConditionMiddleware<T::Transform, S>;
type InitError = T::InitError;
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
if self.enable {
let fut = self.transformer.new_transform(service);
async move {
let wrapped_svc = fut.await?;
Ok(ConditionMiddleware::Enable(wrapped_svc))
}
.boxed_local()
} else {
async move { Ok(ConditionMiddleware::Disable(service)) }.boxed_local()
}
}
}
pub enum ConditionMiddleware<E, D> {
Enable(E),
Disable(D),
}
impl<E, D, Req> Service<Req> for ConditionMiddleware<E, D>
where
E: Service<Req>,
D: Service<Req, Response = E::Response, Error = E::Error>,
{
type Response = E::Response;
type Error = E::Error;
type Future = Either<E::Future, D::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
match self {
ConditionMiddleware::Enable(service) => service.poll_ready(cx),
ConditionMiddleware::Disable(service) => service.poll_ready(cx),
}
}
fn call(&self, req: Req) -> Self::Future {
match self {
ConditionMiddleware::Enable(service) => Either::left(service.call(req)),
ConditionMiddleware::Disable(service) => Either::right(service.call(req)),
}
}
}
#[cfg(test)]
mod tests {
use actix_service::IntoService;
use actix_utils::future::ok;
use super::*;
use crate::{
dev::{ServiceRequest, ServiceResponse},
error::Result,
http::{
header::{HeaderValue, CONTENT_TYPE},
StatusCode,
},
middleware::{err_handlers::*, Compat},
test::{self, TestRequest},
HttpResponse,
};
#[allow(clippy::unnecessary_wraps)]
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}
#[actix_rt::test]
async fn test_handler_enabled() {
let srv = |req: ServiceRequest| {
ok(req.into_response(HttpResponse::InternalServerError().finish()))
};
let mw = Compat::new(
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(true, mw)
.new_transform(srv.into_service())
.await
.unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
}
#[actix_rt::test]
async fn test_handler_disabled() {
let srv = |req: ServiceRequest| {
ok(req.into_response(HttpResponse::InternalServerError().finish()))
};
let mw = Compat::new(
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(false, mw)
.new_transform(srv.into_service())
.await
.unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE), None);
}
}

View File

@ -0,0 +1,261 @@
//! For middleware documentation, see [`DefaultHeaders`].
use std::{
convert::TryFrom,
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_http::error::HttpError;
use actix_utils::future::{ready, Ready};
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::{
dev::{Service, Transform},
http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE},
service::{ServiceRequest, ServiceResponse},
Error,
};
/// Middleware for setting default response headers.
///
/// Headers with the same key that are already set in a response will *not* be overwritten.
///
/// # Examples
/// ```
/// use actix_web::{web, http, middleware, App, HttpResponse};
///
/// let app = App::new()
/// .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
/// .service(
/// web::resource("/test")
/// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed()))
/// );
/// ```
#[derive(Debug, Clone, Default)]
pub struct DefaultHeaders {
inner: Rc<Inner>,
}
#[derive(Debug, Default)]
struct Inner {
headers: HeaderMap,
}
impl DefaultHeaders {
/// Constructs an empty `DefaultHeaders` middleware.
#[inline]
pub fn new() -> DefaultHeaders {
DefaultHeaders::default()
}
/// Adds a header to the default set.
///
/// # Panics
/// Panics when resolved header name or value is invalid.
#[allow(clippy::should_implement_trait)]
pub fn add(mut self, header: impl TryIntoHeaderPair) -> Self {
// standard header terminology `insert` or `append` for this method would make the behavior
// of this middleware less obvious since it only adds the headers if they are not present
match header.try_into_pair() {
Ok((key, value)) => Rc::get_mut(&mut self.inner)
.expect("All default headers must be added before cloning.")
.headers
.append(key, value),
Err(err) => panic!("Invalid header: {}", err.into()),
}
self
}
#[doc(hidden)]
#[deprecated(
since = "4.0.0",
note = "Prefer `.add((key, value))`. Will be removed in v5."
)]
pub fn header<K, V>(self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
self.add((
HeaderName::try_from(key)
.map_err(Into::into)
.expect("Invalid header name"),
HeaderValue::try_from(value)
.map_err(Into::into)
.expect("Invalid header value"),
))
}
/// Adds a default *Content-Type* header if response does not contain one.
///
/// Default is `application/octet-stream`.
pub fn add_content_type(self) -> Self {
#[allow(clippy::declare_interior_mutable_const)]
const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream");
self.add((CONTENT_TYPE, HV_MIME))
}
}
impl<S, B> Transform<S, ServiceRequest> for DefaultHeaders
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = DefaultHeadersMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(DefaultHeadersMiddleware {
service,
inner: Rc::clone(&self.inner),
}))
}
}
pub struct DefaultHeadersMiddleware<S> {
service: S,
inner: Rc<Inner>,
}
impl<S, B> Service<ServiceRequest> for DefaultHeadersMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = DefaultHeaderFuture<S, B>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let inner = self.inner.clone();
let fut = self.service.call(req);
DefaultHeaderFuture {
fut,
inner,
_body: PhantomData,
}
}
}
pin_project! {
pub struct DefaultHeaderFuture<S: Service<ServiceRequest>, B> {
#[pin]
fut: S::Future,
inner: Rc<Inner>,
_body: PhantomData<B>,
}
}
impl<S, B> Future for DefaultHeaderFuture<S, B>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Output = <S::Future as Future>::Output;
#[allow(clippy::borrow_interior_mutable_const)]
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let mut res = ready!(this.fut.poll(cx))?;
// set response headers
for (key, value) in this.inner.headers.iter() {
if !res.headers().contains_key(key) {
res.headers_mut().insert(key.clone(), value.clone());
}
}
Poll::Ready(Ok(res))
}
}
#[cfg(test)]
mod tests {
use actix_service::IntoService;
use actix_utils::future::ok;
use super::*;
use crate::{
dev::ServiceRequest,
http::header::CONTENT_TYPE,
test::{self, TestRequest},
HttpResponse,
};
#[actix_rt::test]
async fn adding_default_headers() {
let mw = DefaultHeaders::new()
.add(("X-TEST", "0001"))
.add(("X-TEST-TWO", HeaderValue::from_static("123")))
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default().to_srv_request();
let res = mw.call(req).await.unwrap();
assert_eq!(res.headers().get("x-test").unwrap(), "0001");
assert_eq!(res.headers().get("x-test-two").unwrap(), "123");
}
#[actix_rt::test]
async fn no_override_existing() {
let req = TestRequest::default().to_srv_request();
let srv = |req: ServiceRequest| {
ok(req.into_response(
HttpResponse::Ok()
.insert_header((CONTENT_TYPE, "0002"))
.finish(),
))
};
let mw = DefaultHeaders::new()
.add((CONTENT_TYPE, "0001"))
.new_transform(srv.into_service())
.await
.unwrap();
let resp = mw.call(req).await.unwrap();
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002");
}
#[actix_rt::test]
async fn adding_content_type() {
let mw = DefaultHeaders::new()
.add_content_type()
.new_transform(test::ok_service())
.await
.unwrap();
let req = TestRequest::default().to_srv_request();
let resp = mw.call(req).await.unwrap();
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
"application/octet-stream"
);
}
#[test]
#[should_panic]
fn invalid_header_name() {
DefaultHeaders::new().add((":", "hello"));
}
#[test]
#[should_panic]
fn invalid_header_value() {
DefaultHeaders::new().add(("x-test", "\n"));
}
}

View File

@ -0,0 +1,274 @@
//! For middleware documentation, see [`ErrorHandlers`].
use std::{
future::Future,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_service::{Service, Transform};
use ahash::AHashMap;
use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project;
use crate::{
body::EitherBody,
dev::{ServiceRequest, ServiceResponse},
http::StatusCode,
Error, Result,
};
/// Return type for [`ErrorHandlers`] custom handlers.
pub enum ErrorHandlerResponse<B> {
/// Immediate HTTP response.
Response(ServiceResponse<EitherBody<B>>),
/// A future that resolves to an HTTP response.
Future(LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>),
}
type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>;
/// Middleware for registering custom status code based error handlers.
///
/// Register handlers with the `ErrorHandlers::handler()` method to register a custom error handler
/// for a given status code. Handlers can modify existing responses or create completely new ones.
///
/// # Examples
/// ```
/// use actix_web::http::{header, StatusCode};
/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
/// use actix_web::{dev, web, App, HttpResponse, Result};
///
/// fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
/// res.response_mut().headers_mut().insert(
/// header::CONTENT_TYPE,
/// header::HeaderValue::from_static("Error"),
/// );
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
/// }
///
/// let app = App::new()
/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header))
/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)));
/// ```
pub struct ErrorHandlers<B> {
handlers: Handlers<B>,
}
type Handlers<B> = Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>;
impl<B> Default for ErrorHandlers<B> {
fn default() -> Self {
ErrorHandlers {
handlers: Default::default(),
}
}
}
impl<B> ErrorHandlers<B> {
/// Construct new `ErrorHandlers` instance.
pub fn new() -> Self {
ErrorHandlers::default()
}
/// Register error handler for specified status code.
pub fn handler<F>(mut self, status: StatusCode, handler: F) -> Self
where
F: Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> + 'static,
{
Rc::get_mut(&mut self.handlers)
.unwrap()
.insert(status, Box::new(handler));
self
}
}
impl<S, B> Transform<S, ServiceRequest> for ErrorHandlers<B>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Transform = ErrorHandlersMiddleware<S, B>;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
let handlers = self.handlers.clone();
Box::pin(async move { Ok(ErrorHandlersMiddleware { service, handlers }) })
}
}
#[doc(hidden)]
pub struct ErrorHandlersMiddleware<S, B> {
service: S,
handlers: Handlers<B>,
}
impl<S, B> Service<ServiceRequest> for ErrorHandlersMiddleware<S, B>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<EitherBody<B>>;
type Error = Error;
type Future = ErrorHandlersFuture<S::Future, B>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let handlers = self.handlers.clone();
let fut = self.service.call(req);
ErrorHandlersFuture::ServiceFuture { fut, handlers }
}
}
pin_project! {
#[project = ErrorHandlersProj]
pub enum ErrorHandlersFuture<Fut, B>
where
Fut: Future,
{
ServiceFuture {
#[pin]
fut: Fut,
handlers: Handlers<B>,
},
ErrorHandlerFuture {
fut: LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>,
},
}
}
impl<Fut, B> Future for ErrorHandlersFuture<Fut, B>
where
Fut: Future<Output = Result<ServiceResponse<B>, Error>>,
{
type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project() {
ErrorHandlersProj::ServiceFuture { fut, handlers } => {
let res = ready!(fut.poll(cx))?;
match handlers.get(&res.status()) {
Some(handler) => match handler(res)? {
ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)),
ErrorHandlerResponse::Future(fut) => {
self.as_mut()
.set(ErrorHandlersFuture::ErrorHandlerFuture { fut });
self.poll(cx)
}
},
None => Poll::Ready(Ok(res.map_into_left_body())),
}
}
ErrorHandlersProj::ErrorHandlerFuture { fut } => fut.as_mut().poll(cx),
}
}
}
#[cfg(test)]
mod tests {
use actix_service::IntoService;
use actix_utils::future::ok;
use bytes::Bytes;
use futures_util::future::FutureExt as _;
use super::*;
use crate::{
http::{
header::{HeaderValue, CONTENT_TYPE},
StatusCode,
},
test::{self, TestRequest},
};
#[actix_rt::test]
async fn add_header_error_handler() {
#[allow(clippy::unnecessary_wraps)]
fn error_handler<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
let mw = ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
.new_transform(srv.into_service())
.await
.unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
}
#[actix_rt::test]
async fn add_header_error_handler_async() {
#[allow(clippy::unnecessary_wraps)]
fn error_handler<B: 'static>(
mut res: ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Future(
ok(res.map_into_left_body()).boxed_local(),
))
}
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
let mw = ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
.new_transform(srv.into_service())
.await
.unwrap();
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
}
#[actix_rt::test]
async fn changes_body_type() {
#[allow(clippy::unnecessary_wraps)]
fn error_handler<B: 'static>(
res: ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> {
let (req, res) = res.into_parts();
let res = res.set_body(Bytes::from("sorry, that's no bueno"));
let res = ServiceResponse::new(req, res)
.map_into_boxed_body()
.map_into_right_body();
Ok(ErrorHandlerResponse::Response(res))
}
let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
let mw = ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
.new_transform(srv.into_service())
.await
.unwrap();
let res = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(test::read_body(res).await, "sorry, that's no bueno");
}
// TODO: test where error is thrown
}

View File

@ -0,0 +1,874 @@
//! For middleware documentation, see [`Logger`].
use std::{
borrow::Cow,
collections::HashSet,
convert::TryFrom,
env,
fmt::{self, Display as _},
future::Future,
marker::PhantomData,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_service::{Service, Transform};
use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use futures_core::ready;
use log::{debug, warn};
use pin_project_lite::pin_project;
use regex::{Regex, RegexSet};
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use crate::{
body::{BodySize, MessageBody},
http::header::HeaderName,
service::{ServiceRequest, ServiceResponse},
Error, HttpResponse, Result,
};
/// Middleware for logging request and response summaries to the terminal.
///
/// This middleware uses the `log` crate to output information. Enable `log`'s output for the
/// "actix_web" scope using [`env_logger`](https://docs.rs/env_logger) or similar crate.
///
/// # Default Format
/// The [`default`](Logger::default) Logger uses the following format:
///
/// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
///
/// Example Output:
/// 127.0.0.1:54278 "GET /test HTTP/1.1" 404 20 "-" "HTTPie/2.2.0" 0.001074
/// ```
///
/// # Examples
/// ```
/// use actix_web::{middleware::Logger, App};
///
/// // access logs are printed with the INFO level so ensure it is enabled by default
/// env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
///
/// let app = App::new()
/// // .wrap(Logger::default())
/// .wrap(Logger::new("%a %{User-Agent}i"));
/// ```
///
/// # Format
/// Variable | Description
/// -------- | -----------
/// `%%` | The percent sign
/// `%a` | Peer IP address (or IP address of reverse proxy if used)
/// `%t` | Time when the request started processing (in RFC 3339 format)
/// `%r` | First line of request (Example: `GET /test HTTP/1.1`)
/// `%s` | Response status code
/// `%b` | Size of response in bytes, including HTTP headers
/// `%T` | Time taken to serve the request, in seconds to 6 decimal places
/// `%D` | Time taken to serve the request, in milliseconds
/// `%U` | Request URL
/// `%{r}a` | "Real IP" remote address **\***
/// `%{FOO}i` | `request.headers["FOO"]`
/// `%{FOO}o` | `response.headers["FOO"]`
/// `%{FOO}e` | `env_var["FOO"]`
/// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO"
///
/// # Security
/// **\*** "Real IP" remote address is calculated using
/// [`ConnectionInfo::realip_remote_addr()`](crate::dev::ConnectionInfo::realip_remote_addr())
///
/// If you use this value, ensure that all requests come from trusted hosts. Otherwise, it is
/// trivial for the remote client to falsify their source IP address.
#[derive(Debug)]
pub struct Logger(Rc<Inner>);
#[derive(Debug, Clone)]
struct Inner {
format: Format,
exclude: HashSet<String>,
exclude_regex: RegexSet,
log_target: Cow<'static, str>,
}
impl Logger {
/// Create `Logger` middleware with the specified `format`.
pub fn new(format: &str) -> Logger {
Logger(Rc::new(Inner {
format: Format::new(format),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
log_target: Cow::Borrowed(module_path!()),
}))
}
/// Ignore and do not log access info for specified path.
pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
Rc::get_mut(&mut self.0)
.unwrap()
.exclude
.insert(path.into());
self
}
/// Ignore and do not log access info for paths that match regex.
pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let mut patterns = inner.exclude_regex.patterns().to_vec();
patterns.push(path.into());
let regex_set = RegexSet::new(patterns).unwrap();
inner.exclude_regex = regex_set;
self
}
/// Sets the logging target to `target`.
///
/// By default, the log target is `module_path!()` of the log call location. In our case, that
/// would be `actix_web::middleware::logger`.
///
/// # Examples
/// Using `.log_target("http_log")` would have this effect on request logs:
/// ```diff
/// - [2015-10-21T07:28:00Z INFO actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// + [2015-10-21T07:28:00Z INFO http_log] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
/// ^^^^^^^^
/// ```
pub fn log_target(mut self, target: impl Into<Cow<'static, str>>) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
inner.log_target = target.into();
self
}
/// Register a function that receives a ServiceRequest and returns a String for use in the
/// log line. The label passed as the first argument should match a replacement substring in
/// the logger format like `%{label}xi`.
///
/// It is convention to print "-" to indicate no output instead of an empty string.
///
/// # Examples
/// ```
/// # use actix_web::http::{header::HeaderValue};
/// # use actix_web::middleware::Logger;
/// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() }
/// Logger::new("example %{JWT_ID}xi")
/// .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization")));
/// ```
pub fn custom_request_replace(
mut self,
label: &str,
f: impl Fn(&ServiceRequest) -> String + 'static,
) -> Self {
let inner = Rc::get_mut(&mut self.0).unwrap();
let ft = inner.format.0.iter_mut().find(
|ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label),
);
if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
// replace into None or previously registered fn using same label
request_fn.replace(CustomRequestFn {
inner_fn: Rc::new(f),
});
} else {
// non-printed request replacement function diagnostic
debug!(
"Attempted to register custom request logging function for nonexistent label: {}",
label
);
}
self
}
}
impl Default for Logger {
/// Create `Logger` middleware with format:
///
/// ```plain
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
/// ```
fn default() -> Logger {
Logger(Rc::new(Inner {
format: Format::default(),
exclude: HashSet::new(),
exclude_regex: RegexSet::empty(),
log_target: Cow::Borrowed(module_path!()),
}))
}
}
impl<S, B> Transform<S, ServiceRequest> for Logger
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
type Response = ServiceResponse<StreamLog<B>>;
type Error = Error;
type Transform = LoggerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
for unit in &self.0.format.0 {
// missing request replacement function diagnostic
if let FormatText::CustomRequest(label, None) = unit {
warn!(
"No custom request replacement function was registered for label \"{}\".",
label
);
}
}
ready(Ok(LoggerMiddleware {
service,
inner: self.0.clone(),
}))
}
}
/// Logger middleware service.
pub struct LoggerMiddleware<S> {
inner: Rc<Inner>,
service: S,
}
impl<S, B> Service<ServiceRequest> for LoggerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
type Response = ServiceResponse<StreamLog<B>>;
type Error = Error;
type Future = LoggerResponse<S, B>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let excluded = self.inner.exclude.contains(req.path())
|| self.inner.exclude_regex.is_match(req.path());
if excluded {
LoggerResponse {
fut: self.service.call(req),
format: None,
time: OffsetDateTime::now_utc(),
log_target: Cow::Borrowed(""),
_phantom: PhantomData,
}
} else {
let now = OffsetDateTime::now_utc();
let mut format = self.inner.format.clone();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
LoggerResponse {
fut: self.service.call(req),
format: Some(format),
time: now,
log_target: self.inner.log_target.clone(),
_phantom: PhantomData,
}
}
}
}
pin_project! {
pub struct LoggerResponse<S, B>
where
B: MessageBody,
S: Service<ServiceRequest>,
{
#[pin]
fut: S::Future,
time: OffsetDateTime,
format: Option<Format>,
log_target: Cow<'static, str>,
_phantom: PhantomData<B>,
}
}
impl<S, B> Future for LoggerResponse<S, B>
where
B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
{
type Output = Result<ServiceResponse<StreamLog<B>>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let res = match ready!(this.fut.poll(cx)) {
Ok(res) => res,
Err(e) => return Poll::Ready(Err(e)),
};
if let Some(error) = res.response().error() {
debug!("Error in response: {:?}", error);
}
if let Some(ref mut format) = this.format {
for unit in &mut format.0 {
unit.render_response(res.response());
}
}
let time = *this.time;
let format = this.format.take();
let log_target = this.log_target.clone();
Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
body,
time,
format,
size: 0,
log_target,
})))
}
}
pin_project! {
pub struct StreamLog<B> {
#[pin]
body: B,
format: Option<Format>,
size: usize,
time: OffsetDateTime,
log_target: Cow<'static, str>,
}
impl<B> PinnedDrop for StreamLog<B> {
fn drop(this: Pin<&mut Self>) {
if let Some(ref format) = this.format {
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, this.size, this.time)?;
}
Ok(())
};
log::info!(
target: this.log_target.as_ref(),
"{}", FormatDisplay(&render)
);
}
}
}
}
impl<B: MessageBody> MessageBody for StreamLog<B> {
type Error = B::Error;
#[inline]
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.project();
match ready!(this.body.poll_next(cx)) {
Some(Ok(chunk)) => {
*this.size += chunk.len();
Poll::Ready(Some(Ok(chunk)))
}
Some(Err(err)) => Poll::Ready(Some(Err(err))),
None => Poll::Ready(None),
}
}
}
/// A formatting style for the `Logger` consisting of multiple concatenated `FormatText` items.
#[derive(Debug, Clone)]
struct Format(Vec<FormatText>);
impl Default for Format {
/// Return the default formatting style for the `Logger`:
fn default() -> Format {
Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
}
}
impl Format {
/// Create a `Format` from a format string.
///
/// Returns `None` if the format string syntax is incorrect.
pub fn new(s: &str) -> Format {
log::trace!("Access log format: {}", s);
let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[%atPrUsbTD]?)").unwrap();
let mut idx = 0;
let mut results = Vec::new();
for cap in fmt.captures_iter(s) {
let m = cap.get(0).unwrap();
let pos = m.start();
if idx != pos {
results.push(FormatText::Str(s[idx..pos].to_owned()));
}
idx = m.end();
if let Some(key) = cap.get(2) {
results.push(match cap.get(3).unwrap().as_str() {
"a" => {
if key.as_str() == "r" {
FormatText::RealIpRemoteAddr
} else {
unreachable!()
}
}
"i" => {
FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap())
}
"o" => {
FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap())
}
"e" => FormatText::EnvironHeader(key.as_str().to_owned()),
"xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
_ => unreachable!(),
})
} else {
let m = cap.get(1).unwrap();
results.push(match m.as_str() {
"%" => FormatText::Percent,
"a" => FormatText::RemoteAddr,
"t" => FormatText::RequestTime,
"r" => FormatText::RequestLine,
"s" => FormatText::ResponseStatus,
"b" => FormatText::ResponseSize,
"U" => FormatText::UrlPath,
"T" => FormatText::Time,
"D" => FormatText::TimeMillis,
_ => FormatText::Str(m.as_str().to_owned()),
});
}
}
if idx != s.len() {
results.push(FormatText::Str(s[idx..].to_owned()));
}
Format(results)
}
}
/// A string of text to be logged.
///
/// This is either one of the data fields supported by the `Logger`, or a custom `String`.
#[non_exhaustive]
#[derive(Debug, Clone)]
enum FormatText {
Str(String),
Percent,
RequestLine,
RequestTime,
ResponseStatus,
ResponseSize,
Time,
TimeMillis,
RemoteAddr,
RealIpRemoteAddr,
UrlPath,
RequestHeader(HeaderName),
ResponseHeader(HeaderName),
EnvironHeader(String),
CustomRequest(String, Option<CustomRequestFn>),
}
#[derive(Clone)]
struct CustomRequestFn {
inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
}
impl CustomRequestFn {
fn call(&self, req: &ServiceRequest) -> String {
(self.inner_fn)(req)
}
}
impl fmt::Debug for CustomRequestFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("custom_request_fn")
}
}
impl FormatText {
fn render(
&self,
fmt: &mut fmt::Formatter<'_>,
size: usize,
entry_time: OffsetDateTime,
) -> Result<(), fmt::Error> {
match self {
FormatText::Str(ref string) => fmt.write_str(string),
FormatText::Percent => "%".fmt(fmt),
FormatText::ResponseSize => size.fmt(fmt),
FormatText::Time => {
let rt = OffsetDateTime::now_utc() - entry_time;
let rt = rt.as_seconds_f64();
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::TimeMillis => {
let rt = OffsetDateTime::now_utc() - entry_time;
let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0;
fmt.write_fmt(format_args!("{:.6}", rt))
}
FormatText::EnvironHeader(ref name) => {
if let Ok(val) = env::var(name) {
fmt.write_fmt(format_args!("{}", val))
} else {
"-".fmt(fmt)
}
}
_ => Ok(()),
}
}
fn render_response<B>(&mut self, res: &HttpResponse<B>) {
match self {
FormatText::ResponseStatus => {
*self = FormatText::Str(format!("{}", res.status().as_u16()))
}
FormatText::ResponseHeader(ref name) => {
let s = if let Some(val) = res.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
*self = FormatText::Str(s.to_string())
}
_ => {}
}
}
fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
match self {
FormatText::RequestLine => {
*self = if req.query_string().is_empty() {
FormatText::Str(format!(
"{} {} {:?}",
req.method(),
req.path(),
req.version()
))
} else {
FormatText::Str(format!(
"{} {}?{} {:?}",
req.method(),
req.path(),
req.query_string(),
req.version()
))
};
}
FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()),
FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) {
if let Ok(s) = val.to_str() {
s
} else {
"-"
}
} else {
"-"
};
*self = FormatText::Str(s.to_string());
}
FormatText::RemoteAddr => {
let s = if let Some(peer) = req.connection_info().peer_addr() {
FormatText::Str((*peer).to_string())
} else {
FormatText::Str("-".to_string())
};
*self = s;
}
FormatText::RealIpRemoteAddr => {
let s = if let Some(remote) = req.connection_info().realip_remote_addr() {
FormatText::Str(remote.to_string())
} else {
FormatText::Str("-".to_string())
};
*self = s;
}
FormatText::CustomRequest(_, request_fn) => {
let s = match request_fn {
Some(f) => FormatText::Str(f.call(req)),
None => FormatText::Str("-".to_owned()),
};
*self = s;
}
_ => {}
}
}
}
/// Converter to get a String from something that writes to a Formatter.
pub(crate) struct FormatDisplay<'a>(
&'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>,
);
impl<'a> fmt::Display for FormatDisplay<'a> {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
(self.0)(fmt)
}
}
#[cfg(test)]
mod tests {
use actix_service::{IntoService, Service, Transform};
use actix_utils::future::ok;
use super::*;
use crate::http::{header, StatusCode};
use crate::test::{self, TestRequest};
#[actix_rt::test]
async fn test_logger() {
let srv = |req: ServiceRequest| {
ok(req.into_response(
HttpResponse::build(StatusCode::OK)
.insert_header(("X-Test", "ttt"))
.finish(),
))
};
let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
let srv = logger.new_transform(srv.into_service()).await.unwrap();
let req = TestRequest::default()
.insert_header((
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
))
.to_srv_request();
let _res = srv.call(req).await;
}
#[actix_rt::test]
async fn test_logger_exclude_regex() {
let srv = |req: ServiceRequest| {
ok(req.into_response(
HttpResponse::build(StatusCode::OK)
.insert_header(("X-Test", "ttt"))
.finish(),
))
};
let logger =
Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w");
let srv = logger.new_transform(srv.into_service()).await.unwrap();
let req = TestRequest::default()
.insert_header((
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
))
.to_srv_request();
let _res = srv.call(req).await.unwrap();
}
#[actix_rt::test]
async fn test_escape_percent() {
let mut format = Format::new("%%{r}a");
let req = TestRequest::default()
.insert_header((
header::FORWARDED,
header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
))
.to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let entry_time = OffsetDateTime::now_utc();
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert_eq!(s, "%{r}a");
}
#[actix_rt::test]
async fn test_url_path() {
let mut format = Format::new("%T %U");
let req = TestRequest::default()
.insert_header((
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
))
.uri("/test/route/yeah")
.to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, now)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("/test/route/yeah"));
}
#[actix_rt::test]
async fn test_default_format() {
let mut format = Format::default();
let req = TestRequest::default()
.insert_header((
header::USER_AGENT,
header::HeaderValue::from_static("ACTIX-WEB"),
))
.peer_addr("127.0.0.1:8081".parse().unwrap())
.to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let entry_time = OffsetDateTime::now_utc();
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("GET / HTTP/1.1"));
assert!(s.contains("127.0.0.1"));
assert!(s.contains("200 1024"));
assert!(s.contains("ACTIX-WEB"));
}
#[actix_rt::test]
async fn test_request_time_format() {
let mut format = Format::new("%t");
let req = TestRequest::default().to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, now)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains(&now.format(&Rfc3339).unwrap()));
}
#[actix_rt::test]
async fn test_remote_addr_format() {
let mut format = Format::new("%{r}a");
let req = TestRequest::default()
.insert_header((
header::FORWARDED,
header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
))
.to_srv_request();
let now = OffsetDateTime::now_utc();
for unit in &mut format.0 {
unit.render_request(now, &req);
}
let resp = HttpResponse::build(StatusCode::OK).force_close().finish();
for unit in &mut format.0 {
unit.render_response(&resp);
}
let entry_time = OffsetDateTime::now_utc();
let render = |fmt: &mut fmt::Formatter<'_>| {
for unit in &format.0 {
unit.render(fmt, 1024, entry_time)?;
}
Ok(())
};
let s = format!("{}", FormatDisplay(&render));
assert!(s.contains("192.0.2.60"));
}
#[actix_rt::test]
async fn test_custom_closure_log() {
let mut logger = Logger::new("test %{CUSTOM}xi")
.custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
String::from("custom_log")
});
let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
let label = match &unit {
FormatText::CustomRequest(label, _) => label,
ft => panic!("expected CustomRequest, found {:?}", ft),
};
assert_eq!(label, "CUSTOM");
let req = TestRequest::default().to_srv_request();
let now = OffsetDateTime::now_utc();
unit.render_request(now, &req);
let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
let log_output = FormatDisplay(&render).to_string();
assert_eq!(log_output, "custom_log");
}
#[actix_rt::test]
async fn test_closure_logger_in_middleware() {
let captured = "custom log replacement";
let logger = Logger::new("%{CUSTOM}xi")
.custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
captured.to_owned()
});
let srv = logger.new_transform(test::ok_service()).await.unwrap();
let req = TestRequest::default().to_srv_request();
srv.call(req).await.unwrap();
}
}

View File

@ -0,0 +1,65 @@
//! A collection of common middleware.
mod compat;
mod condition;
mod default_headers;
mod err_handlers;
mod logger;
#[cfg(test)]
mod noop;
mod normalize;
pub use self::compat::Compat;
pub use self::condition::Condition;
pub use self::default_headers::DefaultHeaders;
pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers};
pub use self::logger::Logger;
#[cfg(test)]
pub(crate) use self::noop::Noop;
pub use self::normalize::{NormalizePath, TrailingSlash};
#[cfg(feature = "__compress")]
mod compress;
#[cfg(feature = "__compress")]
pub use self::compress::Compress;
#[cfg(test)]
mod tests {
use crate::{http::StatusCode, App};
use super::*;
#[test]
fn common_combinations() {
// ensure there's no reason that the built-in middleware cannot compose
let _ = App::new()
.wrap(Compat::new(Logger::default()))
.wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}))
.wrap(Logger::default())
.wrap(NormalizePath::new(TrailingSlash::Trim));
let _ = App::new()
.wrap(NormalizePath::new(TrailingSlash::Trim))
.wrap(Logger::default())
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}))
.wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
.wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(Compat::new(Logger::default()));
#[cfg(feature = "__compress")]
{
let _ = App::new().wrap(Compress::default()).wrap(Logger::default());
let _ = App::new().wrap(Logger::default()).wrap(Compress::default());
let _ = App::new().wrap(Compat::new(Compress::default()));
let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default())));
}
}
}

View File

@ -0,0 +1,37 @@
//! A no-op middleware. See [Noop] for docs.
use actix_utils::future::{ready, Ready};
use crate::dev::{Service, Transform};
/// A no-op middleware that passes through request and response untouched.
pub(crate) struct Noop;
impl<S: Service<Req>, Req> Transform<S, Req> for Noop {
type Response = S::Response;
type Error = S::Error;
type Transform = NoopService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(NoopService { service }))
}
}
#[doc(hidden)]
pub(crate) struct NoopService<S> {
service: S,
}
impl<S: Service<Req>, Req> Service<Req> for NoopService<S> {
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;
crate::dev::forward_ready!(service);
fn call(&self, req: Req) -> Self::Future {
self.service.call(req)
}
}

View File

@ -0,0 +1,489 @@
//! For middleware documentation, see [`NormalizePath`].
use actix_http::uri::{PathAndQuery, Uri};
use actix_service::{Service, Transform};
use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use regex::Regex;
use crate::{
service::{ServiceRequest, ServiceResponse},
Error,
};
/// Determines the behavior of the [`NormalizePath`] middleware.
///
/// The default is `TrailingSlash::Trim`.
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum TrailingSlash {
/// Trim trailing slashes from the end of the path.
///
/// Using this will require all routes to omit trailing slashes for them to be accessible.
Trim,
/// Only merge any present multiple trailing slashes.
///
/// This option provides the best compatibility with behavior in actix-web v2.0.
MergeOnly,
/// Always add a trailing slash to the end of the path.
///
/// Using this will require all routes have a trailing slash for them to be accessible.
Always,
}
impl Default for TrailingSlash {
fn default() -> Self {
TrailingSlash::Trim
}
}
/// Middleware for normalizing a request's path so that routes can be matched more flexibly.
///
/// # Normalization Steps
/// - Merges consecutive slashes into one. (For example, `/path//one` always becomes `/path/one`.)
/// - Appends a trailing slash if one is not present, removes one if present, or keeps trailing
/// slashes as-is, depending on which [`TrailingSlash`] variant is supplied
/// to [`new`](NormalizePath::new()).
///
/// # Default Behavior
/// The default constructor chooses to strip trailing slashes from the end of paths with them
/// ([`TrailingSlash::Trim`]). The implication is that route definitions should be defined without
/// trailing slashes or else they will be inaccessible (or vice versa when using the
/// `TrailingSlash::Always` behavior), as shown in the example tests below.
///
/// # Examples
/// ```
/// use actix_web::{web, middleware, App};
///
/// # actix_web::rt::System::new().block_on(async {
/// let app = App::new()
/// .wrap(middleware::NormalizePath::trim())
/// .route("/test", web::get().to(|| async { "test" }))
/// .route("/unmatchable/", web::get().to(|| async { "unmatchable" }));
///
/// use actix_web::http::StatusCode;
/// use actix_web::test::{call_service, init_service, TestRequest};
///
/// let app = init_service(app).await;
///
/// let req = TestRequest::with_uri("/test").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/test/").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
///
/// let req = TestRequest::with_uri("/unmatchable").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
///
/// let req = TestRequest::with_uri("/unmatchable/").to_request();
/// let res = call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// # })
/// ```
#[derive(Debug, Clone, Copy)]
pub struct NormalizePath(TrailingSlash);
impl Default for NormalizePath {
fn default() -> Self {
log::warn!(
"`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \
in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
);
Self(TrailingSlash::Trim)
}
}
impl NormalizePath {
/// Create new `NormalizePath` middleware with the specified trailing slash style.
pub fn new(trailing_slash_style: TrailingSlash) -> Self {
Self(trailing_slash_style)
}
/// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics.
///
/// Use this instead of `NormalizePath::default()` to avoid deprecation warning.
pub fn trim() -> Self {
Self::new(TrailingSlash::Trim)
}
}
impl<S, B> Transform<S, ServiceRequest> for NormalizePath
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = NormalizePathNormalization<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(NormalizePathNormalization {
service,
merge_slash: Regex::new("//+").unwrap(),
trailing_slash_behavior: self.0,
}))
}
}
pub struct NormalizePathNormalization<S> {
service: S,
merge_slash: Regex,
trailing_slash_behavior: TrailingSlash,
}
impl<S, B> Service<ServiceRequest> for NormalizePathNormalization<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = S::Future;
actix_service::forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let head = req.head_mut();
let original_path = head.uri.path();
// An empty path here means that the URI has no valid path. We skip normalization in this
// case, because adding a path can make the URI invalid
if !original_path.is_empty() {
// Either adds a string to the end (duplicates will be removed anyways) or trims all
// slashes from the end
let path = match self.trailing_slash_behavior {
TrailingSlash::Always => format!("{}/", original_path),
TrailingSlash::MergeOnly => original_path.to_string(),
TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
};
// normalize multiple /'s to one /
let path = self.merge_slash.replace_all(&path, "/");
// Ensure root paths are still resolvable. If resulting path is blank after previous
// step it means the path was one or more slashes. Reduce to single slash.
let path = if path.is_empty() { "/" } else { path.as_ref() };
// Check whether the path has been changed
//
// This check was previously implemented as string length comparison
//
// That approach fails when a trailing slash is added,
// and a duplicate slash is removed,
// since the length of the strings remains the same
//
// For example, the path "/v1//s" will be normalized to "/v1/s/"
// Both of the paths have the same length,
// so the change can not be deduced from the length comparison
if path != original_path {
let mut parts = head.uri.clone().into_parts();
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
let path = match query {
Some(q) => Bytes::from(format!("{}?{}", path, q)),
None => Bytes::copy_from_slice(path.as_bytes()),
};
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
let uri = Uri::from_parts(parts).unwrap();
req.match_info_mut().get_mut().update(&uri);
req.head_mut().uri = uri;
}
}
self.service.call(req)
}
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use actix_service::IntoService;
use super::*;
use crate::{
dev::ServiceRequest,
guard::fn_guard,
test::{call_service, init_service, TestRequest},
web, App, HttpResponse,
};
#[actix_rt::test]
async fn test_wrap() {
let app = init_service(
App::new()
.wrap(NormalizePath::default())
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(
web::resource("/v2/something")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let test_uris = vec![
"/",
"/?query=test",
"///",
"/v1//something",
"/v1//something////",
"//v1/something",
"//v1//////something",
"/v2//something?query=test",
"/v2//something////?query=test",
"//v2/something?query=test",
"//v2//////something?query=test",
];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn trim_trailing_slashes() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Trim))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(
web::resource("/v2/something")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let test_uris = vec![
"/",
"///",
"/v1/something",
"/v1/something/",
"/v1/something////",
"//v1//something",
"//v1//something//",
"/v2/something?query=test",
"/v2/something/?query=test",
"/v2/something////?query=test",
"//v2//something?query=test",
"//v2//something//?query=test",
];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn trim_root_trailing_slashes_with_query() {
let app = init_service(
App::new().wrap(NormalizePath(TrailingSlash::Trim)).service(
web::resource("/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn ensure_trailing_slash() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Always))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something/").to(HttpResponse::Ok))
.service(
web::resource("/v2/something/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let test_uris = vec![
"/",
"///",
"/v1/something",
"/v1/something/",
"/v1/something////",
"//v1//something",
"//v1//something//",
"/v2/something?query=test",
"/v2/something/?query=test",
"/v2/something////?query=test",
"//v2//something?query=test",
"//v2//something//?query=test",
];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn ensure_root_trailing_slash_with_query() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::Always))
.service(
web::resource("/")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn keep_trailing_slash_unchanged() {
let app = init_service(
App::new()
.wrap(NormalizePath(TrailingSlash::MergeOnly))
.service(web::resource("/").to(HttpResponse::Ok))
.service(web::resource("/v1/something").to(HttpResponse::Ok))
.service(web::resource("/v1/").to(HttpResponse::Ok))
.service(
web::resource("/v2/something")
.guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test")))
.to(HttpResponse::Ok),
),
)
.await;
let tests = vec![
("/", true), // root paths should still work
("/?query=test", true),
("///", true),
("/v1/something////", false),
("/v1/something/", false),
("//v1//something", true),
("/v1/", true),
("/v1", false),
("/v1////", true),
("//v1//", true),
("///v1", false),
("/v2/something?query=test", true),
("/v2/something/?query=test", false),
("/v2/something//?query=test", false),
("//v2//something?query=test", true),
];
for (uri, success) in tests {
let req = TestRequest::with_uri(uri).to_request();
let res = call_service(&app, req).await;
assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn no_path() {
let app = init_service(
App::new()
.wrap(NormalizePath::default())
.service(web::resource("/").to(HttpResponse::Ok)),
)
.await;
// This URI will be interpreted as an authority form, i.e. there is no path nor scheme
// (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3)
let req = TestRequest::with_uri("eh").to_request();
let res = call_service(&app, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_in_place_normalization() {
let srv = |req: ServiceRequest| {
assert_eq!("/v1/something", req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let test_uris = vec![
"/v1//something////",
"///v1/something",
"//v1///something",
"/v1//something",
];
for uri in test_uris {
let req = TestRequest::with_uri(uri).to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success(), "Failed uri: {}", uri);
}
}
#[actix_rt::test]
async fn should_normalize_nothing() {
const URI: &str = "/v1/something";
let srv = |req: ServiceRequest| {
assert_eq!(URI, req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let req = TestRequest::with_uri(URI).to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
}
#[actix_rt::test]
async fn should_normalize_no_trail() {
let srv = |req: ServiceRequest| {
assert_eq!("/v1/something", req.path());
ready(Ok(req.into_response(HttpResponse::Ok().finish())))
};
let normalize = NormalizePath::default()
.new_transform(srv.into_service())
.await
.unwrap();
let req = TestRequest::with_uri("/v1/something/").to_srv_request();
let res = normalize.call(req).await.unwrap();
assert!(res.status().is_success());
}
}

914
actix-web/src/request.rs Normal file
View File

@ -0,0 +1,914 @@
use std::{
cell::{Ref, RefCell, RefMut},
fmt, net,
rc::Rc,
str,
};
use actix_http::{Message, RequestHead};
use actix_router::{Path, Url};
use actix_utils::future::{ok, Ready};
#[cfg(feature = "cookies")]
use cookie::{Cookie, ParseError as CookieParseError};
use smallvec::SmallVec;
use crate::{
app_service::AppInitServiceState,
config::AppConfig,
dev::{Extensions, Payload},
error::UrlGenerationError,
http::{header::HeaderMap, Method, Uri, Version},
info::ConnectionInfo,
rmap::ResourceMap,
Error, FromRequest, HttpMessage,
};
#[cfg(feature = "cookies")]
struct Cookies(Vec<Cookie<'static>>);
/// An incoming request.
#[derive(Clone)]
pub struct HttpRequest {
/// # Invariant
/// `Rc<HttpRequestInner>` is used exclusively and NO `Weak<HttpRequestInner>`
/// is allowed anywhere in the code. Weak pointer is purposely ignored when
/// doing `Rc`'s ref counter check. Expect panics if this invariant is violated.
pub(crate) inner: Rc<HttpRequestInner>,
}
pub(crate) struct HttpRequestInner {
pub(crate) head: Message<RequestHead>,
pub(crate) path: Path<Url>,
pub(crate) app_data: SmallVec<[Rc<Extensions>; 4]>,
pub(crate) conn_data: Option<Rc<Extensions>>,
pub(crate) extensions: Rc<RefCell<Extensions>>,
app_state: Rc<AppInitServiceState>,
}
impl HttpRequest {
#[inline]
pub(crate) fn new(
path: Path<Url>,
head: Message<RequestHead>,
app_state: Rc<AppInitServiceState>,
app_data: Rc<Extensions>,
conn_data: Option<Rc<Extensions>>,
extensions: Rc<RefCell<Extensions>>,
) -> HttpRequest {
let mut data = SmallVec::<[Rc<Extensions>; 4]>::new();
data.push(app_data);
HttpRequest {
inner: Rc::new(HttpRequestInner {
head,
path,
app_state,
app_data: data,
conn_data,
extensions,
}),
}
}
}
impl HttpRequest {
/// This method returns reference to the request head
#[inline]
pub fn head(&self) -> &RequestHead {
&self.inner.head
}
/// This method returns mutable reference to the request head.
/// panics if multiple references of HTTP request exists.
#[inline]
pub(crate) fn head_mut(&mut self) -> &mut RequestHead {
&mut Rc::get_mut(&mut self.inner).unwrap().head
}
/// Request's uri.
#[inline]
pub fn uri(&self) -> &Uri {
&self.head().uri
}
/// Read the Request method.
#[inline]
pub fn method(&self) -> &Method {
&self.head().method
}
/// Read the Request Version.
#[inline]
pub fn version(&self) -> Version {
self.head().version
}
#[inline]
/// Returns request's headers.
pub fn headers(&self) -> &HeaderMap {
&self.head().headers
}
/// The target path of this request.
#[inline]
pub fn path(&self) -> &str {
self.head().uri.path()
}
/// The query string in the URL.
///
/// Example: `id=10`
#[inline]
pub fn query_string(&self) -> &str {
self.uri().query().unwrap_or_default()
}
/// Returns a reference to the URL parameters container.
///
/// A URL parameter is specified in the form `{identifier}`, where the identifier can be used
/// later in a request handler to access the matched value for that parameter.
///
/// # Percent Encoding and URL Parameters
/// Because each URL parameter is able to capture multiple path segments, none of
/// `["%2F", "%25", "%2B"]` found in the request URI are decoded into `["/", "%", "+"]` in order
/// to preserve path integrity. If a URL parameter is expected to contain these characters, then
/// it is on the user to decode them or use the [`web::Path`](crate::web::Path) extractor which
/// _will_ decode these special sequences.
#[inline]
pub fn match_info(&self) -> &Path<Url> {
&self.inner.path
}
/// Returns a mutable reference to the URL parameters container.
///
/// # Panics
/// Panics if this `HttpRequest` has been cloned.
#[inline]
pub(crate) fn match_info_mut(&mut self) -> &mut Path<Url> {
&mut Rc::get_mut(&mut self.inner).unwrap().path
}
/// The resource definition pattern that matched the path. Useful for logging and metrics.
///
/// For example, when a resource with pattern `/user/{id}/profile` is defined and a call is made
/// to `/user/123/profile` this function would return `Some("/user/{id}/profile")`.
///
/// Returns a None when no resource is fully matched, including default services.
#[inline]
pub fn match_pattern(&self) -> Option<String> {
self.resource_map().match_pattern(self.path())
}
/// The resource name that matched the path. Useful for logging and metrics.
///
/// Returns a None when no resource is fully matched, including default services.
#[inline]
pub fn match_name(&self) -> Option<&str> {
self.resource_map().match_name(self.path())
}
/// Returns a reference a piece of connection data set in an [on-connect] callback.
///
/// ```ignore
/// let opt_t = req.conn_data::<PeerCertificate>();
/// ```
///
/// [on-connect]: crate::HttpServer::on_connect
pub fn conn_data<T: 'static>(&self) -> Option<&T> {
self.inner
.conn_data
.as_deref()
.and_then(|container| container.get::<T>())
}
/// Generates URL for a named resource.
///
/// This substitutes in sequence all URL parameters that appear in the resource itself and in
/// parent [scopes](crate::web::scope), if any.
///
/// It is worth noting that the characters `['/', '%']` are not escaped and therefore a single
/// URL parameter may expand into multiple path segments and `elements` can be percent-encoded
/// beforehand without worrying about double encoding. Any other character that is not valid in
/// a URL path context is escaped using percent-encoding.
///
/// # Examples
/// ```
/// # use actix_web::{web, App, HttpRequest, HttpResponse};
/// fn index(req: HttpRequest) -> HttpResponse {
/// let url = req.url_for("foo", &["1", "2", "3"]); // <- generate URL for "foo" resource
/// HttpResponse::Ok().into()
/// }
///
/// let app = App::new()
/// .service(web::resource("/test/{one}/{two}/{three}")
/// .name("foo") // <- set resource name so it can be used in `url_for`
/// .route(web::get().to(|| HttpResponse::Ok()))
/// );
/// ```
pub fn url_for<U, I>(&self, name: &str, elements: U) -> Result<url::Url, UrlGenerationError>
where
U: IntoIterator<Item = I>,
I: AsRef<str>,
{
self.resource_map().url_for(self, name, elements)
}
/// Generate URL for named resource
///
/// This method is similar to `HttpRequest::url_for()` but it can be used
/// for urls that do not contain variable parts.
pub fn url_for_static(&self, name: &str) -> Result<url::Url, UrlGenerationError> {
const NO_PARAMS: [&str; 0] = [];
self.url_for(name, &NO_PARAMS)
}
/// Get a reference to a `ResourceMap` of current application.
#[inline]
pub fn resource_map(&self) -> &ResourceMap {
self.app_state().rmap()
}
/// Returns peer socket address.
///
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy.
///
/// For expanded client connection information, use [`connection_info`] instead.
///
/// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used.
///
/// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr
/// [`connection_info`]: Self::connection_info
#[inline]
pub fn peer_addr(&self) -> Option<net::SocketAddr> {
self.head().peer_addr
}
/// Returns connection info for the current request.
///
/// The return type, [`ConnectionInfo`], can also be used as an extractor.
///
/// # Panics
/// Panics if request's extensions container is already borrowed.
#[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
if !self.extensions().contains::<ConnectionInfo>() {
let info = ConnectionInfo::new(self.head(), &*self.app_config());
self.extensions_mut().insert(info);
}
Ref::map(self.extensions(), |data| data.get().unwrap())
}
/// App config
#[inline]
pub fn app_config(&self) -> &AppConfig {
self.app_state().config()
}
/// Retrieves a piece of application state.
///
/// Extracts any object stored with [`App::app_data()`](crate::App::app_data) (or the
/// counterpart methods on [`Scope`](crate::Scope::app_data) and
/// [`Resource`](crate::Resource::app_data)) during application configuration.
///
/// Since the Actix Web router layers application data, the returned object will reference the
/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
/// calling `.app_data::<u32>()` on an `HttpRequest` within that handler will return the
/// `Scope`'s instance. However, using the same router set up and a request that does not get
/// captured by the `Scope`, `.app_data::<u32>()` would return the `App`'s instance.
///
/// If the state was stored using the [`Data`] wrapper, then it must also be retrieved using
/// this same type.
///
/// See also the [`Data`] extractor.
///
/// # Examples
/// ```no_run
/// # use actix_web::{test::TestRequest, web::Data};
/// # let req = TestRequest::default().to_http_request();
/// # type T = u32;
/// let opt_t: Option<&Data<T>> = req.app_data::<Data<T>>();
/// ```
///
/// [`Data`]: crate::web::Data
#[doc(alias = "state")]
pub fn app_data<T: 'static>(&self) -> Option<&T> {
for container in self.inner.app_data.iter().rev() {
if let Some(data) = container.get::<T>() {
return Some(data);
}
}
None
}
#[inline]
fn app_state(&self) -> &AppInitServiceState {
&*self.inner.app_state
}
/// Load request cookies.
#[cfg(feature = "cookies")]
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
use actix_http::header::COOKIE;
if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new();
for hdr in self.headers().get_all(COOKIE) {
let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
for cookie_str in s.split(';').map(|s| s.trim()) {
if !cookie_str.is_empty() {
cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned());
}
}
}
self.extensions_mut().insert(Cookies(cookies));
}
Ok(Ref::map(self.extensions(), |ext| {
&ext.get::<Cookies>().unwrap().0
}))
}
/// Return request cookie.
#[cfg(feature = "cookies")]
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() {
if cookie.name() == name {
return Some(cookie.to_owned());
}
}
}
None
}
}
impl HttpMessage for HttpRequest {
type Stream = ();
#[inline]
fn headers(&self) -> &HeaderMap {
&self.head().headers
}
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.inner.extensions.borrow()
}
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.inner.extensions.borrow_mut()
}
#[inline]
fn take_payload(&mut self) -> Payload<Self::Stream> {
Payload::None
}
}
impl Drop for HttpRequest {
fn drop(&mut self) {
// if possible, contribute to current worker's HttpRequest allocation pool
// This relies on no weak references to inner existing anywhere within the codebase.
if let Some(inner) = Rc::get_mut(&mut self.inner) {
if inner.app_state.pool().is_available() {
// clear additional app_data and keep the root one for reuse.
inner.app_data.truncate(1);
// Inner is borrowed mut here and; get req data mutably to reduce borrow check. Also
// we know the req_data Rc will not have any cloned at this point to unwrap is okay.
Rc::get_mut(&mut inner.extensions)
.unwrap()
.get_mut()
.clear();
// a re-borrow of pool is necessary here.
let req = Rc::clone(&self.inner);
self.app_state().pool().push(req);
}
}
}
}
/// It is possible to get `HttpRequest` as an extractor handler parameter
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpRequest};
/// use serde::Deserialize;
///
/// /// extract `Thing` from request
/// async fn index(req: HttpRequest) -> String {
/// format!("Got thing: {:?}", req)
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/users/{first}").route(
/// web::get().to(index))
/// );
/// }
/// ```
impl FromRequest for HttpRequest {
type Error = Error;
type Future = Ready<Result<Self, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.clone())
}
}
impl fmt::Debug for HttpRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\nHttpRequest {:?} {}:{}",
self.inner.head.version,
self.inner.head.method,
self.path()
)?;
if !self.query_string().is_empty() {
writeln!(f, " query: ?{:?}", self.query_string())?;
}
if !self.match_info().is_empty() {
writeln!(f, " params: {:?}", self.match_info())?;
}
writeln!(f, " headers:")?;
for (key, val) in self.headers().iter() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}
/// Slab-allocated `HttpRequest` Pool
///
/// Since request processing may yield for asynchronous events to complete, a worker may have many
/// requests in-flight at any time. Pooling requests like this amortizes the performance and memory
/// costs of allocating and de-allocating HttpRequest objects as frequently as they otherwise would.
///
/// Request objects are added when they are dropped (see `<HttpRequest as Drop>::drop`) and re-used
/// in `<AppInitService as Service>::call` when there are available objects in the list.
///
/// The pool's default capacity is 128 items.
pub(crate) struct HttpRequestPool {
inner: RefCell<Vec<Rc<HttpRequestInner>>>,
cap: usize,
}
impl Default for HttpRequestPool {
fn default() -> Self {
Self::with_capacity(128)
}
}
impl HttpRequestPool {
pub(crate) fn with_capacity(cap: usize) -> Self {
HttpRequestPool {
inner: RefCell::new(Vec::with_capacity(cap)),
cap,
}
}
/// Re-use a previously allocated (but now completed/discarded) HttpRequest object.
#[inline]
pub(crate) fn pop(&self) -> Option<HttpRequest> {
self.inner
.borrow_mut()
.pop()
.map(|inner| HttpRequest { inner })
}
/// Check if the pool still has capacity for request storage.
#[inline]
pub(crate) fn is_available(&self) -> bool {
self.inner.borrow_mut().len() < self.cap
}
/// Push a request to pool.
#[inline]
pub(crate) fn push(&self, req: Rc<HttpRequestInner>) {
self.inner.borrow_mut().push(req);
}
/// Clears all allocated HttpRequest objects.
pub(crate) fn clear(&self) {
self.inner.borrow_mut().clear()
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
use crate::{
dev::{ResourceDef, ResourceMap, Service},
http::{header, StatusCode},
test::{self, call_service, init_service, read_body, TestRequest},
web, App, HttpResponse,
};
#[test]
fn test_debug() {
let req = TestRequest::default()
.insert_header(("content-type", "text/plain"))
.to_http_request();
let dbg = format!("{:?}", req);
assert!(dbg.contains("HttpRequest"));
}
#[test]
#[cfg(feature = "cookies")]
fn test_no_request_cookies() {
let req = TestRequest::default().to_http_request();
assert!(req.cookies().unwrap().is_empty());
}
#[test]
#[cfg(feature = "cookies")]
fn test_request_cookies() {
let req = TestRequest::default()
.append_header((header::COOKIE, "cookie1=value1"))
.append_header((header::COOKIE, "cookie2=value2"))
.to_http_request();
{
let cookies = req.cookies().unwrap();
assert_eq!(cookies.len(), 2);
assert_eq!(cookies[0].name(), "cookie1");
assert_eq!(cookies[0].value(), "value1");
assert_eq!(cookies[1].name(), "cookie2");
assert_eq!(cookies[1].value(), "value2");
}
let cookie = req.cookie("cookie1");
assert!(cookie.is_some());
let cookie = cookie.unwrap();
assert_eq!(cookie.name(), "cookie1");
assert_eq!(cookie.value(), "value1");
let cookie = req.cookie("cookie-unknown");
assert!(cookie.is_none());
}
#[test]
fn test_request_query() {
let req = TestRequest::with_uri("/?id=test").to_http_request();
assert_eq!(req.query_string(), "id=test");
}
#[test]
fn test_url_for() {
let mut res = ResourceDef::new("/user/{name}.{ext}");
res.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut res, None);
assert!(rmap.has_resource("/user/test.html"));
assert!(!rmap.has_resource("/test/unknown"));
let req = TestRequest::default()
.insert_header((header::HOST, "www.rust-lang.org"))
.rmap(rmap)
.to_http_request();
assert_eq!(
req.url_for("unknown", &["test"]),
Err(UrlGenerationError::ResourceNotFound)
);
assert_eq!(
req.url_for("index", &["test"]),
Err(UrlGenerationError::NotEnoughElements)
);
let url = req.url_for("index", &["test", "html"]);
assert_eq!(
url.ok().unwrap().as_str(),
"http://www.rust-lang.org/user/test.html"
);
}
#[test]
fn test_url_for_static() {
let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html"));
let req = TestRequest::with_uri("/test")
.insert_header((header::HOST, "www.rust-lang.org"))
.rmap(rmap)
.to_http_request();
let url = req.url_for_static("index");
assert_eq!(
url.ok().unwrap().as_str(),
"http://www.rust-lang.org/index.html"
);
}
#[test]
fn test_match_name() {
let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html"));
let req = TestRequest::default()
.uri("/index.html")
.rmap(rmap)
.to_http_request();
assert_eq!(req.match_name(), Some("index"));
}
#[test]
fn test_url_for_external() {
let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}");
rdef.set_name("youtube");
let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None);
let req = TestRequest::default().rmap(rmap).to_http_request();
let url = req.url_for("youtube", &["oHg5SJYRHA0"]);
assert_eq!(
url.ok().unwrap().as_str(),
"https://youtube.com/watch/oHg5SJYRHA0"
);
}
#[actix_rt::test]
async fn test_drop_http_request_pool() {
let srv = init_service(App::new().service(web::resource("/").to(
|req: HttpRequest| {
HttpResponse::Ok()
.insert_header(("pool_cap", req.app_state().pool().cap))
.finish()
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = call_service(&srv, req).await;
drop(srv);
assert_eq!(resp.headers().get("pool_cap").unwrap(), "128");
}
#[actix_rt::test]
async fn test_data() {
let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to(
|req: HttpRequest| {
if req.app_data::<usize>().is_some() {
HttpResponse::Ok()
} else {
HttpResponse::BadRequest()
}
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let srv = init_service(App::new().app_data(10u32).service(web::resource("/").to(
|req: HttpRequest| {
if req.app_data::<usize>().is_some() {
HttpResponse::Ok()
} else {
HttpResponse::BadRequest()
}
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_cascading_data() {
#[allow(dead_code)]
fn echo_usize(req: HttpRequest) -> HttpResponse {
let num = req.app_data::<usize>().unwrap();
HttpResponse::Ok().body(num.to_string())
}
let srv = init_service(
App::new()
.app_data(88usize)
.service(web::resource("/").route(web::get().to(echo_usize)))
.service(
web::resource("/one")
.app_data(1u32)
.route(web::get().to(echo_usize)),
),
)
.await;
let req = TestRequest::get().uri("/").to_request();
let resp = srv.call(req).await.unwrap();
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"88"));
let req = TestRequest::get().uri("/one").to_request();
let resp = srv.call(req).await.unwrap();
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"88"));
}
#[actix_rt::test]
async fn test_overwrite_data() {
#[allow(dead_code)]
fn echo_usize(req: HttpRequest) -> HttpResponse {
let num = req.app_data::<usize>().unwrap();
HttpResponse::Ok().body(num.to_string())
}
let srv = init_service(
App::new()
.app_data(88usize)
.service(web::resource("/").route(web::get().to(echo_usize)))
.service(
web::resource("/one")
.app_data(1usize)
.route(web::get().to(echo_usize)),
),
)
.await;
let req = TestRequest::get().uri("/").to_request();
let resp = srv.call(req).await.unwrap();
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"88"));
let req = TestRequest::get().uri("/one").to_request();
let resp = srv.call(req).await.unwrap();
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"1"));
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_extensions_dropped() {
struct Tracker {
pub dropped: bool,
}
struct Foo {
tracker: Rc<RefCell<Tracker>>,
}
impl Drop for Foo {
fn drop(&mut self) {
self.tracker.borrow_mut().dropped = true;
}
}
let tracker = Rc::new(RefCell::new(Tracker { dropped: false }));
{
let tracker2 = Rc::clone(&tracker);
let srv = init_service(App::new().data(10u32).service(web::resource("/").to(
move |req: HttpRequest| {
req.extensions_mut().insert(Foo {
tracker: Rc::clone(&tracker2),
});
HttpResponse::Ok()
},
)))
.await;
let req = TestRequest::default().to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
assert!(tracker.borrow().dropped);
}
#[actix_rt::test]
async fn extract_path_pattern() {
let srv = init_service(
App::new().service(
web::scope("/user/{id}")
.service(web::resource("/profile").route(web::get().to(
move |req: HttpRequest| {
assert_eq!(
req.match_pattern(),
Some("/user/{id}/profile".to_owned())
);
HttpResponse::Ok().finish()
},
)))
.default_service(web::to(move |req: HttpRequest| {
assert!(req.match_pattern().is_none());
HttpResponse::Ok().finish()
})),
),
)
.await;
let req = TestRequest::get().uri("/user/22/profile").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::get().uri("/user/22/not-exist").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn extract_path_pattern_complex() {
let srv = init_service(
App::new()
.service(web::scope("/user").service(web::scope("/{id}").service(
web::resource("").to(move |req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/user/{id}".to_owned()));
HttpResponse::Ok().finish()
}),
)))
.service(web::resource("/").to(move |req: HttpRequest| {
assert_eq!(req.match_pattern(), Some("/".to_owned()));
HttpResponse::Ok().finish()
}))
.default_service(web::to(move |req: HttpRequest| {
assert!(req.match_pattern().is_none());
HttpResponse::Ok().finish()
})),
)
.await;
let req = TestRequest::get().uri("/user/test").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::get().uri("/").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let req = TestRequest::get().uri("/not-exist").to_request();
let res = call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn url_for_closest_named_resource() {
// we mount the route named 'nested' on 2 different scopes, 'a' and 'b'
let srv = test::init_service(
App::new()
.service(
web::scope("/foo")
.service(web::resource("/nested").name("nested").route(web::get().to(
|req: HttpRequest| {
HttpResponse::Ok()
.body(format!("{}", req.url_for_static("nested").unwrap()))
},
)))
.service(web::scope("/baz").service(web::resource("deep")))
.service(web::resource("{foo_param}")),
)
.service(web::scope("/bar").service(
web::resource("/nested").name("nested").route(web::get().to(
|req: HttpRequest| {
HttpResponse::Ok()
.body(format!("{}", req.url_for_static("nested").unwrap()))
},
)),
)),
)
.await;
let foo_resp =
test::call_service(&srv, TestRequest::with_uri("/foo/nested").to_request()).await;
assert_eq!(foo_resp.status(), StatusCode::OK);
let body = read_body(foo_resp).await;
// `body` equals http://localhost:8080/bar/nested
// because nested from /bar overrides /foo's
// to do this any other way would require something like a custom tree search
// see https://github.com/actix/actix-web/issues/1763
assert_eq!(body, "http://localhost:8080/bar/nested");
let bar_resp =
test::call_service(&srv, TestRequest::with_uri("/bar/nested").to_request()).await;
assert_eq!(bar_resp.status(), StatusCode::OK);
let body = read_body(bar_resp).await;
assert_eq!(body, "http://localhost:8080/bar/nested");
}
}

View File

@ -0,0 +1,175 @@
use std::{any::type_name, ops::Deref};
use actix_utils::future::{err, ok, Ready};
use crate::{
dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpMessage as _,
HttpRequest,
};
/// Request-local data extractor.
///
/// Request-local data is arbitrary data attached to an individual request, usually
/// by middleware. It can be set via `extensions_mut` on [`HttpRequest`][htr_ext_mut]
/// or [`ServiceRequest`][srv_ext_mut].
///
/// Unlike app data, request data is dropped when the request has finished processing. This makes it
/// useful as a kind of messaging system between middleware and request handlers. It uses the same
/// types-as-keys storage system as app data.
///
/// # Mutating Request Data
/// Note that since extractors must output owned data, only types that `impl Clone` can use this
/// extractor. A clone is taken of the required request data and can, therefore, not be directly
/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or
/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not
/// provided to make this potential foot-gun more obvious.
///
/// # Examples
/// ```no_run
/// # use actix_web::{web, HttpResponse, HttpRequest, Responder, HttpMessage as _};
///
/// #[derive(Debug, Clone, PartialEq)]
/// struct FlagFromMiddleware(String);
///
/// /// Use the `ReqData<T>` extractor to access request data in a handler.
/// async fn handler(
/// req: HttpRequest,
/// opt_flag: Option<web::ReqData<FlagFromMiddleware>>,
/// ) -> impl Responder {
/// // use an option extractor if middleware is not guaranteed to add this type of req data
/// if let Some(flag) = opt_flag {
/// assert_eq!(&flag.into_inner(), req.extensions().get::<FlagFromMiddleware>().unwrap());
/// }
///
/// HttpResponse::Ok()
/// }
/// ```
///
/// [htr_ext_mut]: crate::HttpRequest::extensions_mut
/// [srv_ext_mut]: crate::dev::ServiceRequest::extensions_mut
#[derive(Debug, Clone)]
pub struct ReqData<T: Clone + 'static>(T);
impl<T: Clone + 'static> ReqData<T> {
/// Consumes the `ReqData`, returning its wrapped data.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: Clone + 'static> Deref for ReqData<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T: Clone + 'static> FromRequest for ReqData<T> {
type Error = Error;
type Future = Ready<Result<Self, Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
if let Some(st) = req.extensions().get::<T>() {
ok(ReqData(st.clone()))
} else {
log::debug!(
"Failed to construct App-level ReqData extractor. \
Request path: {:?} (type: {})",
req.path(),
type_name::<T>(),
);
err(ErrorInternalServerError(
"Missing expected request extension data",
))
}
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};
use futures_util::TryFutureExt as _;
use super::*;
use crate::{
dev::Service,
http::{Method, StatusCode},
test::{init_service, TestRequest},
web, App, HttpMessage, HttpResponse,
};
#[actix_rt::test]
async fn req_data_extractor() {
let srv = init_service(
App::new()
.wrap_fn(|req, srv| {
if req.method() == Method::POST {
req.extensions_mut().insert(42u32);
}
srv.call(req)
})
.service(web::resource("/test").to(
|req: HttpRequest, data: Option<ReqData<u32>>| {
if req.method() != Method::POST {
assert!(data.is_none());
}
if let Some(data) = data {
assert_eq!(*data, 42);
assert_eq!(
Some(data.into_inner()),
req.extensions().get::<u32>().copied()
);
}
HttpResponse::Ok()
},
)),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::post().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn req_data_internal_mutability() {
let srv = init_service(
App::new()
.wrap_fn(|req, srv| {
let data_before = Rc::new(RefCell::new(42u32));
req.extensions_mut().insert(data_before);
srv.call(req).map_ok(|res| {
{
let ext = res.request().extensions();
let data_after = ext.get::<Rc<RefCell<u32>>>().unwrap();
assert_eq!(*data_after.borrow(), 53u32);
}
res
})
})
.default_service(web::to(|data: ReqData<Rc<RefCell<u32>>>| {
assert_eq!(*data.borrow(), 42);
*data.borrow_mut() += 11;
assert_eq!(*data.borrow(), 53);
HttpResponse::Ok()
})),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}

802
actix-web/src/resource.rs Normal file
View File

@ -0,0 +1,802 @@
use std::{cell::RefCell, fmt, future::Future, rc::Rc};
use actix_http::Extensions;
use actix_router::{IntoPatterns, Patterns};
use actix_service::{
apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory,
ServiceFactoryExt, Transform,
};
use futures_core::future::LocalBoxFuture;
use futures_util::future::join_all;
use crate::{
body::MessageBody,
data::Data,
dev::{ensure_leading_slash, AppService, ResourceDef},
guard::Guard,
handler::Handler,
route::{Route, RouteService},
service::{
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
ServiceResponse,
},
Error, FromRequest, HttpResponse, Responder,
};
/// A collection of [`Route`]s that respond to the same path pattern.
///
/// Resource in turn has at least one route. Route consists of an handlers objects and list of
/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern
/// for configuration. During request handling, resource object iterate through all routes and check
/// guards for specific route, if request matches all guards, route considered matched and route
/// handler get called.
///
/// # Examples
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/")
/// .route(web::get().to(|| HttpResponse::Ok())));
/// ```
///
/// If no matching route could be found, *405* response code get returned. Default behavior could be
/// overridden with `default_resource()` method.
pub struct Resource<T = ResourceEndpoint> {
endpoint: T,
rdef: Patterns,
name: Option<String>,
routes: Vec<Route>,
app_data: Option<Extensions>,
guards: Vec<Box<dyn Guard>>,
default: BoxedHttpServiceFactory,
factory_ref: Rc<RefCell<Option<ResourceFactory>>>,
}
impl Resource {
pub fn new<T: IntoPatterns>(path: T) -> Resource {
let fref = Rc::new(RefCell::new(None));
Resource {
routes: Vec::new(),
rdef: path.patterns(),
name: None,
endpoint: ResourceEndpoint::new(fref.clone()),
factory_ref: fref,
guards: Vec::new(),
app_data: None,
default: boxed::factory(fn_service(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::MethodNotAllowed()))
})),
}
}
}
impl<T> Resource<T>
where
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
{
/// Set resource name.
///
/// Name is used for url generation.
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
/// Add match guard to a resource.
///
/// ```
/// use actix_web::{web, guard, App, HttpResponse};
///
/// async fn index(data: web::Path<(String, String)>) -> &'static str {
/// "Welcome!"
/// }
///
/// fn main() {
/// let app = App::new()
/// .service(
/// web::resource("/app")
/// .guard(guard::Header("content-type", "text/plain"))
/// .route(web::get().to(index))
/// )
/// .service(
/// web::resource("/app")
/// .guard(guard::Header("content-type", "text/json"))
/// .route(web::get().to(|| HttpResponse::MethodNotAllowed()))
/// );
/// }
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Box::new(guard));
self
}
pub(crate) fn add_guards(mut self, guards: Vec<Box<dyn Guard>>) -> Self {
self.guards.extend(guards);
self
}
/// Register a new route.
///
/// ```
/// use actix_web::{web, guard, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/").route(
/// web::route()
/// .guard(guard::Any(guard::Get()).or(guard::Put()))
/// .guard(guard::Header("Content-Type", "text/plain"))
/// .to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// Multiple routes could be added to a resource. Resource object uses
/// match guards for route selection.
///
/// ```
/// use actix_web::{web, guard, App};
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/container/")
/// .route(web::get().to(get_handler))
/// .route(web::post().to(post_handler))
/// .route(web::delete().to(delete_handler))
/// );
/// }
/// # async fn get_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() }
/// # async fn post_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() }
/// # async fn delete_handler() -> impl actix_web::Responder { actix_web::HttpResponse::Ok() }
/// ```
pub fn route(mut self, route: Route) -> Self {
self.routes.push(route);
self
}
/// Add resource data.
///
/// Data of different types from parent contexts will still be accessible. Any `Data<T>` types
/// set here can be extracted in handlers using the `Data<T>` extractor.
///
/// # Examples
/// ```
/// use std::cell::Cell;
/// use actix_web::{web, App, HttpRequest, HttpResponse, Responder};
///
/// struct MyData {
/// count: std::cell::Cell<usize>,
/// }
///
/// async fn handler(req: HttpRequest, counter: web::Data<MyData>) -> impl Responder {
/// // note this cannot use the Data<T> extractor because it was not added with it
/// let incr = *req.app_data::<usize>().unwrap();
/// assert_eq!(incr, 3);
///
/// // update counter using other value from app data
/// counter.count.set(counter.count.get() + incr);
///
/// HttpResponse::Ok().body(counter.count.get().to_string())
/// }
///
/// let app = App::new().service(
/// web::resource("/")
/// .app_data(3usize)
/// .app_data(web::Data::new(MyData { count: Default::default() }))
/// .route(web::get().to(handler))
/// );
/// ```
#[doc(alias = "manage")]
pub fn app_data<U: 'static>(mut self, data: U) -> Self {
self.app_data
.get_or_insert_with(Extensions::new)
.insert(data);
self
}
/// Add resource data after wrapping in `Data<T>`.
///
/// Deprecated in favor of [`app_data`](Self::app_data).
#[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")]
pub fn data<U: 'static>(self, data: U) -> Self {
self.app_data(Data::new(data))
}
/// Register a new route and add handler. This route matches all requests.
///
/// ```
/// use actix_web::{App, HttpRequest, HttpResponse, web};
///
/// async fn index(req: HttpRequest) -> HttpResponse {
/// todo!()
/// }
///
/// App::new().service(web::resource("/").to(index));
/// ```
///
/// This is shortcut for:
///
/// ```
/// # use actix_web::*;
/// # async fn index(req: HttpRequest) -> HttpResponse { todo!() }
/// App::new().service(web::resource("/").route(web::route().to(index)));
/// ```
pub fn to<F, Args>(mut self, handler: F) -> Self
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
self.routes.push(Route::new().to(handler));
self
}
/// Registers a resource middleware.
///
/// `mw` is a middleware component (type), that can modify the request and response across all
/// routes managed by this `Resource`.
///
/// See [`App::wrap`](crate::App::wrap) for more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap<M, B>(
self,
mw: M,
) -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
M: Transform<
T::Service,
ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody,
{
Resource {
endpoint: apply(mw, self.endpoint),
rdef: self.rdef,
name: self.name,
guards: self.guards,
routes: self.routes,
default: self.default,
app_data: self.app_data,
factory_ref: self.factory_ref,
}
}
/// Registers a resource function middleware.
///
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
/// life-cycle (request -> response), modifying request/response as necessary, across all
/// requests handled by the `Resource`.
///
/// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details.
#[doc(alias = "middleware")]
#[doc(alias = "use")] // nodejs terminology
pub fn wrap_fn<F, R, B>(
self,
mw: F,
) -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
>,
>
where
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
R: Future<Output = Result<ServiceResponse<B>, Error>>,
B: MessageBody,
{
Resource {
endpoint: apply_fn_factory(self.endpoint, mw),
rdef: self.rdef,
name: self.name,
guards: self.guards,
routes: self.routes,
default: self.default,
app_data: self.app_data,
factory_ref: self.factory_ref,
}
}
/// Default service to be used if no matching route could be found.
///
/// You can use a [`Route`] as default service.
///
/// If a default service is not registered, an empty `405 Method Not Allowed` response will be
/// sent to the client instead. Unlike [`Scope`](crate::Scope)s, a [`Resource`] does **not**
/// inherit its parent's default service.
pub fn default_service<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
U::InitError: fmt::Debug,
{
// create and configure default resource
self.default = boxed::factory(
f.into_factory()
.map_init_err(|e| log::error!("Can not construct default service: {:?}", e)),
);
self
}
}
impl<T, B> HttpServiceFactory for Resource<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<B>,
Error = Error,
InitError = (),
> + 'static,
B: MessageBody + 'static,
{
fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
Some(std::mem::take(&mut self.guards))
};
let mut rdef = if config.is_root() || !self.rdef.is_empty() {
ResourceDef::new(ensure_leading_slash(self.rdef.clone()))
} else {
ResourceDef::new(self.rdef.clone())
};
if let Some(ref name) = self.name {
rdef.set_name(name);
}
*self.factory_ref.borrow_mut() = Some(ResourceFactory {
routes: self.routes,
default: self.default,
});
let resource_data = self.app_data.map(Rc::new);
// wraps endpoint service (including middleware) call and injects app data for this scope
let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| {
if let Some(ref data) = resource_data {
req.add_data_container(Rc::clone(data));
}
let fut = srv.call(req);
async { Ok(fut.await?.map_into_boxed_body()) }
});
config.register_service(rdef, guards, endpoint, None)
}
}
pub struct ResourceFactory {
routes: Vec<Route>,
default: BoxedHttpServiceFactory,
}
impl ServiceFactory<ServiceRequest> for ResourceFactory {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = ResourceService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
// construct default service factory future.
let default_fut = self.default.new_service(());
// construct route service factory futures
let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(())));
Box::pin(async move {
let default = default_fut.await?;
let routes = factory_fut
.await
.into_iter()
.collect::<Result<Vec<_>, _>>()?;
Ok(ResourceService { routes, default })
})
}
}
pub struct ResourceService {
routes: Vec<RouteService>,
default: BoxedHttpService,
}
impl Service<ServiceRequest> for ResourceService {
type Response = ServiceResponse;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, mut req: ServiceRequest) -> Self::Future {
for route in &self.routes {
if route.check(&mut req) {
return route.call(req);
}
}
self.default.call(req)
}
}
#[doc(hidden)]
pub struct ResourceEndpoint {
factory: Rc<RefCell<Option<ResourceFactory>>>,
}
impl ResourceEndpoint {
fn new(factory: Rc<RefCell<Option<ResourceFactory>>>) -> Self {
ResourceEndpoint { factory }
}
}
impl ServiceFactory<ServiceRequest> for ResourceEndpoint {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = ResourceService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
self.factory.borrow().as_ref().unwrap().new_service(())
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use actix_rt::time::sleep;
use actix_service::Service;
use actix_utils::future::ok;
use super::*;
use crate::{
guard,
http::{
header::{self, HeaderValue},
Method, StatusCode,
},
middleware::DefaultHeaders,
service::{ServiceRequest, ServiceResponse},
test::{call_service, init_service, TestRequest},
web, App, Error, HttpMessage, HttpResponse,
};
#[test]
fn can_be_returned_from_fn() {
fn my_resource_1() -> Resource {
web::resource("/test1").route(web::get().to(|| async { "hello" }))
}
fn my_resource_2() -> Resource<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<impl MessageBody>,
Error = Error,
InitError = (),
>,
> {
web::resource("/test2")
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) }
})
.route(web::get().to(|| async { "hello" }))
}
fn my_resource_3() -> impl HttpServiceFactory {
web::resource("/test3").route(web::get().to(|| async { "hello" }))
}
App::new()
.service(my_resource_1())
.service(my_resource_2())
.service(my_resource_3());
}
#[actix_rt::test]
async fn test_middleware() {
let srv = init_service(
App::new().service(
web::resource("/test")
.name("test")
.wrap(
DefaultHeaders::new()
.add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
)
.route(web::get().to(HttpResponse::Ok)),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_middleware_fn() {
let srv = init_service(
App::new().service(
web::resource("/test")
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async {
fut.await.map(|mut res| {
res.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("0001"),
);
res
})
}
})
.route(web::get().to(HttpResponse::Ok)),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
HeaderValue::from_static("0001")
);
}
#[actix_rt::test]
async fn test_to() {
let srv = init_service(App::new().service(web::resource("/test").to(|| async {
sleep(Duration::from_millis(100)).await;
Ok::<_, Error>(HttpResponse::Ok())
})))
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_pattern() {
let srv = init_service(
App::new().service(
web::resource(["/test", "/test2"])
.to(|| async { Ok::<_, Error>(HttpResponse::Ok()) }),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test2").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_default_resource() {
let srv = init_service(
App::new()
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok)))
.default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::BadRequest()))
}),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test")
.method(Method::POST)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let srv = init_service(
App::new().service(
web::resource("/test")
.route(web::get().to(HttpResponse::Ok))
.default_service(|r: ServiceRequest| {
ok(r.into_response(HttpResponse::BadRequest()))
}),
),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test")
.method(Method::POST)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_resource_guards() {
let srv = init_service(
App::new()
.service(
web::resource("/test/{p}")
.guard(guard::Get())
.to(HttpResponse::Ok),
)
.service(
web::resource("/test/{p}")
.guard(guard::Put())
.to(HttpResponse::Created),
)
.service(
web::resource("/test/{p}")
.guard(guard::Delete())
.to(HttpResponse::NoContent),
),
)
.await;
let req = TestRequest::with_uri("/test/it")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test/it")
.method(Method::PUT)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/test/it")
.method(Method::DELETE)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NO_CONTENT);
}
// allow deprecated `{App, Resource}::data`
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data() {
let srv = init_service(
App::new()
.data(1.0f64)
.data(1usize)
.app_data(web::Data::new('-'))
.service(
web::resource("/test")
.data(10usize)
.app_data(web::Data::new('*'))
.guard(guard::Get())
.to(
|data1: web::Data<usize>,
data2: web::Data<char>,
data3: web::Data<f64>| {
assert_eq!(**data1, 10);
assert_eq!(**data2, '*');
let error = std::f64::EPSILON;
assert!((**data3 - 1.0).abs() < error);
HttpResponse::Ok()
},
),
),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
// allow deprecated `{App, Resource}::data`
#[allow(deprecated)]
#[actix_rt::test]
async fn test_data_default_service() {
let srv = init_service(
App::new().data(1usize).service(
web::resource("/test")
.data(10usize)
.default_service(web::to(|data: web::Data<usize>| {
assert_eq!(**data, 10);
HttpResponse::Ok()
})),
),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_middleware_app_data() {
let srv = init_service(
App::new().service(
web::resource("test")
.app_data(1usize)
.wrap_fn(|req, srv| {
assert_eq!(req.app_data::<usize>(), Some(&1usize));
req.extensions_mut().insert(1usize);
srv.call(req)
})
.route(web::get().to(HttpResponse::Ok))
.default_service(|req: ServiceRequest| async move {
let (req, _) = req.into_parts();
assert_eq!(req.extensions().get::<usize>(), Some(&1));
Ok(ServiceResponse::new(
req,
HttpResponse::BadRequest().finish(),
))
}),
),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::post().uri("/test").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_middleware_body_type() {
let srv = init_service(
App::new().service(
web::resource("/test")
.wrap_fn(|req, srv| {
let fut = srv.call(req);
async { Ok(fut.await?.map_into_right_body::<()>()) }
})
.route(web::get().to(|| async { "hello" })),
),
)
.await;
// test if `try_into_bytes()` is preserved across scope layer
use actix_http::body::MessageBody as _;
let req = TestRequest::with_uri("/test").to_request();
let resp = call_service(&srv, req).await;
let body = resp.into_body();
assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref());
}
}

View File

@ -0,0 +1,535 @@
use std::{
cell::{Ref, RefMut},
convert::TryInto,
future::Future,
pin::Pin,
task::{Context, Poll},
};
use actix_http::{error::HttpError, Response, ResponseHead};
use bytes::Bytes;
use futures_core::Stream;
use serde::Serialize;
use crate::{
body::{BodyStream, BoxBody, MessageBody},
dev::Extensions,
error::{Error, JsonPayloadError},
http::header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
http::{ConnectionType, StatusCode},
BoxError, HttpRequest, HttpResponse, Responder,
};
/// An HTTP response builder.
///
/// This type can be used to construct an instance of `Response` through a builder-like pattern.
pub struct HttpResponseBuilder {
res: Option<Response<BoxBody>>,
error: Option<HttpError>,
}
impl HttpResponseBuilder {
#[inline]
/// Create response builder
pub fn new(status: StatusCode) -> Self {
Self {
res: Some(Response::with_body(status, BoxBody::new(()))),
error: None,
}
}
/// Set HTTP status code of this response.
#[inline]
pub fn status(&mut self, status: StatusCode) -> &mut Self {
if let Some(parts) = self.inner() {
parts.status = status;
}
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
///
/// ```
/// use actix_web::{HttpResponse, http::header};
///
/// HttpResponse::Ok()
/// .insert_header(header::ContentType(mime::APPLICATION_JSON))
/// .insert_header(("X-TEST", "value"))
/// .finish();
/// ```
pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
if let Some(parts) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(e) => self.error = Some(e.into()),
};
}
self
}
/// Append a header, keeping any that were set with an equivalent field name.
///
/// ```
/// use actix_web::{HttpResponse, http::header};
///
/// HttpResponse::Ok()
/// .append_header(header::ContentType(mime::APPLICATION_JSON))
/// .append_header(("X-TEST", "value1"))
/// .append_header(("X-TEST", "value2"))
/// .finish();
/// ```
pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
if let Some(parts) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.error = Some(e.into()),
};
}
self
}
/// Replaced with [`Self::insert_header()`].
#[doc(hidden)]
#[deprecated(
since = "4.0.0",
note = "Replaced with `insert_header((key, value))`. Will be removed in v5."
)]
pub fn set_header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: TryInto<HeaderName>,
K::Error: Into<HttpError>,
V: TryIntoHeaderValue,
{
if self.error.is_some() {
return self;
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.insert_header((name, value)),
(Err(err), _) => self.error = Some(err.into()),
(_, Err(err)) => self.error = Some(err.into()),
}
self
}
/// Replaced with [`Self::append_header()`].
#[doc(hidden)]
#[deprecated(
since = "4.0.0",
note = "Replaced with `append_header((key, value))`. Will be removed in v5."
)]
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: TryInto<HeaderName>,
K::Error: Into<HttpError>,
V: TryIntoHeaderValue,
{
if self.error.is_some() {
return self;
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.append_header((name, value)),
(Err(err), _) => self.error = Some(err.into()),
(_, Err(err)) => self.error = Some(err.into()),
}
self
}
/// Set the custom reason for the response.
#[inline]
pub fn reason(&mut self, reason: &'static str) -> &mut Self {
if let Some(parts) = self.inner() {
parts.reason = Some(reason);
}
self
}
/// Set connection type to KeepAlive
#[inline]
pub fn keep_alive(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::KeepAlive);
}
self
}
/// Set connection type to Upgrade
#[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where
V: TryIntoHeaderValue,
{
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Upgrade);
}
if let Ok(value) = value.try_into_value() {
self.insert_header((header::UPGRADE, value));
}
self
}
/// Force close connection, even if it is marked as keep-alive
#[inline]
pub fn force_close(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Close);
}
self
}
/// Disable chunked transfer encoding for HTTP/1.1 streaming responses.
#[inline]
pub fn no_chunking(&mut self, len: u64) -> &mut Self {
let mut buf = itoa::Buffer::new();
self.insert_header((header::CONTENT_LENGTH, buf.format(len)));
if let Some(parts) = self.inner() {
parts.no_chunking(true);
}
self
}
/// Set response content type.
#[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self
where
V: TryIntoHeaderValue,
{
if let Some(parts) = self.inner() {
match value.try_into_value() {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.error = Some(e.into()),
};
}
self
}
/// Add a cookie to the response.
///
/// To send a "removal" cookie, call [`.make_removal()`](cookie::Cookie::make_removal) on the
/// given cookie. See [`HttpResponse::add_removal_cookie()`] to learn more.
///
/// # Examples
/// Send a new cookie:
/// ```
/// use actix_web::{HttpResponse, cookie::Cookie};
///
/// let res = HttpResponse::Ok()
/// .cookie(
/// Cookie::build("name", "value")
/// .domain("www.rust-lang.org")
/// .path("/")
/// .secure(true)
/// .http_only(true)
/// .finish(),
/// )
/// .finish();
/// ```
///
/// Send a removal cookie:
/// ```
/// use actix_web::{HttpResponse, cookie::Cookie};
///
/// // the name, domain and path match the cookie created in the previous example
/// let mut cookie = Cookie::build("name", "value-does-not-matter")
/// .domain("www.rust-lang.org")
/// .path("/")
/// .finish();
/// cookie.make_removal();
///
/// let res = HttpResponse::Ok()
/// .cookie(cookie)
/// .finish();
/// ```
#[cfg(feature = "cookies")]
pub fn cookie(&mut self, cookie: cookie::Cookie<'_>) -> &mut Self {
match cookie.to_string().try_into_value() {
Ok(hdr_val) => self.append_header((header::SET_COOKIE, hdr_val)),
Err(err) => {
self.error = Some(err.into());
self
}
}
}
/// Returns a reference to the response-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.res
.as_ref()
.expect("cannot reuse response builder")
.extensions()
}
/// Returns a mutable reference to the response-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.res
.as_mut()
.expect("cannot reuse response builder")
.extensions_mut()
}
/// Set a body and build the `HttpResponse`.
///
/// Unlike [`message_body`](Self::message_body), errors are converted into error
/// responses immediately.
///
/// `HttpResponseBuilder` can not be used after this call.
pub fn body<B>(&mut self, body: B) -> HttpResponse<BoxBody>
where
B: MessageBody + 'static,
{
match self.message_body(body) {
Ok(res) => res.map_into_boxed_body(),
Err(err) => HttpResponse::from_error(err),
}
}
/// Set a body and build the `HttpResponse`.
///
/// `HttpResponseBuilder` can not be used after this call.
pub fn message_body<B>(&mut self, body: B) -> Result<HttpResponse<B>, Error> {
if let Some(err) = self.error.take() {
return Err(err.into());
}
let res = self
.res
.take()
.expect("cannot reuse response builder")
.set_body(body);
Ok(HttpResponse::from(res))
}
/// Set a streaming body and build the `HttpResponse`.
///
/// `HttpResponseBuilder` can not be used after this call.
#[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> HttpResponse
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<BoxError> + 'static,
{
self.body(BodyStream::new(stream))
}
/// Set a JSON body and build the `HttpResponse`.
///
/// `HttpResponseBuilder` can not be used after this call.
pub fn json(&mut self, value: impl Serialize) -> HttpResponse {
match serde_json::to_string(&value) {
Ok(body) => {
let contains = if let Some(parts) = self.inner() {
parts.headers.contains_key(header::CONTENT_TYPE)
} else {
true
};
if !contains {
self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
}
self.body(body)
}
Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)),
}
}
/// Set an empty body and build the `HttpResponse`.
///
/// `HttpResponseBuilder` can not be used after this call.
#[inline]
pub fn finish(&mut self) -> HttpResponse {
self.body(())
}
/// This method construct new `HttpResponseBuilder`
pub fn take(&mut self) -> Self {
Self {
res: self.res.take(),
error: self.error.take(),
}
}
fn inner(&mut self) -> Option<&mut ResponseHead> {
if self.error.is_some() {
return None;
}
self.res.as_mut().map(Response::head_mut)
}
}
impl From<HttpResponseBuilder> for HttpResponse {
fn from(mut builder: HttpResponseBuilder) -> Self {
builder.finish()
}
}
impl From<HttpResponseBuilder> for Response<BoxBody> {
fn from(mut builder: HttpResponseBuilder) -> Self {
builder.finish().into()
}
}
impl Future for HttpResponseBuilder {
type Output = Result<HttpResponse, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(self.finish()))
}
}
impl Responder for HttpResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
body,
http::{
header::{self, HeaderValue, CONTENT_TYPE},
StatusCode,
},
test::assert_body_eq,
};
#[test]
fn test_basic_builder() {
let resp = HttpResponse::Ok()
.insert_header(("X-TEST", "value"))
.finish();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_upgrade() {
let resp = HttpResponseBuilder::new(StatusCode::OK)
.upgrade("websocket")
.finish();
assert!(resp.upgrade());
assert_eq!(
resp.headers().get(header::UPGRADE).unwrap(),
HeaderValue::from_static("websocket")
);
}
#[test]
fn test_force_close() {
let resp = HttpResponseBuilder::new(StatusCode::OK)
.force_close()
.finish();
assert!(!resp.keep_alive())
}
#[test]
fn test_content_type() {
let resp = HttpResponseBuilder::new(StatusCode::OK)
.content_type("text/plain")
.body(Bytes::new());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
}
#[actix_rt::test]
async fn test_json() {
let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]);
let ct = res.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("application/json"));
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]);
let ct = res.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("application/json"));
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
// content type override
let res = HttpResponse::Ok()
.insert_header((CONTENT_TYPE, "text/json"))
.json(&vec!["v1", "v2", "v3"]);
let ct = res.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("text/json"));
assert_body_eq!(res, br#"["v1","v2","v3"]"#);
}
#[actix_rt::test]
async fn test_serde_json_in_body() {
let resp = HttpResponse::Ok().body(
serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap(),
);
assert_eq!(
body::to_bytes(resp.into_body()).await.unwrap().as_ref(),
br#"{"test-key":"test-value"}"#
);
}
#[test]
fn response_builder_header_insert_kv() {
let mut res = HttpResponse::Ok();
res.insert_header(("Content-Type", "application/octet-stream"));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_insert_typed() {
let mut res = HttpResponse::Ok();
res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_append_kv() {
let mut res = HttpResponse::Ok();
res.append_header(("Content-Type", "application/octet-stream"));
res.append_header(("Content-Type", "application/json"));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
#[test]
fn response_builder_header_append_typed() {
let mut res = HttpResponse::Ok();
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
}

View File

@ -0,0 +1,241 @@
use actix_http::{
body::EitherBody, error::HttpError, header::HeaderMap, header::TryIntoHeaderPair,
StatusCode,
};
use crate::{HttpRequest, HttpResponse, Responder};
/// Allows overriding status code and headers for a [`Responder`].
///
/// Created by the [`Responder::customize`] method.
pub struct CustomizeResponder<R> {
inner: CustomizeResponderInner<R>,
error: Option<HttpError>,
}
struct CustomizeResponderInner<R> {
responder: R,
status: Option<StatusCode>,
override_headers: HeaderMap,
append_headers: HeaderMap,
}
impl<R: Responder> CustomizeResponder<R> {
pub(crate) fn new(responder: R) -> Self {
CustomizeResponder {
inner: CustomizeResponderInner {
responder,
status: None,
override_headers: HeaderMap::new(),
append_headers: HeaderMap::new(),
},
error: None,
}
}
/// Override a status code for the Responder's response.
///
/// # Examples
/// ```
/// use actix_web::{Responder, http::StatusCode, test::TestRequest};
///
/// let responder = "Welcome!".customize().with_status(StatusCode::ACCEPTED);
///
/// let request = TestRequest::default().to_http_request();
/// let response = responder.respond_to(&request);
/// assert_eq!(response.status(), StatusCode::ACCEPTED);
/// ```
pub fn with_status(mut self, status: StatusCode) -> Self {
if let Some(inner) = self.inner() {
inner.status = Some(status);
}
self
}
/// Insert (override) header in the final response.
///
/// Overrides other headers with the same name.
/// See [`HeaderMap::insert`](crate::http::header::HeaderMap::insert).
///
/// Headers added with this method will be inserted before those added
/// with [`append_header`](Self::append_header). As such, header(s) can be overridden with more
/// than one new header by first calling `insert_header` followed by `append_header`.
///
/// # Examples
/// ```
/// use actix_web::{Responder, test::TestRequest};
///
/// let responder = "Hello world!"
/// .customize()
/// .insert_header(("x-version", "1.2.3"));
///
/// let request = TestRequest::default().to_http_request();
/// let response = responder.respond_to(&request);
/// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3");
/// ```
pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
if let Some(inner) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => {
inner.override_headers.insert(key, value);
}
Err(err) => self.error = Some(err.into()),
};
}
self
}
/// Append header to the final response.
///
/// Unlike [`insert_header`](Self::insert_header), this will not override existing headers.
/// See [`HeaderMap::append`](crate::http::header::HeaderMap::append).
///
/// Headers added here are appended _after_ additions/overrides from `insert_header`.
///
/// # Examples
/// ```
/// use actix_web::{Responder, test::TestRequest};
///
/// let responder = "Hello world!"
/// .customize()
/// .append_header(("x-version", "1.2.3"));
///
/// let request = TestRequest::default().to_http_request();
/// let response = responder.respond_to(&request);
/// assert_eq!(response.headers().get("x-version").unwrap(), "1.2.3");
/// ```
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
if let Some(inner) = self.inner() {
match header.try_into_pair() {
Ok((key, value)) => {
inner.append_headers.append(key, value);
}
Err(err) => self.error = Some(err.into()),
};
}
self
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `insert_header`.")]
pub fn with_header(self, header: impl TryIntoHeaderPair) -> Self
where
Self: Sized,
{
self.insert_header(header)
}
fn inner(&mut self) -> Option<&mut CustomizeResponderInner<R>> {
if self.error.is_some() {
None
} else {
Some(&mut self.inner)
}
}
}
impl<T> Responder for CustomizeResponder<T>
where
T: Responder,
{
type Body = EitherBody<T::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
if let Some(err) = self.error {
return HttpResponse::from_error(err).map_into_right_body();
}
let mut res = self.inner.responder.respond_to(req);
if let Some(status) = self.inner.status {
*res.status_mut() = status;
}
for (k, v) in self.inner.override_headers {
res.headers_mut().insert(k, v);
}
for (k, v) in self.inner.append_headers {
res.headers_mut().append(k, v);
}
res.map_into_left_body()
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use actix_http::body::to_bytes;
use super::*;
use crate::{
http::{
header::{HeaderValue, CONTENT_TYPE},
StatusCode,
},
test::TestRequest,
};
#[actix_rt::test]
async fn customize_responder() {
let req = TestRequest::default().to_http_request();
let res = "test"
.to_string()
.customize()
.with_status(StatusCode::BAD_REQUEST)
.respond_to(&req);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = "test"
.to_string()
.customize()
.insert_header(("content-type", "json"))
.respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("json")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
}
#[actix_rt::test]
async fn tuple_responder_with_status_code() {
let req = TestRequest::default().to_http_request();
let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let req = TestRequest::default().to_http_request();
let res = ("test".to_string(), StatusCode::OK)
.customize()
.insert_header((CONTENT_TYPE, mime::APPLICATION_JSON))
.respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/json")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
}
}

View File

@ -0,0 +1,98 @@
//! Status code based HTTP response builders.
use actix_http::StatusCode;
use crate::{HttpResponse, HttpResponseBuilder};
macro_rules! static_resp {
($name:ident, $status:expr) => {
#[allow(non_snake_case, missing_docs)]
pub fn $name() -> HttpResponseBuilder {
HttpResponseBuilder::new($status)
}
};
}
impl HttpResponse {
static_resp!(Continue, StatusCode::CONTINUE);
static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS);
static_resp!(Processing, StatusCode::PROCESSING);
static_resp!(Ok, StatusCode::OK);
static_resp!(Created, StatusCode::CREATED);
static_resp!(Accepted, StatusCode::ACCEPTED);
static_resp!(
NonAuthoritativeInformation,
StatusCode::NON_AUTHORITATIVE_INFORMATION
);
static_resp!(NoContent, StatusCode::NO_CONTENT);
static_resp!(ResetContent, StatusCode::RESET_CONTENT);
static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT);
static_resp!(MultiStatus, StatusCode::MULTI_STATUS);
static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED);
static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
static_resp!(Found, StatusCode::FOUND);
static_resp!(SeeOther, StatusCode::SEE_OTHER);
static_resp!(NotModified, StatusCode::NOT_MODIFIED);
static_resp!(UseProxy, StatusCode::USE_PROXY);
static_resp!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT);
static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT);
static_resp!(BadRequest, StatusCode::BAD_REQUEST);
static_resp!(NotFound, StatusCode::NOT_FOUND);
static_resp!(Unauthorized, StatusCode::UNAUTHORIZED);
static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED);
static_resp!(Forbidden, StatusCode::FORBIDDEN);
static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED);
static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE);
static_resp!(
ProxyAuthenticationRequired,
StatusCode::PROXY_AUTHENTICATION_REQUIRED
);
static_resp!(RequestTimeout, StatusCode::REQUEST_TIMEOUT);
static_resp!(Conflict, StatusCode::CONFLICT);
static_resp!(Gone, StatusCode::GONE);
static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED);
static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED);
static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED);
static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE);
static_resp!(UriTooLong, StatusCode::URI_TOO_LONG);
static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE);
static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE);
static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
static_resp!(
RequestHeaderFieldsTooLarge,
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
);
static_resp!(
UnavailableForLegalReasons,
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS
);
static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
static_resp!(BadGateway, StatusCode::BAD_GATEWAY);
static_resp!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE);
static_resp!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT);
static_resp!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED);
static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES);
static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE);
static_resp!(LoopDetected, StatusCode::LOOP_DETECTED);
}
#[cfg(test)]
mod tests {
use crate::http::StatusCode;
use crate::HttpResponse;
#[test]
fn test_build() {
let resp = HttpResponse::Ok().finish();
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@ -0,0 +1,14 @@
mod builder;
mod customize_responder;
mod http_codes;
mod responder;
#[allow(clippy::module_inception)]
mod response;
pub use self::builder::HttpResponseBuilder;
pub use self::customize_responder::CustomizeResponder;
pub use self::responder::Responder;
pub use self::response::HttpResponse;
#[cfg(feature = "cookies")]
pub use self::response::CookieIter;

View File

@ -0,0 +1,325 @@
use std::borrow::Cow;
use actix_http::{
body::{BoxBody, EitherBody, MessageBody},
header::TryIntoHeaderPair,
StatusCode,
};
use bytes::{Bytes, BytesMut};
use crate::{Error, HttpRequest, HttpResponse};
use super::CustomizeResponder;
/// Trait implemented by types that can be converted to an HTTP response.
///
/// Any types that implement this trait can be used in the return type of a handler.
// # TODO: more about implementation notes and foreign impls
pub trait Responder {
type Body: MessageBody + 'static;
/// Convert self to `HttpResponse`.
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>;
/// Wraps responder to allow alteration of its response.
///
/// See [`CustomizeResponder`] docs for its capabilities.
///
/// # Examples
/// ```
/// use actix_web::{Responder, http::StatusCode, test::TestRequest};
///
/// let responder = "Hello world!"
/// .customize()
/// .with_status(StatusCode::BAD_REQUEST)
/// .insert_header(("x-hello", "world"));
///
/// let request = TestRequest::default().to_http_request();
/// let response = responder.respond_to(&request);
/// assert_eq!(response.status(), StatusCode::BAD_REQUEST);
/// assert_eq!(response.headers().get("x-hello").unwrap(), "world");
/// ```
#[inline]
fn customize(self) -> CustomizeResponder<Self>
where
Self: Sized,
{
CustomizeResponder::new(self)
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")]
fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder<Self>
where
Self: Sized,
{
self.customize().insert_header(header)
}
}
impl Responder for actix_http::Response<BoxBody> {
type Body = BoxBody;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
HttpResponse::from(self)
}
}
impl Responder for actix_http::ResponseBuilder {
type Body = BoxBody;
#[inline]
fn respond_to(mut self, req: &HttpRequest) -> HttpResponse<Self::Body> {
self.finish().map_into_boxed_body().respond_to(req)
}
}
impl<T> Responder for Option<T>
where
T: Responder,
{
type Body = EitherBody<T::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
match self {
Some(val) => val.respond_to(req).map_into_left_body(),
None => HttpResponse::new(StatusCode::NOT_FOUND).map_into_right_body(),
}
}
}
impl<T, E> Responder for Result<T, E>
where
T: Responder,
E: Into<Error>,
{
type Body = EitherBody<T::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
match self {
Ok(val) => val.respond_to(req).map_into_left_body(),
Err(err) => HttpResponse::from_error(err.into()).map_into_right_body(),
}
}
}
impl<T: Responder> Responder for (T, StatusCode) {
type Body = T::Body;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
let mut res = self.0.respond_to(req);
*res.status_mut() = self.1;
res
}
}
macro_rules! impl_responder_by_forward_into_base_response {
($res:ty, $body:ty) => {
impl Responder for $res {
type Body = $body;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
let res: actix_http::Response<_> = self.into();
res.into()
}
}
};
($res:ty) => {
impl_responder_by_forward_into_base_response!($res, $res);
};
}
impl_responder_by_forward_into_base_response!(&'static [u8]);
impl_responder_by_forward_into_base_response!(Bytes);
impl_responder_by_forward_into_base_response!(BytesMut);
impl_responder_by_forward_into_base_response!(&'static str);
impl_responder_by_forward_into_base_response!(String);
macro_rules! impl_into_string_responder {
($res:ty) => {
impl Responder for $res {
type Body = String;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
let string: String = self.into();
let res: actix_http::Response<_> = string.into();
res.into()
}
}
};
}
impl_into_string_responder!(&'_ String);
impl_into_string_responder!(Cow<'_, str>);
#[cfg(test)]
pub(crate) mod tests {
use actix_service::Service;
use bytes::{Bytes, BytesMut};
use actix_http::body::to_bytes;
use super::*;
use crate::{
error,
http::{
header::{HeaderValue, CONTENT_TYPE},
StatusCode,
},
test::{assert_body_eq, init_service, TestRequest},
web, App,
};
#[actix_rt::test]
async fn test_option_responder() {
let srv = init_service(
App::new()
.service(web::resource("/none").to(|| async { Option::<&'static str>::None }))
.service(web::resource("/some").to(|| async { Some("some") })),
)
.await;
let req = TestRequest::with_uri("/none").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri("/some").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_body_eq!(resp, b"some");
}
#[actix_rt::test]
async fn test_responder() {
let req = TestRequest::default().to_http_request();
let res = "test".respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = b"test".respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = "test".to_string().respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = (&"test".to_string()).respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let s = String::from("test");
let res = Cow::Borrowed(s.as_str()).respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = Cow::<'_, str>::Owned(s).respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = Cow::Borrowed("test").respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = Bytes::from_static(b"test").respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = BytesMut::from(b"test".as_ref()).respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream")
);
assert_eq!(
to_bytes(res.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
// InternalError
let res = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
#[actix_rt::test]
async fn test_result_responder() {
let req = TestRequest::default().to_http_request();
// Result<I, E>
let resp = Ok::<_, Error>("test".to_string()).respond_to(&req);
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8")
);
assert_eq!(
to_bytes(resp.into_body()).await.unwrap(),
Bytes::from_static(b"test"),
);
let res = Err::<String, _>(error::InternalError::new("err", StatusCode::BAD_REQUEST))
.respond_to(&req);
assert_eq!(res.status(), StatusCode::BAD_REQUEST);
}
}

View File

@ -0,0 +1,445 @@
use std::{
cell::{Ref, RefMut},
fmt,
};
use actix_http::{
body::{BoxBody, EitherBody, MessageBody},
header::HeaderMap,
Extensions, Response, ResponseHead, StatusCode,
};
#[cfg(feature = "cookies")]
use {
actix_http::{
error::HttpError,
header::{self, HeaderValue},
},
cookie::Cookie,
};
use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder};
/// An outgoing response.
pub struct HttpResponse<B = BoxBody> {
res: Response<B>,
error: Option<Error>,
}
impl HttpResponse<BoxBody> {
/// Constructs a response.
#[inline]
pub fn new(status: StatusCode) -> Self {
Self {
res: Response::new(status),
error: None,
}
}
/// Constructs a response builder with specific HTTP status.
#[inline]
pub fn build(status: StatusCode) -> HttpResponseBuilder {
HttpResponseBuilder::new(status)
}
/// Create an error response.
#[inline]
pub fn from_error(error: impl Into<Error>) -> Self {
let error = error.into();
let mut response = error.as_response_error().error_response();
response.error = Some(error);
response
}
}
impl<B> HttpResponse<B> {
/// Constructs a response with body
#[inline]
pub fn with_body(status: StatusCode, body: B) -> Self {
Self {
res: Response::with_body(status, body),
error: None,
}
}
/// Returns a reference to response head.
#[inline]
pub fn head(&self) -> &ResponseHead {
self.res.head()
}
/// Returns a mutable reference to response head.
#[inline]
pub fn head_mut(&mut self) -> &mut ResponseHead {
self.res.head_mut()
}
/// The source `error` for this response
#[inline]
pub fn error(&self) -> Option<&Error> {
self.error.as_ref()
}
/// Get the response status code
#[inline]
pub fn status(&self) -> StatusCode {
self.res.status()
}
/// Set the `StatusCode` for this response
#[inline]
pub fn status_mut(&mut self) -> &mut StatusCode {
self.res.status_mut()
}
/// Get the headers from the response
#[inline]
pub fn headers(&self) -> &HeaderMap {
self.res.headers()
}
/// Get a mutable reference to the headers
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
self.res.headers_mut()
}
/// Get an iterator for the cookies set by this response.
#[cfg(feature = "cookies")]
pub fn cookies(&self) -> CookieIter<'_> {
CookieIter {
iter: self.headers().get_all(header::SET_COOKIE),
}
}
/// Add a cookie to this response.
///
/// # Errors
/// Returns an error if the cookie results in a malformed `Set-Cookie` header.
#[cfg(feature = "cookies")]
pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
HeaderValue::from_str(&cookie.to_string())
.map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie))
.map_err(Into::into)
}
/// Add a "removal" cookie to the response that matches attributes of given cookie.
///
/// This will cause browsers/clients to remove stored cookies with this name.
///
/// The `Set-Cookie` header added to the response will have:
/// - name matching given cookie;
/// - domain matching given cookie;
/// - path matching given cookie;
/// - an empty value;
/// - a max-age of `0`;
/// - an expiration date far in the past.
///
/// If the cookie you're trying to remove has an explicit path or domain set, those attributes
/// will need to be included in the cookie passed in here.
///
/// # Errors
/// Returns an error if the given name results in a malformed `Set-Cookie` header.
#[cfg(feature = "cookies")]
pub fn add_removal_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> {
let mut removal_cookie = cookie.to_owned();
removal_cookie.make_removal();
HeaderValue::from_str(&removal_cookie.to_string())
.map(|cookie| self.headers_mut().append(header::SET_COOKIE, cookie))
.map_err(Into::into)
}
/// Remove all cookies with the given name from this response.
///
/// Returns the number of cookies removed.
///
/// This method can _not_ cause a browser/client to delete any of its stored cookies. Its only
/// purpose is to delete cookies that were added to this response using [`add_cookie`]
/// and [`add_removal_cookie`]. Use [`add_removal_cookie`] to send a "removal" cookie.
///
/// [`add_cookie`]: Self::add_cookie
/// [`add_removal_cookie`]: Self::add_removal_cookie
#[cfg(feature = "cookies")]
pub fn del_cookie(&mut self, name: &str) -> usize {
let headers = self.headers_mut();
let vals: Vec<HeaderValue> = headers
.get_all(header::SET_COOKIE)
.map(|v| v.to_owned())
.collect();
headers.remove(header::SET_COOKIE);
let mut count: usize = 0;
for v in vals {
if let Ok(s) = v.to_str() {
if let Ok(c) = Cookie::parse_encoded(s) {
if c.name() == name {
count += 1;
continue;
}
}
}
// put set-cookie header head back if it does not validate
headers.append(header::SET_COOKIE, v);
}
count
}
/// Connection upgrade status
#[inline]
pub fn upgrade(&self) -> bool {
self.res.upgrade()
}
/// Keep-alive status for this connection
pub fn keep_alive(&self) -> bool {
self.res.keep_alive()
}
/// Returns reference to the response-local data/extensions container.
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
self.res.extensions()
}
/// Returns reference to the response-local data/extensions container.
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
self.res.extensions_mut()
}
/// Returns a reference to this response's body.
#[inline]
pub fn body(&self) -> &B {
self.res.body()
}
/// Sets new body.
pub fn set_body<B2>(self, body: B2) -> HttpResponse<B2> {
HttpResponse {
res: self.res.set_body(body),
error: self.error,
}
}
/// Returns split head and body.
///
/// # Implementation Notes
/// Due to internal performance optimizations, the first element of the returned tuple is an
/// `HttpResponse` as well but only contains the head of the response this was called on.
pub fn into_parts(self) -> (HttpResponse<()>, B) {
let (head, body) = self.res.into_parts();
(
HttpResponse {
res: head,
error: self.error,
},
body,
)
}
/// Drops body and returns new response.
pub fn drop_body(self) -> HttpResponse<()> {
HttpResponse {
res: self.res.drop_body(),
error: self.error,
}
}
/// Map the current body type to another using a closure, returning a new response.
///
/// Closure receives the response head and the current body type.
pub fn map_body<F, B2>(self, f: F) -> HttpResponse<B2>
where
F: FnOnce(&mut ResponseHead, B) -> B2,
{
HttpResponse {
res: self.res.map_body(f),
error: self.error,
}
}
/// Map the current body type `B` to `EitherBody::Left(B)`.
///
/// Useful for middleware which can generate their own responses.
#[inline]
pub fn map_into_left_body<R>(self) -> HttpResponse<EitherBody<B, R>> {
self.map_body(|_, body| EitherBody::left(body))
}
/// Map the current body type `B` to `EitherBody::Right(B)`.
///
/// Useful for middleware which can generate their own responses.
#[inline]
pub fn map_into_right_body<L>(self) -> HttpResponse<EitherBody<L, B>> {
self.map_body(|_, body| EitherBody::right(body))
}
/// Map the current body to a type-erased `BoxBody`.
#[inline]
pub fn map_into_boxed_body(self) -> HttpResponse<BoxBody>
where
B: MessageBody + 'static,
{
self.map_body(|_, body| body.boxed())
}
/// Returns the response body, dropping all other parts.
pub fn into_body(self) -> B {
self.res.into_body()
}
}
impl<B> fmt::Debug for HttpResponse<B>
where
B: MessageBody,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HttpResponse")
.field("error", &self.error)
.field("res", &self.res)
.finish()
}
}
impl<B> From<Response<B>> for HttpResponse<B> {
fn from(res: Response<B>) -> Self {
HttpResponse { res, error: None }
}
}
impl From<Error> for HttpResponse {
fn from(err: Error) -> Self {
HttpResponse::from_error(err)
}
}
impl<B> From<HttpResponse<B>> for Response<B> {
fn from(res: HttpResponse<B>) -> Self {
// this impl will always be called as part of dispatcher
// TODO: expose cause somewhere?
// if let Some(err) = res.error {
// return Response::from_error(err);
// }
res.res
}
}
// Rationale for cfg(test): this impl causes false positives on a clippy lint (async_yields_async)
// when returning an HttpResponse from an async function/closure and it's not very useful outside of
// tests anyway.
#[cfg(test)]
mod response_fut_impl {
use std::{
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use super::*;
// Future is only implemented for BoxBody payload type because it's the most useful for making
// simple handlers without async blocks. Making it generic over all MessageBody types requires a
// future impl on Response which would cause it's body field to be, undesirably, Option<B>.
//
// This impl is not particularly efficient due to the Response construction and should probably
// not be invoked if performance is important. Prefer an async fn/block in such cases.
impl Future for HttpResponse<BoxBody> {
type Output = Result<Response<BoxBody>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(err) = self.error.take() {
return Poll::Ready(Err(err));
}
Poll::Ready(Ok(mem::replace(
&mut self.res,
Response::new(StatusCode::default()),
)))
}
}
}
impl<B> Responder for HttpResponse<B>
where
B: MessageBody + 'static,
{
type Body = B;
#[inline]
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
self
}
}
#[cfg(feature = "cookies")]
pub struct CookieIter<'a> {
iter: std::slice::Iter<'a, HeaderValue>,
}
#[cfg(feature = "cookies")]
impl<'a> Iterator for CookieIter<'a> {
type Item = Cookie<'a>;
#[inline]
fn next(&mut self) -> Option<Cookie<'a>> {
for v in self.iter.by_ref() {
if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) {
return Some(c);
}
}
None
}
}
#[cfg(test)]
mod tests {
use static_assertions::assert_impl_all;
use super::*;
use crate::http::header::{HeaderValue, COOKIE};
assert_impl_all!(HttpResponse: Responder);
assert_impl_all!(HttpResponse<String>: Responder);
assert_impl_all!(HttpResponse<&'static str>: Responder);
assert_impl_all!(HttpResponse<crate::body::None>: Responder);
#[test]
fn test_debug() {
let resp = HttpResponse::Ok()
.append_header((COOKIE, HeaderValue::from_static("cookie1=value1; ")))
.append_header((COOKIE, HeaderValue::from_static("cookie2=value2; ")))
.finish();
let dbg = format!("{:?}", resp);
assert!(dbg.contains("HttpResponse"));
}
}
#[cfg(test)]
#[cfg(feature = "cookies")]
mod cookie_tests {
use super::*;
#[test]
fn removal_cookies() {
let mut res = HttpResponse::Ok().finish();
let cookie = Cookie::new("foo", "");
res.add_removal_cookie(&cookie).unwrap();
let set_cookie_hdr = res.headers().get(header::SET_COOKIE).unwrap();
assert_eq!(
&set_cookie_hdr.as_bytes()[..25],
&b"foo=; Max-Age=0; Expires="[..],
"unexpected set-cookie value: {:?}",
set_cookie_hdr.to_str()
);
}
}

560
actix-web/src/rmap.rs Normal file
View File

@ -0,0 +1,560 @@
use std::{
borrow::Cow,
cell::RefCell,
fmt::Write as _,
rc::{Rc, Weak},
};
use actix_router::ResourceDef;
use ahash::AHashMap;
use url::Url;
use crate::{error::UrlGenerationError, request::HttpRequest};
const AVG_PATH_LEN: usize = 24;
#[derive(Clone, Debug)]
pub struct ResourceMap {
pattern: ResourceDef,
/// Named resources within the tree or, for external resources, it points to isolated nodes
/// outside the tree.
named: AHashMap<String, Rc<ResourceMap>>,
parent: RefCell<Weak<ResourceMap>>,
/// Must be `None` for "edge" nodes.
nodes: Option<Vec<Rc<ResourceMap>>>,
}
impl ResourceMap {
/// Creates a _container_ node in the `ResourceMap` tree.
pub fn new(root: ResourceDef) -> Self {
ResourceMap {
pattern: root,
named: AHashMap::default(),
parent: RefCell::new(Weak::new()),
nodes: Some(Vec::new()),
}
}
/// Format resource map as tree structure (unfinished).
#[allow(dead_code)]
pub(crate) fn tree(&self) -> String {
let mut buf = String::new();
self._tree(&mut buf, 0);
buf
}
pub(crate) fn _tree(&self, buf: &mut String, level: usize) {
if let Some(children) = &self.nodes {
for child in children {
writeln!(
buf,
"{}{} {}",
"--".repeat(level),
child.pattern.pattern().unwrap(),
child
.pattern
.name()
.map(|name| format!("({})", name))
.unwrap_or_else(|| "".to_owned())
)
.unwrap();
ResourceMap::_tree(child, buf, level + 1);
}
}
}
/// Adds a (possibly nested) resource.
///
/// To add a non-prefix pattern, `nested` must be `None`.
/// To add external resource, supply a pattern without a leading `/`.
/// The root pattern of `nested`, if present, should match `pattern`.
pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) {
pattern.set_id(self.nodes.as_ref().unwrap().len() as u16);
if let Some(new_node) = nested {
debug_assert_eq!(
&new_node.pattern, pattern,
"`pattern` and `nested` mismatch"
);
// parents absorb references to the named resources of children
self.named.extend(new_node.named.clone().into_iter());
self.nodes.as_mut().unwrap().push(new_node);
} else {
let new_node = Rc::new(ResourceMap {
pattern: pattern.clone(),
named: AHashMap::default(),
parent: RefCell::new(Weak::new()),
nodes: None,
});
if let Some(name) = pattern.name() {
self.named.insert(name.to_owned(), Rc::clone(&new_node));
}
let is_external = match pattern.pattern() {
Some(p) => !p.is_empty() && !p.starts_with('/'),
None => false,
};
// don't add external resources to the tree
if !is_external {
self.nodes.as_mut().unwrap().push(new_node);
}
}
}
pub(crate) fn finish(self: &Rc<Self>) {
for node in self.nodes.iter().flatten() {
node.parent.replace(Rc::downgrade(self));
ResourceMap::finish(node);
}
}
/// Generate URL for named resource.
///
/// Check [`HttpRequest::url_for`] for detailed information.
pub fn url_for<U, I>(
&self,
req: &HttpRequest,
name: &str,
elements: U,
) -> Result<Url, UrlGenerationError>
where
U: IntoIterator<Item = I>,
I: AsRef<str>,
{
let mut elements = elements.into_iter();
let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| {
node.pattern
.resource_path_from_iter(&mut acc, &mut elements)
.then(|| acc)
})
.ok_or(UrlGenerationError::NotEnoughElements)?;
let (base, path): (Cow<'_, _>, _) = if path.starts_with('/') {
// build full URL from connection info parts and resource path
let conn = req.connection_info();
let base = format!("{}://{}", conn.scheme(), conn.host());
(Cow::Owned(base), path.as_str())
} else {
// external resource; third slash would be the root slash in the path
let third_slash_index = path
.char_indices()
.filter_map(|(i, c)| (c == '/').then(|| i))
.nth(2)
.unwrap_or_else(|| path.len());
(
Cow::Borrowed(&path[..third_slash_index]),
&path[third_slash_index..],
)
};
let mut url = Url::parse(&base)?;
url.set_path(path);
Ok(url)
}
/// Returns true if there is a resource that would match `path`.
pub fn has_resource(&self, path: &str) -> bool {
self.find_matching_node(path).is_some()
}
/// Returns the name of the route that matches the given path or None if no full match
/// is possible or the matching resource is not named.
pub fn match_name(&self, path: &str) -> Option<&str> {
self.find_matching_node(path)?.pattern.name()
}
/// Returns the full resource pattern matched against a path or None if no full match
/// is possible.
pub fn match_pattern(&self, path: &str) -> Option<String> {
self.find_matching_node(path)?.root_rmap_fn(
String::with_capacity(AVG_PATH_LEN),
|mut acc, node| {
let pattern = node.pattern.pattern()?;
acc.push_str(pattern);
Some(acc)
},
)
}
fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
self._find_matching_node(path).flatten()
}
/// Returns `None` if root pattern doesn't match;
/// `Some(None)` if root pattern matches but there is no matching child pattern.
/// Don't search sideways when `Some(none)` is returned.
fn _find_matching_node(&self, path: &str) -> Option<Option<&ResourceMap>> {
let matched_len = self.pattern.find_match(path)?;
let path = &path[matched_len..];
Some(match &self.nodes {
// find first sub-node to match remaining path
Some(nodes) => nodes
.iter()
.filter_map(|node| node._find_matching_node(path))
.next()
.flatten(),
// only terminate at edge nodes
None => Some(self),
})
}
/// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
fn root_rmap_fn<F, B>(&self, init: B, mut f: F) -> Option<B>
where
F: FnMut(B, &ResourceMap) -> Option<B>,
{
self._root_rmap_fn(init, &mut f)
}
/// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
fn _root_rmap_fn<F, B>(&self, init: B, f: &mut F) -> Option<B>
where
F: FnMut(B, &ResourceMap) -> Option<B>,
{
let data = match self.parent.borrow().upgrade() {
Some(ref parent) => parent._root_rmap_fn(init, f)?,
None => init,
};
f(data, self)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extract_matched_pattern() {
let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None);
user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
user_map.add(
&mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
None,
);
root.add(&mut ResourceDef::new("/info"), None);
root.add(&mut ResourceDef::new("/v{version:[[:digit:]]{1}}"), None);
root.add(
&mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)),
);
root.add(&mut ResourceDef::new("/info"), None);
let root = Rc::new(root);
ResourceMap::finish(&root);
// sanity check resource map setup
assert!(root.has_resource("/info"));
assert!(!root.has_resource("/bar"));
assert!(root.has_resource("/v1"));
assert!(root.has_resource("/v2"));
assert!(!root.has_resource("/v33"));
assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/profile"));
// extract patterns from paths
assert!(root.match_pattern("/bar").is_none());
assert!(root.match_pattern("/v44").is_none());
assert_eq!(root.match_pattern("/info"), Some("/info".to_owned()));
assert_eq!(
root.match_pattern("/v1"),
Some("/v{version:[[:digit:]]{1}}".to_owned())
);
assert_eq!(
root.match_pattern("/v2"),
Some("/v{version:[[:digit:]]{1}}".to_owned())
);
assert_eq!(
root.match_pattern("/user/22/profile"),
Some("/user/{id}/profile".to_owned())
);
assert_eq!(
root.match_pattern("/user/602CFB82-7709-4B17-ADCF-4C347B6F2203/profile"),
Some("/user/{id}/profile".to_owned())
);
assert_eq!(
root.match_pattern("/user/22/article/44"),
Some("/user/{id}/article/{id}".to_owned())
);
assert_eq!(
root.match_pattern("/user/22/post/my-post"),
Some("/user/{id}/post/{post_id}".to_owned())
);
assert_eq!(
root.match_pattern("/user/22/post/other-post/comment/42"),
Some("/user/{id}/post/{post_id}/comment/{comment_id}".to_owned())
);
}
#[test]
fn extract_matched_name() {
let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut rdef = ResourceDef::new("/info");
rdef.set_name("root_info");
root.add(&mut rdef, None);
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
let mut rdef = ResourceDef::new("/");
user_map.add(&mut rdef, None);
let mut rdef = ResourceDef::new("/post/{post_id}");
rdef.set_name("user_post");
user_map.add(&mut rdef, None);
root.add(
&mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)),
);
let root = Rc::new(root);
ResourceMap::finish(&root);
// sanity check resource map setup
assert!(root.has_resource("/info"));
assert!(!root.has_resource("/bar"));
assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/post/55"));
// extract patterns from paths
assert!(root.match_name("/bar").is_none());
assert!(root.match_name("/v44").is_none());
assert_eq!(root.match_name("/info"), Some("root_info"));
assert_eq!(root.match_name("/user/22"), None);
assert_eq!(root.match_name("/user/22/"), None);
assert_eq!(root.match_name("/user/22/post/55"), Some("user_post"));
}
#[test]
fn bug_fix_issue_1582_debug_print_exits() {
// ref: https://github.com/actix/actix-web/issues/1582
let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None);
user_map.add(&mut ResourceDef::new("/post/{post_id}"), None);
user_map.add(
&mut ResourceDef::new("/post/{post_id}/comment/{comment_id}"),
None,
);
root.add(
&mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)),
);
let root = Rc::new(root);
ResourceMap::finish(&root);
// check root has no parent
assert!(root.parent.borrow().upgrade().is_none());
// check child has parent reference
assert!(root.nodes.as_ref().unwrap()[0]
.parent
.borrow()
.upgrade()
.is_some());
// check child's parent root id matches root's root id
assert!(Rc::ptr_eq(
&root.nodes.as_ref().unwrap()[0]
.parent
.borrow()
.upgrade()
.unwrap(),
&root
));
let output = format!("{:?}", root);
assert!(output.starts_with("ResourceMap {"));
assert!(output.ends_with(" }"));
}
#[test]
fn short_circuit() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_root = ResourceDef::prefix("/user");
let mut user_map = ResourceMap::new(user_root.clone());
user_map.add(&mut ResourceDef::new("/u1"), None);
user_map.add(&mut ResourceDef::new("/u2"), None);
root.add(&mut ResourceDef::new("/user/u3"), None);
root.add(&mut user_root, Some(Rc::new(user_map)));
root.add(&mut ResourceDef::new("/user/u4"), None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(rmap.has_resource("/user/u1"));
assert!(rmap.has_resource("/user/u2"));
assert!(rmap.has_resource("/user/u3"));
assert!(!rmap.has_resource("/user/u4"));
}
#[test]
fn url_for() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_scope_rdef = ResourceDef::prefix("/user");
let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone());
let mut user_rdef = ResourceDef::new("/{user_id}");
let mut user_map = ResourceMap::new(user_rdef.clone());
let mut post_rdef = ResourceDef::new("/post/{sub_id}");
post_rdef.set_name("post");
user_map.add(&mut post_rdef, None);
user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map)));
root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map)));
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
let url = rmap
.url_for(&req, "post", &["u123", "foobar"])
.unwrap()
.to_string();
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
}
#[test]
fn url_for_parser() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef_1 = ResourceDef::new("/{var}");
rdef_1.set_name("internal");
let mut rdef_2 = ResourceDef::new("http://host.dom/{var}");
rdef_2.set_name("external.1");
let mut rdef_3 = ResourceDef::new("{var}");
rdef_3.set_name("external.2");
root.add(&mut rdef_1, None);
root.add(&mut rdef_2, None);
root.add(&mut rdef_3, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
const INPUT: &[&str] = &["a/../quick brown%20fox/%nan?query#frag"];
const OUTPUT: &str = "/quick%20brown%20fox/%nan%3Fquery%23frag";
let url = rmap.url_for(&req, "internal", INPUT).unwrap();
assert_eq!(url.path(), OUTPUT);
let url = rmap.url_for(&req, "external.1", INPUT).unwrap();
assert_eq!(url.path(), OUTPUT);
assert!(rmap.url_for(&req, "external.2", INPUT).is_err());
assert!(rmap.url_for(&req, "external.2", &[""]).is_err());
}
#[test]
fn external_resource_with_no_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
}
#[test]
fn external_resource_with_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
rdef.set_name("duck");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
assert_eq!(
rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(),
"https://duck.com/abcd"
);
}
#[test]
fn url_for_override_within_map() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut foo_rdef = ResourceDef::prefix("/foo");
let mut foo_map = ResourceMap::new(foo_rdef.clone());
let mut nested_rdef = ResourceDef::new("/nested");
nested_rdef.set_name("nested");
foo_map.add(&mut nested_rdef, None);
root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
let mut foo_rdef = ResourceDef::prefix("/bar");
let mut foo_map = ResourceMap::new(foo_rdef.clone());
let mut nested_rdef = ResourceDef::new("/nested");
nested_rdef.set_name("nested");
foo_map.add(&mut nested_rdef, None);
root.add(&mut foo_rdef, Some(Rc::new(foo_map)));
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let req = crate::test::TestRequest::default().to_http_request();
let url = rmap.url_for(&req, "nested", &[""; 0]).unwrap().to_string();
assert_eq!(url, "http://localhost:8080/bar/nested");
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
}
}

386
actix-web/src/route.rs Normal file
View File

@ -0,0 +1,386 @@
use std::{mem, rc::Rc};
use actix_http::Method;
use actix_service::{
boxed::{self, BoxService},
fn_service, Service, ServiceFactory, ServiceFactoryExt,
};
use futures_core::future::LocalBoxFuture;
use crate::{
guard::{self, Guard},
handler::{handler_service, Handler},
service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse},
Error, FromRequest, HttpResponse, Responder,
};
/// A request handler with [guards](guard).
///
/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found`
/// handler is used.
pub struct Route {
service: BoxedHttpServiceFactory,
guards: Rc<Vec<Box<dyn Guard>>>,
}
impl Route {
/// Create new route which matches any request.
#[allow(clippy::new_without_default)]
pub fn new() -> Route {
Route {
service: boxed::factory(fn_service(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::NotFound()))
})),
guards: Rc::new(Vec::new()),
}
}
pub(crate) fn take_guards(&mut self) -> Vec<Box<dyn Guard>> {
mem::take(Rc::get_mut(&mut self.guards).unwrap())
}
}
impl ServiceFactory<ServiceRequest> for Route {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type Service = RouteService;
type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
let fut = self.service.new_service(());
let guards = self.guards.clone();
Box::pin(async move {
let service = fut.await?;
Ok(RouteService { service, guards })
})
}
}
pub struct RouteService {
service: BoxService<ServiceRequest, ServiceResponse, Error>,
guards: Rc<Vec<Box<dyn Guard>>>,
}
impl RouteService {
// TODO: does this need to take &mut ?
pub fn check(&self, req: &mut ServiceRequest) -> bool {
let guard_ctx = req.guard_ctx();
for guard in self.guards.iter() {
if !guard.check(&guard_ctx) {
return false;
}
}
true
}
}
impl Service<ServiceRequest> for RouteService {
type Response = ServiceResponse;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
self.service.call(req)
}
}
impl Route {
/// Add method guard to the route.
///
/// # Examples
/// ```
/// # use actix_web::*;
/// # fn main() {
/// App::new().service(web::resource("/path").route(
/// web::get()
/// .method(http::Method::CONNECT)
/// .guard(guard::Header("content-type", "text/plain"))
/// .to(|req: HttpRequest| HttpResponse::Ok()))
/// );
/// # }
/// ```
pub fn method(mut self, method: Method) -> Self {
Rc::get_mut(&mut self.guards)
.unwrap()
.push(Box::new(guard::Method(method)));
self
}
/// Add guard to the route.
///
/// # Examples
/// ```
/// # use actix_web::*;
/// # fn main() {
/// App::new().service(web::resource("/path").route(
/// web::route()
/// .guard(guard::Get())
/// .guard(guard::Header("content-type", "text/plain"))
/// .to(|req: HttpRequest| HttpResponse::Ok()))
/// );
/// # }
/// ```
pub fn guard<F: Guard + 'static>(mut self, f: F) -> Self {
Rc::get_mut(&mut self.guards).unwrap().push(Box::new(f));
self
}
/// Set handler function, use request extractors for parameters.
///
/// # Examples
/// ```
/// use actix_web::{web, http, App};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// extract path info using serde
/// async fn index(info: web::Path<Info>) -> String {
/// format!("Welcome {}!", info.username)
/// }
///
/// let app = App::new().service(
/// web::resource("/{username}/index.html") // <- define path parameters
/// .route(web::get().to(index)) // <- register handler
/// );
/// ```
///
/// It is possible to use multiple extractors for one handler function.
/// ```
/// # use std::collections::HashMap;
/// # use serde::Deserialize;
/// use actix_web::{web, App};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// extract path info using serde
/// async fn index(
/// path: web::Path<Info>,
/// query: web::Query<HashMap<String, String>>,
/// body: web::Json<Info>
/// ) -> String {
/// format!("Welcome {}!", path.username)
/// }
///
/// let app = App::new().service(
/// web::resource("/{username}/index.html") // <- define path parameters
/// .route(web::get().to(index))
/// );
/// ```
pub fn to<F, Args>(mut self, handler: F) -> Self
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
self.service = handler_service(handler);
self
}
/// Set raw service to be constructed and called as the request handler.
///
/// # Examples
/// ```
/// # use std::convert::Infallible;
/// # use futures_util::future::LocalBoxFuture;
/// # use actix_web::{*, dev::*, http::header};
/// struct HelloWorld;
///
/// impl Service<ServiceRequest> for HelloWorld {
/// type Response = ServiceResponse;
/// type Error = Infallible;
/// type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
///
/// dev::always_ready!();
///
/// fn call(&self, req: ServiceRequest) -> Self::Future {
/// let (req, _) = req.into_parts();
///
/// let res = HttpResponse::Ok()
/// .insert_header(header::ContentType::plaintext())
/// .body("Hello world!");
///
/// Box::pin(async move { Ok(ServiceResponse::new(req, res)) })
/// }
/// }
///
/// App::new().route(
/// "/",
/// web::get().service(fn_factory(|| async { Ok(HelloWorld) })),
/// );
/// ```
pub fn service<S, E>(mut self, service_factory: S) -> Self
where
S: ServiceFactory<
ServiceRequest,
Response = ServiceResponse,
Error = E,
InitError = (),
Config = (),
> + 'static,
E: Into<Error> + 'static,
{
self.service = boxed::factory(service_factory.map_err(Into::into));
self
}
}
#[cfg(test)]
mod tests {
use std::{convert::Infallible, time::Duration};
use actix_rt::time::sleep;
use bytes::Bytes;
use futures_core::future::LocalBoxFuture;
use serde::Serialize;
use crate::dev::{always_ready, fn_factory, fn_service, Service};
use crate::http::{header, Method, StatusCode};
use crate::service::{ServiceRequest, ServiceResponse};
use crate::test::{call_service, init_service, read_body, TestRequest};
use crate::{error, web, App, HttpResponse};
#[derive(Serialize, PartialEq, Debug)]
struct MyObject {
name: String,
}
#[actix_rt::test]
async fn test_route() {
let srv = init_service(
App::new()
.service(
web::resource("/test")
.route(web::get().to(HttpResponse::Ok))
.route(web::put().to(|| async {
Err::<HttpResponse, _>(error::ErrorBadRequest("err"))
}))
.route(web::post().to(|| async {
sleep(Duration::from_millis(100)).await;
Ok::<_, Infallible>(HttpResponse::Created())
}))
.route(web::delete().to(|| async {
sleep(Duration::from_millis(100)).await;
Err::<HttpResponse, _>(error::ErrorBadRequest("err"))
})),
)
.service(web::resource("/json").route(web::get().to(|| async {
sleep(Duration::from_millis(25)).await;
web::Json(MyObject {
name: "test".to_string(),
})
}))),
)
.await;
let req = TestRequest::with_uri("/test")
.method(Method::GET)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/test")
.method(Method::POST)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/test")
.method(Method::PUT)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/test")
.method(Method::DELETE)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/test")
.method(Method::HEAD)
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let req = TestRequest::with_uri("/json").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}"));
}
#[actix_rt::test]
async fn test_service_handler() {
struct HelloWorld;
impl Service<ServiceRequest> for HelloWorld {
type Response = ServiceResponse;
type Error = crate::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts();
let res = HttpResponse::Ok()
.insert_header(header::ContentType::plaintext())
.body("Hello world!");
Box::pin(async move { Ok(ServiceResponse::new(req, res)) })
}
}
let srv = init_service(
App::new()
.route(
"/hello",
web::get().service(fn_factory(|| async { Ok(HelloWorld) })),
)
.route(
"/bye",
web::get().service(fn_factory(|| async {
Ok::<_, ()>(fn_service(|req: ServiceRequest| async {
let (req, _) = req.into_parts();
let res = HttpResponse::Ok()
.insert_header(header::ContentType::plaintext())
.body("Goodbye, and thanks for all the fish!");
Ok::<_, Infallible>(ServiceResponse::new(req, res))
}))
})),
),
)
.await;
let req = TestRequest::get().uri("/hello").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"Hello world!"));
let req = TestRequest::get().uri("/bye").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let body = read_body(resp).await;
assert_eq!(
body,
Bytes::from_static(b"Goodbye, and thanks for all the fish!")
);
}
}

1233
actix-web/src/scope.rs Normal file

File diff suppressed because it is too large Load Diff

688
actix-web/src/server.rs Normal file
View File

@ -0,0 +1,688 @@
use std::{
any::Any,
cmp, fmt, io,
marker::PhantomData,
net,
sync::{Arc, Mutex},
time::Duration,
};
use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response};
use actix_server::{Server, ServerBuilder};
use actix_service::{
map_config, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
};
#[cfg(feature = "openssl")]
use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder};
#[cfg(feature = "rustls")]
use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig;
use crate::{config::AppConfig, Error};
struct Socket {
scheme: &'static str,
addr: net::SocketAddr,
}
struct Config {
host: Option<String>,
keep_alive: KeepAlive,
client_request_timeout: Duration,
client_disconnect_timeout: Duration,
}
/// An HTTP Server.
///
/// Create new HTTP server with application factory.
///
/// ```no_run
/// use actix_web::{web, App, HttpResponse, HttpServer};
///
/// #[actix_rt::main]
/// async fn main() -> std::io::Result<()> {
/// HttpServer::new(
/// || App::new()
/// .service(web::resource("/").to(|| HttpResponse::Ok())))
/// .bind("127.0.0.1:59090")?
/// .run()
/// .await
/// }
/// ```
pub struct HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(super) factory: F,
config: Arc<Mutex<Config>>,
backlog: u32,
sockets: Vec<Socket>,
builder: ServerBuilder,
#[allow(clippy::type_complexity)]
on_connect_fn: Option<Arc<dyn Fn(&dyn Any, &mut Extensions) + Send + Sync>>,
_phantom: PhantomData<(S, B)>,
}
impl<F, I, S, B> HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
S::Service: 'static,
B: MessageBody + 'static,
{
/// Create new HTTP server with application factory
pub fn new(factory: F) -> Self {
HttpServer {
factory,
config: Arc::new(Mutex::new(Config {
host: None,
keep_alive: KeepAlive::default(),
client_request_timeout: Duration::from_secs(5),
client_disconnect_timeout: Duration::from_secs(1),
})),
backlog: 1024,
sockets: Vec::new(),
builder: ServerBuilder::default(),
on_connect_fn: None,
_phantom: PhantomData,
}
}
/// Sets function that will be called once before each connection is handled.
/// It will receive a `&std::any::Any`, which contains underlying connection type and an
/// [Extensions] container so that connection data can be accessed in middleware and handlers.
///
/// # Connection Types
/// - `actix_tls::accept::openssl::TlsStream<actix_web::rt::net::TcpStream>` when using openssl.
/// - `actix_tls::accept::rustls::TlsStream<actix_web::rt::net::TcpStream>` when using rustls.
/// - `actix_web::rt::net::TcpStream` when no encryption is used.
///
/// See the `on_connect` example for additional details.
pub fn on_connect<CB>(self, f: CB) -> HttpServer<F, I, S, B>
where
CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static,
{
HttpServer {
factory: self.factory,
config: self.config,
backlog: self.backlog,
sockets: self.sockets,
builder: self.builder,
on_connect_fn: Some(Arc::new(f)),
_phantom: PhantomData,
}
}
/// Set number of workers to start.
///
/// By default, server uses number of available logical CPU as thread count.
pub fn workers(mut self, num: usize) -> Self {
self.builder = self.builder.workers(num);
self
}
/// Set the maximum number of pending connections.
///
/// This refers to the number of clients that can be waiting to be served.
/// Exceeding this number results in the client getting an error when
/// attempting to connect. It should only affect servers under significant
/// load.
///
/// Generally set in the 64-2048 range. Default value is 2048.
///
/// This method should be called before `bind()` method call.
pub fn backlog(mut self, backlog: u32) -> Self {
self.backlog = backlog;
self.builder = self.builder.backlog(backlog);
self
}
/// Sets the maximum per-worker number of concurrent connections.
///
/// All socket listeners will stop accepting connections when this limit is reached for
/// each worker.
///
/// By default max connections is set to a 25k.
pub fn max_connections(mut self, num: usize) -> Self {
self.builder = self.builder.max_concurrent_connections(num);
self
}
/// Sets the maximum per-worker concurrent connection establish process.
///
/// All listeners will stop accepting connections when this limit is reached. It can be used to
/// limit the global TLS CPU usage.
///
/// By default max connections is set to a 256.
#[allow(unused_variables)]
pub fn max_connection_rate(self, num: usize) -> Self {
#[cfg(any(feature = "rustls", feature = "openssl"))]
actix_tls::accept::max_concurrent_tls_connect(num);
self
}
/// Set max number of threads for each worker's blocking task thread pool.
///
/// One thread pool is set up **per worker**; not shared across workers.
///
/// By default set to 512 / workers.
pub fn worker_max_blocking_threads(mut self, num: usize) -> Self {
self.builder = self.builder.worker_max_blocking_threads(num);
self
}
/// Set server keep-alive setting.
///
/// By default keep alive is set to a 5 seconds.
pub fn keep_alive<T: Into<KeepAlive>>(self, val: T) -> Self {
self.config.lock().unwrap().keep_alive = val.into();
self
}
/// Set server client timeout in milliseconds for first request.
///
/// Defines a timeout for reading client request header. If a client does not transmit
/// the entire set headers within this time, the request is terminated with
/// the 408 (Request Time-out) error.
///
/// To disable timeout set value to 0.
///
/// By default client timeout is set to 5000 milliseconds.
pub fn client_request_timeout(self, dur: Duration) -> Self {
self.config.lock().unwrap().client_request_timeout = dur;
self
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `client_request_timeout`.")]
pub fn client_timeout(self, dur: Duration) -> Self {
self.client_request_timeout(dur)
}
/// Set server connection shutdown timeout in milliseconds.
///
/// Defines a timeout for shutdown connection. If a shutdown procedure does not complete
/// within this time, the request is dropped.
///
/// To disable timeout set value to 0.
///
/// By default client timeout is set to 5000 milliseconds.
pub fn client_disconnect_timeout(self, dur: Duration) -> Self {
self.config.lock().unwrap().client_disconnect_timeout = dur;
self
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `client_request_timeout`.")]
pub fn client_shutdown(self, dur: u64) -> Self {
self.client_disconnect_timeout(Duration::from_millis(dur))
}
/// Set server host name.
///
/// Host name is used by application router as a hostname for url generation.
/// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
/// documentation for more information.
///
/// By default host name is set to a "localhost" value.
pub fn server_hostname<T: AsRef<str>>(self, val: T) -> Self {
self.config.lock().unwrap().host = Some(val.as_ref().to_owned());
self
}
/// Stop Actix `System` after server shutdown.
pub fn system_exit(mut self) -> Self {
self.builder = self.builder.system_exit();
self
}
/// Disable signal handling
pub fn disable_signals(mut self) -> Self {
self.builder = self.builder.disable_signals();
self
}
/// Timeout for graceful workers shutdown.
///
/// After receiving a stop signal, workers have this much time to finish
/// serving requests. Workers still alive after the timeout are force
/// dropped.
///
/// By default shutdown timeout sets to 30 seconds.
pub fn shutdown_timeout(mut self, sec: u64) -> Self {
self.builder = self.builder.shutdown_timeout(sec);
self
}
/// Get addresses of bound sockets.
pub fn addrs(&self) -> Vec<net::SocketAddr> {
self.sockets.iter().map(|s| s.addr).collect()
}
/// Get addresses of bound sockets and the scheme for it.
///
/// This is useful when the server is bound from different sources
/// with some sockets listening on HTTP and some listening on HTTPS
/// and the user should be presented with an enumeration of which
/// socket requires which protocol.
pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> {
self.sockets.iter().map(|s| (s.addr, s.scheme)).collect()
}
/// Use listener for accepting incoming connection requests
///
/// HttpServer does not change any configuration for TcpListener,
/// it needs to be configured before passing it to listen() method.
pub fn listen(mut self, lst: net::TcpListener) -> io::Result<Self> {
let cfg = self.config.clone();
let factory = self.factory.clone();
let addr = lst.local_addr().unwrap();
self.sockets.push(Socket {
addr,
scheme: "http",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder =
self.builder
.listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let mut svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout)
.local_addr(addr);
if let Some(handler) = on_connect_fn.clone() {
svc = svc.on_connect_ext(move |io: &_, ext: _| {
(handler)(io as &dyn Any, ext)
})
};
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| {
AppConfig::new(false, host.clone(), addr)
}))
.tcp()
})?;
Ok(self)
}
#[cfg(feature = "openssl")]
/// Use listener for accepting incoming tls connection requests
///
/// This method sets alpn protocols to "h2" and "http/1.1"
pub fn listen_openssl(
self,
lst: net::TcpListener,
builder: SslAcceptorBuilder,
) -> io::Result<Self> {
self.listen_ssl_inner(lst, openssl_acceptor(builder)?)
}
#[cfg(feature = "openssl")]
fn listen_ssl_inner(
mut self,
lst: net::TcpListener,
acceptor: SslAcceptor,
) -> io::Result<Self> {
let factory = self.factory.clone();
let cfg = self.config.clone();
let addr = lst.local_addr().unwrap();
self.sockets.push(Socket {
addr,
scheme: "https",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder =
self.builder
.listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout)
.local_addr(addr);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| {
(&*handler)(io as &dyn Any, ext)
})
} else {
svc
};
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr)
}))
.openssl(acceptor.clone())
})?;
Ok(self)
}
#[cfg(feature = "rustls")]
/// Use listener for accepting incoming tls connection requests
///
/// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
pub fn listen_rustls(
self,
lst: net::TcpListener,
config: RustlsServerConfig,
) -> io::Result<Self> {
self.listen_rustls_inner(lst, config)
}
#[cfg(feature = "rustls")]
fn listen_rustls_inner(
mut self,
lst: net::TcpListener,
config: RustlsServerConfig,
) -> io::Result<Self> {
let factory = self.factory.clone();
let cfg = self.config.clone();
let addr = lst.local_addr().unwrap();
self.sockets.push(Socket {
addr,
scheme: "https",
});
let on_connect_fn = self.on_connect_fn.clone();
self.builder =
self.builder
.listen(format!("actix-web-service-{}", addr), lst, move || {
let c = cfg.lock().unwrap();
let host = c.host.clone().unwrap_or_else(|| format!("{}", addr));
let svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout);
let svc = if let Some(handler) = on_connect_fn.clone() {
svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext))
} else {
svc
};
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| {
AppConfig::new(true, host.clone(), addr)
}))
.rustls(config.clone())
})?;
Ok(self)
}
/// The socket address to bind
///
/// To bind multiple addresses this method can be called multiple times.
pub fn bind<A: net::ToSocketAddrs>(mut self, addr: A) -> io::Result<Self> {
let sockets = self.bind2(addr)?;
for lst in sockets {
self = self.listen(lst)?;
}
Ok(self)
}
fn bind2<A: net::ToSocketAddrs>(&self, addr: A) -> io::Result<Vec<net::TcpListener>> {
let mut err = None;
let mut success = false;
let mut sockets = Vec::new();
for addr in addr.to_socket_addrs()? {
match create_tcp_listener(addr, self.backlog) {
Ok(lst) => {
success = true;
sockets.push(lst);
}
Err(e) => err = Some(e),
}
}
if success {
Ok(sockets)
} else if let Some(e) = err.take() {
Err(e)
} else {
Err(io::Error::new(
io::ErrorKind::Other,
"Can not bind to address.",
))
}
}
#[cfg(feature = "openssl")]
/// Start listening for incoming tls connections.
///
/// This method sets alpn protocols to "h2" and "http/1.1"
pub fn bind_openssl<A>(mut self, addr: A, builder: SslAcceptorBuilder) -> io::Result<Self>
where
A: net::ToSocketAddrs,
{
let sockets = self.bind2(addr)?;
let acceptor = openssl_acceptor(builder)?;
for lst in sockets {
self = self.listen_ssl_inner(lst, acceptor.clone())?;
}
Ok(self)
}
#[cfg(feature = "rustls")]
/// Start listening for incoming tls connections.
///
/// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
pub fn bind_rustls<A: net::ToSocketAddrs>(
mut self,
addr: A,
config: RustlsServerConfig,
) -> io::Result<Self> {
let sockets = self.bind2(addr)?;
for lst in sockets {
self = self.listen_rustls_inner(lst, config.clone())?;
}
Ok(self)
}
#[cfg(unix)]
/// Start listening for unix domain (UDS) connections on existing listener.
pub fn listen_uds(mut self, lst: std::os::unix::net::UnixListener) -> io::Result<Self> {
use actix_http::Protocol;
use actix_rt::net::UnixStream;
use actix_service::{fn_service, ServiceFactoryExt as _};
let cfg = self.config.clone();
let factory = self.factory.clone();
let socket_addr =
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
self.sockets.push(Socket {
scheme: "http",
addr: socket_addr,
});
let addr = lst.local_addr()?;
let name = format!("actix-web-service-{:?}", addr);
let on_connect_fn = self.on_connect_fn.clone();
self.builder = self.builder.listen_uds(name, lst, move || {
let c = cfg.lock().unwrap();
let config = AppConfig::new(
false,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr,
);
fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({
let mut svc = HttpService::build()
.keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout);
if let Some(handler) = on_connect_fn.clone() {
svc = svc
.on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext));
}
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
svc.finish(map_config(fac, move |_| config.clone()))
})
})?;
Ok(self)
}
/// Start listening for incoming unix domain connections.
#[cfg(unix)]
pub fn bind_uds<A>(mut self, addr: A) -> io::Result<Self>
where
A: AsRef<std::path::Path>,
{
use actix_http::Protocol;
use actix_rt::net::UnixStream;
use actix_service::{fn_service, ServiceFactoryExt as _};
let cfg = self.config.clone();
let factory = self.factory.clone();
let socket_addr =
net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080);
self.sockets.push(Socket {
scheme: "http",
addr: socket_addr,
});
self.builder = self.builder.bind_uds(
format!("actix-web-service-{:?}", addr.as_ref()),
addr,
move || {
let c = cfg.lock().unwrap();
let config = AppConfig::new(
false,
c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)),
socket_addr,
);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then(
HttpService::build()
.keep_alive(c.keep_alive)
.client_request_timeout(c.client_request_timeout)
.client_disconnect_timeout(c.client_disconnect_timeout)
.finish(map_config(fac, move |_| config.clone())),
)
},
)?;
Ok(self)
}
}
impl<F, I, S, B> HttpServer<F, I, S, B>
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
S::Service: 'static,
B: MessageBody,
{
/// Start listening for incoming connections.
///
/// This method starts number of HTTP workers in separate threads.
/// For each address this method starts separate thread which does
/// `accept()` in a loop.
///
/// This methods panics if no socket address can be bound or an `Actix` system is not yet
/// configured.
///
/// ```no_run
/// use std::io;
/// use actix_web::{web, App, HttpResponse, HttpServer};
///
/// #[actix_rt::main]
/// async fn main() -> io::Result<()> {
/// HttpServer::new(|| App::new().service(web::resource("/").to(|| HttpResponse::Ok())))
/// .bind("127.0.0.1:0")?
/// .run()
/// .await
/// }
/// ```
pub fn run(self) -> Server {
self.builder.run()
}
}
fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result<net::TcpListener> {
use socket2::{Domain, Protocol, Socket, Type};
let domain = Domain::for_address(addr);
let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?;
socket.set_reuse_address(true)?;
socket.bind(&addr.into())?;
// clamp backlog to max u32 that fits in i32 range
let backlog = cmp::min(backlog, i32::MAX as u32) as i32;
socket.listen(backlog)?;
Ok(net::TcpListener::from(socket))
}
/// Configure `SslAcceptorBuilder` with custom server flags.
#[cfg(feature = "openssl")]
fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result<SslAcceptor> {
builder.set_alpn_select_callback(|_, protocols| {
const H2: &[u8] = b"\x02h2";
const H11: &[u8] = b"\x08http/1.1";
if protocols.windows(3).any(|window| window == H2) {
Ok(b"h2")
} else if protocols.windows(9).any(|window| window == H11) {
Ok(b"http/1.1")
} else {
Err(AlpnError::NOACK)
}
});
builder.set_alpn_protos(b"\x08http/1.1\x02h2")?;
Ok(builder.build())
}

844
actix-web/src/service.rs Normal file
View File

@ -0,0 +1,844 @@
use std::{
cell::{Ref, RefMut},
fmt, net,
rc::Rc,
};
use actix_http::{
body::{BoxBody, EitherBody, MessageBody},
header::HeaderMap,
BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response,
ResponseHead, StatusCode, Uri, Version,
};
use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url};
use actix_service::{
boxed::{BoxService, BoxServiceFactory},
IntoServiceFactory, ServiceFactory,
};
#[cfg(feature = "cookies")]
use cookie::{Cookie, ParseError as CookieParseError};
use crate::{
config::{AppConfig, AppService},
dev::ensure_leading_slash,
guard::{Guard, GuardContext},
info::ConnectionInfo,
rmap::ResourceMap,
Error, HttpRequest, HttpResponse,
};
pub(crate) type BoxedHttpService = BoxService<ServiceRequest, ServiceResponse<BoxBody>, Error>;
pub(crate) type BoxedHttpServiceFactory =
BoxServiceFactory<(), ServiceRequest, ServiceResponse<BoxBody>, Error, ()>;
pub trait HttpServiceFactory {
fn register(self, config: &mut AppService);
}
impl<T: HttpServiceFactory> HttpServiceFactory for Vec<T> {
fn register(self, config: &mut AppService) {
self.into_iter()
.for_each(|factory| factory.register(config));
}
}
pub(crate) trait AppServiceFactory {
fn register(&mut self, config: &mut AppService);
}
pub(crate) struct ServiceFactoryWrapper<T> {
factory: Option<T>,
}
impl<T> ServiceFactoryWrapper<T> {
pub fn new(factory: T) -> Self {
Self {
factory: Some(factory),
}
}
}
impl<T> AppServiceFactory for ServiceFactoryWrapper<T>
where
T: HttpServiceFactory,
{
fn register(&mut self, config: &mut AppService) {
if let Some(item) = self.factory.take() {
item.register(config)
}
}
}
/// A service level request wrapper.
///
/// Allows mutable access to request's internal structures.
pub struct ServiceRequest {
req: HttpRequest,
payload: Payload,
}
impl ServiceRequest {
/// Construct service request
pub(crate) fn new(req: HttpRequest, payload: Payload) -> Self {
Self { req, payload }
}
/// Deconstruct request into parts
#[inline]
pub fn into_parts(self) -> (HttpRequest, Payload) {
(self.req, self.payload)
}
/// Get mutable access to inner `HttpRequest` and `Payload`
#[inline]
pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) {
(&mut self.req, &mut self.payload)
}
/// Construct request from parts.
pub fn from_parts(req: HttpRequest, payload: Payload) -> Self {
#[cfg(debug_assertions)]
if Rc::strong_count(&req.inner) > 1 {
log::warn!("Cloning an `HttpRequest` might cause panics.");
}
Self { req, payload }
}
/// Construct request from request.
///
/// The returned `ServiceRequest` would have no payload.
#[inline]
pub fn from_request(req: HttpRequest) -> Self {
ServiceRequest {
req,
payload: Payload::None,
}
}
/// Create service response
#[inline]
pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> {
let res = HttpResponse::from(res.into());
ServiceResponse::new(self.req, res)
}
/// Create service response for error
#[inline]
pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse {
let res = HttpResponse::from_error(err.into());
ServiceResponse::new(self.req, res)
}
/// This method returns reference to the request head
#[inline]
pub fn head(&self) -> &RequestHead {
self.req.head()
}
/// This method returns reference to the request head
#[inline]
pub fn head_mut(&mut self) -> &mut RequestHead {
self.req.head_mut()
}
/// Request's uri.
#[inline]
pub fn uri(&self) -> &Uri {
&self.head().uri
}
/// Read the Request method.
#[inline]
pub fn method(&self) -> &Method {
&self.head().method
}
/// Read the Request Version.
#[inline]
pub fn version(&self) -> Version {
self.head().version
}
#[inline]
/// Returns request's headers.
pub fn headers(&self) -> &HeaderMap {
&self.head().headers
}
#[inline]
/// Returns mutable request's headers.
pub fn headers_mut(&mut self) -> &mut HeaderMap {
&mut self.head_mut().headers
}
/// The target path of this Request.
#[inline]
pub fn path(&self) -> &str {
self.head().uri.path()
}
/// Counterpart to [`HttpRequest::query_string`].
#[inline]
pub fn query_string(&self) -> &str {
self.req.query_string()
}
/// Peer socket address.
///
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy.
///
/// To get client connection information `ConnectionInfo` should be used.
///
/// Will only return None when called in unit tests.
#[inline]
pub fn peer_addr(&self) -> Option<net::SocketAddr> {
self.head().peer_addr
}
/// Get *ConnectionInfo* for the current request.
#[inline]
pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> {
self.req.connection_info()
}
/// Returns a reference to the Path parameters.
///
/// Params is a container for URL parameters.
/// A variable segment is specified in the form `{identifier}`,
/// where the identifier can be used later in a request handler to
/// access the matched value for that segment.
#[inline]
pub fn match_info(&self) -> &Path<Url> {
self.req.match_info()
}
/// Returns a mutable reference to the Path parameters.
#[inline]
pub fn match_info_mut(&mut self) -> &mut Path<Url> {
self.req.match_info_mut()
}
/// Counterpart to [`HttpRequest::match_name`].
#[inline]
pub fn match_name(&self) -> Option<&str> {
self.req.match_name()
}
/// Counterpart to [`HttpRequest::match_pattern`].
#[inline]
pub fn match_pattern(&self) -> Option<String> {
self.req.match_pattern()
}
/// Get a reference to a `ResourceMap` of current application.
#[inline]
pub fn resource_map(&self) -> &ResourceMap {
self.req.resource_map()
}
/// Service configuration
#[inline]
pub fn app_config(&self) -> &AppConfig {
self.req.app_config()
}
/// Counterpart to [`HttpRequest::app_data`].
#[inline]
pub fn app_data<T: 'static>(&self) -> Option<&T> {
for container in self.req.inner.app_data.iter().rev() {
if let Some(data) = container.get::<T>() {
return Some(data);
}
}
None
}
/// Counterpart to [`HttpRequest::conn_data`].
#[inline]
pub fn conn_data<T: 'static>(&self) -> Option<&T> {
self.req.conn_data()
}
#[cfg(feature = "cookies")]
#[inline]
pub fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
self.req.cookies()
}
/// Return request cookie.
#[cfg(feature = "cookies")]
#[inline]
pub fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
self.req.cookie(name)
}
/// Set request payload.
#[inline]
pub fn set_payload(&mut self, payload: Payload) {
self.payload = payload;
}
/// Add data container to request's resolution set.
///
/// In middleware, prefer [`extensions_mut`](ServiceRequest::extensions_mut) for request-local
/// data since it is assumed that the same app data is presented for every request.
pub fn add_data_container(&mut self, extensions: Rc<Extensions>) {
Rc::get_mut(&mut (self.req).inner)
.unwrap()
.app_data
.push(extensions);
}
/// Creates a context object for use with a [guard](crate::guard).
///
/// Useful if you are implementing
#[inline]
pub fn guard_ctx(&self) -> GuardContext<'_> {
GuardContext { req: self }
}
}
impl Resource for ServiceRequest {
type Path = Url;
#[inline]
fn resource_path(&mut self) -> &mut Path<Self::Path> {
self.match_info_mut()
}
}
impl HttpMessage for ServiceRequest {
type Stream = BoxedPayloadStream;
#[inline]
fn headers(&self) -> &HeaderMap {
&self.head().headers
}
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
self.req.extensions()
}
#[inline]
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.req.extensions_mut()
}
#[inline]
fn take_payload(&mut self) -> Payload<Self::Stream> {
self.payload.take()
}
}
impl fmt::Debug for ServiceRequest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
"\nServiceRequest {:?} {}:{}",
self.head().version,
self.head().method,
self.path()
)?;
if !self.query_string().is_empty() {
writeln!(f, " query: ?{:?}", self.query_string())?;
}
if !self.match_info().is_empty() {
writeln!(f, " params: {:?}", self.match_info())?;
}
writeln!(f, " headers:")?;
for (key, val) in self.headers().iter() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}
/// A service level response wrapper.
pub struct ServiceResponse<B = BoxBody> {
request: HttpRequest,
response: HttpResponse<B>,
}
impl ServiceResponse<BoxBody> {
/// Create service response from the error
pub fn from_err<E: Into<Error>>(err: E, request: HttpRequest) -> Self {
let response = HttpResponse::from_error(err);
ServiceResponse { request, response }
}
}
impl<B> ServiceResponse<B> {
/// Create service response instance
pub fn new(request: HttpRequest, response: HttpResponse<B>) -> Self {
ServiceResponse { request, response }
}
/// Create service response for error
#[inline]
pub fn error_response<E: Into<Error>>(self, err: E) -> ServiceResponse {
ServiceResponse::from_err(err, self.request)
}
/// Create service response
#[inline]
pub fn into_response<B1>(self, response: HttpResponse<B1>) -> ServiceResponse<B1> {
ServiceResponse::new(self.request, response)
}
/// Returns reference to original request.
#[inline]
pub fn request(&self) -> &HttpRequest {
&self.request
}
/// Returns reference to response.
#[inline]
pub fn response(&self) -> &HttpResponse<B> {
&self.response
}
/// Returns mutable reference to response.
#[inline]
pub fn response_mut(&mut self) -> &mut HttpResponse<B> {
&mut self.response
}
/// Returns response status code.
#[inline]
pub fn status(&self) -> StatusCode {
self.response.status()
}
/// Returns response's headers.
#[inline]
pub fn headers(&self) -> &HeaderMap {
self.response.headers()
}
/// Returns mutable response's headers.
#[inline]
pub fn headers_mut(&mut self) -> &mut HeaderMap {
self.response.headers_mut()
}
/// Destructures `ServiceResponse` into request and response components.
#[inline]
pub fn into_parts(self) -> (HttpRequest, HttpResponse<B>) {
(self.request, self.response)
}
/// Map the current body type to another using a closure. Returns a new response.
///
/// Closure receives the response head and the current body type.
#[inline]
pub fn map_body<F, B2>(self, f: F) -> ServiceResponse<B2>
where
F: FnOnce(&mut ResponseHead, B) -> B2,
{
let response = self.response.map_body(f);
ServiceResponse {
response,
request: self.request,
}
}
#[inline]
pub fn map_into_left_body<R>(self) -> ServiceResponse<EitherBody<B, R>> {
self.map_body(|_, body| EitherBody::left(body))
}
#[inline]
pub fn map_into_right_body<L>(self) -> ServiceResponse<EitherBody<L, B>> {
self.map_body(|_, body| EitherBody::right(body))
}
#[inline]
pub fn map_into_boxed_body(self) -> ServiceResponse<BoxBody>
where
B: MessageBody + 'static,
{
self.map_body(|_, body| body.boxed())
}
/// Consumes the response and returns its body.
#[inline]
pub fn into_body(self) -> B {
self.response.into_body()
}
}
impl<B> From<ServiceResponse<B>> for HttpResponse<B> {
fn from(res: ServiceResponse<B>) -> HttpResponse<B> {
res.response
}
}
impl<B> From<ServiceResponse<B>> for Response<B> {
fn from(res: ServiceResponse<B>) -> Response<B> {
res.response.into()
}
}
impl<B> fmt::Debug for ServiceResponse<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let res = writeln!(
f,
"\nServiceResponse {:?} {}{}",
self.response.head().version,
self.response.head().status,
self.response.head().reason.unwrap_or(""),
);
let _ = writeln!(f, " headers:");
for (key, val) in self.response.head().headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
let _ = writeln!(f, " body: {:?}", self.response.body().size());
res
}
}
pub struct WebService {
rdef: Patterns,
name: Option<String>,
guards: Vec<Box<dyn Guard>>,
}
impl WebService {
/// Create new `WebService` instance.
pub fn new<T: IntoPatterns>(path: T) -> Self {
WebService {
rdef: path.patterns(),
name: None,
guards: Vec::new(),
}
}
/// Set service name.
///
/// Name is used for URL generation.
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
/// Add match guard to a web service.
///
/// ```
/// use actix_web::{web, guard, dev, App, Error, HttpResponse};
///
/// async fn index(req: dev::ServiceRequest) -> Result<dev::ServiceResponse, Error> {
/// Ok(req.into_response(HttpResponse::Ok().finish()))
/// }
///
/// let app = App::new()
/// .service(
/// web::service("/app")
/// .guard(guard::Header("content-type", "text/plain"))
/// .finish(index)
/// );
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Box::new(guard));
self
}
/// Set a service factory implementation and generate web service.
pub fn finish<T, F>(self, service: F) -> impl HttpServiceFactory
where
F: IntoServiceFactory<T, ServiceRequest>,
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = Error,
InitError = (),
> + 'static,
{
WebServiceImpl {
srv: service.into_factory(),
rdef: self.rdef,
name: self.name,
guards: self.guards,
}
}
}
struct WebServiceImpl<T> {
srv: T,
rdef: Patterns,
name: Option<String>,
guards: Vec<Box<dyn Guard>>,
}
impl<T> HttpServiceFactory for WebServiceImpl<T>
where
T: ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = Error,
InitError = (),
> + 'static,
{
fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
Some(std::mem::take(&mut self.guards))
};
let mut rdef = if config.is_root() || !self.rdef.is_empty() {
ResourceDef::new(ensure_leading_slash(self.rdef))
} else {
ResourceDef::new(self.rdef)
};
if let Some(ref name) = self.name {
rdef.set_name(name);
}
config.register_service(rdef, guards, self.srv, None)
}
}
/// Macro helping register different types of services at the sametime.
///
/// The service type must be implementing [`HttpServiceFactory`](self::HttpServiceFactory) trait.
///
/// The max number of services can be grouped together is 12.
///
/// # Examples
/// ```
/// use actix_web::{services, web, App};
///
/// let services = services![
/// web::resource("/test2").to(|| async { "test2" }),
/// web::scope("/test3").route("/", web::get().to(|| async { "test3" }))
/// ];
///
/// let app = App::new().service(services);
///
/// // services macro just convert multiple services to a tuple.
/// // below would also work without importing the macro.
/// let app = App::new().service((
/// web::resource("/test2").to(|| async { "test2" }),
/// web::scope("/test3").route("/", web::get().to(|| async { "test3" }))
/// ));
/// ```
#[macro_export]
macro_rules! services {
($($x:expr),+ $(,)?) => {
($($x,)+)
}
}
/// HttpServiceFactory trait impl for tuples
macro_rules! service_tuple ({ $($T:ident)+ } => {
impl<$($T: HttpServiceFactory),+> HttpServiceFactory for ($($T,)+) {
#[allow(non_snake_case)]
fn register(self, config: &mut AppService) {
let ($($T,)*) = self;
$($T.register(config);)+
}
}
});
service_tuple! { A }
service_tuple! { A B }
service_tuple! { A B C }
service_tuple! { A B C D }
service_tuple! { A B C D E }
service_tuple! { A B C D E F }
service_tuple! { A B C D E F G }
service_tuple! { A B C D E F G H }
service_tuple! { A B C D E F G H I }
service_tuple! { A B C D E F G H I J }
service_tuple! { A B C D E F G H I J K }
service_tuple! { A B C D E F G H I J K L }
#[cfg(test)]
mod tests {
use super::*;
use crate::test::{self, init_service, TestRequest};
use crate::{guard, http, web, App, HttpResponse};
use actix_service::Service;
use actix_utils::future::ok;
#[actix_rt::test]
async fn test_service() {
let srv = init_service(
App::new().service(web::service("/test").name("test").finish(
|req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())),
)),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let srv = init_service(
App::new().service(web::service("/test").guard(guard::Get()).finish(
|req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())),
)),
)
.await;
let req = TestRequest::with_uri("/test")
.method(http::Method::PUT)
.to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::NOT_FOUND);
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_service_data() {
let srv =
init_service(
App::new()
.data(42u32)
.service(web::service("/test").name("test").finish(
|req: ServiceRequest| {
assert_eq!(req.app_data::<web::Data<u32>>().unwrap().as_ref(), &42);
ok(req.into_response(HttpResponse::Ok().finish()))
},
)),
)
.await;
let req = TestRequest::with_uri("/test").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[test]
fn test_fmt_debug() {
let req = TestRequest::get()
.uri("/index.html?test=1")
.insert_header(("x-test", "111"))
.to_srv_request();
let s = format!("{:?}", req);
assert!(s.contains("ServiceRequest"));
assert!(s.contains("test=1"));
assert!(s.contains("x-test"));
let res = HttpResponse::Ok().insert_header(("x-test", "111")).finish();
let res = TestRequest::post()
.uri("/index.html?test=1")
.to_srv_response(res);
let s = format!("{:?}", res);
assert!(s.contains("ServiceResponse"));
assert!(s.contains("x-test"));
}
#[actix_rt::test]
async fn test_services_macro() {
let scoped = services![
web::service("/scoped_test1").name("scoped_test1").finish(
|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::Ok().finish()))
}
),
web::resource("/scoped_test2").to(|| async { "test2" }),
];
let services = services![
web::service("/test1")
.name("test")
.finish(|req: ServiceRequest| async {
Ok(req.into_response(HttpResponse::Ok().finish()))
}),
web::resource("/test2").to(|| async { "test2" }),
web::scope("/test3").service(scoped)
];
let srv = init_service(App::new().service(services)).await;
let req = TestRequest::with_uri("/test1").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test2").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test3/scoped_test1").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test3/scoped_test2").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[actix_rt::test]
async fn test_services_vec() {
let services = vec![
web::resource("/test1").to(|| async { "test1" }),
web::resource("/test2").to(|| async { "test2" }),
];
let scoped = vec![
web::resource("/scoped_test1").to(|| async { "test1" }),
web::resource("/scoped_test2").to(|| async { "test2" }),
];
let srv = init_service(
App::new()
.service(services)
.service(web::scope("/test3").service(scoped)),
)
.await;
let req = TestRequest::with_uri("/test1").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test2").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test3/scoped_test1").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
let req = TestRequest::with_uri("/test3/scoped_test2").to_request();
let resp = srv.call(req).await.unwrap();
assert_eq!(resp.status(), http::StatusCode::OK);
}
#[actix_rt::test]
#[should_panic(expected = "called `Option::unwrap()` on a `None` value")]
async fn cloning_request_panics() {
async fn index(_name: web::Path<(String,)>) -> &'static str {
""
}
let app = test::init_service(
App::new()
.wrap_fn(|req, svc| {
let (req, pl) = req.into_parts();
let _req2 = req.clone();
let req = ServiceRequest::from_parts(req, pl);
svc.call(req)
})
.route("/", web::get().to(|| async { "" }))
.service(
web::resource("/resource1/{name}/index.html").route(web::get().to(index)),
),
)
.await;
let req = test::TestRequest::default().to_request();
let _res = test::call_service(&app, req).await;
}
}

81
actix-web/src/test/mod.rs Normal file
View File

@ -0,0 +1,81 @@
//! Various helpers for Actix applications to use during testing.
//!
//! # Creating A Test Service
//! - [`init_service`]
//!
//! # Off-The-Shelf Test Services
//! - [`ok_service`]
//! - [`simple_service`]
//!
//! # Calling Test Service
//! - [`TestRequest`]
//! - [`call_service`]
//! - [`call_and_read_body`]
//! - [`call_and_read_body_json`]
//!
//! # Reading Response Payloads
//! - [`read_body`]
//! - [`read_body_json`]
// TODO: more docs on generally how testing works with these parts
pub use actix_http::test::TestBuffer;
mod test_request;
mod test_services;
mod test_utils;
pub use self::test_request::TestRequest;
#[allow(deprecated)]
pub use self::test_services::{default_service, ok_service, simple_service};
#[allow(deprecated)]
pub use self::test_utils::{
call_and_read_body, call_and_read_body_json, call_service, init_service, read_body,
read_body_json, read_response, read_response_json,
};
#[cfg(test)]
pub(crate) use self::test_utils::try_init_service;
/// Reduces boilerplate code when testing expected response payloads.
///
/// Must be used inside an async test. Works for both `ServiceRequest` and `HttpRequest`.
///
/// # Examples
/// ```
/// use actix_web::{http::StatusCode, HttpResponse};
///
/// let res = HttpResponse::with_body(StatusCode::OK, "http response");
/// assert_body_eq!(res, b"http response");
/// ```
#[cfg(test)]
macro_rules! assert_body_eq {
($res:ident, $expected:expr) => {
assert_eq!(
::actix_http::body::to_bytes($res.into_body())
.await
.expect("error reading test response body"),
::bytes::Bytes::from_static($expected),
)
};
}
#[cfg(test)]
pub(crate) use assert_body_eq;
#[cfg(test)]
mod tests {
use super::*;
use crate::{http::StatusCode, service::ServiceResponse, HttpResponse};
#[actix_rt::test]
async fn assert_body_works_for_service_and_regular_response() {
let res = HttpResponse::with_body(StatusCode::OK, "http response");
assert_body_eq!(res, b"http response");
let req = TestRequest::default().to_http_request();
let res = HttpResponse::with_body(StatusCode::OK, "service response");
let res = ServiceResponse::new(req, res);
assert_body_eq!(res, b"service response");
}
}

View File

@ -0,0 +1,434 @@
use std::{borrow::Cow, net::SocketAddr, rc::Rc};
use actix_http::{test::TestRequest as HttpTestRequest, Request};
use serde::Serialize;
use crate::{
app_service::AppInitServiceState,
config::AppConfig,
data::Data,
dev::{Extensions, Path, Payload, ResourceDef, Service, Url},
http::header::ContentType,
http::{header::TryIntoHeaderPair, Method, Uri, Version},
rmap::ResourceMap,
service::{ServiceRequest, ServiceResponse},
test,
web::Bytes,
HttpRequest, HttpResponse,
};
#[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar};
/// Test `Request` builder.
///
/// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern.
/// You can generate various types of request via TestRequest's methods:
/// * `TestRequest::to_request` creates `actix_http::Request` instance.
/// * `TestRequest::to_srv_request` creates `ServiceRequest` instance, which is used for testing middlewares and chain adapters.
/// * `TestRequest::to_srv_response` creates `ServiceResponse` instance.
/// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers.
///
/// ```
/// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage};
/// use actix_web::http::{header, StatusCode};
///
/// async fn index(req: HttpRequest) -> HttpResponse {
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
/// HttpResponse::Ok().into()
/// } else {
/// HttpResponse::BadRequest().into()
/// }
/// }
///
/// #[actix_web::test]
/// async fn test_index() {
/// let req = test::TestRequest::default().insert_header("content-type", "text/plain")
/// .to_http_request();
///
/// let resp = index(req).await.unwrap();
/// assert_eq!(resp.status(), StatusCode::OK);
///
/// let req = test::TestRequest::default().to_http_request();
/// let resp = index(req).await.unwrap();
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
/// }
/// ```
pub struct TestRequest {
req: HttpTestRequest,
rmap: ResourceMap,
config: AppConfig,
path: Path<Url>,
peer_addr: Option<SocketAddr>,
app_data: Extensions,
#[cfg(feature = "cookies")]
cookies: CookieJar,
}
impl Default for TestRequest {
fn default() -> TestRequest {
TestRequest {
req: HttpTestRequest::default(),
rmap: ResourceMap::new(ResourceDef::new("")),
config: AppConfig::default(),
path: Path::new(Url::new(Uri::default())),
peer_addr: None,
app_data: Extensions::new(),
#[cfg(feature = "cookies")]
cookies: CookieJar::new(),
}
}
}
#[allow(clippy::wrong_self_convention)]
impl TestRequest {
/// Create TestRequest and set request uri
pub fn with_uri(path: &str) -> TestRequest {
TestRequest::default().uri(path)
}
/// Create TestRequest and set method to `Method::GET`
pub fn get() -> TestRequest {
TestRequest::default().method(Method::GET)
}
/// Create TestRequest and set method to `Method::POST`
pub fn post() -> TestRequest {
TestRequest::default().method(Method::POST)
}
/// Create TestRequest and set method to `Method::PUT`
pub fn put() -> TestRequest {
TestRequest::default().method(Method::PUT)
}
/// Create TestRequest and set method to `Method::PATCH`
pub fn patch() -> TestRequest {
TestRequest::default().method(Method::PATCH)
}
/// Create TestRequest and set method to `Method::DELETE`
pub fn delete() -> TestRequest {
TestRequest::default().method(Method::DELETE)
}
/// Set HTTP version of this request
pub fn version(mut self, ver: Version) -> Self {
self.req.version(ver);
self
}
/// Set HTTP method of this request
pub fn method(mut self, meth: Method) -> Self {
self.req.method(meth);
self
}
/// Set HTTP URI of this request
pub fn uri(mut self, path: &str) -> Self {
self.req.uri(path);
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
self.req.insert_header(header);
self
}
/// Append a header, keeping any that were set with an equivalent field name.
pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
self.req.append_header(header);
self
}
/// Set cookie for this request.
#[cfg(feature = "cookies")]
pub fn cookie(mut self, cookie: Cookie<'_>) -> Self {
self.cookies.add(cookie.into_owned());
self
}
/// Set request path pattern parameter.
///
/// # Examples
/// ```
/// use actix_web::test::TestRequest;
///
/// let req = TestRequest::default().param("foo", "bar");
/// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned());
/// ```
pub fn param(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.path.add_static(name, value);
self
}
/// Set peer addr.
pub fn peer_addr(mut self, addr: SocketAddr) -> Self {
self.peer_addr = Some(addr);
self
}
/// Set request payload.
pub fn set_payload(mut self, data: impl Into<Bytes>) -> Self {
self.req.set_payload(data);
self
}
/// Serialize `data` to a URL encoded form and set it as the request payload.
///
/// The `Content-Type` header is set to `application/x-www-form-urlencoded`.
pub fn set_form(mut self, data: impl Serialize) -> Self {
let bytes = serde_urlencoded::to_string(&data)
.expect("Failed to serialize test data as a urlencoded form");
self.req.set_payload(bytes);
self.req.insert_header(ContentType::form_url_encoded());
self
}
/// Serialize `data` to JSON and set it as the request payload.
///
/// The `Content-Type` header is set to `application/json`.
pub fn set_json(mut self, data: impl Serialize) -> Self {
let bytes =
serde_json::to_string(&data).expect("Failed to serialize test data to json");
self.req.set_payload(bytes);
self.req.insert_header(ContentType::json());
self
}
/// Set application data. This is equivalent of `App::data()` method
/// for testing purpose.
pub fn data<T: 'static>(mut self, data: T) -> Self {
self.app_data.insert(Data::new(data));
self
}
/// Set application data. This is equivalent of `App::app_data()` method
/// for testing purpose.
pub fn app_data<T: 'static>(mut self, data: T) -> Self {
self.app_data.insert(data);
self
}
#[cfg(test)]
/// Set request config
pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self {
self.rmap = rmap;
self
}
fn finish(&mut self) -> Request {
// mut used when cookie feature is enabled
#[allow(unused_mut)]
let mut req = self.req.finish();
#[cfg(feature = "cookies")]
{
use actix_http::header::{HeaderValue, COOKIE};
let cookie: String = self
.cookies
.delta()
// ensure only name=value is written to cookie header
.map(|c| c.stripped().encoded().to_string())
.collect::<Vec<_>>()
.join("; ");
if !cookie.is_empty() {
req.headers_mut()
.insert(COOKIE, HeaderValue::from_str(&cookie).unwrap());
}
}
req
}
/// Complete request creation and generate `Request` instance
pub fn to_request(mut self) -> Request {
let mut req = self.finish();
req.head_mut().peer_addr = self.peer_addr;
req
}
/// Complete request creation and generate `ServiceRequest` instance
pub fn to_srv_request(mut self) -> ServiceRequest {
let (mut head, payload) = self.finish().into_parts();
head.peer_addr = self.peer_addr;
self.path.get_mut().update(&head.uri);
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
ServiceRequest::new(
HttpRequest::new(
self.path,
head,
app_state,
Rc::new(self.app_data),
None,
Default::default(),
),
payload,
)
}
/// Complete request creation and generate `ServiceResponse` instance
pub fn to_srv_response<B>(self, res: HttpResponse<B>) -> ServiceResponse<B> {
self.to_srv_request().into_response(res)
}
/// Complete request creation and generate `HttpRequest` instance
pub fn to_http_request(mut self) -> HttpRequest {
let (mut head, _) = self.finish().into_parts();
head.peer_addr = self.peer_addr;
self.path.get_mut().update(&head.uri);
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
HttpRequest::new(
self.path,
head,
app_state,
Rc::new(self.app_data),
None,
Default::default(),
)
}
/// Complete request creation and generate `HttpRequest` and `Payload` instances
pub fn to_http_parts(mut self) -> (HttpRequest, Payload) {
let (mut head, payload) = self.finish().into_parts();
head.peer_addr = self.peer_addr;
self.path.get_mut().update(&head.uri);
let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone());
let req = HttpRequest::new(
self.path,
head,
app_state,
Rc::new(self.app_data),
None,
Default::default(),
);
(req, payload)
}
/// Complete request creation, calls service and waits for response future completion.
pub async fn send_request<S, B, E>(self, app: &S) -> S::Response
where
S: Service<Request, Response = ServiceResponse<B>, Error = E>,
E: std::fmt::Debug,
{
let req = self.to_request();
test::call_service(app, req).await
}
#[cfg(test)]
pub fn set_server_hostname(&mut self, host: &str) {
self.config.set_host(host)
}
}
#[cfg(test)]
mod tests {
use std::time::SystemTime;
use super::*;
use crate::{http::header, test::init_service, web, App, Error, HttpResponse, Responder};
#[actix_rt::test]
async fn test_basics() {
let req = TestRequest::default()
.version(Version::HTTP_2)
.insert_header(header::ContentType::json())
.insert_header(header::Date(SystemTime::now().into()))
.param("test", "123")
.data(10u32)
.app_data(20u64)
.peer_addr("127.0.0.1:8081".parse().unwrap())
.to_http_request();
assert!(req.headers().contains_key(header::CONTENT_TYPE));
assert!(req.headers().contains_key(header::DATE));
assert_eq!(
req.head().peer_addr,
Some("127.0.0.1:8081".parse().unwrap())
);
assert_eq!(&req.match_info()["test"], "123");
assert_eq!(req.version(), Version::HTTP_2);
let data = req.app_data::<Data<u32>>().unwrap();
assert!(req.app_data::<Data<u64>>().is_none());
assert_eq!(*data.get_ref(), 10);
assert!(req.app_data::<u32>().is_none());
let data = req.app_data::<u64>().unwrap();
assert_eq!(*data, 20);
}
#[actix_rt::test]
async fn test_send_request() {
let app = init_service(
App::new().service(
web::resource("/index.html")
.route(web::get().to(|| HttpResponse::Ok().body("welcome!"))),
),
)
.await;
let resp = TestRequest::get()
.uri("/index.html")
.send_request(&app)
.await;
let result = test::read_body(resp).await;
assert_eq!(result, Bytes::from_static(b"welcome!"));
}
#[actix_rt::test]
async fn test_async_with_block() {
async fn async_with_block() -> Result<HttpResponse, Error> {
let res = web::block(move || Some(4usize).ok_or("wrong")).await;
match res {
Ok(value) => Ok(HttpResponse::Ok()
.content_type("text/plain")
.body(format!("Async with block value: {:?}", value))),
Err(_) => panic!("Unexpected"),
}
}
let app =
init_service(App::new().service(web::resource("/index.html").to(async_with_block)))
.await;
let req = TestRequest::post().uri("/index.html").to_request();
let res = app.call(req).await.unwrap();
assert!(res.status().is_success());
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_server_data() {
async fn handler(data: web::Data<usize>) -> impl Responder {
assert_eq!(**data, 10);
HttpResponse::Ok()
}
let app = init_service(
App::new()
.data(10usize)
.service(web::resource("/index.html").to(handler)),
)
.await;
let req = TestRequest::post().uri("/index.html").to_request();
let res = app.call(req).await.unwrap();
assert!(res.status().is_success());
}
}

View File

@ -0,0 +1,31 @@
use actix_utils::future::ok;
use crate::{
body::BoxBody,
dev::{fn_service, Service, ServiceRequest, ServiceResponse},
http::StatusCode,
Error, HttpResponseBuilder,
};
/// Creates service that always responds with `200 OK` and no body.
pub fn ok_service(
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
simple_service(StatusCode::OK)
}
/// Creates service that always responds with given status code and no body.
pub fn simple_service(
status_code: StatusCode,
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
fn_service(move |req: ServiceRequest| {
ok(req.into_response(HttpResponseBuilder::new(status_code).finish()))
})
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `simple_service`.")]
pub fn default_service(
status_code: StatusCode,
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
simple_service(status_code)
}

View File

@ -0,0 +1,505 @@
use std::error::Error as StdError;
use actix_http::Request;
use actix_service::IntoServiceFactory;
use serde::de::DeserializeOwned;
use crate::{
body::{self, MessageBody},
config::AppConfig,
dev::{Service, ServiceFactory},
service::ServiceResponse,
web::Bytes,
Error,
};
/// Initialize service from application builder instance.
///
/// # Examples
/// ```
/// use actix_service::Service;
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
///
/// #[actix_web::test]
/// async fn test_init_service() {
/// let app = test::init_service(
/// App::new()
/// .service(web::resource("/test").to(|| async { "OK" }))
/// ).await;
///
/// // Create request object
/// let req = test::TestRequest::with_uri("/test").to_request();
///
/// // Execute application
/// let res = app.call(req).await.unwrap();
/// assert_eq!(res.status(), StatusCode::OK);
/// }
/// ```
///
/// # Panics
/// Panics if service initialization returns an error.
pub async fn init_service<R, S, B, E>(
app: R,
) -> impl Service<Request, Response = ServiceResponse<B>, Error = E>
where
R: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
S::InitError: std::fmt::Debug,
{
try_init_service(app)
.await
.expect("service initialization failed")
}
/// Fallible version of [`init_service`] that allows testing initialization errors.
pub(crate) async fn try_init_service<R, S, B, E>(
app: R,
) -> Result<impl Service<Request, Response = ServiceResponse<B>, Error = E>, S::InitError>
where
R: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig, Response = ServiceResponse<B>, Error = E>,
S::InitError: std::fmt::Debug,
{
let srv = app.into_factory();
srv.new_service(AppConfig::default()).await
}
/// Calls service and waits for response future completion.
///
/// # Examples
/// ```
/// use actix_web::{test, web, App, HttpResponse, http::StatusCode};
///
/// #[actix_web::test]
/// async fn test_response() {
/// let app = test::init_service(
/// App::new()
/// .service(web::resource("/test").to(|| async {
/// HttpResponse::Ok()
/// }))
/// ).await;
///
/// // Create request object
/// let req = test::TestRequest::with_uri("/test").to_request();
///
/// // Call application
/// let res = test::call_service(&app, req).await;
/// assert_eq!(res.status(), StatusCode::OK);
/// }
/// ```
///
/// # Panics
/// Panics if service call returns error.
pub async fn call_service<S, R, B, E>(app: &S, req: R) -> S::Response
where
S: Service<R, Response = ServiceResponse<B>, Error = E>,
E: std::fmt::Debug,
{
app.call(req)
.await
.expect("test service call returned error")
}
/// Helper function that returns a response body of a TestRequest
///
/// # Examples
/// ```
/// use actix_web::{test, web, App, HttpResponse, http::header};
/// use bytes::Bytes;
///
/// #[actix_web::test]
/// async fn test_index() {
/// let app = test::init_service(
/// App::new().service(
/// web::resource("/index.html")
/// .route(web::post().to(|| async {
/// HttpResponse::Ok().body("welcome!")
/// })))
/// ).await;
///
/// let req = test::TestRequest::post()
/// .uri("/index.html")
/// .header(header::CONTENT_TYPE, "application/json")
/// .to_request();
///
/// let result = test::call_and_read_body(&app, req).await;
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
/// }
/// ```
///
/// # Panics
/// Panics if:
/// - service call returns error;
/// - body yields an error while it is being read.
pub async fn call_and_read_body<S, B>(app: &S, req: Request) -> Bytes
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
let res = call_service(app, req).await;
read_body(res).await
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body`.")]
pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{
let res = call_service(app, req).await;
read_body(res).await
}
/// Helper function that returns a response body of a ServiceResponse.
///
/// # Examples
/// ```
/// use actix_web::{test, web, App, HttpResponse, http::header};
/// use bytes::Bytes;
///
/// #[actix_web::test]
/// async fn test_index() {
/// let app = test::init_service(
/// App::new().service(
/// web::resource("/index.html")
/// .route(web::post().to(|| async {
/// HttpResponse::Ok().body("welcome!")
/// })))
/// ).await;
///
/// let req = test::TestRequest::post()
/// .uri("/index.html")
/// .header(header::CONTENT_TYPE, "application/json")
/// .to_request();
///
/// let res = test::call_service(&app, req).await;
/// let result = test::read_body(res).await;
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
/// }
/// ```
///
/// # Panics
/// Panics if body yields an error while it is being read.
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where
B: MessageBody,
{
let body = res.into_body();
body::to_bytes(body)
.await
.map_err(Into::<Box<dyn StdError>>::into)
.expect("error reading test response body")
}
/// Helper function that returns a deserialized response body of a ServiceResponse.
///
/// # Examples
/// ```
/// use actix_web::{App, test, web, HttpResponse, http::header};
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// pub struct Person {
/// id: String,
/// name: String,
/// }
///
/// #[actix_web::test]
/// async fn test_post_person() {
/// let app = test::init_service(
/// App::new().service(
/// web::resource("/people")
/// .route(web::post().to(|person: web::Json<Person>| async {
/// HttpResponse::Ok()
/// .json(person)})
/// ))
/// ).await;
///
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
///
/// let res = test::TestRequest::post()
/// .uri("/people")
/// .header(header::CONTENT_TYPE, "application/json")
/// .set_payload(payload)
/// .send_request(&mut app)
/// .await;
///
/// assert!(res.status().is_success());
///
/// let result: Person = test::read_body_json(res).await;
/// }
/// ```
///
/// # Panics
/// Panics if:
/// - body yields an error while it is being read;
/// - received body is not a valid JSON representation of `T`.
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
where
B: MessageBody,
T: DeserializeOwned,
{
let body = read_body(res).await;
serde_json::from_slice(&body).unwrap_or_else(|err| {
panic!(
"could not deserialize body into a {}\nerr: {}\nbody: {:?}",
std::any::type_name::<T>(),
err,
body,
)
})
}
/// Helper function that returns a deserialized response body of a TestRequest
///
/// # Examples
/// ```
/// use actix_web::{App, test, web, HttpResponse, http::header};
/// use serde::{Serialize, Deserialize};
///
/// #[derive(Serialize, Deserialize)]
/// pub struct Person {
/// id: String,
/// name: String
/// }
///
/// #[actix_web::test]
/// async fn test_add_person() {
/// let app = test::init_service(
/// App::new().service(
/// web::resource("/people")
/// .route(web::post().to(|person: web::Json<Person>| async {
/// HttpResponse::Ok()
/// .json(person)})
/// ))
/// ).await;
///
/// let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
///
/// let req = test::TestRequest::post()
/// .uri("/people")
/// .header(header::CONTENT_TYPE, "application/json")
/// .set_payload(payload)
/// .to_request();
///
/// let result: Person = test::call_and_read_body_json(&mut app, req).await;
/// }
/// ```
///
/// # Panics
/// Panics if:
/// - service call returns an error body yields an error while it is being read;
/// - body yields an error while it is being read;
/// - received body is not a valid JSON representation of `T`.
pub async fn call_and_read_body_json<S, B, T>(app: &S, req: Request) -> T
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
T: DeserializeOwned,
{
let res = call_service(app, req).await;
read_body_json(res).await
}
#[doc(hidden)]
#[deprecated(since = "4.0.0", note = "Renamed to `call_and_read_body_json`.")]
pub async fn read_response_json<S, B, T>(app: &S, req: Request) -> T
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
T: DeserializeOwned,
{
call_and_read_body_json(app, req).await
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::*;
use crate::{
dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage,
HttpResponse,
};
#[actix_rt::test]
async fn test_request_methods() {
let app = init_service(
App::new().service(
web::resource("/index.html")
.route(web::put().to(|| HttpResponse::Ok().body("put!")))
.route(web::patch().to(|| HttpResponse::Ok().body("patch!")))
.route(web::delete().to(|| HttpResponse::Ok().body("delete!"))),
),
)
.await;
let put_req = TestRequest::put()
.uri("/index.html")
.insert_header((header::CONTENT_TYPE, "application/json"))
.to_request();
let result = call_and_read_body(&app, put_req).await;
assert_eq!(result, Bytes::from_static(b"put!"));
let patch_req = TestRequest::patch()
.uri("/index.html")
.insert_header((header::CONTENT_TYPE, "application/json"))
.to_request();
let result = call_and_read_body(&app, patch_req).await;
assert_eq!(result, Bytes::from_static(b"patch!"));
let delete_req = TestRequest::delete().uri("/index.html").to_request();
let result = call_and_read_body(&app, delete_req).await;
assert_eq!(result, Bytes::from_static(b"delete!"));
}
#[derive(Serialize, Deserialize)]
pub struct Person {
id: String,
name: String,
}
#[actix_rt::test]
async fn test_response_json() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
let req = TestRequest::post()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.to_request();
let result: Person = call_and_read_body_json(&app, req).await;
assert_eq!(&result.id, "12345");
}
#[actix_rt::test]
async fn test_body_json() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes();
let res = TestRequest::post()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.send_request(&app)
.await;
let result: Person = read_body_json(res).await;
assert_eq!(&result.name, "User name");
}
#[actix_rt::test]
async fn test_request_response_form() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Form<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = Person {
id: "12345".to_string(),
name: "User name".to_string(),
};
let req = TestRequest::post()
.uri("/people")
.set_form(&payload)
.to_request();
assert_eq!(req.content_type(), "application/x-www-form-urlencoded");
let result: Person = call_and_read_body_json(&app, req).await;
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[actix_rt::test]
async fn test_response() {
let app = init_service(
App::new().service(
web::resource("/index.html")
.route(web::post().to(|| HttpResponse::Ok().body("welcome!"))),
),
)
.await;
let req = TestRequest::post()
.uri("/index.html")
.insert_header((header::CONTENT_TYPE, "application/json"))
.to_request();
let result = call_and_read_body(&app, req).await;
assert_eq!(result, Bytes::from_static(b"welcome!"));
}
#[actix_rt::test]
async fn test_request_response_json() {
let app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
)))
.await;
let payload = Person {
id: "12345".to_string(),
name: "User name".to_string(),
};
let req = TestRequest::post()
.uri("/people")
.set_json(&payload)
.to_request();
assert_eq!(req.content_type(), "application/json");
let result: Person = call_and_read_body_json(&app, req).await;
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[actix_rt::test]
#[allow(dead_code)]
async fn return_opaque_types() {
fn test_app() -> App<
impl ServiceFactory<
ServiceRequest,
Config = (),
Response = ServiceResponse<impl MessageBody>,
Error = crate::Error,
InitError = (),
>,
> {
App::new().service(web::resource("/people").route(
web::post().to(|person: web::Json<Person>| HttpResponse::Ok().json(person)),
))
}
async fn test_service(
) -> impl Service<Request, Response = ServiceResponse<impl MessageBody>, Error = crate::Error>
{
init_service(test_app()).await
}
async fn compile_test(mut req: Vec<Request>) {
let svc = test_service().await;
call_service(&svc, req.pop().unwrap()).await;
call_and_read_body(&svc, req.pop().unwrap()).await;
read_body(call_service(&svc, req.pop().unwrap()).await).await;
let _: String = call_and_read_body_json(&svc, req.pop().unwrap()).await;
let _: String = read_body_json(call_service(&svc, req.pop().unwrap()).await).await;
}
}
}

View File

@ -0,0 +1,369 @@
//! For either helper, see [`Either`].
use std::{
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::{
body::EitherBody,
dev,
web::{Form, Json},
Error, FromRequest, HttpRequest, HttpResponse, Responder,
};
/// Combines two extractor or responder types into a single type.
///
/// # Extractor
/// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for
/// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
///
/// It is important to note that this extractor, by necessity, buffers the entire request payload
/// as part of its implementation. Though, it does respect any `PayloadConfig` maximum size limits.
///
/// ```
/// use actix_web::{post, web, Either};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// name: String,
/// }
///
/// // handler that accepts form as JSON or form-urlencoded.
/// #[post("/")]
/// async fn index(form: Either<web::Json<Info>, web::Form<Info>>) -> String {
/// let name: String = match form {
/// Either::Left(json) => json.name.to_owned(),
/// Either::Right(form) => form.name.to_owned(),
/// };
///
/// format!("Welcome {}!", name)
/// }
/// ```
///
/// # Responder
/// It may be desireable to use a concrete type for a response with multiple branches. As long as
/// both types implement `Responder`, so will the `Either` type, enabling it to be used as a
/// handler's return type.
///
/// All properties of a response are determined by the Responder branch returned.
///
/// ```
/// use actix_web::{get, Either, Error, HttpResponse};
///
/// #[get("/")]
/// async fn index() -> Either<&'static str, Result<HttpResponse, Error>> {
/// if 1 == 2 {
/// // respond with Left variant
/// Either::Left("Bad data")
/// } else {
/// // respond with Right variant
/// Either::Right(
/// Ok(HttpResponse::Ok()
/// .content_type(mime::TEXT_HTML)
/// .body("<p>Hello!</p>"))
/// )
/// }
/// }
/// ```
#[derive(Debug, PartialEq)]
pub enum Either<L, R> {
/// A value of type `L`.
Left(L),
/// A value of type `R`.
Right(R),
}
impl<T> Either<Form<T>, Json<T>> {
pub fn into_inner(self) -> T {
match self {
Either::Left(form) => form.into_inner(),
Either::Right(form) => form.into_inner(),
}
}
}
impl<T> Either<Json<T>, Form<T>> {
pub fn into_inner(self) -> T {
match self {
Either::Left(form) => form.into_inner(),
Either::Right(form) => form.into_inner(),
}
}
}
#[cfg(test)]
impl<L, R> Either<L, R> {
pub(self) fn unwrap_left(self) -> L {
match self {
Either::Left(data) => data,
Either::Right(_) => {
panic!("Cannot unwrap Left branch. Either contains an `R` type.")
}
}
}
pub(self) fn unwrap_right(self) -> R {
match self {
Either::Left(_) => {
panic!("Cannot unwrap Right branch. Either contains an `L` type.")
}
Either::Right(data) => data,
}
}
}
/// See [here](#responder) for example of usage as a handler return type.
impl<L, R> Responder for Either<L, R>
where
L: Responder,
R: Responder,
{
type Body = EitherBody<L::Body, R::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
match self {
Either::Left(a) => a.respond_to(req).map_into_left_body(),
Either::Right(b) => b.respond_to(req).map_into_right_body(),
}
}
}
/// A composite error resulting from failure to extract an `Either<L, R>`.
///
/// The implementation of `Into<actix_web::Error>` will return the payload buffering error or the
/// error from the primary extractor. To access the fallback error, use a match clause.
#[derive(Debug)]
pub enum EitherExtractError<L, R> {
/// Error from payload buffering, such as exceeding payload max size limit.
Bytes(Error),
/// Error from primary and fallback extractors.
Extract(L, R),
}
impl<L, R> From<EitherExtractError<L, R>> for Error
where
L: Into<Error>,
R: Into<Error>,
{
fn from(err: EitherExtractError<L, R>) -> Error {
match err {
EitherExtractError::Bytes(err) => err,
EitherExtractError::Extract(a_err, _b_err) => a_err.into(),
}
}
}
/// See [here](#extractor) for example of usage as an extractor.
impl<L, R> FromRequest for Either<L, R>
where
L: FromRequest + 'static,
R: FromRequest + 'static,
{
type Error = EitherExtractError<L::Error, R::Error>;
type Future = EitherExtractFut<L, R>;
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
EitherExtractFut {
req: req.clone(),
state: EitherExtractState::Bytes {
bytes: Bytes::from_request(req, payload),
},
}
}
}
pin_project! {
pub struct EitherExtractFut<L, R>
where
R: FromRequest,
L: FromRequest,
{
req: HttpRequest,
#[pin]
state: EitherExtractState<L, R>,
}
}
pin_project! {
#[project = EitherExtractProj]
pub enum EitherExtractState<L, R>
where
L: FromRequest,
R: FromRequest,
{
Bytes {
#[pin]
bytes: <Bytes as FromRequest>::Future,
},
Left {
#[pin]
left: L::Future,
fallback: Bytes,
},
Right {
#[pin]
right: R::Future,
left_err: Option<L::Error>,
},
}
}
impl<R, RF, RE, L, LF, LE> Future for EitherExtractFut<L, R>
where
L: FromRequest<Future = LF, Error = LE>,
R: FromRequest<Future = RF, Error = RE>,
LF: Future<Output = Result<L, LE>> + 'static,
RF: Future<Output = Result<R, RE>> + 'static,
LE: Into<Error>,
RE: Into<Error>,
{
type Output = Result<Either<L, R>, EitherExtractError<LE, RE>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.project();
let ready = loop {
let next = match this.state.as_mut().project() {
EitherExtractProj::Bytes { bytes } => {
let res = ready!(bytes.poll(cx));
match res {
Ok(bytes) => {
let fallback = bytes.clone();
let left =
L::from_request(this.req, &mut payload_from_bytes(bytes));
EitherExtractState::Left { left, fallback }
}
Err(err) => break Err(EitherExtractError::Bytes(err)),
}
}
EitherExtractProj::Left { left, fallback } => {
let res = ready!(left.poll(cx));
match res {
Ok(extracted) => break Ok(Either::Left(extracted)),
Err(left_err) => {
let right = R::from_request(
this.req,
&mut payload_from_bytes(mem::take(fallback)),
);
EitherExtractState::Right {
left_err: Some(left_err),
right,
}
}
}
}
EitherExtractProj::Right { right, left_err } => {
let res = ready!(right.poll(cx));
match res {
Ok(data) => break Ok(Either::Right(data)),
Err(err) => {
break Err(EitherExtractError::Extract(
left_err.take().unwrap(),
err,
));
}
}
}
};
this.state.set(next);
};
Poll::Ready(ready)
}
}
fn payload_from_bytes(bytes: Bytes) -> dev::Payload {
let (_, mut h1_payload) = actix_http::h1::Payload::create(true);
h1_payload.unread_data(bytes);
dev::Payload::from(h1_payload)
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::*;
use crate::{
test::TestRequest,
web::{Form, Json},
};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestForm {
hello: String,
}
#[actix_rt::test]
async fn test_either_extract_first_try() {
let (req, mut pl) = TestRequest::default()
.set_form(&TestForm {
hello: "world".to_owned(),
})
.to_http_parts();
let form = Either::<Form<TestForm>, Json<TestForm>>::from_request(&req, &mut pl)
.await
.unwrap()
.unwrap_left()
.into_inner();
assert_eq!(&form.hello, "world");
}
#[actix_rt::test]
async fn test_either_extract_fallback() {
let (req, mut pl) = TestRequest::default()
.set_json(&TestForm {
hello: "world".to_owned(),
})
.to_http_parts();
let form = Either::<Form<TestForm>, Json<TestForm>>::from_request(&req, &mut pl)
.await
.unwrap()
.unwrap_right()
.into_inner();
assert_eq!(&form.hello, "world");
}
#[actix_rt::test]
async fn test_either_extract_recursive_fallback() {
let (req, mut pl) = TestRequest::default()
.set_payload(Bytes::from_static(b"!@$%^&*()"))
.to_http_parts();
let payload = Either::<Either<Form<TestForm>, Json<TestForm>>, Bytes>::from_request(
&req, &mut pl,
)
.await
.unwrap()
.unwrap_right();
assert_eq!(&payload.as_ref(), &b"!@$%^&*()");
}
#[actix_rt::test]
async fn test_either_extract_recursive_fallback_inner() {
let (req, mut pl) = TestRequest::default()
.set_json(&TestForm {
hello: "world".to_owned(),
})
.to_http_parts();
let form = Either::<Either<Form<TestForm>, Json<TestForm>>, Bytes>::from_request(
&req, &mut pl,
)
.await
.unwrap()
.unwrap_left()
.unwrap_right()
.into_inner();
assert_eq!(&form.hello, "world");
}
}

562
actix-web/src/types/form.rs Normal file
View File

@ -0,0 +1,562 @@
//! For URL encoded form helper documentation, see [`Form`].
use std::{
borrow::Cow,
fmt,
future::Future,
ops,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_http::Payload;
use bytes::BytesMut;
use encoding_rs::{Encoding, UTF_8};
use futures_core::{future::LocalBoxFuture, ready};
use futures_util::{FutureExt as _, StreamExt as _};
use serde::{de::DeserializeOwned, Serialize};
#[cfg(feature = "__compress")]
use crate::dev::Decompress;
use crate::{
body::EitherBody, error::UrlencodedError, extract::FromRequest,
http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse,
Responder,
};
/// URL encoded payload extractor and responder.
///
/// `Form` has two uses: URL encoded responses, and extracting typed data from URL request payloads.
///
/// # Extractor
/// To extract typed data from a request body, the inner type `T` must implement the
/// [`DeserializeOwned`] trait.
///
/// Use [`FormConfig`] to configure extraction options.
///
/// ```
/// use actix_web::{post, web};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// name: String,
/// }
///
/// // This handler is only called if:
/// // - request headers declare the content type as `application/x-www-form-urlencoded`
/// // - request payload is deserialized into a `Info` struct from the URL encoded format
/// #[post("/")]
/// async fn index(form: web::Form<Info>) -> String {
/// format!("Welcome {}!", form.name)
/// }
/// ```
///
/// # Responder
/// The `Form` type also allows you to create URL encoded responses:
/// simply return a value of type Form<T> where T is the type to be URL encoded.
/// The type must implement [`serde::Serialize`].
///
/// Responses use
///
/// ```
/// use actix_web::{get, web};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct SomeForm {
/// name: String,
/// age: u8
/// }
///
/// // Response will have:
/// // - status: 200 OK
/// // - header: `Content-Type: application/x-www-form-urlencoded`
/// // - body: `name=actix&age=123`
/// #[get("/")]
/// async fn index() -> web::Form<SomeForm> {
/// web::Form(SomeForm {
/// name: "actix".into(),
/// age: 123
/// })
/// }
/// ```
///
/// # Panics
/// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into
/// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic.
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Form<T>(pub T);
impl<T> Form<T> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> ops::Deref for Form<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> ops::DerefMut for Form<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> Serialize for Form<T>
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
/// See [here](#extractor) for example of usage as an extractor.
impl<T> FromRequest for Form<T>
where
T: DeserializeOwned + 'static,
{
type Error = Error;
type Future = FormExtractFut<T>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone();
FormExtractFut {
fut: UrlEncoded::new(req, payload).limit(limit),
req: req.clone(),
err_handler,
}
}
}
type FormErrHandler = Option<Rc<dyn Fn(UrlencodedError, &HttpRequest) -> Error>>;
pub struct FormExtractFut<T> {
fut: UrlEncoded<T>,
err_handler: FormErrHandler,
req: HttpRequest,
}
impl<T> Future for FormExtractFut<T>
where
T: DeserializeOwned + 'static,
{
type Output = Result<Form<T>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let res = ready!(Pin::new(&mut this.fut).poll(cx));
let res = match res {
Err(err) => match &this.err_handler {
Some(err_handler) => Err((err_handler)(err, &this.req)),
None => Err(err.into()),
},
Ok(item) => Ok(Form(item)),
};
Poll::Ready(res)
}
}
impl<T: fmt::Display> fmt::Display for Form<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// See [here](#responder) for example of usage as a handler return type.
impl<T: Serialize> Responder for Form<T> {
type Body = EitherBody<String>;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
match serde_urlencoded::to_string(&self.0) {
Ok(body) => match HttpResponse::Ok()
.content_type(mime::APPLICATION_WWW_FORM_URLENCODED)
.message_body(body)
{
Ok(res) => res.map_into_left_body(),
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
},
Err(err) => {
HttpResponse::from_error(UrlencodedError::Serialize(err)).map_into_right_body()
}
}
}
}
/// [`Form`] extractor configuration.
///
/// ```
/// use actix_web::{post, web, App, FromRequest, Result};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// // Custom `FormConfig` is applied to App.
/// // Max payload size for URL encoded forms is set to 4kB.
/// #[post("/")]
/// async fn index(form: web::Form<Info>) -> Result<String> {
/// Ok(format!("Welcome {}!", form.username))
/// }
///
/// App::new()
/// .app_data(web::FormConfig::default().limit(4096))
/// .service(index);
/// ```
#[derive(Clone)]
pub struct FormConfig {
limit: usize,
err_handler: FormErrHandler,
}
impl FormConfig {
/// Set maximum accepted payload size. By default this limit is 16kB.
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
/// Set custom error handler
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(UrlencodedError, &HttpRequest) -> Error + 'static,
{
self.err_handler = Some(Rc::new(f));
self
}
/// Extract payload config from app data.
///
/// Checks both `T` and `Data<T>`, in that order, and falls back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
}
/// Allow shared refs used as default.
const DEFAULT_CONFIG: FormConfig = FormConfig {
limit: 16_384, // 2^14 bytes (~16kB)
err_handler: None,
};
impl Default for FormConfig {
fn default() -> Self {
DEFAULT_CONFIG
}
}
/// Future that resolves to some `T` when parsed from a URL encoded payload.
///
/// Form can be deserialized from any type `T` that implements [`serde::Deserialize`].
///
/// Returns error if:
/// - content type is not `application/x-www-form-urlencoded`
/// - content length is greater than [limit](UrlEncoded::limit())
pub struct UrlEncoded<T> {
#[cfg(feature = "__compress")]
stream: Option<Decompress<Payload>>,
#[cfg(not(feature = "__compress"))]
stream: Option<Payload>,
limit: usize,
length: Option<usize>,
encoding: &'static Encoding,
err: Option<UrlencodedError>,
fut: Option<LocalBoxFuture<'static, Result<T, UrlencodedError>>>,
}
#[allow(clippy::borrow_interior_mutable_const)]
impl<T> UrlEncoded<T> {
/// Create a new future to decode a URL encoded request payload.
pub fn new(req: &HttpRequest, payload: &mut Payload) -> Self {
// check content type
if req.content_type().to_lowercase() != "application/x-www-form-urlencoded" {
return Self::err(UrlencodedError::ContentType);
}
let encoding = match req.encoding() {
Ok(enc) => enc,
Err(_) => return Self::err(UrlencodedError::ContentType),
};
let mut len = None;
if let Some(l) = req.headers().get(&CONTENT_LENGTH) {
if let Ok(s) = l.to_str() {
if let Ok(l) = s.parse::<usize>() {
len = Some(l)
} else {
return Self::err(UrlencodedError::UnknownLength);
}
} else {
return Self::err(UrlencodedError::UnknownLength);
}
};
let payload = {
cfg_if::cfg_if! {
if #[cfg(feature = "__compress")] {
Decompress::from_headers(payload.take(), req.headers())
} else {
payload.take()
}
}
};
UrlEncoded {
encoding,
stream: Some(payload),
limit: 32_768,
length: len,
fut: None,
err: None,
}
}
fn err(err: UrlencodedError) -> Self {
UrlEncoded {
stream: None,
limit: 32_768,
fut: None,
err: Some(err),
length: None,
encoding: UTF_8,
}
}
/// Set maximum accepted payload size. The default limit is 256kB.
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
}
impl<T> Future for UrlEncoded<T>
where
T: DeserializeOwned + 'static,
{
type Output = Result<T, UrlencodedError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if let Some(ref mut fut) = self.fut {
return Pin::new(fut).poll(cx);
}
if let Some(err) = self.err.take() {
return Poll::Ready(Err(err));
}
// payload size
let limit = self.limit;
if let Some(len) = self.length.take() {
if len > limit {
return Poll::Ready(Err(UrlencodedError::Overflow { size: len, limit }));
}
}
// future
let encoding = self.encoding;
let mut stream = self.stream.take().unwrap();
self.fut = Some(
async move {
let mut body = BytesMut::with_capacity(8192);
while let Some(item) = stream.next().await {
let chunk = item?;
if (body.len() + chunk.len()) > limit {
return Err(UrlencodedError::Overflow {
size: body.len() + chunk.len(),
limit,
});
} else {
body.extend_from_slice(&chunk);
}
}
if encoding == UTF_8 {
serde_urlencoded::from_bytes::<T>(&body).map_err(UrlencodedError::Parse)
} else {
let body = encoding
.decode_without_bom_handling_and_without_replacement(&body)
.map(Cow::into_owned)
.ok_or(UrlencodedError::Encoding)?;
serde_urlencoded::from_str::<T>(&body).map_err(UrlencodedError::Parse)
}
}
.boxed_local(),
);
self.poll(cx)
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use super::*;
use crate::test::TestRequest;
use crate::{
http::{
header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE},
StatusCode,
},
test::assert_body_eq,
};
#[derive(Deserialize, Serialize, Debug, PartialEq)]
struct Info {
hello: String,
counter: i64,
}
#[actix_rt::test]
async fn test_form() {
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((CONTENT_LENGTH, 11))
.set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts();
let Form(s) = Form::<Info>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(
s,
Info {
hello: "world".into(),
counter: 123
}
);
}
fn eq(err: UrlencodedError, other: UrlencodedError) -> bool {
match err {
UrlencodedError::Overflow { .. } => {
matches!(other, UrlencodedError::Overflow { .. })
}
UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength),
UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType),
_ => false,
}
}
#[actix_rt::test]
async fn test_urlencoded_error() {
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((CONTENT_LENGTH, "xxxx"))
.to_http_parts();
let info = UrlEncoded::<Info>::new(&req, &mut pl).await;
assert!(eq(info.err().unwrap(), UrlencodedError::UnknownLength));
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((CONTENT_LENGTH, "1000000"))
.to_http_parts();
let info = UrlEncoded::<Info>::new(&req, &mut pl).await;
assert!(eq(
info.err().unwrap(),
UrlencodedError::Overflow { size: 0, limit: 0 }
));
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, "text/plain"))
.insert_header((CONTENT_LENGTH, 10))
.to_http_parts();
let info = UrlEncoded::<Info>::new(&req, &mut pl).await;
assert!(eq(info.err().unwrap(), UrlencodedError::ContentType));
}
#[actix_rt::test]
async fn test_urlencoded() {
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, "application/x-www-form-urlencoded"))
.insert_header((CONTENT_LENGTH, 11))
.set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts();
let info = UrlEncoded::<Info>::new(&req, &mut pl).await.unwrap();
assert_eq!(
info,
Info {
hello: "world".to_owned(),
counter: 123
}
);
let (req, mut pl) = TestRequest::default()
.insert_header((
CONTENT_TYPE,
"application/x-www-form-urlencoded; charset=utf-8",
))
.insert_header((CONTENT_LENGTH, 11))
.set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts();
let info = UrlEncoded::<Info>::new(&req, &mut pl).await.unwrap();
assert_eq!(
info,
Info {
hello: "world".to_owned(),
counter: 123
}
);
}
#[actix_rt::test]
async fn test_responder() {
let req = TestRequest::default().to_http_request();
let form = Form(Info {
hello: "world".to_string(),
counter: 123,
});
let res = form.respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/x-www-form-urlencoded")
);
assert_body_eq!(res, b"hello=world&counter=123");
}
#[actix_rt::test]
async fn test_with_config_in_data_wrapper() {
let ctype = HeaderValue::from_static("application/x-www-form-urlencoded");
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, ctype))
.insert_header((CONTENT_LENGTH, HeaderValue::from_static("20")))
.set_payload(Bytes::from_static(b"hello=test&counter=4"))
.app_data(web::Data::new(FormConfig::default().limit(10)))
.to_http_parts();
let s = Form::<Info>::from_request(&req, &mut pl).await;
assert!(s.is_err());
let err_str = s.err().unwrap().to_string();
assert!(err_str.starts_with("URL encoded payload is larger"));
}
}

View File

@ -0,0 +1,102 @@
//! For header extractor helper documentation, see [`Header`](crate::types::Header).
use std::{fmt, ops};
use actix_utils::future::{err, ok, Ready};
use crate::{
dev::Payload, error::ParseError, extract::FromRequest, http::header::Header as ParseHeader,
HttpRequest,
};
/// Extract typed headers from the request.
///
/// To extract a header, the inner type `T` must implement the
/// [`Header`](crate::http::header::Header) trait.
///
/// # Examples
/// ```
/// use actix_web::{get, web, http::header};
///
/// #[get("/")]
/// async fn index(date: web::Header<header::Date>) -> String {
/// format!("Request was sent at {}", date.to_string())
/// }
/// ```
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Header<T>(pub T);
impl<T> Header<T> {
/// Unwrap into the inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> ops::Deref for Header<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> ops::DerefMut for Header<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T> fmt::Display for Header<T>
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<T> FromRequest for Header<T>
where
T: ParseHeader,
{
type Error = ParseError;
type Future = Ready<Result<Self, Self::Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
match ParseHeader::parse(req) {
Ok(header) => ok(Header(header)),
Err(e) => err(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::http::{header, Method};
use crate::test::TestRequest;
#[actix_rt::test]
async fn test_header_extract() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
.insert_header((header::ALLOW, header::Allow(vec![Method::GET])))
.to_http_parts();
let s = Header::<header::ContentType>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(s.into_inner().0, mime::APPLICATION_JSON);
let s = Header::<header::Allow>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(s.into_inner().0, vec![Method::GET]);
assert!(Header::<header::Date>::from_request(&req, &mut pl)
.await
.is_err());
}
}

753
actix-web/src/types/json.rs Normal file
View File

@ -0,0 +1,753 @@
//! For JSON helper documentation, see [`Json`].
use std::{
fmt,
future::Future,
marker::PhantomData,
ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use bytes::BytesMut;
use futures_core::{ready, Stream as _};
use serde::{de::DeserializeOwned, Serialize};
use actix_http::Payload;
#[cfg(feature = "__compress")]
use crate::dev::Decompress;
use crate::{
body::EitherBody,
error::{Error, JsonPayloadError},
extract::FromRequest,
http::header::CONTENT_LENGTH,
request::HttpRequest,
web, HttpMessage, HttpResponse, Responder,
};
/// JSON extractor and responder.
///
/// `Json` has two uses: JSON responses, and extracting typed data from JSON request payloads.
///
/// # Extractor
/// To extract typed data from a request body, the inner type `T` must implement the
/// [`serde::Deserialize`] trait.
///
/// Use [`JsonConfig`] to configure extraction options.
///
/// ```
/// use actix_web::{post, web, App};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's body
/// #[post("/")]
/// async fn index(info: web::Json<Info>) -> String {
/// format!("Welcome {}!", info.username)
/// }
/// ```
///
/// # Responder
/// The `Json` type JSON formatted responses. A handler may return a value of type
/// `Json<T>` where `T` is the type of a structure to serialize into JSON. The type `T` must
/// implement [`serde::Serialize`].
///
/// ```
/// use actix_web::{post, web, HttpRequest};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct Info {
/// name: String,
/// }
///
/// #[post("/{name}")]
/// async fn index(req: HttpRequest) -> web::Json<Info> {
/// web::Json(Info {
/// name: req.match_info().get("name").unwrap().to_owned(),
/// })
/// }
/// ```
#[derive(Debug)]
pub struct Json<T>(pub T);
impl<T> Json<T> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T> ops::Deref for Json<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> ops::DerefMut for Json<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T: fmt::Display> fmt::Display for Json<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl<T: Serialize> Serialize for Json<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
self.0.serialize(serializer)
}
}
/// Creates response with OK status code, correct content type header, and serialized JSON payload.
///
/// If serialization failed
impl<T: Serialize> Responder for Json<T> {
type Body = EitherBody<String>;
fn respond_to(self, _: &HttpRequest) -> HttpResponse<Self::Body> {
match serde_json::to_string(&self.0) {
Ok(body) => match HttpResponse::Ok()
.content_type(mime::APPLICATION_JSON)
.message_body(body)
{
Ok(res) => res.map_into_left_body(),
Err(err) => HttpResponse::from_error(err).map_into_right_body(),
},
Err(err) => {
HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body()
}
}
}
}
/// See [here](#extractor) for example of usage as an extractor.
impl<T: DeserializeOwned> FromRequest for Json<T> {
type Error = Error;
type Future = JsonExtractFut<T>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let config = JsonConfig::from_req(req);
let limit = config.limit;
let ctype_required = config.content_type_required;
let ctype_fn = config.content_type.as_deref();
let err_handler = config.err_handler.clone();
JsonExtractFut {
req: Some(req.clone()),
fut: JsonBody::new(req, payload, ctype_fn, ctype_required).limit(limit),
err_handler,
}
}
}
type JsonErrorHandler =
Option<Arc<dyn Fn(JsonPayloadError, &HttpRequest) -> Error + Send + Sync>>;
pub struct JsonExtractFut<T> {
req: Option<HttpRequest>,
fut: JsonBody<T>,
err_handler: JsonErrorHandler,
}
impl<T: DeserializeOwned> Future for JsonExtractFut<T> {
type Output = Result<Json<T>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
let res = ready!(Pin::new(&mut this.fut).poll(cx));
let res = match res {
Err(err) => {
let req = this.req.take().unwrap();
log::debug!(
"Failed to deserialize Json from payload. \
Request path: {}",
req.path()
);
if let Some(err_handler) = this.err_handler.as_ref() {
Err((*err_handler)(err, &req))
} else {
Err(err.into())
}
}
Ok(data) => Ok(Json(data)),
};
Poll::Ready(res)
}
}
/// `Json` extractor configuration.
///
/// # Examples
/// ```
/// use actix_web::{error, post, web, App, FromRequest, HttpResponse};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// name: String,
/// }
///
/// // `Json` extraction is bound by custom `JsonConfig` applied to App.
/// #[post("/")]
/// async fn index(info: web::Json<Info>) -> String {
/// format!("Welcome {}!", info.name)
/// }
///
/// // custom `Json` extractor configuration
/// let json_cfg = web::JsonConfig::default()
/// // limit request payload size
/// .limit(4096)
/// // only accept text/plain content type
/// .content_type(|mime| mime == mime::TEXT_PLAIN)
/// // use custom error handler
/// .error_handler(|err, req| {
/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into()
/// });
///
/// App::new()
/// .app_data(json_cfg)
/// .service(index);
/// ```
#[derive(Clone)]
pub struct JsonConfig {
limit: usize,
err_handler: JsonErrorHandler,
content_type: Option<Arc<dyn Fn(mime::Mime) -> bool + Send + Sync>>,
content_type_required: bool,
}
impl JsonConfig {
/// Set maximum accepted payload size. By default this limit is 2MB.
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
/// Set custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(JsonPayloadError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
/// Set predicate for allowed content types.
pub fn content_type<F>(mut self, predicate: F) -> Self
where
F: Fn(mime::Mime) -> bool + Send + Sync + 'static,
{
self.content_type = Some(Arc::new(predicate));
self
}
/// Sets whether or not the request must have a `Content-Type` header to be parsed.
pub fn content_type_required(mut self, content_type_required: bool) -> Self {
self.content_type_required = content_type_required;
self
}
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
}
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
/// Allow shared refs used as default.
const DEFAULT_CONFIG: JsonConfig = JsonConfig {
limit: DEFAULT_LIMIT,
err_handler: None,
content_type: None,
content_type_required: true,
};
impl Default for JsonConfig {
fn default() -> Self {
DEFAULT_CONFIG.clone()
}
}
/// Future that resolves to some `T` when parsed from a JSON payload.
///
/// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize].
///
/// Returns error if:
/// - `Content-Type` is not `application/json` when `ctype_required` (passed to [`new`][Self::new])
/// is `true`.
/// - `Content-Length` is greater than [limit](JsonBody::limit()).
/// - The payload, when consumed, is not valid JSON.
pub enum JsonBody<T> {
Error(Option<JsonPayloadError>),
Body {
limit: usize,
/// Length as reported by `Content-Length` header, if present.
length: Option<usize>,
#[cfg(feature = "__compress")]
payload: Decompress<Payload>,
#[cfg(not(feature = "__compress"))]
payload: Payload,
buf: BytesMut,
_res: PhantomData<T>,
},
}
impl<T> Unpin for JsonBody<T> {}
impl<T: DeserializeOwned> JsonBody<T> {
/// Create a new future to decode a JSON request payload.
#[allow(clippy::borrow_interior_mutable_const)]
pub fn new(
req: &HttpRequest,
payload: &mut Payload,
ctype_fn: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>,
ctype_required: bool,
) -> Self {
// check content-type
let can_parse_json = if let Ok(Some(mime)) = req.mime_type() {
mime.subtype() == mime::JSON
|| mime.suffix() == Some(mime::JSON)
|| ctype_fn.map_or(false, |predicate| predicate(mime))
} else {
// if `ctype_required` is false, assume payload is
// json even when content-type header is missing
!ctype_required
};
if !can_parse_json {
return JsonBody::Error(Some(JsonPayloadError::ContentType));
}
let length = req
.headers()
.get(&CONTENT_LENGTH)
.and_then(|l| l.to_str().ok())
.and_then(|s| s.parse::<usize>().ok());
// Notice the content-length is not checked against limit of json config here.
// As the internal usage always call JsonBody::limit after JsonBody::new.
// And limit check to return an error variant of JsonBody happens there.
let payload = {
cfg_if::cfg_if! {
if #[cfg(feature = "__compress")] {
Decompress::from_headers(payload.take(), req.headers())
} else {
payload.take()
}
}
};
JsonBody::Body {
limit: DEFAULT_LIMIT,
length,
payload,
buf: BytesMut::with_capacity(8192),
_res: PhantomData,
}
}
/// Set maximum accepted payload size. The default limit is 2MB.
pub fn limit(self, limit: usize) -> Self {
match self {
JsonBody::Body {
length,
payload,
buf,
..
} => {
if let Some(len) = length {
if len > limit {
return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength {
length: len,
limit,
}));
}
}
JsonBody::Body {
limit,
length,
payload,
buf,
_res: PhantomData,
}
}
JsonBody::Error(e) => JsonBody::Error(e),
}
}
}
impl<T: DeserializeOwned> Future for JsonBody<T> {
type Output = Result<T, JsonPayloadError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match this {
JsonBody::Body {
limit,
buf,
payload,
..
} => loop {
let res = ready!(Pin::new(&mut *payload).poll_next(cx));
match res {
Some(chunk) => {
let chunk = chunk?;
let buf_len = buf.len() + chunk.len();
if buf_len > *limit {
return Poll::Ready(Err(JsonPayloadError::Overflow {
limit: *limit,
}));
} else {
buf.extend_from_slice(&chunk);
}
}
None => {
let json = serde_json::from_slice::<T>(buf)
.map_err(JsonPayloadError::Deserialize)?;
return Poll::Ready(Ok(json));
}
}
},
JsonBody::Error(e) => Poll::Ready(Err(e.take().unwrap())),
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use serde::{Deserialize, Serialize};
use super::*;
use crate::{
body,
error::InternalError,
http::{
header::{self, CONTENT_LENGTH, CONTENT_TYPE},
StatusCode,
},
test::{assert_body_eq, TestRequest},
};
#[derive(Serialize, Deserialize, PartialEq, Debug)]
struct MyObject {
name: String,
}
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
match err {
JsonPayloadError::Overflow { .. } => {
matches!(other, JsonPayloadError::Overflow { .. })
}
JsonPayloadError::OverflowKnownLength { .. } => {
matches!(other, JsonPayloadError::OverflowKnownLength { .. })
}
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
_ => false,
}
}
#[actix_rt::test]
async fn test_responder() {
let req = TestRequest::default().to_http_request();
let j = Json(MyObject {
name: "test".to_string(),
});
let res = j.respond_to(&req);
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_TYPE).unwrap(),
header::HeaderValue::from_static("application/json")
);
assert_body_eq!(res, b"{\"name\":\"test\"}");
}
#[actix_rt::test]
async fn test_custom_error_responder() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().limit(10).error_handler(|err, _| {
let msg = MyObject {
name: "invalid request".to_string(),
};
let resp =
HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap());
InternalError::from_response(err, resp).into()
}))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
let resp = HttpResponse::from_error(s.unwrap_err());
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = body::to_bytes(resp.into_body()).await.unwrap();
let msg: MyObject = serde_json::from_slice(&body).unwrap();
assert_eq!(msg.name, "invalid request");
}
#[actix_rt::test]
async fn test_extract() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s.name, "test");
assert_eq!(
s.into_inner(),
MyObject {
name: "test".to_string()
}
);
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().limit(10))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(format!("{}", s.err().unwrap())
.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(
JsonConfig::default()
.limit(10)
.error_handler(|_, _| JsonPayloadError::ContentType.into()),
)
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(format!("{}", s.err().unwrap()).contains("Content type error"));
}
#[actix_rt::test]
async fn test_json_body() {
let (req, mut pl) = TestRequest::default().to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true).await;
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/text"),
))
.to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true).await;
assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("10000"),
))
.to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
.limit(100)
.await;
assert!(json_eq(
json.err().unwrap(),
JsonPayloadError::OverflowKnownLength {
length: 10000,
limit: 100
}
));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.set_payload(Bytes::from_static(&[0u8; 1000]))
.to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true)
.limit(100)
.await;
assert!(json_eq(
json.err().unwrap(),
JsonPayloadError::Overflow { limit: 100 }
));
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/json"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.to_http_parts();
let json = JsonBody::<MyObject>::new(&req, &mut pl, None, true).await;
assert_eq!(
json.ok().unwrap(),
MyObject {
name: "test".to_owned()
}
);
}
#[actix_rt::test]
async fn test_with_json_and_bad_content_type() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().limit(4096))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(s.is_err())
}
#[actix_rt::test]
async fn test_with_json_and_good_custom_content_type() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().content_type(|mime: mime::Mime| {
mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN
}))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(s.is_ok())
}
#[actix_rt::test]
async fn test_with_json_and_bad_custom_content_type() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_TYPE,
header::HeaderValue::from_static("text/html"),
))
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().content_type(|mime: mime::Mime| {
mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN
}))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(s.is_err())
}
#[actix_rt::test]
async fn test_json_with_no_content_type() {
let (req, mut pl) = TestRequest::default()
.insert_header((
header::CONTENT_LENGTH,
header::HeaderValue::from_static("16"),
))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(JsonConfig::default().content_type_required(false))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(s.is_ok())
}
#[actix_rt::test]
async fn test_with_config_in_data_wrapper() {
let (req, mut pl) = TestRequest::default()
.insert_header((CONTENT_TYPE, mime::APPLICATION_JSON))
.insert_header((CONTENT_LENGTH, 16))
.set_payload(Bytes::from_static(b"{\"name\": \"test\"}"))
.app_data(web::Data::new(JsonConfig::default().limit(10)))
.to_http_parts();
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
assert!(s.is_err());
let err_str = s.err().unwrap().to_string();
assert!(err_str
.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
}
}

View File

@ -0,0 +1,19 @@
//! Common extractors and responders.
mod either;
mod form;
mod header;
mod json;
mod path;
mod payload;
mod query;
mod readlines;
pub use self::either::Either;
pub use self::form::{Form, FormConfig, UrlEncoded};
pub use self::header::Header;
pub use self::json::{Json, JsonBody, JsonConfig};
pub use self::path::{Path, PathConfig};
pub use self::payload::{Payload, PayloadConfig};
pub use self::query::{Query, QueryConfig};
pub use self::readlines::Readlines;

291
actix-web/src/types/path.rs Normal file
View File

@ -0,0 +1,291 @@
//! For path segment extractor documentation, see [`Path`].
use std::sync::Arc;
use actix_router::PathDeserializer;
use actix_utils::future::{ready, Ready};
use derive_more::{AsRef, Deref, DerefMut, Display, From};
use serde::de;
use crate::{
dev::Payload,
error::{Error, ErrorNotFound, PathError},
web::Data,
FromRequest, HttpRequest,
};
/// Extract typed data from request path segments.
///
/// Use [`PathConfig`] to configure extraction option.
///
/// Unlike, [`HttpRequest::match_info`], this extractor will fully percent-decode dynamic segments,
/// including `/`, `%`, and `+`.
///
/// # Examples
/// ```
/// use actix_web::{get, web};
///
/// // extract path info from "/{name}/{count}/index.html" into tuple
/// // {name} - deserialize a String
/// // {count} - deserialize a u32
/// #[get("/{name}/{count}/index.html")]
/// async fn index(path: web::Path<(String, u32)>) -> String {
/// let (name, count) = path.into_inner();
/// format!("Welcome {}! {}", name, count)
/// }
/// ```
///
/// Path segments also can be deserialized into any type that implements [`serde::Deserialize`].
/// Path segment labels will be matched with struct field names.
///
/// ```
/// use actix_web::{get, web};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// name: String,
/// }
///
/// // extract `Info` from a path using serde
/// #[get("/{name}")]
/// async fn index(info: web::Path<Info>) -> String {
/// format!("Welcome {}!", info.name)
/// }
/// ```
#[derive(
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From,
)]
pub struct Path<T>(T);
impl<T> Path<T> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
/// See [here](#Examples) for example of usage as an extractor.
impl<T> FromRequest for Path<T>
where
T: de::DeserializeOwned,
{
type Error = Error;
type Future = Ready<Result<Self, Self::Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req
.app_data::<PathConfig>()
.or_else(|| req.app_data::<Data<PathConfig>>().map(Data::get_ref))
.and_then(|c| c.err_handler.clone());
ready(
de::Deserialize::deserialize(PathDeserializer::new(req.match_info()))
.map(Path)
.map_err(move |err| {
log::debug!(
"Failed during Path extractor deserialization. \
Request path: {:?}",
req.path()
);
if let Some(error_handler) = error_handler {
let e = PathError::Deserialize(err);
(error_handler)(e, req)
} else {
ErrorNotFound(err)
}
}),
)
}
}
/// Path extractor configuration
///
/// ```
/// use actix_web::web::PathConfig;
/// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde::Deserialize;
///
/// #[derive(Deserialize, Debug)]
/// enum Folder {
/// #[serde(rename = "inbox")]
/// Inbox,
///
/// #[serde(rename = "outbox")]
/// Outbox,
/// }
///
/// // deserialize `Info` from request's path
/// async fn index(folder: web::Path<Folder>) -> String {
/// format!("Selected folder: {:?}!", folder)
/// }
///
/// let app = App::new().service(
/// web::resource("/messages/{folder}")
/// .app_data(PathConfig::default().error_handler(|err, req| {
/// error::InternalError::from_response(
/// err,
/// HttpResponse::Conflict().into(),
/// )
/// .into()
/// }))
/// .route(web::post().to(index)),
/// );
/// ```
#[derive(Clone, Default)]
pub struct PathConfig {
err_handler: Option<Arc<dyn Fn(PathError, &HttpRequest) -> Error + Send + Sync>>,
}
impl PathConfig {
/// Set custom error handler.
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
}
#[cfg(test)]
mod tests {
use actix_router::ResourceDef;
use derive_more::Display;
use serde::Deserialize;
use super::*;
use crate::test::TestRequest;
use crate::{error, http, HttpResponse};
#[derive(Deserialize, Debug, Display)]
#[display(fmt = "MyStruct({}, {})", key, value)]
struct MyStruct {
key: String,
value: String,
}
#[derive(Deserialize)]
struct Test2 {
key: String,
value: u32,
}
#[actix_rt::test]
async fn test_extract_path_single() {
let resource = ResourceDef::new("/{value}/");
let mut req = TestRequest::with_uri("/32/").to_srv_request();
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
assert_eq!(*Path::<i8>::from_request(&req, &mut pl).await.unwrap(), 32);
assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
}
#[actix_rt::test]
async fn test_tuple_extract() {
let resource = ResourceDef::new("/{key}/{value}/");
let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(res.0, "name");
assert_eq!(res.1, "user1");
let (Path(a), Path(b)) =
<(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(a.0, "name");
assert_eq!(a.1, "user1");
assert_eq!(b.0, "name");
assert_eq!(b.1, "user1");
let () = <()>::from_request(&req, &mut pl).await.unwrap();
}
#[actix_rt::test]
async fn test_request_extract() {
let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
let resource = ResourceDef::new("/{key}/{value}/");
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let mut s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s.key, "name");
assert_eq!(s.value, "user1");
s.value = "user2".to_string();
assert_eq!(s.value, "user2");
assert_eq!(
format!("{}, {:?}", s, s),
"MyStruct(name, user2), Path(MyStruct { key: \"name\", value: \"user2\" })"
);
let s = s.into_inner();
assert_eq!(s.value, "user2");
let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(s.0, "name");
assert_eq!(s.1, "user1");
let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
let resource = ResourceDef::new("/{key}/{value}/");
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s.as_ref().key, "name");
assert_eq!(s.value, 32);
let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(s.0, "name");
assert_eq!(s.1, 32);
let res = Path::<Vec<String>>::from_request(&req, &mut pl)
.await
.unwrap();
assert_eq!(res[0], "name".to_owned());
assert_eq!(res[1], "32".to_owned());
}
#[actix_rt::test]
async fn paths_decoded() {
let resource = ResourceDef::new("/{key}/{value}");
let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
resource.capture_match_info(req.match_info_mut());
let (req, mut pl) = req.into_parts();
let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(path_items.key, "na+me");
assert_eq!(path_items.value, "us/er%42");
assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542");
}
#[actix_rt::test]
async fn test_custom_err_handler() {
let (req, mut pl) = TestRequest::with_uri("/name/user1/")
.app_data(PathConfig::default().error_handler(|err, _| {
error::InternalError::from_response(err, HttpResponse::Conflict().finish())
.into()
}))
.to_http_parts();
let s = Path::<(usize,)>::from_request(&req, &mut pl)
.await
.unwrap_err();
let res = HttpResponse::from_error(s);
assert_eq!(res.status(), http::StatusCode::CONFLICT);
}
}

View File

@ -0,0 +1,538 @@
//! Basic binary and string payload extractors.
use std::{
borrow::Cow,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
};
use actix_http::error::PayloadError;
use actix_utils::future::{ready, Either, Ready};
use bytes::{Bytes, BytesMut};
use encoding_rs::{Encoding, UTF_8};
use futures_core::{ready, stream::Stream};
use mime::Mime;
use crate::{
dev, error::ErrorBadRequest, http::header, web, Error, FromRequest, HttpMessage,
HttpRequest,
};
/// Extract a request's raw payload stream.
///
/// See [`PayloadConfig`] for important notes when using this advanced extractor.
///
/// # Examples
/// ```
/// use std::future::Future;
/// use futures_util::stream::StreamExt as _;
/// use actix_web::{post, web};
///
/// // `body: web::Payload` parameter extracts raw payload stream from request
/// #[post("/")]
/// async fn index(mut body: web::Payload) -> actix_web::Result<String> {
/// // for demonstration only; in a normal case use the `Bytes` extractor
/// // collect payload stream into a bytes object
/// let mut bytes = web::BytesMut::new();
/// while let Some(item) = body.next().await {
/// bytes.extend_from_slice(&item?);
/// }
///
/// Ok(format!("Request Body Bytes:\n{:?}", bytes))
/// }
/// ```
pub struct Payload(dev::Payload);
impl Payload {
/// Unwrap to inner Payload type.
#[inline]
pub fn into_inner(self) -> dev::Payload {
self.0
}
}
impl Stream for Payload {
type Item = Result<Bytes, PayloadError>;
#[inline]
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Pin::new(&mut self.0).poll_next(cx)
}
}
/// See [here](#Examples) for example of usage as an extractor.
impl FromRequest for Payload {
type Error = Error;
type Future = Ready<Result<Payload, Error>>;
#[inline]
fn from_request(_: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
ready(Ok(Payload(payload.take())))
}
}
/// Extract binary data from a request's payload.
///
/// Collects request payload stream into a [Bytes] instance.
///
/// Use [`PayloadConfig`] to configure extraction process.
///
/// # Examples
/// ```
/// use actix_web::{post, web};
///
/// /// extract binary data from request
/// #[post("/")]
/// async fn index(body: web::Bytes) -> String {
/// format!("Body {:?}!", body)
/// }
/// ```
impl FromRequest for Bytes {
type Error = Error;
type Future = Either<BytesExtractFut, Ready<Result<Bytes, Error>>>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
// allow both Config and Data<Config>
let cfg = PayloadConfig::from_req(req);
if let Err(err) = cfg.check_mimetype(req) {
return Either::right(ready(Err(err)));
}
Either::left(BytesExtractFut {
body_fut: HttpMessageBody::new(req, payload).limit(cfg.limit),
})
}
}
/// Future for `Bytes` extractor.
pub struct BytesExtractFut {
body_fut: HttpMessageBody,
}
impl<'a> Future for BytesExtractFut {
type Output = Result<Bytes, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.body_fut).poll(cx).map_err(Into::into)
}
}
/// Extract text information from a request's body.
///
/// Text extractor automatically decode body according to the request's charset.
///
/// Use [`PayloadConfig`] to configure extraction process.
///
/// # Examples
/// ```
/// use actix_web::{post, web, FromRequest};
///
/// // extract text data from request
/// #[post("/")]
/// async fn index(text: String) -> String {
/// format!("Body {}!", text)
/// }
impl FromRequest for String {
type Error = Error;
type Future = Either<StringExtractFut, Ready<Result<String, Error>>>;
#[inline]
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
let cfg = PayloadConfig::from_req(req);
// check content-type
if let Err(err) = cfg.check_mimetype(req) {
return Either::right(ready(Err(err)));
}
// check charset
let encoding = match req.encoding() {
Ok(enc) => enc,
Err(err) => return Either::right(ready(Err(err.into()))),
};
let limit = cfg.limit;
let body_fut = HttpMessageBody::new(req, payload).limit(limit);
Either::left(StringExtractFut { body_fut, encoding })
}
}
/// Future for `String` extractor.
pub struct StringExtractFut {
body_fut: HttpMessageBody,
encoding: &'static Encoding,
}
impl<'a> Future for StringExtractFut {
type Output = Result<String, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let encoding = self.encoding;
Pin::new(&mut self.body_fut).poll(cx).map(|out| {
let body = out?;
bytes_to_string(body, encoding)
})
}
}
fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result<String, Error> {
if encoding == UTF_8 {
Ok(str::from_utf8(body.as_ref())
.map_err(|_| ErrorBadRequest("Can not decode body"))?
.to_owned())
} else {
Ok(encoding
.decode_without_bom_handling_and_without_replacement(&body)
.map(Cow::into_owned)
.ok_or_else(|| ErrorBadRequest("Can not decode body"))?)
}
}
/// Configuration for request payloads.
///
/// Applies to the built-in [`Bytes`] and [`String`] extractors.
/// Note that the [`Payload`] extractor does not automatically check
/// conformance with this configuration to allow more flexibility when
/// building extractors on top of [`Payload`].
///
/// By default, the payload size limit is 256kB and there is no mime type condition.
///
/// To use this, add an instance of it to your [`app`](crate::App), [`scope`](crate::Scope)
/// or [`resource`](crate::Resource) through the associated `.app_data()` method.
#[derive(Clone)]
pub struct PayloadConfig {
limit: usize,
mimetype: Option<Mime>,
}
impl PayloadConfig {
/// Create new instance with a size limit (in bytes) and no mime type condition.
pub fn new(limit: usize) -> Self {
Self {
limit,
..Default::default()
}
}
/// Set maximum accepted payload size in bytes. The default limit is 256KiB.
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
/// Set required mime type of the request. By default mime type is not enforced.
pub fn mimetype(mut self, mt: Mime) -> Self {
self.mimetype = Some(mt);
self
}
fn check_mimetype(&self, req: &HttpRequest) -> Result<(), Error> {
// check content-type
if let Some(ref mt) = self.mimetype {
match req.mime_type() {
Ok(Some(ref req_mt)) => {
if mt != req_mt {
return Err(ErrorBadRequest("Unexpected Content-Type"));
}
}
Ok(None) => {
return Err(ErrorBadRequest("Content-Type is expected"));
}
Err(err) => {
return Err(err.into());
}
}
}
Ok(())
}
/// Extract payload config from app data. Check both `T` and `Data<T>`, in that order, and fall
/// back to the default payload config if neither is found.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
}
const DEFAULT_CONFIG_LIMIT: usize = 262_144; // 2^18 bytes (~256kB)
/// Allow shared refs used as defaults.
const DEFAULT_CONFIG: PayloadConfig = PayloadConfig {
limit: DEFAULT_CONFIG_LIMIT,
mimetype: None,
};
impl Default for PayloadConfig {
fn default() -> Self {
DEFAULT_CONFIG.clone()
}
}
/// Future that resolves to a complete HTTP body payload.
///
/// By default only 256kB payload is accepted before `PayloadError::Overflow` is returned.
/// Use `MessageBody::limit()` method to change upper limit.
pub struct HttpMessageBody {
limit: usize,
length: Option<usize>,
#[cfg(feature = "__compress")]
stream: dev::Decompress<dev::Payload>,
#[cfg(not(feature = "__compress"))]
stream: dev::Payload,
buf: BytesMut,
err: Option<PayloadError>,
}
impl HttpMessageBody {
/// Create `MessageBody` for request.
#[allow(clippy::borrow_interior_mutable_const)]
pub fn new(req: &HttpRequest, payload: &mut dev::Payload) -> HttpMessageBody {
let mut length = None;
let mut err = None;
if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) {
match l.to_str() {
Ok(s) => match s.parse::<usize>() {
Ok(l) => {
if l > DEFAULT_CONFIG_LIMIT {
err = Some(PayloadError::Overflow);
}
length = Some(l)
}
Err(_) => err = Some(PayloadError::UnknownLength),
},
Err(_) => err = Some(PayloadError::UnknownLength),
}
}
let stream = {
cfg_if::cfg_if! {
if #[cfg(feature = "__compress")] {
dev::Decompress::from_headers(payload.take(), req.headers())
} else {
payload.take()
}
}
};
HttpMessageBody {
stream,
limit: DEFAULT_CONFIG_LIMIT,
length,
buf: BytesMut::with_capacity(8192),
err,
}
}
/// Change max size of payload. By default max size is 256kB
pub fn limit(mut self, limit: usize) -> Self {
if let Some(l) = self.length {
self.err = if l > limit {
Some(PayloadError::Overflow)
} else {
None
};
}
self.limit = limit;
self
}
}
impl Future for HttpMessageBody {
type Output = Result<Bytes, PayloadError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
if let Some(err) = this.err.take() {
return Poll::Ready(Err(err));
}
loop {
let res = ready!(Pin::new(&mut this.stream).poll_next(cx));
match res {
Some(chunk) => {
let chunk = chunk?;
if this.buf.len() + chunk.len() > this.limit {
return Poll::Ready(Err(PayloadError::Overflow));
} else {
this.buf.extend_from_slice(&chunk);
}
}
None => return Poll::Ready(Ok(this.buf.split().freeze())),
}
}
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use super::*;
use crate::http::{header, StatusCode};
use crate::test::{call_service, init_service, TestRequest};
use crate::{web, App, Responder};
#[actix_rt::test]
async fn test_payload_config() {
let req = TestRequest::default().to_http_request();
let cfg = PayloadConfig::default().mimetype(mime::APPLICATION_JSON);
assert!(cfg.check_mimetype(&req).is_err());
let req = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded"))
.to_http_request();
assert!(cfg.check_mimetype(&req).is_err());
let req = TestRequest::default()
.insert_header((header::CONTENT_TYPE, "application/json"))
.to_http_request();
assert!(cfg.check_mimetype(&req).is_ok());
}
// allow deprecated App::data
#[allow(deprecated)]
#[actix_rt::test]
async fn test_config_recall_locations() {
async fn bytes_handler(_: Bytes) -> impl Responder {
"payload is probably json bytes"
}
async fn string_handler(_: String) -> impl Responder {
"payload is probably json string"
}
let srv = init_service(
App::new()
.service(
web::resource("/bytes-app-data")
.app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON))
.route(web::get().to(bytes_handler)),
)
.service(
web::resource("/bytes-data")
.data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON))
.route(web::get().to(bytes_handler)),
)
.service(
web::resource("/string-app-data")
.app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON))
.route(web::get().to(string_handler)),
)
.service(
web::resource("/string-data")
.data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON))
.route(web::get().to(string_handler)),
),
)
.await;
let req = TestRequest::with_uri("/bytes-app-data").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/bytes-data").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/string-app-data").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/string-data").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let req = TestRequest::with_uri("/bytes-app-data")
.insert_header(header::ContentType(mime::APPLICATION_JSON))
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/bytes-data")
.insert_header(header::ContentType(mime::APPLICATION_JSON))
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/string-app-data")
.insert_header(header::ContentType(mime::APPLICATION_JSON))
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/string-data")
.insert_header(header::ContentType(mime::APPLICATION_JSON))
.to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_bytes() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_LENGTH, "11"))
.set_payload(Bytes::from_static(b"hello=world"))
.to_http_parts();
let s = Bytes::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s, Bytes::from_static(b"hello=world"));
}
#[actix_rt::test]
async fn test_string() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_LENGTH, "11"))
.set_payload(Bytes::from_static(b"hello=world"))
.to_http_parts();
let s = String::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s, "hello=world");
}
#[actix_rt::test]
async fn test_message_body() {
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_LENGTH, "xxxx"))
.to_srv_request()
.into_parts();
let res = HttpMessageBody::new(&req, &mut pl).await;
match res.err().unwrap() {
PayloadError::UnknownLength => {}
_ => unreachable!("error"),
}
let (req, mut pl) = TestRequest::default()
.insert_header((header::CONTENT_LENGTH, "1000000"))
.to_srv_request()
.into_parts();
let res = HttpMessageBody::new(&req, &mut pl).await;
match res.err().unwrap() {
PayloadError::Overflow => {}
_ => unreachable!("error"),
}
let (req, mut pl) = TestRequest::default()
.set_payload(Bytes::from_static(b"test"))
.to_http_parts();
let res = HttpMessageBody::new(&req, &mut pl).await;
assert_eq!(res.ok().unwrap(), Bytes::from_static(b"test"));
let (req, mut pl) = TestRequest::default()
.set_payload(Bytes::from_static(b"11111111111111"))
.to_http_parts();
let res = HttpMessageBody::new(&req, &mut pl).limit(5).await;
match res.err().unwrap() {
PayloadError::Overflow => {}
_ => unreachable!("error"),
}
}
}

View File

@ -0,0 +1,273 @@
//! For query parameter extractor documentation, see [`Query`].
use std::{fmt, ops, sync::Arc};
use actix_utils::future::{err, ok, Ready};
use serde::de::DeserializeOwned;
use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest};
/// Extract typed information from the request's query.
///
/// To extract typed data from the URL query string, the inner type `T` must implement the
/// [`DeserializeOwned`] trait.
///
/// Use [`QueryConfig`] to configure extraction process.
///
/// # Panics
/// A query string consists of unordered `key=value` pairs, therefore it cannot be decoded into any
/// type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic.
///
/// # Examples
/// ```
/// use actix_web::{get, web};
/// use serde::Deserialize;
///
/// #[derive(Debug, Deserialize)]
/// pub enum ResponseType {
/// Token,
/// Code
/// }
///
/// #[derive(Debug, Deserialize)]
/// pub struct AuthRequest {
/// id: u64,
/// response_type: ResponseType,
/// }
///
/// // Deserialize `AuthRequest` struct from query string.
/// // This handler gets called only if the request's query parameters contain both fields.
/// // A valid request path for this handler would be `/?id=64&response_type=Code"`.
/// #[get("/")]
/// async fn index(info: web::Query<AuthRequest>) -> String {
/// format!("Authorization request for id={} and type={:?}!", info.id, info.response_type)
/// }
///
/// // To access the entire underlying query struct, use `.into_inner()`.
/// #[get("/debug1")]
/// async fn debug1(info: web::Query<AuthRequest>) -> String {
/// dbg!("Authorization object = {:?}", info.into_inner());
/// "OK".to_string()
/// }
///
/// // Or use destructuring, which is equivalent to `.into_inner()`.
/// #[get("/debug2")]
/// async fn debug2(web::Query(info): web::Query<AuthRequest>) -> String {
/// dbg!("Authorization object = {:?}", info);
/// "OK".to_string()
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Query<T>(pub T);
impl<T> Query<T> {
/// Unwrap into inner `T` value.
pub fn into_inner(self) -> T {
self.0
}
}
impl<T: DeserializeOwned> Query<T> {
/// Deserialize a `T` from the URL encoded query parameter string.
///
/// ```
/// # use std::collections::HashMap;
/// # use actix_web::web::Query;
/// let numbers = Query::<HashMap<String, u32>>::from_query("one=1&two=2").unwrap();
/// assert_eq!(numbers.get("one"), Some(&1));
/// assert_eq!(numbers.get("two"), Some(&2));
/// assert!(numbers.get("three").is_none());
/// ```
pub fn from_query(query_str: &str) -> Result<Self, QueryPayloadError> {
serde_urlencoded::from_str::<T>(query_str)
.map(Self)
.map_err(QueryPayloadError::Deserialize)
}
}
impl<T> ops::Deref for Query<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
impl<T> ops::DerefMut for Query<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}
impl<T: fmt::Display> fmt::Display for Query<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
/// See [here](#Examples) for example of usage as an extractor.
impl<T: DeserializeOwned> FromRequest for Query<T> {
type Error = Error;
type Future = Ready<Result<Self, Error>>;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req
.app_data::<QueryConfig>()
.and_then(|c| c.err_handler.clone());
serde_urlencoded::from_str::<T>(req.query_string())
.map(|val| ok(Query(val)))
.unwrap_or_else(move |e| {
let e = QueryPayloadError::Deserialize(e);
log::debug!(
"Failed during Query extractor deserialization. \
Request path: {:?}",
req.path()
);
let e = if let Some(error_handler) = error_handler {
(error_handler)(e, req)
} else {
e.into()
};
err(e)
})
}
}
/// Query extractor configuration.
///
/// # Examples
/// ```
/// use actix_web::{error, get, web, App, FromRequest, HttpResponse};
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's querystring
/// #[get("/")]
/// async fn index(info: web::Query<Info>) -> String {
/// format!("Welcome {}!", info.username)
/// }
///
/// // custom `Query` extractor configuration
/// let query_cfg = web::QueryConfig::default()
/// // use custom error handler
/// .error_handler(|err, req| {
/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
/// });
///
/// App::new()
/// .app_data(query_cfg)
/// .service(index);
/// ```
#[derive(Clone, Default)]
pub struct QueryConfig {
err_handler: Option<Arc<dyn Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>,
}
impl QueryConfig {
/// Set custom error handler
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.err_handler = Some(Arc::new(f));
self
}
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use derive_more::Display;
use serde::Deserialize;
use super::*;
use crate::{error::InternalError, test::TestRequest, HttpResponse};
#[derive(Deserialize, Debug, Display)]
struct Id {
id: String,
}
#[actix_rt::test]
async fn test_service_request_extract() {
let req = TestRequest::with_uri("/name/user1/").to_srv_request();
assert!(Query::<Id>::from_query(req.query_string()).is_err());
let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
let mut s = Query::<Id>::from_query(req.query_string()).unwrap();
assert_eq!(s.id, "test");
assert_eq!(
format!("{}, {:?}", s, s),
"test, Query(Id { id: \"test\" })"
);
s.id = "test1".to_string();
let s = s.into_inner();
assert_eq!(s.id, "test1");
}
#[actix_rt::test]
async fn test_request_extract() {
let req = TestRequest::with_uri("/name/user1/").to_srv_request();
let (req, mut pl) = req.into_parts();
assert!(Query::<Id>::from_request(&req, &mut pl).await.is_err());
let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
let (req, mut pl) = req.into_parts();
let mut s = Query::<Id>::from_request(&req, &mut pl).await.unwrap();
assert_eq!(s.id, "test");
assert_eq!(
format!("{}, {:?}", s, s),
"test, Query(Id { id: \"test\" })"
);
s.id = "test1".to_string();
let s = s.into_inner();
assert_eq!(s.id, "test1");
}
#[actix_rt::test]
#[should_panic]
async fn test_tuple_panic() {
let req = TestRequest::with_uri("/?one=1&two=2").to_srv_request();
let (req, mut pl) = req.into_parts();
Query::<(u32, u32)>::from_request(&req, &mut pl)
.await
.unwrap();
}
#[actix_rt::test]
async fn test_custom_error_responder() {
let req = TestRequest::with_uri("/name/user1/")
.app_data(QueryConfig::default().error_handler(|e, _| {
let resp = HttpResponse::UnprocessableEntity().finish();
InternalError::from_response(e, resp).into()
}))
.to_srv_request();
let (req, mut pl) = req.into_parts();
let query = Query::<Id>::from_request(&req, &mut pl).await;
assert!(query.is_err());
assert_eq!(
query
.unwrap_err()
.as_response_error()
.error_response()
.status(),
StatusCode::UNPROCESSABLE_ENTITY
);
}
}

View File

@ -0,0 +1,211 @@
//! For request line reader documentation, see [`Readlines`].
use std::{
borrow::Cow,
pin::Pin,
str,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use encoding_rs::{Encoding, UTF_8};
use futures_core::{ready, stream::Stream};
use crate::{
dev::Payload,
error::{PayloadError, ReadlinesError},
HttpMessage,
};
/// Stream that reads request line by line.
pub struct Readlines<T: HttpMessage> {
stream: Payload<T::Stream>,
buff: BytesMut,
limit: usize,
checked_buff: bool,
encoding: &'static Encoding,
err: Option<ReadlinesError>,
}
impl<T> Readlines<T>
where
T: HttpMessage,
T::Stream: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
{
/// Create a new stream to read request line by line.
pub fn new(req: &mut T) -> Self {
let encoding = match req.encoding() {
Ok(enc) => enc,
Err(err) => return Self::err(err.into()),
};
Readlines {
stream: req.take_payload(),
buff: BytesMut::with_capacity(262_144),
limit: 262_144,
checked_buff: true,
err: None,
encoding,
}
}
/// Set maximum accepted payload size. The default limit is 256kB.
pub fn limit(mut self, limit: usize) -> Self {
self.limit = limit;
self
}
fn err(err: ReadlinesError) -> Self {
Readlines {
stream: Payload::None,
buff: BytesMut::new(),
limit: 262_144,
checked_buff: true,
encoding: UTF_8,
err: Some(err),
}
}
}
impl<T> Stream for Readlines<T>
where
T: HttpMessage,
T::Stream: Stream<Item = Result<Bytes, PayloadError>> + Unpin,
{
type Item = Result<String, ReadlinesError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
if let Some(err) = this.err.take() {
return Poll::Ready(Some(Err(err)));
}
// check if there is a newline in the buffer
if !this.checked_buff {
let mut found: Option<usize> = None;
for (ind, b) in this.buff.iter().enumerate() {
if *b == b'\n' {
found = Some(ind);
break;
}
}
if let Some(ind) = found {
// check if line is longer than limit
if ind + 1 > this.limit {
return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow)));
}
let line = if this.encoding == UTF_8 {
str::from_utf8(&this.buff.split_to(ind + 1))
.map_err(|_| ReadlinesError::EncodingError)?
.to_owned()
} else {
this.encoding
.decode_without_bom_handling_and_without_replacement(
&this.buff.split_to(ind + 1),
)
.map(Cow::into_owned)
.ok_or(ReadlinesError::EncodingError)?
};
return Poll::Ready(Some(Ok(line)));
}
this.checked_buff = true;
}
// poll req for more bytes
match ready!(Pin::new(&mut this.stream).poll_next(cx)) {
Some(Ok(mut bytes)) => {
// check if there is a newline in bytes
let mut found: Option<usize> = None;
for (ind, b) in bytes.iter().enumerate() {
if *b == b'\n' {
found = Some(ind);
break;
}
}
if let Some(ind) = found {
// check if line is longer than limit
if ind + 1 > this.limit {
return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow)));
}
let line = if this.encoding == UTF_8 {
str::from_utf8(&bytes.split_to(ind + 1))
.map_err(|_| ReadlinesError::EncodingError)?
.to_owned()
} else {
this.encoding
.decode_without_bom_handling_and_without_replacement(
&bytes.split_to(ind + 1),
)
.map(Cow::into_owned)
.ok_or(ReadlinesError::EncodingError)?
};
// extend buffer with rest of the bytes;
this.buff.extend_from_slice(&bytes);
this.checked_buff = false;
return Poll::Ready(Some(Ok(line)));
}
this.buff.extend_from_slice(&bytes);
Poll::Pending
}
None => {
if this.buff.is_empty() {
return Poll::Ready(None);
}
if this.buff.len() > this.limit {
return Poll::Ready(Some(Err(ReadlinesError::LimitOverflow)));
}
let line = if this.encoding == UTF_8 {
str::from_utf8(&this.buff)
.map_err(|_| ReadlinesError::EncodingError)?
.to_owned()
} else {
this.encoding
.decode_without_bom_handling_and_without_replacement(&this.buff)
.map(Cow::into_owned)
.ok_or(ReadlinesError::EncodingError)?
};
this.buff.clear();
Poll::Ready(Some(Ok(line)))
}
Some(Err(err)) => Poll::Ready(Some(Err(ReadlinesError::from(err)))),
}
}
}
#[cfg(test)]
mod tests {
use futures_util::stream::StreamExt as _;
use super::*;
use crate::test::TestRequest;
#[actix_rt::test]
async fn test_readlines() {
let mut req = TestRequest::default()
.set_payload(Bytes::from_static(
b"Lorem Ipsum is simply dummy text of the printing and typesetting\n\
industry. Lorem Ipsum has been the industry's standard dummy\n\
Contrary to popular belief, Lorem Ipsum is not simply random text.",
))
.to_request();
let mut stream = Readlines::new(&mut req);
assert_eq!(
stream.next().await.unwrap().unwrap(),
"Lorem Ipsum is simply dummy text of the printing and typesetting\n"
);
assert_eq!(
stream.next().await.unwrap().unwrap(),
"industry. Lorem Ipsum has been the industry's standard dummy\n"
);
assert_eq!(
stream.next().await.unwrap().unwrap(),
"Contrary to popular belief, Lorem Ipsum is not simply random text."
);
}
}

183
actix-web/src/web.rs Normal file
View File

@ -0,0 +1,183 @@
//! Essentials helper functions and types for application registration.
use std::future::Future;
use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut};
use crate::{
error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource,
Responder, Route, Scope,
};
pub use crate::config::ServiceConfig;
pub use crate::data::Data;
pub use crate::request::HttpRequest;
pub use crate::request_data::ReqData;
pub use crate::response::HttpResponse;
pub use crate::types::*;
/// Creates a new resource for a specific path.
///
/// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c`
/// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`.
///
/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used
/// later in a request handler to access the matched value for that segment. This is done by looking
/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method.
///
/// By default, each segment matches the regular expression `[^{}/]+`.
///
/// You can also specify a custom regex in the form `{identifier:regex}`:
///
/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store
/// `userid` and `friend` in the exposed `Path` object:
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/users/{userid}/{friend}")
/// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()))
/// );
/// ```
pub fn resource<T: IntoPatterns>(path: T) -> Resource {
Resource::new(path)
}
/// Creates scope for common path prefix.
///
/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic
/// path segments.
///
/// # Avoid Trailing Slashes
/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost
/// certainly not have the expected behavior. See the [documentation on resource definitions][pat]
/// to understand why this is the case and how to correctly construct scope/prefix definitions.
///
/// # Examples
/// In this example, three routes are set up (and will handle any method):
/// - `/{project_id}/path1`
/// - `/{project_id}/path2`
/// - `/{project_id}/path3`
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::scope("/{project_id}")
/// .service(web::resource("/path1").to(|| HttpResponse::Ok()))
/// .service(web::resource("/path2").to(|| HttpResponse::Ok()))
/// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
/// );
/// ```
///
/// [pat]: crate::dev::ResourceDef#prefix-resources
pub fn scope(path: &str) -> Scope {
Scope::new(path)
}
/// Creates a new un-configured route.
pub fn route() -> Route {
Route::new()
}
macro_rules! method_route {
($method_fn:ident, $method_const:ident) => {
#[doc = concat!(" Creates a new route with `", stringify!($method_const), "` method guard.")]
///
/// # Examples
#[doc = concat!(" In this example, one `", stringify!($method_const), " /{project_id}` route is set up:")]
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
#[doc = concat!(" .route(web::", stringify!($method_fn), "().to(|| HttpResponse::Ok()))")]
///
/// );
/// ```
pub fn $method_fn() -> Route {
method(Method::$method_const)
}
};
}
method_route!(get, GET);
method_route!(post, POST);
method_route!(put, PUT);
method_route!(patch, PATCH);
method_route!(delete, DELETE);
method_route!(head, HEAD);
method_route!(trace, TRACE);
/// Creates a new route with specified method guard.
///
/// # Examples
/// In this example, one `GET /{project_id}` route is set up:
///
/// ```
/// use actix_web::{web, http, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok()))
/// );
/// ```
pub fn method(method: Method) -> Route {
Route::new().method(method)
}
/// Creates a new any-method route with handler.
///
/// ```
/// use actix_web::{web, App, HttpResponse, Responder};
///
/// async fn index() -> impl Responder {
/// HttpResponse::Ok()
/// }
///
/// App::new().service(
/// web::resource("/").route(
/// web::to(index))
/// );
/// ```
pub fn to<F, Args>(handler: F) -> Route
where
F: Handler<Args>,
Args: FromRequest + 'static,
F::Output: Responder + 'static,
{
Route::new().to(handler)
}
/// Creates a raw service for a specific path.
///
/// ```
/// use actix_web::{dev, web, guard, App, Error, HttpResponse};
///
/// async fn my_service(req: dev::ServiceRequest) -> Result<dev::ServiceResponse, Error> {
/// Ok(req.into_response(HttpResponse::Ok().finish()))
/// }
///
/// let app = App::new().service(
/// web::service("/users/*")
/// .guard(guard::Header("content-type", "text/plain"))
/// .finish(my_service)
/// );
/// ```
pub fn service<T: IntoPatterns>(path: T) -> WebService {
WebService::new(path)
}
/// Executes blocking function on a thread pool, returns future that resolves to result of the
/// function execution.
pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>
where
F: FnOnce() -> R + Send + 'static,
R: Send + 'static,
{
let fut = actix_rt::task::spawn_blocking(f);
async { fut.await.map_err(|_| BlockingError) }
}

View File

@ -0,0 +1,310 @@
use actix_http::ContentEncoding;
use actix_web::{
http::{header, StatusCode},
middleware::Compress,
web, App, HttpResponse,
};
use bytes::Bytes;
mod utils;
static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt");
static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz");
static LOREM_BR: &[u8] = include_bytes!("fixtures/lorem.txt.br");
static LOREM_ZSTD: &[u8] = include_bytes!("fixtures/lorem.txt.zst");
static LOREM_XZ: &[u8] = include_bytes!("fixtures/lorem.txt.xz");
macro_rules! test_server {
() => {
actix_test::start(|| {
App::new()
.wrap(Compress::default())
.route(
"/static",
web::to(|| async { HttpResponse::Ok().body(LOREM) }),
)
.route(
"/static-gzip",
web::to(|| async {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Gzip)
.body(LOREM_GZIP)
}),
)
.route(
"/static-br",
web::to(|| async {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Brotli)
.body(LOREM_BR)
}),
)
.route(
"/static-zstd",
web::to(|| async {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded
.insert_header(ContentEncoding::Zstd)
.body(LOREM_ZSTD)
}),
)
.route(
"/static-xz",
web::to(|| async {
HttpResponse::Ok()
// signal to compressor that content should not be altered
// signal to client that content is encoded as 7zip
.insert_header((header::CONTENT_ENCODING, "xz"))
.body(LOREM_XZ)
}),
)
.route(
"/echo",
web::to(|body: Bytes| async move { HttpResponse::Ok().body(body) }),
)
})
};
}
#[actix_rt::test]
async fn negotiate_encoding_identity() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "identity"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING), None);
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_gzip() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::gzip::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_br() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::brotli::decode(bytes), LOREM);
srv.stop().await;
}
#[actix_rt::test]
async fn negotiate_encoding_zstd() {
let srv = test_server!();
let req = srv
.post("/static")
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "zstd");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
let mut res = srv
.post("/static")
.no_decompress()
.insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br"))
.send()
.await
.unwrap();
let bytes = res.body().await.unwrap();
assert_eq!(utils::zstd::decode(bytes), LOREM);
srv.stop().await;
}
#[cfg(all(
feature = "compress-brotli",
feature = "compress-gzip",
feature = "compress-zstd",
))]
#[actix_rt::test]
async fn client_encoding_prefers_brotli() {
let srv = test_server!();
let req = srv.post("/static").send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn gzip_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-gzip")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_GZIP));
srv.stop().await;
}
#[actix_rt::test]
async fn manual_custom_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding() {
let srv = test_server!();
let req = srv
.post("/static")
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM));
srv.stop().await;
}
#[actix_rt::test]
async fn deny_identity_coding_no_decompress() {
let srv = test_server!();
let req = srv
.post("/static-br")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "br, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_BR));
srv.stop().await;
}
// TODO: fix test
// currently fails because negotiation doesn't consider unknown encoding types
#[ignore]
#[actix_rt::test]
async fn deny_identity_for_manual_coding() {
let srv = test_server!();
let req = srv
.post("/static-xz")
// don't decompress response body
.no_decompress()
// signal that we want a compressed body
.insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0"))
.send();
let mut res = req.await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz");
let bytes = res.body().await.unwrap();
assert_eq!(bytes, Bytes::from_static(LOREM_XZ));
srv.stop().await;
}

5
actix-web/tests/fixtures/lorem.txt vendored Normal file
View File

@ -0,0 +1,5 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin interdum tincidunt lacus, sed tempor lorem consectetur et. Pellentesque et egestas sem, at cursus massa. Nunc feugiat elit sit amet ipsum commodo luctus. Proin auctor dignissim pharetra. Integer iaculis quam a tellus auctor, vitae auctor nisl viverra. Nullam consequat maximus porttitor. Pellentesque tortor enim, molestie at varius non, tempor non nibh. Suspendisse tempus erat lorem, vel faucibus magna blandit vel. Sed pellentesque ligula augue, vitae fermentum eros blandit et. Cras dignissim in massa ut varius. Vestibulum commodo nunc sit amet pellentesque dignissim.
Donec imperdiet blandit lobortis. Suspendisse fringilla nunc quis venenatis tempor. Nunc tempor sed erat sed convallis. Pellentesque aliquet elit lectus, quis vulputate arcu pharetra sed. Etiam laoreet aliquet arcu cursus vehicula. Maecenas odio odio, elementum faucibus sollicitudin vitae, pellentesque ac purus. Donec venenatis faucibus lorem, et finibus lacus tincidunt vitae. Quisque laoreet metus sapien, vitae euismod mauris lobortis malesuada. Integer sit amet elementum turpis. Maecenas ex mauris, dapibus eu placerat vitae, rutrum convallis enim. Nulla vitae orci ultricies, sagittis turpis et, lacinia dui. Praesent egestas urna turpis, sit amet feugiat mauris tristique eu. Quisque id tempor libero. Donec ullamcorper dapibus lorem, vel consequat risus congue a.
Nullam dignissim ut lectus vitae tempor. Pellentesque ut odio fringilla, volutpat mi et, vulputate tellus. Fusce eget diam non odio tincidunt viverra eu vel augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam sed eleifend purus, vitae aliquam orci. Cras fringilla justo eget tempus bibendum. Phasellus venenatis, odio nec pulvinar commodo, quam neque lacinia turpis, ut rutrum tortor massa eu nulla. Vivamus tincidunt ut lectus a gravida. Donec varius mi quis enim interdum ultrices. Sed aliquam consectetur nisi vitae viverra. Praesent nec ligula egestas, porta lectus sed, consectetur augue.

BIN
actix-web/tests/fixtures/lorem.txt.br vendored Normal file

Binary file not shown.

BIN
actix-web/tests/fixtures/lorem.txt.gz vendored Normal file

Binary file not shown.

BIN
actix-web/tests/fixtures/lorem.txt.xz vendored Normal file

Binary file not shown.

BIN
actix-web/tests/fixtures/lorem.txt.zst vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,15 @@
//! Checks that test macro does not cause problems in the presence of imports named "test" that
//! could be either a module with test items or the "test with runtime" macro itself.
//!
//! Before actix/actix-net#399 was implemented, this macro was running twice. The first run output
//! `#[test]` and it got run again and since it was in scope.
//!
//! Prevented by using the fully-qualified test marker (`#[::core::prelude::v1::test]`).
use actix_web::test;
#[actix_web::test]
async fn test_macro_naming_conflict() {
let _req = test::TestRequest::default();
assert_eq!(async { 1 }.await, 1);
}

Some files were not shown because too many files have changed in this diff Show More