1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-23 16:21:06 +01:00

Merge branch 'master' into robjtede/issue2502

This commit is contained in:
Rob Ede 2021-12-18 10:49:19 +00:00 committed by GitHub
commit 44e9d790fd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
101 changed files with 2576 additions and 2283 deletions

View File

@ -3,6 +3,31 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.15 - 2021-12-17
### Added
* Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510]
* Implement `Debug` for `DefaultHeaders`. [#2510]
### Changed
* Align `DefaultHeader` method terminology, deprecating previous methods. [#2510]
* Response service types in `ErrorHandlers` middleware now use `ServiceResponse<EitherBody<B>>` to allow changing the body type. [#2515]
* Both variants in `ErrorHandlerResponse` now use `ServiceResponse<EitherBody<B>>`. [#2515]
* Rename `test::{default_service => simple_service}`. Old name is deprecated. [#2518]
* Rename `test::{read_response_json => call_and_read_body_json}`. Old name is deprecated. [#2518]
* Rename `test::{read_response => call_and_read_body}`. Old name is deprecated. [#2518]
* Relax body type and error bounds on test utilities. [#2518]
### Removed
* Top-level `EitherExtractError` export. [#2510]
* Conversion implementations for `either` crate. [#2516]
* `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518]
[#2510]: https://github.com/actix/actix-web/pull/2510
[#2515]: https://github.com/actix/actix-web/pull/2515
[#2516]: https://github.com/actix/actix-web/pull/2516
[#2518]: https://github.com/actix/actix-web/pull/2518
## 4.0.0-beta.14 - 2021-12-11 ## 4.0.0-beta.14 - 2021-12-11
### Added ### Added
* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] * Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.14" version = "4.0.0-beta.15"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -77,8 +77,8 @@ actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
actix-router = "0.5.0-beta.2" actix-router = "0.5.0-beta.3"
actix-web-codegen = "0.5.0-beta.6" actix-web-codegen = "0.5.0-beta.6"
ahash = "0.7" ahash = "0.7"
@ -86,7 +86,6 @@ bytes = "1"
cfg-if = "1" cfg-if = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true } cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }
@ -107,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.13", features = ["openssl"] } awc = { version = "3.0.0-beta.14", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![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-beta.14)](https://docs.rs/actix-web/4.0.0-beta.14) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.15)](https://docs.rs/actix-web/4.0.0-beta.15)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.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-beta.14/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.14) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.15/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.15)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)

View File

@ -22,10 +22,10 @@ path = "src/lib.rs"
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
actix-service = "2" actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4.0.0-beta.14", default-features = false } actix-web = { version = "4.0.0-beta.15", default-features = false }
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.8" actix-test = "0.1.0-beta.9"
actix-web = "4.0.0-beta.14" actix-web = "4.0.0-beta.15"

View File

@ -2,14 +2,10 @@ use std::{
fmt, fmt,
fs::Metadata, fs::Metadata,
io, io,
ops::{Deref, DerefMut},
path::{Path, PathBuf}, path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
#[cfg(unix)]
use std::os::unix::fs::MetadataExt;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
@ -27,6 +23,7 @@ use actix_web::{
Error, HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use mime_guess::from_path; use mime_guess::from_path;
@ -71,8 +68,11 @@ impl Default for Flags {
/// NamedFile::open_async("./static/index.html").await /// NamedFile::open_async("./static/index.html").await
/// } /// }
/// ``` /// ```
#[derive(Deref, DerefMut)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
#[deref]
#[deref_mut]
file: File, file: File,
modified: Option<SystemTime>, modified: Option<SystemTime>,
pub(crate) md: Metadata, pub(crate) md: Metadata,
@ -364,14 +364,18 @@ impl NamedFile {
self self
} }
/// Creates a etag in a format is similar to Apache's.
pub(crate) fn etag(&self) -> Option<header::EntityTag> { pub(crate) fn etag(&self) -> Option<header::EntityTag> {
// This etag format is similar to Apache's.
self.modified.as_ref().map(|mtime| { self.modified.as_ref().map(|mtime| {
let ino = { let ino = {
#[cfg(unix)] #[cfg(unix)]
{ {
#[cfg(unix)]
use std::os::unix::fs::MetadataExt as _;
self.md.ino() self.md.ino()
} }
#[cfg(not(unix))] #[cfg(not(unix))]
{ {
0 0
@ -472,17 +476,17 @@ impl NamedFile {
false false
}; };
let mut resp = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) { if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); let ct = equiv_utf8_text(self.content_type.clone());
resp.insert_header((header::CONTENT_TYPE, ct.to_string())); res.insert_header((header::CONTENT_TYPE, ct.to_string()));
} else { } else {
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
} }
if self.flags.contains(Flags::CONTENT_DISPOSITION) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.insert_header(( res.insert_header((
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
)); ));
@ -490,18 +494,18 @@ impl NamedFile {
// default compressing // default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); res.encoding(current_encoding);
} }
if let Some(lm) = last_modified { if let Some(lm) = last_modified {
resp.insert_header((header::LAST_MODIFIED, lm.to_string())); res.insert_header((header::LAST_MODIFIED, lm.to_string()));
} }
if let Some(etag) = etag { if let Some(etag) = etag {
resp.insert_header((header::ETAG, etag.to_string())); res.insert_header((header::ETAG, etag.to_string()));
} }
resp.insert_header((header::ACCEPT_RANGES, "bytes")); res.insert_header((header::ACCEPT_RANGES, "bytes"));
let mut length = self.md.len(); let mut length = self.md.len();
let mut offset = 0; let mut offset = 0;
@ -513,24 +517,24 @@ impl NamedFile {
length = ranges[0].length; length = ranges[0].length;
offset = ranges[0].start; offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity); res.encoding(ContentEncoding::Identity);
resp.insert_header(( res.insert_header((
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
)); ));
} else { } else {
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
}; };
} else { } else {
return resp.status(StatusCode::BAD_REQUEST).finish(); return res.status(StatusCode::BAD_REQUEST).finish();
}; };
}; };
if precondition_failed { if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish(); return res.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified { } else if not_modified {
return resp return res
.status(StatusCode::NOT_MODIFIED) .status(StatusCode::NOT_MODIFIED)
.body(body::None::new()) .body(body::None::new())
.map_into_boxed_body(); .map_into_boxed_body();
@ -539,10 +543,10 @@ impl NamedFile {
let reader = chunked::new_chunked_read(length, offset, self.file); let reader = chunked::new_chunked_read(length, offset, self.file);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); res.status(StatusCode::PARTIAL_CONTENT);
} }
resp.body(SizedStream::new(length, reader)) res.body(SizedStream::new(length, reader))
} }
} }
@ -586,20 +590,6 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
} }
} }
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
impl Responder for NamedFile { impl Responder for NamedFile {
type Body = BoxBody; type Body = BoxBody;

View File

@ -35,7 +35,7 @@ actix-tls = "3.0.0-rc.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-rc.1"
awc = { version = "3.0.0-beta.13", default-features = false } awc = { version = "3.0.0-beta.14", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.2", features = ["sync"] } tokio = { version = "1.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"

View File

@ -1,6 +1,26 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Removed
* `header::map::GetAll` iterator, its `Iterator::size_hint` method was wrongly implemented. Replaced with `std::slice::Iter`. [#2527]
[#2527]: https://github.com/actix/actix-web/pull/2527
## 3.0.0-beta.16 - 2021-12-17
### Added
* New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522]
### Changed
* Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510]
* Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510]
* Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
### Removed
* `MessageBody::{is_complete_body,take_complete_body}`. [#2522]
[#2510]: https://github.com/actix/actix-web/pull/2510
[#2522]: https://github.com/actix/actix-web/pull/2522
## 3.0.0-beta.15 - 2021-12-11 ## 3.0.0-beta.15 - 2021-12-11
@ -21,7 +41,8 @@
* `Request::take_conn_data()`. [#2491] * `Request::take_conn_data()`. [#2491]
* `Request::take_req_data()`. [#2487] * `Request::take_req_data()`. [#2487]
* `impl Clone` for `RequestHead`. [#2487] * `impl Clone` for `RequestHead`. [#2487]
* New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimisations on body types that are done in exactly one poll/chunk. [#2497] * New methods on `MessageBody` trait, `is_complete_body` and `take_complete_body`, both with default implementations, for optimizations on body types that are done in exactly one poll/chunk. [#2497]
* New `boxed` method on `MessageBody` trait for wrapping body type. [#2520]
### Changed ### Changed
* Rename `body::BoxBody::{from_body => new}`. [#2468] * Rename `body::BoxBody::{from_body => new}`. [#2468]
@ -50,6 +71,7 @@
[#2488]: https://github.com/actix/actix-web/pull/2488 [#2488]: https://github.com/actix/actix-web/pull/2488
[#2491]: https://github.com/actix/actix-web/pull/2491 [#2491]: https://github.com/actix/actix-web/pull/2491
[#2497]: https://github.com/actix/actix-web/pull/2497 [#2497]: https://github.com/actix/actix-web/pull/2497
[#2520]: https://github.com/actix/actix-web/pull/2520
## 3.0.0-beta.14 - 2021-11-30 ## 3.0.0-beta.14 - 2021-11-30
@ -260,7 +282,7 @@
## 3.0.0-beta.2 - 2021-02-10 ## 3.0.0-beta.2 - 2021-02-10
### Added ### Added
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] * `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] * `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869] * `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
* `TestRequest::insert_header` method which allows using typed headers. [#1869] * `TestRequest::insert_header` method which allows using typed headers. [#1869]
@ -271,9 +293,9 @@
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] * `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
### Changed ### Changed
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed * `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed
`mime` types. [#1894] `mime` types. [#1894]
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std * Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
`TryInto` trait. [#1894] `TryInto` trait. [#1894]
* `Extensions::insert` returns Option of replaced item. [#1904] * `Extensions::insert` returns Option of replaced item. [#1904]
* Remove `HttpResponseBuilder::json2()`. [#1903] * Remove `HttpResponseBuilder::json2()`. [#1903]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.15" version = "3.0.0-beta.16"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@ -45,7 +45,7 @@ __compress = []
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = { version = "2.2", default-features = false }
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.13"
@ -55,7 +55,7 @@ bytestring = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } futures-task = { version = "0.3.7", default-features = false, features = ["alloc"] }
h2 = "0.3.9" h2 = "0.3.9"
http = "0.2.5" http = "0.2.5"
httparse = "1.5.1" httparse = "1.5.1"
@ -66,7 +66,6 @@ local-channel = "0.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0"
pin-project-lite = "0.2" pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
sha-1 = "0.9" sha-1 = "0.9"
@ -84,11 +83,12 @@ zstd = { version = "0.9", optional = true }
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-rc.1"
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
actix-web = "4.0.0-beta.14" actix-web = "4.0.0-beta.15"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
rcgen = "0.8" rcgen = "0.8"
regex = "1.3" regex = "1.3"
rustls-pemfile = "0.2" rustls-pemfile = "0.2"

View File

@ -3,11 +3,11 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.15)](https://docs.rs/actix-http/3.0.0-beta.15) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.16)](https://docs.rs/actix-http/3.0.0-beta.16)
[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.15/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.15) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.16/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.16)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

View File

@ -27,6 +27,7 @@ where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
#[inline]
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
BodyStream { stream } BodyStream { stream }
} }
@ -39,6 +40,7 @@ where
{ {
type Error = E; type Error = E;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Stream BodySize::Stream
} }

View File

@ -8,76 +8,97 @@ use std::{
use bytes::Bytes; use bytes::Bytes;
use super::{BodySize, MessageBody, MessageBodyMapErr}; use super::{BodySize, MessageBody, MessageBodyMapErr};
use crate::Error; use crate::body;
/// A boxed message body with boxed errors. /// A boxed message body with boxed errors.
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>); #[derive(Debug)]
pub struct BoxBody(BoxBodyInner);
enum BoxBodyInner {
None(body::None),
Bytes(Bytes),
Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
}
impl fmt::Debug for BoxBodyInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None(arg0) => f.debug_tuple("None").field(arg0).finish(),
Self::Bytes(arg0) => f.debug_tuple("Bytes").field(arg0).finish(),
Self::Stream(_) => f.debug_tuple("Stream").field(&"dyn MessageBody").finish(),
}
}
}
impl BoxBody { impl BoxBody {
/// Boxes a `MessageBody` and any errors it generates. /// Same as `MessageBody::boxed`.
///
/// If the body type to wrap is unknown or generic it is better to use [`MessageBody::boxed`] to
/// avoid double boxing.
#[inline]
pub fn new<B>(body: B) -> Self pub fn new<B>(body: B) -> Self
where where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
match body.size() {
BodySize::None => Self(BoxBodyInner::None(body::None)),
_ => match body.try_into_bytes() {
Ok(bytes) => Self(BoxBodyInner::Bytes(bytes)),
Err(body) => {
let body = MessageBodyMapErr::new(body, Into::into); let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body)) Self(BoxBodyInner::Stream(Box::pin(body)))
}
},
}
} }
/// Returns a mutable pinned reference to the inner message body type. /// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(&mut self) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> { #[inline]
self.0.as_mut() pub fn as_pin_mut(&mut self) -> Pin<&mut Self> {
} Pin::new(self)
}
impl fmt::Debug for BoxBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxBody(dyn MessageBody)")
} }
} }
impl MessageBody for BoxBody { impl MessageBody for BoxBody {
type Error = Error; type Error = Box<dyn StdError>;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.0.size() match &self.0 {
BoxBodyInner::None(none) => none.size(),
BoxBodyInner::Bytes(bytes) => bytes.size(),
BoxBodyInner::Stream(stream) => stream.size(),
}
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.0 match &mut self.0 {
.as_mut() BoxBodyInner::None(body) => {
.poll_next(cx) Pin::new(body).poll_next(cx).map_err(|err| match err {})
.map_err(|err| Error::new_body().with_cause(err))
} }
BoxBodyInner::Bytes(body) => {
fn is_complete_body(&self) -> bool { Pin::new(body).poll_next(cx).map_err(|err| match err {})
self.0.is_complete_body()
} }
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
fn take_complete_body(&mut self) -> Bytes {
debug_assert!(
self.is_complete_body(),
"boxed type does not allow taking complete body; caller should make sure to \
call `is_complete_body` first",
);
// we do not have DerefMut access to call take_complete_body directly but since
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
let waker = futures_util::task::noop_waker();
let mut cx = Context::from_waker(&waker);
match self.as_pin_mut().poll_next(&mut cx) {
Poll::Ready(Some(Ok(data))) => data,
_ => {
panic!(
"boxed type indicated it allows taking complete body but failed to \
return Bytes when polled",
);
} }
} }
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self.0 {
BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
_ => Err(self),
}
}
#[inline]
fn boxed(self) -> BoxBody {
self
} }
} }

View File

@ -23,6 +23,7 @@ pin_project! {
impl<L> EitherBody<L, BoxBody> { impl<L> EitherBody<L, BoxBody> {
/// Creates new `EitherBody` using left variant and boxed right variant. /// Creates new `EitherBody` using left variant and boxed right variant.
#[inline]
pub fn new(body: L) -> Self { pub fn new(body: L) -> Self {
Self::Left { body } Self::Left { body }
} }
@ -30,11 +31,13 @@ impl<L> EitherBody<L, BoxBody> {
impl<L, R> EitherBody<L, R> { impl<L, R> EitherBody<L, R> {
/// Creates new `EitherBody` using left variant. /// Creates new `EitherBody` using left variant.
#[inline]
pub fn left(body: L) -> Self { pub fn left(body: L) -> Self {
Self::Left { body } Self::Left { body }
} }
/// Creates new `EitherBody` using right variant. /// Creates new `EitherBody` using right variant.
#[inline]
pub fn right(body: R) -> Self { pub fn right(body: R) -> Self {
Self::Right { body } Self::Right { body }
} }
@ -47,6 +50,7 @@ where
{ {
type Error = Error; type Error = Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EitherBody::Left { body } => body.size(), EitherBody::Left { body } => body.size(),
@ -54,6 +58,7 @@ where
} }
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
@ -68,17 +73,23 @@ where
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
match self { match self {
EitherBody::Left { body } => body.is_complete_body(), EitherBody::Left { body } => body
EitherBody::Right { body } => body.is_complete_body(), .try_into_bytes()
.map_err(|body| EitherBody::Left { body }),
EitherBody::Right { body } => body
.try_into_bytes()
.map_err(|body| EitherBody::Right { body }),
} }
} }
fn take_complete_body(&mut self) -> Bytes { #[inline]
fn boxed(self) -> BoxBody {
match self { match self {
EitherBody::Left { body } => body.take_complete_body(), EitherBody::Left { body } => body.boxed(),
EitherBody::Right { body } => body.take_complete_body(), EitherBody::Right { body } => body.boxed(),
} }
} }
} }

View File

@ -12,16 +12,20 @@ use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use super::BodySize; use super::{BodySize, BoxBody};
/// An interface types that can converted to bytes and used as response bodies. /// An interface types that can converted to bytes and used as response bodies.
// TODO: examples // TODO: examples
pub trait MessageBody { pub trait MessageBody {
// TODO: consider this bound to only fmt::Display since the error type is not really used /// The type of error that will be returned if streaming body fails.
// and there is an impl for Into<Box<StdError>> on String ///
/// Since it is not appropriate to generate a response mid-stream, it only requires `Error` for
/// internal use and logging.
type Error: Into<Box<dyn StdError>>; type Error: Into<Box<dyn StdError>>;
/// Body size hint. /// Body size hint.
///
/// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
/// Attempt to pull out the next chunk of body bytes. /// Attempt to pull out the next chunk of body bytes.
@ -31,51 +35,32 @@ pub trait MessageBody {
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>>; ) -> Poll<Option<Result<Bytes, Self::Error>>>;
/// Returns true if entire body bytes chunk is obtainable in one call to `poll_next`. /// Try to convert into the complete chunk of body bytes.
/// ///
/// This method's implementation should agree with [`take_complete_body`] and should always be /// Implement this method if the entire body can be trivially extracted. This is useful for
/// checked before taking the body. /// optimizations where `poll_next` calls can be avoided.
/// ///
/// The default implementation returns `false. /// Body types with [`BodySize::None`] are allowed to return empty `Bytes`. Although, if calling
/// this method, it is recommended to check `size` first and return early.
/// ///
/// [`take_complete_body`]: MessageBody::take_complete_body /// # Errors
fn is_complete_body(&self) -> bool { /// The default implementation will error and return the original type back to the caller for
false /// further use.
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self>
where
Self: Sized,
{
Err(self)
} }
/// Returns the complete chunk of body bytes. /// Converts this body into `BoxBody`.
/// #[inline]
/// Implementors of this method should note the following: fn boxed(self) -> BoxBody
/// - It is acceptable to skip the omit checks of [`is_complete_body`]. The responsibility of where
/// performing this check is delegated to the caller. Self: Sized + 'static,
/// - If the result of [`is_complete_body`] is conditional, that condition should be given {
/// equivalent attention here. BoxBody::new(self)
/// - A second call call to [`take_complete_body`] should return an empty `Bytes` or panic.
/// - A call to [`poll_next`] after calling [`take_complete_body`] should return `None` unless
/// the chunk is guaranteed to be empty.
///
/// The default implementation panics unconditionally, indicating a control flow bug in the
/// calling code.
///
/// # Panics
/// With a correct implementation, panics if called without first checking [`is_complete_body`].
///
/// [`is_complete_body`]: MessageBody::is_complete_body
/// [`take_complete_body`]: MessageBody::take_complete_body
/// [`poll_next`]: MessageBody::poll_next
fn take_complete_body(&mut self) -> Bytes {
assert!(
self.is_complete_body(),
"type ({}) allows taking complete body but did not provide an implementation \
of `take_complete_body`",
std::any::type_name::<Self>()
);
unimplemented!(
"type ({}) does not allow taking complete body; caller should make sure to \
check `is_complete_body` first",
std::any::type_name::<Self>()
);
} }
} }
@ -85,26 +70,16 @@ mod foreign_impls {
impl MessageBody for Infallible { impl MessageBody for Infallible {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match *self {} match *self {}
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match *self {} match *self {}
} }
fn is_complete_body(&self) -> bool {
true
}
fn take_complete_body(&mut self) -> Bytes {
match *self {}
}
} }
impl MessageBody for () { impl MessageBody for () {
@ -124,19 +99,14 @@ mod foreign_impls {
} }
#[inline] #[inline]
fn is_complete_body(&self) -> bool { fn try_into_bytes(self) -> Result<Bytes, Self> {
true Ok(Bytes::new())
}
#[inline]
fn take_complete_body(&mut self) -> Bytes {
Bytes::new()
} }
} }
impl<B> MessageBody for Box<B> impl<B> MessageBody for Box<B>
where where
B: MessageBody + Unpin, B: MessageBody + Unpin + ?Sized,
{ {
type Error = B::Error; type Error = B::Error;
@ -152,21 +122,11 @@ mod foreign_impls {
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx) Pin::new(self.get_mut().as_mut()).poll_next(cx)
} }
#[inline]
fn is_complete_body(&self) -> bool {
self.as_ref().is_complete_body()
}
#[inline]
fn take_complete_body(&mut self) -> Bytes {
self.as_mut().take_complete_body()
}
} }
impl<B> MessageBody for Pin<Box<B>> impl<B> MessageBody for Pin<Box<B>>
where where
B: MessageBody, B: MessageBody + ?Sized,
{ {
type Error = B::Error; type Error = B::Error;
@ -177,160 +137,126 @@ mod foreign_impls {
#[inline] #[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.as_mut().poll_next(cx) self.get_mut().as_mut().poll_next(cx)
}
#[inline]
fn is_complete_body(&self) -> bool {
self.as_ref().is_complete_body()
}
#[inline]
fn take_complete_body(&mut self) -> Bytes {
debug_assert!(
self.is_complete_body(),
"inner type \"{}\" does not allow taking complete body; caller should make sure to \
call `is_complete_body` first",
std::any::type_name::<B>(),
);
// we do not have DerefMut access to call take_complete_body directly but since
// is_complete_body is true we should expect the entire bytes chunk in one poll_next
let waker = futures_util::task::noop_waker();
let mut cx = Context::from_waker(&waker);
match self.as_mut().poll_next(&mut cx) {
Poll::Ready(Some(Ok(data))) => data,
_ => {
panic!(
"inner type \"{}\" indicated it allows taking complete body but failed to \
return Bytes when polled",
std::any::type_name::<B>()
);
}
}
} }
} }
impl MessageBody for &'static [u8] { impl MessageBody for &'static [u8] {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(self.take_complete_body()))) Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(Bytes::from_static(self))
fn take_complete_body(&mut self) -> Bytes {
Bytes::from_static(mem::take(self))
} }
} }
impl MessageBody for Bytes { impl MessageBody for Bytes {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(self.take_complete_body()))) Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(self)
fn take_complete_body(&mut self) -> Bytes {
mem::take(self)
} }
} }
impl MessageBody for BytesMut { impl MessageBody for BytesMut {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(self.take_complete_body()))) Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(self.freeze())
fn take_complete_body(&mut self) -> Bytes {
mem::take(self).freeze()
} }
} }
impl MessageBody for Vec<u8> { impl MessageBody for Vec<u8> {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(self.take_complete_body()))) Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(Bytes::from(self))
fn take_complete_body(&mut self) -> Bytes {
Bytes::from(mem::take(self))
} }
} }
impl MessageBody for &'static str { impl MessageBody for &'static str {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
@ -344,22 +270,21 @@ mod foreign_impls {
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(Bytes::from_static(self.as_bytes()))
fn take_complete_body(&mut self) -> Bytes {
Bytes::from_static(mem::take(self).as_bytes())
} }
} }
impl MessageBody for String { impl MessageBody for String {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
@ -372,22 +297,21 @@ mod foreign_impls {
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(Bytes::from(self))
fn take_complete_body(&mut self) -> Bytes {
Bytes::from(mem::take(self))
} }
} }
impl MessageBody for bytestring::ByteString { impl MessageBody for bytestring::ByteString {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_cx: &mut Context<'_>, _cx: &mut Context<'_>,
@ -396,12 +320,9 @@ mod foreign_impls {
Poll::Ready(Some(Ok(string.into_bytes()))) Poll::Ready(Some(Ok(string.into_bytes())))
} }
fn is_complete_body(&self) -> bool { #[inline]
true fn try_into_bytes(self) -> Result<Bytes, Self> {
} Ok(self.into_bytes())
fn take_complete_body(&mut self) -> Bytes {
mem::take(self).into_bytes()
} }
} }
} }
@ -435,6 +356,7 @@ where
{ {
type Error = E; type Error = E;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.body.size() self.body.size()
} }
@ -455,6 +377,12 @@ where
None => Poll::Ready(None), None => Poll::Ready(None),
} }
} }
#[inline]
fn try_into_bytes(self) -> Result<Bytes, Self> {
let Self { body, mapper } = self;
body.try_into_bytes().map_err(|body| Self { body, mapper })
}
} }
#[cfg(test)] #[cfg(test)]
@ -464,6 +392,7 @@ mod tests {
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use super::*; use super::*;
use crate::body::{self, EitherBody};
macro_rules! assert_poll_next { macro_rules! assert_poll_next {
($pin:expr, $exp:expr) => { ($pin:expr, $exp:expr) => {
@ -565,49 +494,45 @@ mod tests {
assert_poll_next!(pl, Bytes::from("test")); assert_poll_next!(pl, Bytes::from("test"));
} }
#[test] #[actix_rt::test]
fn take_string() { async fn complete_body_combinators() {
let mut data = "test".repeat(2); let body = Bytes::from_static(b"test");
let data_bytes = Bytes::from(data.clone()); let body = BoxBody::new(body);
assert!(data.is_complete_body()); let body = EitherBody::<_, ()>::left(body);
assert_eq!(data.take_complete_body(), data_bytes); let body = EitherBody::<(), _>::right(body);
// Do not support try_into_bytes:
// let body = Box::new(body);
// let body = Box::pin(body);
let mut big_data = "test".repeat(64 * 1024); assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
let data_bytes = Bytes::from(big_data.clone());
assert!(big_data.is_complete_body());
assert_eq!(big_data.take_complete_body(), data_bytes);
} }
#[test] #[actix_rt::test]
fn take_boxed_equivalence() { async fn complete_body_combinators_poll() {
let mut data = Bytes::from_static(b"test"); let body = Bytes::from_static(b"test");
assert!(data.is_complete_body()); let body = BoxBody::new(body);
assert_eq!(data.take_complete_body(), b"test".as_ref()); let body = EitherBody::<_, ()>::left(body);
let body = EitherBody::<(), _>::right(body);
let mut body = body;
let mut data = Box::new(Bytes::from_static(b"test")); assert_eq!(body.size(), BodySize::Sized(4));
assert!(data.is_complete_body()); assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
assert_eq!(data.take_complete_body(), b"test".as_ref()); assert_poll_next_none!(Pin::new(&mut body));
let mut data = Box::pin(Bytes::from_static(b"test"));
assert!(data.is_complete_body());
assert_eq!(data.take_complete_body(), b"test".as_ref());
} }
#[test] #[actix_rt::test]
fn take_policy() { async fn none_body_combinators() {
let mut data = Bytes::from_static(b"test"); fn none_body() -> BoxBody {
// first call returns chunk let body = body::None;
assert_eq!(data.take_complete_body(), b"test".as_ref()); let body = BoxBody::new(body);
// second call returns empty let body = EitherBody::<_, ()>::left(body);
assert_eq!(data.take_complete_body(), b"".as_ref()); let body = EitherBody::<(), _>::right(body);
body.boxed()
}
let waker = futures_util::task::noop_waker(); assert_eq!(none_body().size(), BodySize::None);
let mut cx = Context::from_waker(&waker); assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
let mut data = Bytes::from_static(b"test"); assert_poll_next_none!(Pin::new(&mut none_body()));
// take returns whole chunk
assert_eq!(data.take_complete_body(), b"test".as_ref());
// subsequent poll_next returns None
assert_eq!(Pin::new(&mut data).poll_next(&mut cx), Poll::Ready(None));
} }
// down-casting used to be done with a method on MessageBody trait // down-casting used to be done with a method on MessageBody trait

View File

@ -42,12 +42,7 @@ impl MessageBody for None {
} }
#[inline] #[inline]
fn is_complete_body(&self) -> bool { fn try_into_bytes(self) -> Result<Bytes, Self> {
true Ok(Bytes::new())
}
#[inline]
fn take_complete_body(&mut self) -> Bytes {
Bytes::new()
} }
} }

View File

@ -1,9 +1,11 @@
/// Body size hint. /// Body size hint.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodySize { pub enum BodySize {
/// Absence of body can be assumed from method or status code. /// Implicitly empty body.
/// ///
/// Will skip writing Content-Length header. /// Will omit the Content-Length header. Used for responses to certain methods (e.g., `HEAD`) or
/// with particular status codes (e.g., 204 No Content). Consumers that read this as a body size
/// hint are allowed to make optimizations that skip reading or writing the payload.
None, None,
/// Known size body. /// Known size body.
@ -18,6 +20,9 @@ pub enum BodySize {
} }
impl BodySize { impl BodySize {
/// Equivalent to `BodySize::Sized(0)`;
pub const ZERO: Self = Self::Sized(0);
/// Returns true if size hint indicates omitted or empty body. /// Returns true if size hint indicates omitted or empty body.
/// ///
/// Streams will return false because it cannot be known without reading the stream. /// Streams will return false because it cannot be known without reading the stream.

View File

@ -27,6 +27,7 @@ where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static, E: Into<Box<dyn StdError>> + 'static,
{ {
#[inline]
pub fn new(size: u64, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream }
} }
@ -41,6 +42,7 @@ where
{ {
type Error = E; type Error = E;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64) BodySize::Sized(self.size as u64)
} }

View File

@ -25,7 +25,7 @@ use zstd::stream::write::Encoder as ZstdEncoder;
use super::Writer; use super::Writer;
use crate::{ use crate::{
body::{BodySize, MessageBody}, body::{self, BodySize, MessageBody},
error::BlockingError, error::BlockingError,
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING}, header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
ResponseHead, StatusCode, ResponseHead, StatusCode,
@ -46,14 +46,16 @@ pin_project! {
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
fn none() -> Self { fn none() -> Self {
Encoder { Encoder {
body: EncoderBody::None, body: EncoderBody::None {
body: body::None::new(),
},
encoder: None, encoder: None,
fut: None, fut: None,
eof: true, eof: true,
} }
} }
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, mut body: B) -> Self { pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT || head.status == StatusCode::NO_CONTENT
@ -65,11 +67,9 @@ impl<B: MessageBody> Encoder<B> {
return Self::none(); return Self::none();
} }
let body = if body.is_complete_body() { let body = match body.try_into_bytes() {
let body = body.take_complete_body(); Ok(body) => EncoderBody::Full { body },
EncoderBody::Full { body } Err(body) => EncoderBody::Stream { body },
} else {
EncoderBody::Stream { body }
}; };
if can_encode { if can_encode {
@ -98,7 +98,7 @@ impl<B: MessageBody> Encoder<B> {
pin_project! { pin_project! {
#[project = EncoderBodyProj] #[project = EncoderBodyProj]
enum EncoderBody<B> { enum EncoderBody<B> {
None, None { body: body::None },
Full { body: Bytes }, Full { body: Bytes },
Stream { #[pin] body: B }, Stream { #[pin] body: B },
} }
@ -110,9 +110,10 @@ where
{ {
type Error = EncoderError; type Error = EncoderError;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::None => BodySize::None, EncoderBody::None { body } => body.size(),
EncoderBody::Full { body } => body.size(), EncoderBody::Full { body } => body.size(),
EncoderBody::Stream { body } => body.size(), EncoderBody::Stream { body } => body.size(),
} }
@ -123,7 +124,9 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::None => Poll::Ready(None), EncoderBodyProj::None { body } => {
Pin::new(body).poll_next(cx).map_err(|err| match err {})
}
EncoderBodyProj::Full { body } => { EncoderBodyProj::Full { body } => {
Pin::new(body).poll_next(cx).map_err(|err| match err {}) Pin::new(body).poll_next(cx).map_err(|err| match err {})
} }
@ -133,21 +136,15 @@ where
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
fn try_into_bytes(self) -> Result<Bytes, Self>
where
Self: Sized,
{
match self { match self {
EncoderBody::None => true, EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
EncoderBody::Full { .. } => true, EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
EncoderBody::Stream { .. } => false, _ => Err(self),
}
}
fn take_complete_body(&mut self) -> Bytes {
match self {
EncoderBody::None => Bytes::new(),
EncoderBody::Full { body } => body.take_complete_body(),
EncoderBody::Stream { .. } => {
panic!("EncoderBody::Stream variant cannot be taken")
}
} }
} }
} }
@ -158,6 +155,7 @@ where
{ {
type Error = EncoderError; type Error = EncoderError;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_some() { if self.encoder.is_some() {
BodySize::Stream BodySize::Stream
@ -234,19 +232,21 @@ where
} }
} }
fn is_complete_body(&self) -> bool { #[inline]
fn try_into_bytes(mut self) -> Result<Bytes, Self>
where
Self: Sized,
{
if self.encoder.is_some() { if self.encoder.is_some() {
false Err(self)
} else { } else {
self.body.is_complete_body() match self.body.try_into_bytes() {
Ok(body) => Ok(body),
Err(body) => {
self.body = body;
Err(self)
} }
} }
fn take_complete_body(&mut self) -> Bytes {
if self.encoder.is_some() {
panic!("compressed body stream cannot be taken")
} else {
self.body.take_complete_body()
} }
} }
} }

View File

@ -332,31 +332,28 @@ impl From<PayloadError> for Error {
} }
/// A set of errors that can occur during dispatching HTTP requests. /// A set of errors that can occur during dispatching HTTP requests.
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error.
// FIXME: display and error type
#[display(fmt = "Service Error")] #[display(fmt = "Service Error")]
Service(#[error(not(source))] Response<BoxBody>), Service(Response<BoxBody>),
/// Body error /// Body streaming error.
// FIXME: display and error type #[display(fmt = "Body error: {}", _0)]
#[display(fmt = "Body Error")] Body(Box<dyn StdError>),
Body(#[error(not(source))] Box<dyn StdError>),
/// Upgrade service error /// Upgrade service error.
Upgrade, Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "IO error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
/// Http request parse error. /// Request parse error.
#[display(fmt = "Parse error: {}", _0)] #[display(fmt = "Request parse error: {}", _0)]
Parse(ParseError), Parse(ParseError),
/// Http/2 error /// HTTP/2 error.
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
H2(h2::Error), H2(h2::Error),
@ -368,21 +365,23 @@ pub enum DispatchError {
#[display(fmt = "Connection shutdown timeout")] #[display(fmt = "Connection shutdown timeout")]
DisconnectTimeout, DisconnectTimeout,
/// Payload is not consumed /// Internal error.
#[display(fmt = "Task is completed but request's payload is not consumed")]
PayloadIsNotConsumed,
/// Malformed request
#[display(fmt = "Malformed request")]
MalformedRequest,
/// Internal error
#[display(fmt = "Internal error")] #[display(fmt = "Internal error")]
InternalError, InternalError,
}
/// Unknown error impl StdError for DispatchError {
#[display(fmt = "Unknown error")] fn source(&self) -> Option<&(dyn StdError + 'static)> {
Unknown, match self {
// TODO: error source extraction?
DispatchError::Service(_res) => None,
DispatchError::Body(err) => Some(&**err),
DispatchError::Io(err) => Some(err),
DispatchError::Parse(err) => Some(err),
DispatchError::H2(err) => Some(err),
_ => None,
}
}
} }
/// A set of error that can occur during parsing content type. /// A set of error that can occur during parsing content type.

View File

@ -15,14 +15,14 @@ use bitflags::bitflags;
use bytes::{Buf, BytesMut}; use bytes::{Buf, BytesMut};
use futures_core::ready; use futures_core::ready;
use log::{error, trace}; use log::{error, trace};
use pin_project::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError}, error::{DispatchError, ParseError, PayloadError},
service::HttpFlow, service::HttpFlow,
Extensions, OnConnectData, Request, Response, StatusCode, Error, Extensions, OnConnectData, Request, Response, StatusCode,
}; };
use super::{ use super::{
@ -46,10 +46,16 @@ bitflags! {
} }
} }
#[pin_project] // there's 2 versions of Dispatcher state because of:
/// Dispatcher for HTTP/1.1 protocol // https://github.com/taiki-e/pin-project-lite/issues/3
pub struct Dispatcher<T, S, B, X, U> //
where // tl;dr: pin-project-lite doesn't play well with other attribute macros
#[cfg(not(test))]
pin_project! {
/// Dispatcher for HTTP/1.1 protocol
pub struct Dispatcher<T, S, B, X, U>
where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<BoxBody>>,
@ -60,17 +66,40 @@ where
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
#[pin]
inner: DispatcherState<T, S, B, X, U>,
}
}
#[cfg(test)]
pin_project! {
/// Dispatcher for HTTP/1.1 protocol
pub struct Dispatcher<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Response<BoxBody>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
#[pin] #[pin]
inner: DispatcherState<T, S, B, X, U>, inner: DispatcherState<T, S, B, X, U>,
#[cfg(test)] // used in tests
poll_count: u64, poll_count: u64,
}
} }
#[pin_project(project = DispatcherStateProj)] pin_project! {
enum DispatcherState<T, S, B, X, U> #[project = DispatcherStateProj]
where enum DispatcherState<T, S, B, X, U>
where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<BoxBody>>,
@ -81,14 +110,16 @@ where
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
Normal(#[pin] InnerDispatcher<T, S, B, X, U>), Normal { #[pin] inner: InnerDispatcher<T, S, B, X, U> },
Upgrade(#[pin] U::Future), Upgrade { #[pin] fut: U::Future },
}
} }
#[pin_project(project = InnerDispatcherProj)] pin_project! {
struct InnerDispatcher<T, S, B, X, U> #[project = InnerDispatcherProj]
where struct InnerDispatcher<T, S, B, X, U>
where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<BoxBody>>, S::Error: Into<Response<BoxBody>>,
@ -99,7 +130,7 @@ where
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
flags: Flags, flags: Flags,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
@ -119,6 +150,7 @@ where
read_buf: BytesMut, read_buf: BytesMut,
write_buf: BytesMut, write_buf: BytesMut,
codec: Codec, codec: Codec,
}
} }
enum DispatcherMessage { enum DispatcherMessage {
@ -127,19 +159,21 @@ enum DispatcherMessage {
Error(Response<()>), Error(Response<()>),
} }
#[pin_project(project = StateProj)] pin_project! {
enum State<S, B, X> #[project = StateProj]
where enum State<S, B, X>
where
S: Service<Request>, S: Service<Request>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
{ {
None, None,
ExpectCall(#[pin] X::Future), ExpectCall { #[pin] fut: X::Future },
ServiceCall(#[pin] S::Future), ServiceCall { #[pin] fut: S::Future },
SendPayload(#[pin] B), SendPayload { #[pin] body: B },
SendErrorPayload(#[pin] BoxBody), SendErrorPayload { #[pin] body: BoxBody },
}
} }
impl<S, B, X> State<S, B, X> impl<S, B, X> State<S, B, X>
@ -198,7 +232,8 @@ where
}; };
Dispatcher { Dispatcher {
inner: DispatcherState::Normal(InnerDispatcher { inner: DispatcherState::Normal {
inner: InnerDispatcher {
flow, flow,
flags, flags,
peer_addr, peer_addr,
@ -216,7 +251,8 @@ where
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
codec: Codec::new(config), codec: Codec::new(config),
}), },
},
#[cfg(test)] #[cfg(test)]
poll_count: 0, poll_count: 0,
@ -316,7 +352,7 @@ where
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Sized(0) => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendPayload(body), _ => State::SendPayload { body },
}; };
self.project().state.set(state); self.project().state.set(state);
Ok(()) Ok(())
@ -330,7 +366,7 @@ where
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Sized(0) => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendErrorPayload(body), _ => State::SendErrorPayload { body },
}; };
self.project().state.set(state); self.project().state.set(state);
Ok(()) Ok(())
@ -356,12 +392,12 @@ where
// Handle `EXPECT: 100-Continue` header // Handle `EXPECT: 100-Continue` header
if req.head().expect() { if req.head().expect() {
// set InnerDispatcher state and continue loop to poll it. // set InnerDispatcher state and continue loop to poll it.
let task = this.flow.expect.call(req); let fut = this.flow.expect.call(req);
this.state.set(State::ExpectCall(task)); this.state.set(State::ExpectCall { fut });
} else { } else {
// the same as expect call. // the same as expect call.
let task = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(task)); this.state.set(State::ServiceCall { fut });
}; };
} }
@ -381,7 +417,7 @@ where
// all messages are dealt with. // all messages are dealt with.
None => return Ok(PollResponse::DoNothing), None => return Ok(PollResponse::DoNothing),
}, },
StateProj::ServiceCall(fut) => match fut.poll(cx) { StateProj::ServiceCall { fut } => match fut.poll(cx) {
// service call resolved. send response. // service call resolved. send response.
Poll::Ready(Ok(res)) => { Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(()); let (res, body) = res.into().replace_body(());
@ -407,11 +443,11 @@ where
} }
}, },
StateProj::SendPayload(mut stream) => { StateProj::SendPayload { mut body } => {
// keep populate writer buffer until buffer size limit hit, // keep populate writer buffer until buffer size limit hit,
// get blocked or finished. // get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
match stream.as_mut().poll_next(cx) { match body.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => { Poll::Ready(Some(Ok(item))) => {
this.codec this.codec
.encode(Message::Chunk(Some(item)), this.write_buf)?; .encode(Message::Chunk(Some(item)), this.write_buf)?;
@ -437,13 +473,13 @@ where
return Ok(PollResponse::DrainWriteBuf); return Ok(PollResponse::DrainWriteBuf);
} }
StateProj::SendErrorPayload(mut stream) => { StateProj::SendErrorPayload { mut body } => {
// TODO: de-dupe impl with SendPayload // TODO: de-dupe impl with SendPayload
// keep populate writer buffer until buffer size limit hit, // keep populate writer buffer until buffer size limit hit,
// get blocked or finished. // get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
match stream.as_mut().poll_next(cx) { match body.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => { Poll::Ready(Some(Ok(item))) => {
this.codec this.codec
.encode(Message::Chunk(Some(item)), this.write_buf)?; .encode(Message::Chunk(Some(item)), this.write_buf)?;
@ -458,7 +494,9 @@ where
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err.into())) return Err(DispatchError::Body(
Error::new_body().with_cause(err).into(),
))
} }
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
@ -469,14 +507,14 @@ where
return Ok(PollResponse::DrainWriteBuf); return Ok(PollResponse::DrainWriteBuf);
} }
StateProj::ExpectCall(fut) => match fut.poll(cx) { StateProj::ExpectCall { fut } => match fut.poll(cx) {
// expect resolved. write continue to buffer and set InnerDispatcher state // expect resolved. write continue to buffer and set InnerDispatcher state
// to service call. // to service call.
Poll::Ready(Ok(req)) => { Poll::Ready(Ok(req)) => {
this.write_buf this.write_buf
.extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
let fut = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(fut)); this.state.set(State::ServiceCall { fut });
} }
// send expect error as response // send expect error as response
@ -502,25 +540,25 @@ where
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
if req.head().expect() { if req.head().expect() {
// set dispatcher state so the future is pinned. // set dispatcher state so the future is pinned.
let task = this.flow.expect.call(req); let fut = this.flow.expect.call(req);
this.state.set(State::ExpectCall(task)); this.state.set(State::ExpectCall { fut });
} else { } else {
// the same as above. // the same as above.
let task = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(task)); this.state.set(State::ServiceCall { fut });
}; };
// eagerly poll the future for once(or twice if expect is resolved immediately). // eagerly poll the future for once(or twice if expect is resolved immediately).
loop { loop {
match self.as_mut().project().state.project() { match self.as_mut().project().state.project() {
StateProj::ExpectCall(fut) => { StateProj::ExpectCall { fut } => {
match fut.poll(cx) { match fut.poll(cx) {
// expect is resolved. continue loop and poll the service call branch. // expect is resolved. continue loop and poll the service call branch.
Poll::Ready(Ok(req)) => { Poll::Ready(Ok(req)) => {
self.as_mut().send_continue(); self.as_mut().send_continue();
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
let task = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(task)); this.state.set(State::ServiceCall { fut });
continue; continue;
} }
// future is pending. return Ok(()) to notify that a new state is // future is pending. return Ok(()) to notify that a new state is
@ -536,7 +574,7 @@ where
} }
} }
} }
StateProj::ServiceCall(fut) => { StateProj::ServiceCall { fut } => {
// return no matter the service call future's result. // return no matter the service call future's result.
return match fut.poll(cx) { return match fut.poll(cx) {
// future is resolved. send response and return a result. On success // future is resolved. send response and return a result. On success
@ -901,7 +939,7 @@ where
} }
match this.inner.project() { match this.inner.project() {
DispatcherStateProj::Normal(mut inner) => { DispatcherStateProj::Normal { mut inner } => {
inner.as_mut().poll_keepalive(cx)?; inner.as_mut().poll_keepalive(cx)?;
if inner.flags.contains(Flags::SHUTDOWN) { if inner.flags.contains(Flags::SHUTDOWN) {
@ -941,7 +979,7 @@ where
self.as_mut() self.as_mut()
.project() .project()
.inner .inner
.set(DispatcherState::Upgrade(upgrade)); .set(DispatcherState::Upgrade { fut: upgrade });
return self.poll(cx); return self.poll(cx);
} }
}; };
@ -993,8 +1031,8 @@ where
} }
} }
} }
DispatcherStateProj::Upgrade(fut) => fut.poll(cx).map_err(|e| { DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
error!("Upgrade handler error: {}", e); error!("Upgrade handler error: {}", err);
DispatchError::Upgrade DispatchError::Upgrade
}), }),
} }
@ -1088,7 +1126,7 @@ mod tests {
Poll::Ready(res) => assert!(res.is_err()), Poll::Ready(res) => assert!(res.is_err()),
} }
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
assert!(inner.flags.contains(Flags::READ_DISCONNECT)); assert!(inner.flags.contains(Flags::READ_DISCONNECT));
assert_eq!( assert_eq!(
&inner.project().io.take().unwrap().write_buf[..26], &inner.project().io.take().unwrap().write_buf[..26],
@ -1123,7 +1161,7 @@ mod tests {
actix_rt::pin!(h1); actix_rt::pin!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal(_))); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
match h1.as_mut().poll(cx) { match h1.as_mut().poll(cx) {
Poll::Pending => panic!("first poll should not be pending"), Poll::Pending => panic!("first poll should not be pending"),
@ -1133,7 +1171,7 @@ mod tests {
// polls: initial => shutdown // polls: initial => shutdown
assert_eq!(h1.poll_count, 2); assert_eq!(h1.poll_count, 2);
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
let res = &mut inner.project().io.take().unwrap().write_buf[..]; let res = &mut inner.project().io.take().unwrap().write_buf[..];
stabilize_date_header(res); stabilize_date_header(res);
@ -1177,7 +1215,7 @@ mod tests {
actix_rt::pin!(h1); actix_rt::pin!(h1);
assert!(matches!(&h1.inner, DispatcherState::Normal(_))); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
match h1.as_mut().poll(cx) { match h1.as_mut().poll(cx) {
Poll::Pending => panic!("first poll should not be pending"), Poll::Pending => panic!("first poll should not be pending"),
@ -1187,7 +1225,7 @@ mod tests {
// polls: initial => shutdown // polls: initial => shutdown
assert_eq!(h1.poll_count, 1); assert_eq!(h1.poll_count, 1);
if let DispatcherStateProj::Normal(inner) = h1.project().inner.project() { if let DispatcherStateProj::Normal { inner } = h1.project().inner.project() {
let res = &mut inner.project().io.take().unwrap().write_buf[..]; let res = &mut inner.project().io.take().unwrap().write_buf[..];
stabilize_date_header(res); stabilize_date_header(res);
@ -1237,13 +1275,13 @@ mod tests {
actix_rt::pin!(h1); actix_rt::pin!(h1);
assert!(h1.as_mut().poll(cx).is_pending()); assert!(h1.as_mut().poll(cx).is_pending());
assert!(matches!(&h1.inner, DispatcherState::Normal(_))); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
// polls: manual // polls: manual
assert_eq!(h1.poll_count, 1); assert_eq!(h1.poll_count, 1);
eprintln!("poll count: {}", h1.poll_count); eprintln!("poll count: {}", h1.poll_count);
if let DispatcherState::Normal(ref inner) = h1.inner { if let DispatcherState::Normal { ref inner } = h1.inner {
let io = inner.io.as_ref().unwrap(); let io = inner.io.as_ref().unwrap();
let res = &io.write_buf()[..]; let res = &io.write_buf()[..];
assert_eq!( assert_eq!(
@ -1258,7 +1296,7 @@ mod tests {
// polls: manual manual shutdown // polls: manual manual shutdown
assert_eq!(h1.poll_count, 3); assert_eq!(h1.poll_count, 3);
if let DispatcherState::Normal(ref inner) = h1.inner { if let DispatcherState::Normal { ref inner } = h1.inner {
let io = inner.io.as_ref().unwrap(); let io = inner.io.as_ref().unwrap();
let mut res = (&io.write_buf()[..]).to_owned(); let mut res = (&io.write_buf()[..]).to_owned();
stabilize_date_header(&mut res); stabilize_date_header(&mut res);
@ -1309,12 +1347,12 @@ mod tests {
actix_rt::pin!(h1); actix_rt::pin!(h1);
assert!(h1.as_mut().poll(cx).is_ready()); assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Normal(_))); assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
// polls: manual shutdown // polls: manual shutdown
assert_eq!(h1.poll_count, 2); assert_eq!(h1.poll_count, 2);
if let DispatcherState::Normal(ref inner) = h1.inner { if let DispatcherState::Normal { ref inner } = h1.inner {
let io = inner.io.as_ref().unwrap(); let io = inner.io.as_ref().unwrap();
let mut res = (&io.write_buf()[..]).to_owned(); let mut res = (&io.write_buf()[..]).to_owned();
stabilize_date_header(&mut res); stabilize_date_header(&mut res);
@ -1386,7 +1424,7 @@ mod tests {
actix_rt::pin!(h1); actix_rt::pin!(h1);
assert!(h1.as_mut().poll(cx).is_ready()); assert!(h1.as_mut().poll(cx).is_ready());
assert!(matches!(&h1.inner, DispatcherState::Upgrade(_))); assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
// polls: manual shutdown // polls: manual shutdown
assert_eq!(h1.poll_count, 2); assert_eq!(h1.poll_count, 2);

View File

@ -356,9 +356,9 @@ where
type Future = Dispatcher<T, S, B, X, U>; type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self._poll_ready(cx).map_err(|e| { self._poll_ready(cx).map_err(|err| {
log::error!("HTTP/1 service readiness error: {:?}", e); log::error!("HTTP/1 service readiness error: {:?}", err);
DispatchError::Service(e) DispatchError::Service(err)
}) })
} }

View File

@ -270,10 +270,10 @@ where
type Future = H2ServiceHandlerResponse<T, S, B>; type Future = H2ServiceHandlerResponse<T, S, B>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.flow.service.poll_ready(cx).map_err(|e| { self.flow.service.poll_ready(cx).map_err(|err| {
let e = e.into(); let err = err.into();
error!("Service readiness error: {:?}", e); error!("Service readiness error: {:?}", err);
DispatchError::Service(e) DispatchError::Service(err)
}) })
} }
@ -297,7 +297,6 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S::Future: 'static, S::Future: 'static,
{ {
Incoming(Dispatcher<T, S, B, (), ()>),
Handshake( Handshake(
Option<Rc<HttpFlow<S, (), ()>>>, Option<Rc<HttpFlow<S, (), ()>>>,
Option<ServiceConfig>, Option<ServiceConfig>,
@ -305,6 +304,7 @@ where
OnConnectData, OnConnectData,
HandshakeWithTimeout<T>, HandshakeWithTimeout<T>,
), ),
Established(Dispatcher<T, S, B, (), ()>),
} }
pub struct H2ServiceHandlerResponse<T, S, B> pub struct H2ServiceHandlerResponse<T, S, B>
@ -332,7 +332,6 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.state { match self.state {
State::Incoming(ref mut disp) => Pin::new(disp).poll(cx),
State::Handshake( State::Handshake(
ref mut srv, ref mut srv,
ref mut config, ref mut config,
@ -343,7 +342,7 @@ where
Ok((conn, timer)) => { Ok((conn, timer)) => {
let on_connect_data = mem::take(conn_data); let on_connect_data = mem::take(conn_data);
self.state = State::Incoming(Dispatcher::new( self.state = State::Established(Dispatcher::new(
conn, conn,
srv.take().unwrap(), srv.take().unwrap(),
config.take().unwrap(), config.take().unwrap(),
@ -360,6 +359,8 @@ where
Poll::Ready(Err(err)) Poll::Ready(Err(err))
} }
}, },
State::Established(ref mut disp) => Pin::new(disp).poll(cx),
} }
} }
} }

View File

@ -16,6 +16,7 @@ pub trait Sealed {
} }
impl Sealed for HeaderName { impl Sealed for HeaderName {
#[inline]
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self)) Ok(Cow::Borrowed(self))
} }
@ -23,6 +24,7 @@ impl Sealed for HeaderName {
impl AsHeaderName for HeaderName {} impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName { impl Sealed for &HeaderName {
#[inline]
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self)) Ok(Cow::Borrowed(*self))
} }
@ -30,6 +32,7 @@ impl Sealed for &HeaderName {
impl AsHeaderName for &HeaderName {} impl AsHeaderName for &HeaderName {}
impl Sealed for &str { impl Sealed for &str {
#[inline]
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
@ -37,6 +40,7 @@ impl Sealed for &str {
impl AsHeaderName for &str {} impl AsHeaderName for &str {}
impl Sealed for String { impl Sealed for String {
#[inline]
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
@ -44,6 +48,7 @@ impl Sealed for String {
impl AsHeaderName for String {} impl AsHeaderName for String {}
impl Sealed for &String { impl Sealed for &String {
#[inline]
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }

View File

@ -1,22 +1,20 @@
//! [`IntoHeaderPair`] trait and implementations. //! [`TryIntoHeaderPair`] trait and implementations.
use std::convert::TryFrom as _; use std::convert::TryFrom as _;
use http::{ use super::{
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue,
Error as HttpError, HeaderValue,
}; };
use crate::error::HttpError;
use super::{Header, IntoHeaderValue}; /// An interface for types that can be converted into a [`HeaderName`] + [`HeaderValue`] pair for
/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for
/// insertion into a [`HeaderMap`]. /// insertion into a [`HeaderMap`].
/// ///
/// [`HeaderMap`]: super::HeaderMap /// [`HeaderMap`]: super::HeaderMap
pub trait IntoHeaderPair: Sized { pub trait TryIntoHeaderPair: Sized {
type Error: Into<HttpError>; type Error: Into<HttpError>;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>; fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
} }
#[derive(Debug)] #[derive(Debug)]
@ -34,14 +32,14 @@ impl From<InvalidHeaderPart> for HttpError {
} }
} }
impl<V> IntoHeaderPair for (HeaderName, V) impl<V> TryIntoHeaderPair for (HeaderName, V)
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
V::Error: Into<InvalidHeaderValue>, V::Error: Into<InvalidHeaderValue>,
{ {
type Error = InvalidHeaderPart; type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self; let (name, value) = self;
let value = value let value = value
.try_into_value() .try_into_value()
@ -50,14 +48,14 @@ where
} }
} }
impl<V> IntoHeaderPair for (&HeaderName, V) impl<V> TryIntoHeaderPair for (&HeaderName, V)
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
V::Error: Into<InvalidHeaderValue>, V::Error: Into<InvalidHeaderValue>,
{ {
type Error = InvalidHeaderPart; type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self; let (name, value) = self;
let value = value let value = value
.try_into_value() .try_into_value()
@ -66,14 +64,14 @@ where
} }
} }
impl<V> IntoHeaderPair for (&[u8], V) impl<V> TryIntoHeaderPair for (&[u8], V)
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
V::Error: Into<InvalidHeaderValue>, V::Error: Into<InvalidHeaderValue>,
{ {
type Error = InvalidHeaderPart; type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self; let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?; let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value let value = value
@ -83,14 +81,14 @@ where
} }
} }
impl<V> IntoHeaderPair for (&str, V) impl<V> TryIntoHeaderPair for (&str, V)
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
V::Error: Into<InvalidHeaderValue>, V::Error: Into<InvalidHeaderValue>,
{ {
type Error = InvalidHeaderPart; type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self; let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?; let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value let value = value
@ -100,23 +98,25 @@ where
} }
} }
impl<V> IntoHeaderPair for (String, V) impl<V> TryIntoHeaderPair for (String, V)
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
V::Error: Into<InvalidHeaderValue>, V::Error: Into<InvalidHeaderValue>,
{ {
type Error = InvalidHeaderPart; type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { #[inline]
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self; let (name, value) = self;
(name.as_str(), value).try_into_header_pair() (name.as_str(), value).try_into_pair()
} }
} }
impl<T: Header> IntoHeaderPair for T { impl<T: Header> TryIntoHeaderPair for T {
type Error = <T as IntoHeaderValue>::Error; type Error = <T as TryIntoHeaderValue>::Error;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> { #[inline]
fn try_into_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
Ok((T::name(), self.try_into_value()?)) Ok((T::name(), self.try_into_value()?))
} }
} }

View File

@ -1,4 +1,4 @@
//! [`IntoHeaderValue`] trait and implementations. //! [`TryIntoHeaderValue`] trait and implementations.
use std::convert::TryFrom as _; use std::convert::TryFrom as _;
@ -7,7 +7,7 @@ use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use mime::Mime; use mime::Mime;
/// An interface for types that can be converted into a [`HeaderValue`]. /// An interface for types that can be converted into a [`HeaderValue`].
pub trait IntoHeaderValue: Sized { pub trait TryIntoHeaderValue: Sized {
/// The type returned in the event of a conversion error. /// The type returned in the event of a conversion error.
type Error: Into<HttpError>; type Error: Into<HttpError>;
@ -15,7 +15,7 @@ pub trait IntoHeaderValue: Sized {
fn try_into_value(self) -> Result<HeaderValue, Self::Error>; fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
} }
impl IntoHeaderValue for HeaderValue { impl TryIntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -24,7 +24,7 @@ impl IntoHeaderValue for HeaderValue {
} }
} }
impl IntoHeaderValue for &HeaderValue { impl TryIntoHeaderValue for &HeaderValue {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -33,7 +33,7 @@ impl IntoHeaderValue for &HeaderValue {
} }
} }
impl IntoHeaderValue for &str { impl TryIntoHeaderValue for &str {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -42,7 +42,7 @@ impl IntoHeaderValue for &str {
} }
} }
impl IntoHeaderValue for &[u8] { impl TryIntoHeaderValue for &[u8] {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -51,7 +51,7 @@ impl IntoHeaderValue for &[u8] {
} }
} }
impl IntoHeaderValue for Bytes { impl TryIntoHeaderValue for Bytes {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -60,7 +60,7 @@ impl IntoHeaderValue for Bytes {
} }
} }
impl IntoHeaderValue for Vec<u8> { impl TryIntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -69,7 +69,7 @@ impl IntoHeaderValue for Vec<u8> {
} }
} }
impl IntoHeaderValue for String { impl TryIntoHeaderValue for String {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -78,7 +78,7 @@ impl IntoHeaderValue for String {
} }
} }
impl IntoHeaderValue for usize { impl TryIntoHeaderValue for usize {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -87,7 +87,7 @@ impl IntoHeaderValue for usize {
} }
} }
impl IntoHeaderValue for i64 { impl TryIntoHeaderValue for i64 {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -96,7 +96,7 @@ impl IntoHeaderValue for i64 {
} }
} }
impl IntoHeaderValue for u64 { impl TryIntoHeaderValue for u64 {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -105,7 +105,7 @@ impl IntoHeaderValue for u64 {
} }
} }
impl IntoHeaderValue for i32 { impl TryIntoHeaderValue for i32 {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -114,7 +114,7 @@ impl IntoHeaderValue for i32 {
} }
} }
impl IntoHeaderValue for u32 { impl TryIntoHeaderValue for u32 {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]
@ -123,7 +123,7 @@ impl IntoHeaderValue for u32 {
} }
} }
impl IntoHeaderValue for Mime { impl TryIntoHeaderValue for Mime {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
#[inline] #[inline]

View File

@ -306,8 +306,11 @@ impl HeaderMap {
/// assert_eq!(set_cookies_iter.next().unwrap(), "two=2"); /// assert_eq!(set_cookies_iter.next().unwrap(), "two=2");
/// assert!(set_cookies_iter.next().is_none()); /// assert!(set_cookies_iter.next().is_none());
/// ``` /// ```
pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> { pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
GetAll::new(self.get_value(key)) match self.get_value(key) {
Some(value) => value.iter(),
None => (&[]).iter(),
}
} }
// TODO: get_all_mut ? // TODO: get_all_mut ?
@ -333,7 +336,7 @@ impl HeaderMap {
} }
} }
/// Inserts a name-value pair into the map. /// Inserts (overrides) a name-value pair in the map.
/// ///
/// If the map already contained this key, the new value is associated with the key and all /// If the map already contained this key, the new value is associated with the key and all
/// previous values are removed and returned as a `Removed` iterator. The key is not updated; /// previous values are removed and returned as a `Removed` iterator. The key is not updated;
@ -372,7 +375,7 @@ impl HeaderMap {
Removed::new(value) Removed::new(value)
} }
/// Inserts a name-value pair into the map. /// Appends a name-value pair to the map.
/// ///
/// If the map already contained this key, the new value is added to the list of values /// If the map already contained this key, the new value is added to the list of values
/// currently associated with the key. The key is not updated; this matters for types that can /// currently associated with the key. The key is not updated; this matters for types that can
@ -602,52 +605,6 @@ impl<'a> IntoIterator for &'a HeaderMap {
} }
} }
/// Iterator over borrowed values with the same associated name.
///
/// See [`HeaderMap::get_all`].
#[derive(Debug)]
pub struct GetAll<'a> {
idx: usize,
value: Option<&'a Value>,
}
impl<'a> GetAll<'a> {
fn new(value: Option<&'a Value>) -> Self {
Self { idx: 0, value }
}
}
impl<'a> Iterator for GetAll<'a> {
type Item = &'a HeaderValue;
fn next(&mut self) -> Option<Self::Item> {
let val = self.value?;
match val.get(self.idx) {
Some(val) => {
self.idx += 1;
Some(val)
}
None => {
// current index is none; remove value to fast-path future next calls
self.value = None;
None
}
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
match self.value {
Some(val) => (val.len(), Some(val.len())),
None => (0, Some(0)),
}
}
}
impl ExactSizeIterator for GetAll<'_> {}
impl iter::FusedIterator for GetAll<'_> {}
/// Iterator over removed, owned values with the same associated name. /// Iterator over removed, owned values with the same associated name.
/// ///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] /// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
@ -895,7 +852,7 @@ mod tests {
assert_impl_all!(HeaderMap: IntoIterator); assert_impl_all!(HeaderMap: IntoIterator);
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(std::slice::Iter<'_, HeaderValue>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator); assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);

View File

@ -37,8 +37,8 @@ mod shared;
mod utils; mod utils;
pub use self::as_name::AsHeaderName; pub use self::as_name::AsHeaderName;
pub use self::into_pair::IntoHeaderPair; pub use self::into_pair::TryIntoHeaderPair;
pub use self::into_value::IntoHeaderValue; pub use self::into_value::TryIntoHeaderValue;
pub use self::map::HeaderMap; pub use self::map::HeaderMap;
pub use self::shared::{ pub use self::shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag, parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
@ -49,7 +49,7 @@ pub use self::utils::{
}; };
/// An interface for types that already represent a valid header. /// An interface for types that already represent a valid header.
pub trait Header: IntoHeaderValue { pub trait Header: TryIntoHeaderValue {
/// Returns the name of the header field /// Returns the name of the header field
fn name() -> HeaderName; fn name() -> HeaderName;

View File

@ -5,7 +5,7 @@ use http::header::InvalidHeaderValue;
use crate::{ use crate::{
error::ParseError, error::ParseError,
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue}, header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue},
HttpMessage, HttpMessage,
}; };
@ -96,7 +96,7 @@ impl TryFrom<&str> for ContentEncoding {
} }
} }
impl IntoHeaderValue for ContentEncoding { impl TryIntoHeaderValue for ContentEncoding {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> { fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {

View File

@ -4,7 +4,8 @@ use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use crate::{ use crate::{
config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, helpers::MutWriter, config::DATE_VALUE_LENGTH, error::ParseError, header::TryIntoHeaderValue,
helpers::MutWriter,
}; };
/// A timestamp with HTTP-style formatting and parsing. /// A timestamp with HTTP-style formatting and parsing.
@ -29,7 +30,7 @@ impl fmt::Display for HttpDate {
} }
} }
impl IntoHeaderValue for HttpDate { impl TryIntoHeaderValue for HttpDate {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {

View File

@ -7,10 +7,10 @@ use h2::RecvStream;
use crate::error::PayloadError; use crate::error::PayloadError;
/// Type represent boxed payload /// A boxed payload.
pub type PayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>; pub type PayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
/// Type represent streaming payload /// A streaming payload.
pub enum Payload<S = PayloadStream> { pub enum Payload<S = PayloadStream> {
None, None,
H1(crate::h1::Payload), H1(crate::h1::Payload),

View File

@ -11,7 +11,7 @@ use bytestring::ByteString;
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
extensions::Extensions, extensions::Extensions,
header::{self, HeaderMap, IntoHeaderValue}, header::{self, HeaderMap, TryIntoHeaderValue},
message::{BoxedResponseHead, ResponseHead}, message::{BoxedResponseHead, ResponseHead},
Error, ResponseBuilder, StatusCode, Error, ResponseBuilder, StatusCode,
}; };
@ -170,7 +170,7 @@ impl<B> Response<B> {
/// Returns split head and body. /// Returns split head and body.
/// ///
/// # Implementation Notes /// # Implementation Notes
/// Due to internal performance optimisations, the first element of the returned tuple is a /// Due to internal performance optimizations, the first element of the returned tuple is a
/// `Response` as well but only contains the head of the response this was called on. /// `Response` as well but only contains the head of the response this was called on.
pub fn into_parts(self) -> (Response<()>, B) { pub fn into_parts(self) -> (Response<()>, B) {
self.replace_body(()) self.replace_body(())
@ -194,7 +194,7 @@ impl<B> Response<B> {
where where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
self.map_body(|_, body| BoxBody::new(body)) self.map_body(|_, body| body.boxed())
} }
/// Returns body, consuming this response. /// Returns body, consuming this response.

View File

@ -8,7 +8,7 @@ use std::{
use crate::{ use crate::{
body::{EitherBody, MessageBody}, body::{EitherBody, MessageBody},
error::{Error, HttpError}, error::{Error, HttpError},
header::{self, IntoHeaderPair, IntoHeaderValue}, header::{self, TryIntoHeaderPair, TryIntoHeaderValue},
message::{BoxedResponseHead, ConnectionType, ResponseHead}, message::{BoxedResponseHead, ConnectionType, ResponseHead},
Extensions, Response, StatusCode, Extensions, Response, StatusCode,
}; };
@ -90,12 +90,9 @@ impl ResponseBuilder {
/// assert!(res.headers().contains_key("content-type")); /// assert!(res.headers().contains_key("content-type"));
/// assert!(res.headers().contains_key("x-test")); /// assert!(res.headers().contains_key("x-test"));
/// ``` /// ```
pub fn insert_header<H>(&mut self, header: H) -> &mut Self pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match header.try_into_header_pair() { match header.try_into_pair() {
Ok((key, value)) => { Ok((key, value)) => {
parts.headers.insert(key, value); parts.headers.insert(key, value);
} }
@ -121,12 +118,9 @@ impl ResponseBuilder {
/// assert_eq!(res.headers().get_all("content-type").count(), 1); /// assert_eq!(res.headers().get_all("content-type").count(), 1);
/// assert_eq!(res.headers().get_all("x-test").count(), 2); /// assert_eq!(res.headers().get_all("x-test").count(), 2);
/// ``` /// ```
pub fn append_header<H>(&mut self, header: H) -> &mut Self pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match header.try_into_header_pair() { match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value), Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}; };
@ -157,7 +151,7 @@ impl ResponseBuilder {
#[inline] #[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Upgrade); parts.set_connection_type(ConnectionType::Upgrade);
@ -195,7 +189,7 @@ impl ResponseBuilder {
#[inline] #[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self pub fn content_type<V>(&mut self, value: V) -> &mut Self
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match value.try_into_value() { match value.try_into_value() {

View File

@ -493,9 +493,9 @@ where
type Future = HttpServiceHandlerResponse<T, S, B, X, U>; type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self._poll_ready(cx).map_err(|e| { self._poll_ready(cx).map_err(|err| {
log::error!("HTTP service readiness error: {:?}", e); log::error!("HTTP service readiness error: {:?}", err);
DispatchError::Service(e) DispatchError::Service(err)
}) })
} }

View File

@ -14,7 +14,7 @@ use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{Method, Uri, Version};
use crate::{ use crate::{
header::{HeaderMap, IntoHeaderPair}, header::{HeaderMap, TryIntoHeaderPair},
payload::Payload, payload::Payload,
Request, Request,
}; };
@ -92,11 +92,8 @@ impl TestRequest {
} }
/// Insert a header, replacing any that were set with an equivalent field name. /// Insert a header, replacing any that were set with an equivalent field name.
pub fn insert_header<H>(&mut self, header: H) -> &mut Self pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where match header.try_into_pair() {
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => { Ok((key, value)) => {
parts(&mut self.0).headers.insert(key, value); parts(&mut self.0).headers.insert(key, value);
} }
@ -109,11 +106,8 @@ impl TestRequest {
} }
/// Append a header, keeping any that were set with an equivalent field name. /// Append a header, keeping any that were set with an equivalent field name.
pub fn append_header<H>(&mut self, header: H) -> &mut Self pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where match header.try_into_pair() {
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => { Ok((key, value)) => {
parts(&mut self.0).headers.append(key, value); parts(&mut self.0).headers.append(key, value);
} }
@ -270,7 +264,7 @@ impl TestSeqBuffer {
/// Create new empty `TestBuffer` instance. /// Create new empty `TestBuffer` instance.
pub fn empty() -> Self { pub fn empty() -> Self {
Self::new("") Self::new(BytesMut::new())
} }
pub fn read_buf(&self) -> Ref<'_, BytesMut> { pub fn read_buf(&self) -> Ref<'_, BytesMut> {

View File

@ -15,7 +15,7 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.14", default-features = false } actix-web = { version = "4.0.0-beta.15", default-features = false }
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
@ -28,7 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -1,6 +1,9 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-beta.3 - 2021-12-17
* Minimum supported Rust version (MSRV) is now 1.52. * Minimum supported Rust version (MSRV) is now 1.52.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-router" name = "actix-router"
version = "0.5.0-beta.2" version = "0.5.0-beta.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",

View File

@ -7,144 +7,20 @@
mod de; mod de;
mod path; mod path;
mod pattern;
mod resource; mod resource;
mod resource_path;
mod router; mod router;
pub use self::de::PathDeserializer;
pub use self::path::Path;
pub use self::resource::ResourceDef;
pub use self::router::{ResourceInfo, Router, RouterBuilder};
// TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest
pub trait Resource<T: ResourcePath> {
fn resource_path(&mut self) -> &mut Path<T>;
}
pub trait ResourcePath {
fn path(&self) -> &str;
}
impl ResourcePath for String {
fn path(&self) -> &str {
self.as_str()
}
}
impl<'a> ResourcePath for &'a str {
fn path(&self) -> &str {
self
}
}
impl ResourcePath for bytestring::ByteString {
fn path(&self) -> &str {
&*self
}
}
/// One or many patterns.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Patterns {
Single(String),
List(Vec<String>),
}
impl Patterns {
pub fn is_empty(&self) -> bool {
match self {
Patterns::Single(_) => false,
Patterns::List(pats) => pats.is_empty(),
}
}
}
/// Helper trait for type that could be converted to one or more path pattern.
pub trait IntoPatterns {
fn patterns(&self) -> Patterns;
}
impl IntoPatterns for String {
fn patterns(&self) -> Patterns {
Patterns::Single(self.clone())
}
}
impl<'a> IntoPatterns for &'a String {
fn patterns(&self) -> Patterns {
Patterns::Single((*self).clone())
}
}
impl<'a> IntoPatterns for &'a str {
fn patterns(&self) -> Patterns {
Patterns::Single((*self).to_owned())
}
}
impl IntoPatterns for bytestring::ByteString {
fn patterns(&self) -> Patterns {
Patterns::Single(self.to_string())
}
}
impl IntoPatterns for Patterns {
fn patterns(&self) -> Patterns {
self.clone()
}
}
impl<T: AsRef<str>> IntoPatterns for Vec<T> {
fn patterns(&self) -> Patterns {
let mut patterns = self.iter().map(|v| v.as_ref().to_owned());
match patterns.size_hint() {
(1, _) => Patterns::Single(patterns.next().unwrap()),
_ => Patterns::List(patterns.collect()),
}
}
}
macro_rules! array_patterns_single (($tp:ty) => {
impl IntoPatterns for [$tp; 1] {
fn patterns(&self) -> Patterns {
Patterns::Single(self[0].to_owned())
}
}
});
macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => {
// for each array length specified in $num
$(
impl IntoPatterns for [$tp; $num] {
fn patterns(&self) -> Patterns {
Patterns::List(self.iter().map($str_fn).collect())
}
}
)+
});
array_patterns_single!(&str);
array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
array_patterns_single!(String);
array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
#[cfg(feature = "http")] #[cfg(feature = "http")]
mod url; mod url;
pub use self::de::PathDeserializer;
pub use self::path::Path;
pub use self::pattern::{IntoPatterns, Patterns};
pub use self::resource::ResourceDef;
pub use self::resource_path::{Resource, ResourcePath};
pub use self::router::{ResourceInfo, Router, RouterBuilder};
#[cfg(feature = "http")] #[cfg(feature = "http")]
pub use self::url::{Quoter, Url}; pub use self::url::{Quoter, Url};
#[cfg(feature = "http")]
mod http_impls {
use http::Uri;
use super::ResourcePath;
impl ResourcePath for Uri {
fn path(&self) -> &str {
self.path()
}
}
}

View File

@ -0,0 +1,92 @@
/// One or many patterns.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Patterns {
Single(String),
List(Vec<String>),
}
impl Patterns {
pub fn is_empty(&self) -> bool {
match self {
Patterns::Single(_) => false,
Patterns::List(pats) => pats.is_empty(),
}
}
}
/// Helper trait for type that could be converted to one or more path patterns.
pub trait IntoPatterns {
fn patterns(&self) -> Patterns;
}
impl IntoPatterns for String {
fn patterns(&self) -> Patterns {
Patterns::Single(self.clone())
}
}
impl IntoPatterns for &String {
fn patterns(&self) -> Patterns {
(*self).patterns()
}
}
impl IntoPatterns for str {
fn patterns(&self) -> Patterns {
Patterns::Single(self.to_owned())
}
}
impl IntoPatterns for &str {
fn patterns(&self) -> Patterns {
(*self).patterns()
}
}
impl IntoPatterns for bytestring::ByteString {
fn patterns(&self) -> Patterns {
Patterns::Single(self.to_string())
}
}
impl IntoPatterns for Patterns {
fn patterns(&self) -> Patterns {
self.clone()
}
}
impl<T: AsRef<str>> IntoPatterns for Vec<T> {
fn patterns(&self) -> Patterns {
let mut patterns = self.iter().map(|v| v.as_ref().to_owned());
match patterns.size_hint() {
(1, _) => Patterns::Single(patterns.next().unwrap()),
_ => Patterns::List(patterns.collect()),
}
}
}
macro_rules! array_patterns_single (($tp:ty) => {
impl IntoPatterns for [$tp; 1] {
fn patterns(&self) -> Patterns {
Patterns::Single(self[0].to_owned())
}
}
});
macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => {
// for each array length specified in space-separated $num
$(
impl IntoPatterns for [$tp; $num] {
fn patterns(&self) -> Patterns {
Patterns::List(self.iter().map($str_fn).collect())
}
}
)+
});
array_patterns_single!(&str);
array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);
array_patterns_single!(String);
array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16);

View File

@ -0,0 +1,36 @@
use crate::Path;
// TODO: this trait is necessary, document it
// see impl Resource for ServiceRequest
pub trait Resource<T: ResourcePath> {
fn resource_path(&mut self) -> &mut Path<T>;
}
pub trait ResourcePath {
fn path(&self) -> &str;
}
impl ResourcePath for String {
fn path(&self) -> &str {
self.as_str()
}
}
impl<'a> ResourcePath for &'a str {
fn path(&self) -> &str {
self
}
}
impl ResourcePath for bytestring::ByteString {
fn path(&self) -> &str {
&*self
}
}
#[cfg(feature = "http")]
impl ResourcePath for http::Uri {
fn path(&self) -> &str {
self.path()
}
}

View File

@ -2,22 +2,28 @@ use crate::ResourcePath;
#[allow(dead_code)] #[allow(dead_code)]
const GEN_DELIMS: &[u8] = b":/?#[]@"; const GEN_DELIMS: &[u8] = b":/?#[]@";
#[allow(dead_code)] #[allow(dead_code)]
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
#[allow(dead_code)] #[allow(dead_code)]
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
#[allow(dead_code)] #[allow(dead_code)]
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
#[allow(dead_code)] #[allow(dead_code)]
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890 1234567890
-._~"; -._~";
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLMNOPQRSTUVWXYZ ABCDEFGHIJKLMNOPQRSTUVWXYZ
1234567890 1234567890
-._~ -._~
!$'()*,"; !$'()*,";
const QS: &[u8] = b"+&=;b"; const QS: &[u8] = b"+&=;b";
#[inline] #[inline]
@ -34,19 +40,20 @@ thread_local! {
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
} }
#[derive(Default, Clone, Debug)] #[derive(Debug, Clone, Default)]
pub struct Url { pub struct Url {
uri: http::Uri, uri: http::Uri,
path: Option<String>, path: Option<String>,
} }
impl Url { impl Url {
#[inline]
pub fn new(uri: http::Uri) -> Url { pub fn new(uri: http::Uri) -> Url {
let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
Url { uri, path } Url { uri, path }
} }
#[inline]
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
Url { Url {
path: quoter.requote(uri.path().as_bytes()), path: quoter.requote(uri.path().as_bytes()),
@ -54,15 +61,16 @@ impl Url {
} }
} }
#[inline]
pub fn uri(&self) -> &http::Uri { pub fn uri(&self) -> &http::Uri {
&self.uri &self.uri
} }
#[inline]
pub fn path(&self) -> &str { pub fn path(&self) -> &str {
if let Some(ref s) = self.path { match self.path {
s Some(ref path) => path,
} else { _ => self.uri.path(),
self.uri.path()
} }
} }
@ -86,6 +94,7 @@ impl ResourcePath for Url {
} }
} }
/// A quoter
pub struct Quoter { pub struct Quoter {
safe_table: [u8; 16], safe_table: [u8; 16],
protected_table: [u8; 16], protected_table: [u8; 16],
@ -93,7 +102,7 @@ pub struct Quoter {
impl Quoter { impl Quoter {
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
let mut q = Quoter { let mut quoter = Quoter {
safe_table: [0; 16], safe_table: [0; 16],
protected_table: [0; 16], protected_table: [0; 16],
}; };
@ -101,24 +110,24 @@ impl Quoter {
// prepare safe table // prepare safe table
for i in 0..128 { for i in 0..128 {
if ALLOWED.contains(&i) { if ALLOWED.contains(&i) {
set_bit(&mut q.safe_table, i); set_bit(&mut quoter.safe_table, i);
} }
if QS.contains(&i) { if QS.contains(&i) {
set_bit(&mut q.safe_table, i); set_bit(&mut quoter.safe_table, i);
} }
} }
for ch in safe { for ch in safe {
set_bit(&mut q.safe_table, *ch) set_bit(&mut quoter.safe_table, *ch)
} }
// prepare protected table // prepare protected table
for ch in protected { for ch in protected {
set_bit(&mut q.safe_table, *ch); set_bit(&mut quoter.safe_table, *ch);
set_bit(&mut q.protected_table, *ch); set_bit(&mut quoter.protected_table, *ch);
} }
q quoter
} }
pub fn requote(&self, val: &[u8]) -> Option<String> { pub fn requote(&self, val: &[u8]) -> Option<String> {
@ -215,7 +224,7 @@ mod tests {
} }
#[test] #[test]
fn test_parse_url() { fn parse_url() {
let re = "/user/{id}/test"; let re = "/user/{id}/test";
let path = match_url(re, "/user/2345/test"); let path = match_url(re, "/user/2345/test");
@ -231,24 +240,24 @@ mod tests {
} }
#[test] #[test]
fn test_protected_chars() { fn protected_chars() {
let encoded = percent_encode(PROTECTED); let encoded = percent_encode(PROTECTED);
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
assert_eq!(path.get("id").unwrap(), &encoded); assert_eq!(path.get("id").unwrap(), &encoded);
} }
#[test] #[test]
fn test_non_protecteed_ascii() { fn non_protected_ascii() {
let nonprotected_ascii = ('\u{0}'..='\u{7F}') let non_protected_ascii = ('\u{0}'..='\u{7F}')
.filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
.collect::<String>(); .collect::<String>();
let encoded = percent_encode(nonprotected_ascii.as_bytes()); let encoded = percent_encode(non_protected_ascii.as_bytes());
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
assert_eq!(path.get("id").unwrap(), &nonprotected_ascii); assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
} }
#[test] #[test]
fn test_valid_utf8_multibyte() { fn valid_utf8_multibyte() {
let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>(); let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
let encoded = percent_encode(test.as_bytes()); let encoded = percent_encode(test.as_bytes());
let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
@ -256,7 +265,7 @@ mod tests {
} }
#[test] #[test]
fn test_invalid_utf8() { fn invalid_utf8() {
let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice()); let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap(); let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
let path = Path::new(Url::new(uri)); let path = Path::new(Url::new(uri));
@ -266,7 +275,7 @@ mod tests {
} }
#[test] #[test]
fn test_from_hex() { fn hex_encoding() {
let hex = b"0123456789abcdefABCDEF"; let hex = b"0123456789abcdefABCDEF";
for i in 0..256 { for i in 0..256 {

View File

@ -3,6 +3,13 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.1.0-beta.9 - 2021-12-17
* Re-export `actix_http::body::to_bytes`. [#2518]
* Update `actix_web::test` re-exports. [#2518]
[#2518]: https://github.com/actix/actix-web/pull/2518
## 0.1.0-beta.8 - 2021-12-11 ## 0.1.0-beta.8 - 2021-12-11
* No significant changes since `0.1.0-beta.7`. * No significant changes since `0.1.0-beta.7`.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.8" version = "0.1.0-beta.9"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
actix-http-test = "3.0.0-beta.9" actix-http-test = "3.0.0-beta.9"
actix-rt = "2.1" actix-rt = "2.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] }
awc = { version = "3.0.0-beta.13", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.14", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }

View File

@ -37,9 +37,14 @@ extern crate tls_rustls as rustls;
use std::{fmt, net, thread, time::Duration}; use std::{fmt, net, thread, time::Duration};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer; pub use actix_http::{body::to_bytes, test::TestBuffer};
use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response}; use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response};
pub use actix_http_test::unused_addr;
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
pub use actix_web::test::{
call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service,
read_body, read_body_json, simple_service, TestRequest,
};
use actix_web::{ use actix_web::{
body::MessageBody, body::MessageBody,
dev::{AppConfig, Server, ServerHandle, Service}, dev::{AppConfig, Server, ServerHandle, Service},
@ -48,12 +53,6 @@ use actix_web::{
}; };
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream; use futures_core::Stream;
pub use actix_http_test::unused_addr;
pub use actix_web::test::{
call_service, default_service, init_service, load_stream, ok_service, read_body,
read_body_json, read_response, read_response_json, TestRequest,
};
use tokio::sync::mpsc; use tokio::sync::mpsc;
/// Start default [`TestServer`]. /// Start default [`TestServer`].

View File

@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
actix-web = { version = "4.0.0-beta.14", default-features = false } actix-web = { version = "4.0.0-beta.15", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -27,8 +27,8 @@ tokio = { version = "1", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.8" actix-test = "0.1.0-beta.9"
awc = { version = "3.0.0-beta.13", default-features = false } awc = { version = "3.0.0-beta.14", default-features = false }
env_logger = "0.9" env_logger = "0.9"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -18,14 +18,14 @@ proc-macro = true
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1" proc-macro2 = "1"
actix-router = "0.5.0-beta.2" actix-router = "0.5.0-beta.3"
[dev-dependencies] [dev-dependencies]
actix-macros = "0.2.3" actix-macros = "0.2.3"
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.8" actix-test = "0.1.0-beta.9"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.14" actix-web = "4.0.0-beta.15"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@ -7,6 +7,12 @@
[#2503]: https://github.com/actix/actix-web/pull/2503 [#2503]: https://github.com/actix/actix-web/pull/2503
## 3.0.0-beta.14 - 2021-12-17
* Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510]
[#2510]: https://github.com/actix/actix-web/pull/2510
## 3.0.0-beta.13 - 2021-12-11 ## 3.0.0-beta.13 - 2021-12-11
* No significant changes since `3.0.0-beta.12`. * No significant changes since `3.0.0-beta.12`.
@ -64,7 +70,7 @@
* `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] * `ConnectorService` type is renamed to `BoxConnectorService`. [#2081]
* Fix http/https encoding when enabling `compress` feature. [#2116] * Fix http/https encoding when enabling `compress` feature. [#2116]
* Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header * Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header
methods now take `IntoHeaderPair` tuples. [#2094] methods now take `TryIntoHeaderPair` tuples. [#2094]
[#2081]: https://github.com/actix/actix-web/pull/2081 [#2081]: https://github.com/actix/actix-web/pull/2081
[#2094]: https://github.com/actix/actix-web/pull/2094 [#2094]: https://github.com/actix/actix-web/pull/2094

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.13" version = "3.0.0-beta.14"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -60,7 +60,7 @@ dangerous-h2c = []
[dependencies] [dependencies]
actix-codec = "0.4.1" actix-codec = "0.4.1"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.15" actix-http = "3.0.0-beta.16"
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
actix-tls = { version = "3.0.0-rc.2", features = ["connect", "uri"] } actix-tls = { version = "3.0.0-rc.2", features = ["connect", "uri"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
@ -70,8 +70,8 @@ base64 = "0.13"
bytes = "1" bytes = "1"
cfg-if = "1" cfg-if = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.9" h2 = "0.3.9"
http = "0.2.5" http = "0.2.5"
itoa = "0.4" itoa = "0.4"
@ -93,13 +93,13 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
trust-dns-resolver = { version = "0.20.0", optional = true } trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http = { version = "3.0.0-beta.15", features = ["openssl"] } actix-http = { version = "3.0.0-beta.16", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
actix-server = "2.0.0-rc.1" actix-server = "2.0.0-rc.1"
actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] }
actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.14", features = ["openssl"] } actix-web = { version = "4.0.0-beta.15", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
env_logger = "0.9" env_logger = "0.9"

View File

@ -3,9 +3,9 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.13)](https://docs.rs/awc/3.0.0-beta.13) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.14)](https://docs.rs/awc/3.0.0-beta.14)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.13/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.13) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.14/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.14)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources

View File

@ -45,9 +45,7 @@ impl AnyBody {
where where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
Self::Body { Self::Body { body: body.boxed() }
body: BoxBody::new(body),
}
} }
/// Constructs new `AnyBody` instance from a slice of bytes by copying it. /// Constructs new `AnyBody` instance from a slice of bytes by copying it.

View File

@ -2,7 +2,7 @@ use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
use actix_http::{ use actix_http::{
error::HttpError, error::HttpError,
header::{self, HeaderMap, HeaderName}, header::{self, HeaderMap, HeaderName, TryIntoHeaderPair},
Uri, Uri,
}; };
use actix_rt::net::{ActixStream, TcpStream}; use actix_rt::net::{ActixStream, TcpStream};
@ -21,11 +21,11 @@ use crate::{
/// This type can be used to construct an instance of `Client` through a /// This type can be used to construct an instance of `Client` through a
/// builder-like pattern. /// builder-like pattern.
pub struct ClientBuilder<S = (), M = ()> { pub struct ClientBuilder<S = (), M = ()> {
default_headers: bool,
max_http_version: Option<http::Version>, max_http_version: Option<http::Version>,
stream_window_size: Option<u32>, stream_window_size: Option<u32>,
conn_window_size: Option<u32>, conn_window_size: Option<u32>,
headers: HeaderMap, fundamental_headers: bool,
default_headers: HeaderMap,
timeout: Option<Duration>, timeout: Option<Duration>,
connector: Connector<S>, connector: Connector<S>,
middleware: M, middleware: M,
@ -44,15 +44,15 @@ impl ClientBuilder {
(), (),
> { > {
ClientBuilder { ClientBuilder {
middleware: (),
default_headers: true,
headers: HeaderMap::new(),
timeout: Some(Duration::from_secs(5)),
local_address: None,
connector: Connector::new(),
max_http_version: None, max_http_version: None,
stream_window_size: None, stream_window_size: None,
conn_window_size: None, conn_window_size: None,
fundamental_headers: true,
default_headers: HeaderMap::new(),
timeout: Some(Duration::from_secs(5)),
connector: Connector::new(),
middleware: (),
local_address: None,
max_redirects: 10, max_redirects: 10,
} }
} }
@ -78,8 +78,8 @@ where
{ {
ClientBuilder { ClientBuilder {
middleware: self.middleware, middleware: self.middleware,
fundamental_headers: self.fundamental_headers,
default_headers: self.default_headers, default_headers: self.default_headers,
headers: self.headers,
timeout: self.timeout, timeout: self.timeout,
local_address: self.local_address, local_address: self.local_address,
connector, connector,
@ -153,30 +153,46 @@ where
self self
} }
/// Do not add default request headers. /// Do not add fundamental default request headers.
///
/// By default `Date` and `User-Agent` headers are set. /// By default `Date` and `User-Agent` headers are set.
pub fn no_default_headers(mut self) -> Self { pub fn no_default_headers(mut self) -> Self {
self.default_headers = false; self.fundamental_headers = false;
self self
} }
/// Add default header. Headers added by this method /// Add default header.
/// get added to every request. ///
/// Headers added by this method get added to every request unless overriden by .
///
/// # Panics
/// Panics if header name or value is invalid.
pub fn add_default_header(mut self, header: impl TryIntoHeaderPair) -> Self {
match header.try_into_pair() {
Ok((key, value)) => self.default_headers.append(key, value),
Err(err) => panic!("Header error: {:?}", err.into()),
}
self
}
#[doc(hidden)]
#[deprecated(since = "3.0.0", note = "Prefer `add_default_header((key, value))`.")]
pub fn header<K, V>(mut self, key: K, value: V) -> Self pub fn header<K, V>(mut self, key: K, value: V) -> Self
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: fmt::Debug + Into<HttpError>, <HeaderName as TryFrom<K>>::Error: fmt::Debug + Into<HttpError>,
V: header::IntoHeaderValue, V: header::TryIntoHeaderValue,
V::Error: fmt::Debug, V::Error: fmt::Debug,
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => match value.try_into_value() { Ok(key) => match value.try_into_value() {
Ok(value) => { Ok(value) => {
self.headers.append(key, value); self.default_headers.append(key, value);
} }
Err(e) => log::error!("Header value error: {:?}", e), Err(err) => log::error!("Header value error: {:?}", err),
}, },
Err(e) => log::error!("Header name error: {:?}", e), Err(err) => log::error!("Header name error: {:?}", err),
} }
self self
} }
@ -190,10 +206,10 @@ where
Some(password) => format!("{}:{}", username, password), Some(password) => format!("{}:{}", username, password),
None => format!("{}:", username), None => format!("{}:", username),
}; };
self.header( self.add_default_header((
header::AUTHORIZATION, header::AUTHORIZATION,
format!("Basic {}", base64::encode(&auth)), format!("Basic {}", base64::encode(&auth)),
) ))
} }
/// Set client wide HTTP bearer authentication header /// Set client wide HTTP bearer authentication header
@ -201,13 +217,12 @@ where
where where
T: fmt::Display, T: fmt::Display,
{ {
self.header(header::AUTHORIZATION, format!("Bearer {}", token)) self.add_default_header((header::AUTHORIZATION, format!("Bearer {}", token)))
} }
/// Registers middleware, in the form of a middleware component (type), /// Registers middleware, in the form of a middleware component (type), that runs during inbound
/// that runs during inbound and/or outbound processing in the request /// and/or outbound processing in the request life-cycle (request -> response),
/// life-cycle (request -> response), modifying request/response as /// modifying request/response as necessary, across all requests managed by the `Client`.
/// necessary, across all requests managed by the Client.
pub fn wrap<S1, M1>( pub fn wrap<S1, M1>(
self, self,
mw: M1, mw: M1,
@ -218,11 +233,11 @@ where
{ {
ClientBuilder { ClientBuilder {
middleware: NestTransform::new(self.middleware, mw), middleware: NestTransform::new(self.middleware, mw),
default_headers: self.default_headers, fundamental_headers: self.fundamental_headers,
max_http_version: self.max_http_version, max_http_version: self.max_http_version,
stream_window_size: self.stream_window_size, stream_window_size: self.stream_window_size,
conn_window_size: self.conn_window_size, conn_window_size: self.conn_window_size,
headers: self.headers, default_headers: self.default_headers,
timeout: self.timeout, timeout: self.timeout,
connector: self.connector, connector: self.connector,
local_address: self.local_address, local_address: self.local_address,
@ -237,10 +252,10 @@ where
M::Transform: M::Transform:
Service<ConnectRequest, Response = ConnectResponse, Error = SendRequestError>, Service<ConnectRequest, Response = ConnectResponse, Error = SendRequestError>,
{ {
let redirect_time = self.max_redirects; let max_redirects = self.max_redirects;
if redirect_time > 0 { if max_redirects > 0 {
self.wrap(Redirect::new().max_redirect_times(redirect_time)) self.wrap(Redirect::new().max_redirect_times(max_redirects))
._finish() ._finish()
} else { } else {
self._finish() self._finish()
@ -272,7 +287,7 @@ where
let connector = boxed::rc_service(self.middleware.new_transform(connector)); let connector = boxed::rc_service(self.middleware.new_transform(connector));
Client(ClientConfig { Client(ClientConfig {
headers: Rc::new(self.headers), default_headers: Rc::new(self.default_headers),
timeout: self.timeout, timeout: self.timeout,
connector, connector,
}) })
@ -288,7 +303,7 @@ mod tests {
let client = ClientBuilder::new().basic_auth("username", Some("password")); let client = ClientBuilder::new().basic_auth("username", Some("password"));
assert_eq!( assert_eq!(
client client
.headers .default_headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.unwrap() .unwrap()
.to_str() .to_str()
@ -299,7 +314,7 @@ mod tests {
let client = ClientBuilder::new().basic_auth("username", None); let client = ClientBuilder::new().basic_auth("username", None);
assert_eq!( assert_eq!(
client client
.headers .default_headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.unwrap() .unwrap()
.to_str() .to_str()
@ -313,7 +328,7 @@ mod tests {
let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n");
assert_eq!( assert_eq!(
client client
.headers .default_headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
.unwrap() .unwrap()
.to_str() .to_str()

View File

@ -9,7 +9,7 @@ use actix_http::{
body::{BodySize, MessageBody}, body::{BodySize, MessageBody},
error::PayloadError, error::PayloadError,
h1, h1,
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, header::{HeaderMap, TryIntoHeaderValue, EXPECT, HOST},
Payload, RequestHeadType, ResponseHead, StatusCode, Payload, RequestHeadType, ResponseHead, StatusCode,
}; };
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;

View File

@ -6,7 +6,7 @@ use serde::Serialize;
use actix_http::{ use actix_http::{
error::HttpError, error::HttpError,
header::{HeaderMap, HeaderName, IntoHeaderValue}, header::{HeaderMap, HeaderName, TryIntoHeaderValue},
Method, RequestHead, Uri, Method, RequestHead, Uri,
}; };
@ -114,7 +114,7 @@ impl FrozenClientRequest {
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
self.extra_headers(HeaderMap::new()) self.extra_headers(HeaderMap::new())
.extra_header(key, value) .extra_header(key, value)
@ -142,7 +142,7 @@ impl FrozenSendBuilder {
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => match value.try_into_value() { Ok(key) => match value.try_into_value() {

View File

@ -168,7 +168,7 @@ pub struct Client(ClientConfig);
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ClientConfig { pub(crate) struct ClientConfig {
pub(crate) connector: BoxConnectorService, pub(crate) connector: BoxConnectorService,
pub(crate) headers: Rc<HeaderMap>, pub(crate) default_headers: Rc<HeaderMap>,
pub(crate) timeout: Option<Duration>, pub(crate) timeout: Option<Duration>,
} }
@ -204,7 +204,9 @@ impl Client {
{ {
let mut req = ClientRequest::new(method, url, self.0.clone()); let mut req = ClientRequest::new(method, url, self.0.clone());
for header in self.0.headers.iter() { for header in self.0.default_headers.iter() {
// header map is empty
// TODO: probably append instead
req = req.insert_header_if_none(header); req = req.insert_header_if_none(header);
} }
req req
@ -297,7 +299,7 @@ impl Client {
<Uri as TryFrom<U>>::Error: Into<HttpError>, <Uri as TryFrom<U>>::Error: Into<HttpError>,
{ {
let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); let mut req = ws::WebsocketsRequest::new(url, self.0.clone());
for (key, value) in self.0.headers.iter() { for (key, value) in self.0.default_headers.iter() {
req.head.headers.insert(key.clone(), value.clone()); req.head.headers.insert(key.clone(), value.clone());
} }
req req
@ -308,6 +310,6 @@ impl Client {
/// Returns Some(&mut HeaderMap) when Client object is unique /// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time). /// (No other clone of client exists at the same time).
pub fn headers(&mut self) -> Option<&mut HeaderMap> { pub fn headers(&mut self) -> Option<&mut HeaderMap> {
Rc::get_mut(&mut self.0.headers) Rc::get_mut(&mut self.0.default_headers)
} }
} }

View File

@ -442,13 +442,15 @@ mod tests {
}); });
let client = ClientBuilder::new() let client = ClientBuilder::new()
.header("custom", "value") .add_default_header(("custom", "value"))
.disable_redirects() .disable_redirects()
.finish(); .finish();
let res = client.get(srv.url("/")).send().await.unwrap(); let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 302); assert_eq!(res.status().as_u16(), 302);
let client = ClientBuilder::new().header("custom", "value").finish(); let client = ClientBuilder::new()
.add_default_header(("custom", "value"))
.finish();
let res = client.get(srv.url("/")).send().await.unwrap(); let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 200); assert_eq!(res.status().as_u16(), 200);
@ -520,7 +522,7 @@ mod tests {
// send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header // send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header
let client = ClientBuilder::new() let client = ClientBuilder::new()
.header(header::AUTHORIZATION, "auth_key_value") .add_default_header((header::AUTHORIZATION, "auth_key_value"))
.finish(); .finish();
let res = client.get(srv1.url("/")).send().await.unwrap(); let res = client.get(srv1.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 200); assert_eq!(res.status().as_u16(), 200);

View File

@ -6,7 +6,7 @@ use serde::Serialize;
use actix_http::{ use actix_http::{
error::HttpError, error::HttpError,
header::{self, HeaderMap, HeaderValue, IntoHeaderPair}, header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair},
ConnectionType, Method, RequestHead, Uri, Version, ConnectionType, Method, RequestHead, Uri, Version,
}; };
@ -147,11 +147,8 @@ impl ClientRequest {
} }
/// Insert a header, replacing any that were set with an equivalent field name. /// Insert a header, replacing any that were set with an equivalent field name.
pub fn insert_header<H>(mut self, header: H) -> Self pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
where match header.try_into_pair() {
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => { Ok((key, value)) => {
self.head.headers.insert(key, value); self.head.headers.insert(key, value);
} }
@ -162,11 +159,8 @@ impl ClientRequest {
} }
/// Insert a header only if it is not yet set. /// Insert a header only if it is not yet set.
pub fn insert_header_if_none<H>(mut self, header: H) -> Self pub fn insert_header_if_none(mut self, header: impl TryIntoHeaderPair) -> Self {
where match header.try_into_pair() {
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => { Ok((key, value)) => {
if !self.head.headers.contains_key(&key) { if !self.head.headers.contains_key(&key) {
self.head.headers.insert(key, value); self.head.headers.insert(key, value);
@ -192,11 +186,8 @@ impl ClientRequest {
/// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)); /// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON));
/// # } /// # }
/// ``` /// ```
pub fn append_header<H>(mut self, header: H) -> Self pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
where match header.try_into_pair() {
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => self.head.headers.append(key, value), Ok((key, value)) => self.head.headers.append(key, value),
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}; };
@ -588,7 +579,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_client_header() { async fn test_client_header() {
let req = Client::builder() let req = Client::builder()
.header(header::CONTENT_TYPE, "111") .add_default_header((header::CONTENT_TYPE, "111"))
.finish() .finish()
.get("/"); .get("/");
@ -606,7 +597,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_client_header_override() { async fn test_client_header_override() {
let req = Client::builder() let req = Client::builder()
.header(header::CONTENT_TYPE, "111") .add_default_header((header::CONTENT_TYPE, "111"))
.finish() .finish()
.get("/") .get("/")
.insert_header((header::CONTENT_TYPE, "222")); .insert_header((header::CONTENT_TYPE, "222"));

View File

@ -10,7 +10,7 @@ use std::{
use actix_http::{ use actix_http::{
body::BodyStream, body::BodyStream,
error::HttpError, error::HttpError,
header::{self, HeaderMap, HeaderName, IntoHeaderValue}, header::{self, HeaderMap, HeaderName, TryIntoHeaderValue},
RequestHead, RequestHeadType, RequestHead, RequestHeadType,
}; };
use actix_rt::time::{sleep, Sleep}; use actix_rt::time::{sleep, Sleep};
@ -298,7 +298,7 @@ impl RequestSender {
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
match self { match self {
RequestSender::Owned(head) => { RequestSender::Owned(head) => {

View File

@ -1,6 +1,6 @@
//! Test helpers for actix http client to use during testing. //! Test helpers for actix http client to use during testing.
use actix_http::{h1, header::IntoHeaderPair, Payload, ResponseHead, StatusCode, Version}; use actix_http::{h1, header::TryIntoHeaderPair, Payload, ResponseHead, StatusCode, Version};
use bytes::Bytes; use bytes::Bytes;
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
@ -28,10 +28,7 @@ impl Default for TestResponse {
impl TestResponse { impl TestResponse {
/// Create TestResponse and set header /// Create TestResponse and set header
pub fn with_header<H>(header: H) -> Self pub fn with_header(header: impl TryIntoHeaderPair) -> Self {
where
H: IntoHeaderPair,
{
Self::default().insert_header(header) Self::default().insert_header(header)
} }
@ -42,11 +39,8 @@ impl TestResponse {
} }
/// Insert a header /// Insert a header
pub fn insert_header<H>(mut self, header: H) -> Self pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
where if let Ok((key, value)) = header.try_into_pair() {
H: IntoHeaderPair,
{
if let Ok((key, value)) = header.try_into_header_pair() {
self.head.headers.insert(key, value); self.head.headers.insert(key, value);
return self; return self;
} }
@ -54,11 +48,8 @@ impl TestResponse {
} }
/// Append a header /// Append a header
pub fn append_header<H>(mut self, header: H) -> Self pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
where if let Ok((key, value)) = header.try_into_pair() {
H: IntoHeaderPair,
{
if let Ok((key, value)) = header.try_into_header_pair() {
self.head.headers.append(key, value); self.head.headers.append(key, value);
return self; return self;
} }

View File

@ -39,7 +39,7 @@ use crate::{
connect::{BoxedSocket, ConnectRequest}, connect::{BoxedSocket, ConnectRequest},
error::{HttpError, InvalidUrl, SendRequestError, WsClientError}, error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
http::{ http::{
header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}, header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
ConnectionType, Method, StatusCode, Uri, Version, ConnectionType, Method, StatusCode, Uri, Version,
}, },
response::ClientResponse, response::ClientResponse,
@ -171,7 +171,7 @@ impl WebsocketsRequest {
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => match value.try_into_value() { Ok(key) => match value.try_into_value() {
@ -190,7 +190,7 @@ impl WebsocketsRequest {
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => match value.try_into_value() { Ok(key) => match value.try_into_value() {
@ -209,7 +209,7 @@ impl WebsocketsRequest {
where where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
match HeaderName::try_from(key) { match HeaderName::try_from(key) {
Ok(key) => { Ok(key) => {
@ -445,7 +445,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_header_override() { async fn test_header_override() {
let req = Client::builder() let req = Client::builder()
.header(header::CONTENT_TYPE, "111") .add_default_header((header::CONTENT_TYPE, "111"))
.finish() .finish()
.ws("/") .ws("/")
.set_header(header::CONTENT_TYPE, "222"); .set_header(header::CONTENT_TYPE, "222");

View File

@ -22,14 +22,14 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
.wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(index) .service(index)
.service(no_params) .service(no_params)
.service( .service(
web::resource("/resource2/index.html") web::resource("/resource2/index.html")
.wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3")))
.default_service(web::route().to(HttpResponse::MethodNotAllowed)) .default_service(web::route().to(HttpResponse::MethodNotAllowed))
.route(web::get().to(index_async)), .route(web::get().to(index_async)),
) )

View File

@ -26,14 +26,14 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(|| { HttpServer::new(|| {
App::new() App::new()
.wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
.wrap(middleware::Compress::default()) .wrap(middleware::Compress::default())
.wrap(middleware::Logger::default()) .wrap(middleware::Logger::default())
.service(index) .service(index)
.service(no_params) .service(no_params)
.service( .service(
web::resource("/resource2/index.html") web::resource("/resource2/index.html")
.wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) .wrap(middleware::DefaultHeaders::new().add(("X-Version-R2", "0.3")))
.default_service(web::route().to(HttpResponse::MethodNotAllowed)) .default_service(web::route().to(HttpResponse::MethodNotAllowed))
.route(web::get().to(index_async)), .route(web::get().to(index_async)),
) )

View File

@ -55,6 +55,11 @@ else
read -p "Update version to: " NEW_VERSION read -p "Update version to: " NEW_VERSION
fi fi
# strip leading v from input
if [ "${NEW_VERSION:0:1}" = "v" ]; then
NEW_VERSION="${NEW_VERSION:1}"
fi
DATE="$(date -u +"%Y-%m-%d")" DATE="$(date -u +"%Y-%m-%d")"
echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)" echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)"
@ -124,15 +129,20 @@ SHORT_PACKAGE_NAME="$(echo $PACKAGE_NAME | sed 's/^actix-web-//' | sed 's/^actix
GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)"
RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)"
if [ "$(echo $NEW_VERSION | grep beta)" ] || [ "$(echo $NEW_VERSION | grep rc)" ] || [ "$(echo $NEW_VERSION | grep alpha)" ]; then
PRERELEASE="--prerelease"
fi
echo echo
echo "GitHub release command:" echo "GitHub release command:"
echo "gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" --prerelease" GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${PRERELEASE:-}"
echo "$GH_CMD"
read -p "Submit draft GH release: (y/N) " GH_RELEASE read -p "Submit draft GH release: (y/N) " GH_RELEASE
GH_RELEASE="${GH_RELEASE:-n}" GH_RELEASE="${GH_RELEASE:-n}"
if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then
gh release create "$GIT_TAG" --draft --title "$RELEASE_TITLE" --notes-file "$CHANGE_CHUNK_FILE" --prerelease eval "$GH_CMD"
fi fi
echo echo

View File

@ -4,15 +4,25 @@
set -x set -x
cargo test --lib --tests -p=actix-router --all-features EXIT=0
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
cargo test --workspace --doc save_exit_code() {
eval $@
local CMD_EXIT=$?
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
}
save_exit_code cargo test --lib --tests -p=actix-router --all-features
save_exit_code cargo test --lib --tests -p=actix-http --all-features
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features
save_exit_code cargo test --lib --tests -p=awc --all-features
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features
save_exit_code cargo test --lib --tests -p=actix-test --all-features
save_exit_code cargo test --lib --tests -p=actix-files
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features
save_exit_code cargo test --workspace --doc
exit $EXIT

41
scripts/unreleased Executable file
View File

@ -0,0 +1,41 @@
#!/bin/sh
set -euo pipefail
bold="\033[1m"
reset="\033[0m"
unreleased_for() {
DIR=$1
CARGO_MANIFEST=$DIR/Cargo.toml
CHANGELOG_FILE=$DIR/CHANGES.md
# get current version
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"
CHANGE_CHUNK_FILE="$(mktemp)"
# get changelog chunk and save to temp file
cat "$CHANGELOG_FILE" |
# skip up to unreleased heading
sed '1,/Unreleased/ d' |
# take up to previous version heading
sed "/$CURRENT_VERSION/ q" |
# drop last line
sed '$d' \
>"$CHANGE_CHUNK_FILE"
# if word count of changelog chunk is 0 then exit
if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then
return 0;
fi
echo "${bold}# ${PACKAGE_NAME}${reset} since ${bold}v$CURRENT_VERSION${reset}"
cat "$CHANGE_CHUNK_FILE"
}
for f in $(fd --absolute-path CHANGES.md); do
unreleased_for $(dirname $f)
done

View File

@ -486,19 +486,21 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_service::Service; use actix_service::Service as _;
use actix_utils::future::{err, ok}; use actix_utils::future::{err, ok};
use bytes::Bytes; use bytes::Bytes;
use super::*; use super::*;
use crate::http::{ use crate::{
http::{
header::{self, HeaderValue}, header::{self, HeaderValue},
Method, StatusCode, Method, StatusCode,
},
middleware::DefaultHeaders,
service::ServiceRequest,
test::{call_service, init_service, read_body, try_init_service, TestRequest},
web, HttpRequest, HttpResponse,
}; };
use crate::middleware::DefaultHeaders;
use crate::service::ServiceRequest;
use crate::test::{call_service, init_service, read_body, try_init_service, TestRequest};
use crate::{web, HttpRequest, HttpResponse};
#[actix_rt::test] #[actix_rt::test]
async fn test_default_resource() { async fn test_default_resource() {
@ -602,7 +604,7 @@ mod tests {
App::new() App::new()
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
.header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
) )
.route("/test", web::get().to(HttpResponse::Ok)), .route("/test", web::get().to(HttpResponse::Ok)),
) )
@ -623,7 +625,7 @@ mod tests {
.route("/test", web::get().to(HttpResponse::Ok)) .route("/test", web::get().to(HttpResponse::Ok))
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
.header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
), ),
) )
.await; .await;
@ -706,4 +708,25 @@ mod tests {
let body = read_body(resp).await; let body = read_body(resp).await;
assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
} }
/// compile-only test for returning app type from function
pub fn foreign_app_type() -> 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" }))
}
#[test]
fn return_foreign_app_type() {
let _app = foreign_app_type();
}
} }

View File

@ -22,6 +22,7 @@ use crate::{
type Guards = Vec<Box<dyn Guard>>; type Guards = Vec<Box<dyn Guard>>;
/// Service factory to convert `Request` to a `ServiceRequest<S>`. /// Service factory to convert `Request` to a `ServiceRequest<S>`.
///
/// It also executes data factories. /// It also executes data factories.
pub struct AppInit<T, B> pub struct AppInit<T, B>
where where

View File

@ -102,41 +102,3 @@ impl<B> BodyEncoding for crate::HttpResponse<B> {
self self
} }
} }
// TODO: remove this if it doesn't appear to be needed
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) enum AnyBody {
None,
Full { body: crate::web::Bytes },
Boxed { body: actix_http::body::BoxBody },
}
impl crate::body::MessageBody for AnyBody {
type Error = crate::BoxError;
/// Body size hint.
fn size(&self) -> crate::body::BodySize {
match self {
AnyBody::None => crate::body::BodySize::None,
AnyBody::Full { body } => body.size(),
AnyBody::Boxed { body } => body.size(),
}
}
/// Attempt to pull out the next chunk of body bytes.
fn poll_next(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Option<Result<crate::web::Bytes, Self::Error>>> {
match self.get_mut() {
AnyBody::None => std::task::Poll::Ready(None),
AnyBody::Full { body } => {
let bytes = std::mem::take(body);
std::task::Poll::Ready(Some(Ok(bytes)))
}
AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx),
}
}
}

View File

@ -2,7 +2,7 @@ use std::{cell::RefCell, fmt, io::Write as _};
use actix_http::{ use actix_http::{
body::BoxBody, body::BoxBody,
header::{self, IntoHeaderValue as _}, header::{self, TryIntoHeaderValue as _},
StatusCode, StatusCode,
}; };
use bytes::{BufMut as _, BytesMut}; use bytes::{BufMut as _, BytesMut};

View File

@ -1,4 +1,5 @@
//! Error and Result module //! Error and Result module
// This is meant to be a glob import of the whole error module except for `Error`. Rustdoc can't yet // 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 // correctly resolve the conflicting `Error` type defined in this module, so these re-exports are
// expanded manually. // expanded manually.

View File

@ -8,7 +8,7 @@ use std::{
use actix_http::{ use actix_http::{
body::BoxBody, body::BoxBody,
header::{self, IntoHeaderValue}, header::{self, TryIntoHeaderValue},
Response, StatusCode, Response, StatusCode,
}; };
use bytes::BytesMut; use bytes::BytesMut;

View File

@ -14,7 +14,7 @@ use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use super::{ExtendedValue, Header, IntoHeaderValue, Writer}; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
use crate::http::header; use crate::http::header;
/// Split at the index of the first `needle` if it exists or at the end. /// Split at the index of the first `needle` if it exists or at the end.
@ -454,7 +454,7 @@ impl ContentDisposition {
} }
} }
impl IntoHeaderValue for ContentDisposition { impl TryIntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValue; type Error = header::InvalidHeaderValue;
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> { fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {

View File

@ -3,7 +3,7 @@ use std::{
str::FromStr, str::FromStr,
}; };
use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer, CONTENT_RANGE};
use crate::error::ParseError; use crate::error::ParseError;
crate::http::header::common_header! { crate::http::header::common_header! {
@ -196,7 +196,7 @@ impl Display for ContentRangeSpec {
} }
} }
impl IntoHeaderValue for ContentRangeSpec { impl TryIntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {

View File

@ -3,7 +3,7 @@ use std::{
str::FromStr, str::FromStr,
}; };
use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer};
/// check that each char in the slice is either: /// check that each char in the slice is either:
/// 1. `%x21`, or /// 1. `%x21`, or
@ -159,7 +159,7 @@ impl FromStr for EntityTag {
} }
} }
impl IntoHeaderValue for EntityTag { impl TryIntoHeaderValue for EntityTag {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {

View File

@ -1,8 +1,8 @@
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use super::{ use super::{
from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue,
InvalidHeaderValue, Writer, TryIntoHeaderValue, Writer,
}; };
use crate::error::ParseError; use crate::error::ParseError;
use crate::http::header; use crate::http::header;
@ -96,7 +96,7 @@ impl Display for IfRange {
} }
} }
impl IntoHeaderValue for IfRange { impl TryIntoHeaderValue for IfRange {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {

View File

@ -125,7 +125,7 @@ macro_rules! common_header {
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; type Error = $crate::http::header::InvalidHeaderValue;
#[inline] #[inline]
@ -172,7 +172,7 @@ macro_rules! common_header {
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; type Error = $crate::http::header::InvalidHeaderValue;
#[inline] #[inline]
@ -211,7 +211,7 @@ macro_rules! common_header {
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; type Error = $crate::http::header::InvalidHeaderValue;
#[inline] #[inline]
@ -266,7 +266,7 @@ macro_rules! common_header {
} }
} }
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::TryIntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; type Error = $crate::http::header::InvalidHeaderValue;
#[inline] #[inline]

View File

@ -6,7 +6,7 @@ use std::{
use actix_http::{error::ParseError, header, HttpMessage}; use actix_http::{error::ParseError, header, HttpMessage};
use super::{Header, HeaderName, HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; use super::{Header, HeaderName, HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer};
/// `Range` header, defined /// `Range` header, defined
/// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) /// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1)
@ -274,7 +274,7 @@ impl Header for Range {
} }
} }
impl IntoHeaderValue for Range { impl TryIntoHeaderValue for Range {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> { fn try_into_value(self) -> Result<HeaderValue, Self::Error> {

View File

@ -86,7 +86,6 @@ pub mod middleware;
mod request; mod request;
mod request_data; mod request_data;
mod resource; mod resource;
mod responder;
mod response; mod response;
mod rmap; mod rmap;
mod route; mod route;
@ -109,12 +108,10 @@ pub use crate::error::{Error, ResponseError, Result};
pub use crate::extract::FromRequest; pub use crate::extract::FromRequest;
pub use crate::request::HttpRequest; pub use crate::request::HttpRequest;
pub use crate::resource::Resource; pub use crate::resource::Resource;
pub use crate::responder::Responder; pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder};
pub use crate::response::{HttpResponse, HttpResponseBuilder};
pub use crate::route::Route; pub use crate::route::Route;
pub use crate::scope::Scope; pub use crate::scope::Scope;
pub use crate::server::HttpServer; pub use crate::server::HttpServer;
// TODO: is exposing the error directly really needed pub use crate::types::Either;
pub use crate::types::{Either, EitherExtractError};
pub(crate) type BoxError = Box<dyn std::error::Error>; pub(crate) type BoxError = Box<dyn std::error::Error>;

View File

@ -6,12 +6,15 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_http::body::MessageBody;
use actix_service::{Service, Transform};
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{error::Error, service::ServiceResponse}; 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), /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap),
/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). /// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition).
@ -52,7 +55,7 @@ where
T::Response: MapServiceResponseBody, T::Response: MapServiceResponseBody,
T::Error: Into<Error>, T::Error: Into<Error>,
{ {
type Response = ServiceResponse; type Response = ServiceResponse<BoxBody>;
type Error = Error; type Error = Error;
type Transform = CompatMiddleware<T::Transform>; type Transform = CompatMiddleware<T::Transform>;
type InitError = T::InitError; type InitError = T::InitError;
@ -77,7 +80,7 @@ where
S::Response: MapServiceResponseBody, S::Response: MapServiceResponseBody,
S::Error: Into<Error>, S::Error: Into<Error>,
{ {
type Response = ServiceResponse; type Response = ServiceResponse<BoxBody>;
type Error = Error; type Error = Error;
type Future = CompatMiddlewareFuture<S::Future>; type Future = CompatMiddlewareFuture<S::Future>;
@ -102,7 +105,7 @@ where
T: MapServiceResponseBody, T: MapServiceResponseBody,
E: Into<Error>, E: Into<Error>,
{ {
type Output = Result<ServiceResponse, Error>; type Output = Result<ServiceResponse<BoxBody>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let res = match ready!(self.project().fut.poll(cx)) { let res = match ready!(self.project().fut.poll(cx)) {
@ -116,14 +119,15 @@ where
/// Convert `ServiceResponse`'s `ResponseBody<B>` generic type to `ResponseBody<Body>`. /// Convert `ServiceResponse`'s `ResponseBody<B>` generic type to `ResponseBody<Body>`.
pub trait MapServiceResponseBody { pub trait MapServiceResponseBody {
fn map_body(self) -> ServiceResponse; fn map_body(self) -> ServiceResponse<BoxBody>;
} }
impl<B> MapServiceResponseBody for ServiceResponse<B> impl<B> MapServiceResponseBody for ServiceResponse<B>
where where
B: MessageBody + Unpin + 'static, B: MessageBody + 'static,
{ {
fn map_body(self) -> ServiceResponse { #[inline]
fn map_body(self) -> ServiceResponse<BoxBody> {
self.map_into_boxed_body() self.map_into_boxed_body()
} }
} }

View File

@ -106,7 +106,7 @@ mod tests {
header::{HeaderValue, CONTENT_TYPE}, header::{HeaderValue, CONTENT_TYPE},
StatusCode, StatusCode,
}, },
middleware::err_handlers::*, middleware::{err_handlers::*, Compat},
test::{self, TestRequest}, test::{self, TestRequest},
HttpResponse, HttpResponse,
}; };
@ -116,7 +116,8 @@ mod tests {
res.response_mut() res.response_mut()
.headers_mut() .headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001")); .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Response(res))
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
} }
#[actix_rt::test] #[actix_rt::test]
@ -125,7 +126,9 @@ mod tests {
ok(req.into_response(HttpResponse::InternalServerError().finish())) ok(req.into_response(HttpResponse::InternalServerError().finish()))
}; };
let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Compat::new(
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(true, mw) let mw = Condition::new(true, mw)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
@ -141,7 +144,9 @@ mod tests {
ok(req.into_response(HttpResponse::InternalServerError().finish())) ok(req.into_response(HttpResponse::InternalServerError().finish()))
}; };
let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); let mw = Compat::new(
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500),
);
let mw = Condition::new(false, mw) let mw = Condition::new(false, mw)
.new_transform(srv.into_service()) .new_transform(srv.into_service())

View File

@ -16,7 +16,7 @@ use pin_project_lite::pin_project;
use crate::{ use crate::{
dev::{Service, Transform}, dev::{Service, Transform},
http::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE}, http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE},
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, Error,
}; };
@ -29,79 +29,81 @@ use crate::{
/// ``` /// ```
/// use actix_web::{web, http, middleware, App, HttpResponse}; /// use actix_web::{web, http, middleware, App, HttpResponse};
/// ///
/// fn main() {
/// let app = App::new() /// let app = App::new()
/// .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2")) /// .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
/// .service( /// .service(
/// web::resource("/test") /// web::resource("/test")
/// .route(web::get().to(|| HttpResponse::Ok())) /// .route(web::get().to(|| HttpResponse::Ok()))
/// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed())) /// .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed()))
/// ); /// );
/// }
/// ``` /// ```
#[derive(Clone)] #[derive(Debug, Clone, Default)]
pub struct DefaultHeaders { pub struct DefaultHeaders {
inner: Rc<Inner>, inner: Rc<Inner>,
} }
#[derive(Debug, Default)]
struct Inner { struct Inner {
headers: HeaderMap, headers: HeaderMap,
} }
impl Default for DefaultHeaders {
fn default() -> Self {
DefaultHeaders {
inner: Rc::new(Inner {
headers: HeaderMap::new(),
}),
}
}
}
impl DefaultHeaders { impl DefaultHeaders {
/// Constructs an empty `DefaultHeaders` middleware. /// Constructs an empty `DefaultHeaders` middleware.
#[inline]
pub fn new() -> DefaultHeaders { pub fn new() -> DefaultHeaders {
DefaultHeaders::default() DefaultHeaders::default()
} }
/// Adds a header to the default set. /// Adds a header to the default set.
#[inline] ///
pub fn header<K, V>(mut self, key: K, value: V) -> Self /// # 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 where
HeaderName: TryFrom<K>, HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>, <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
HeaderValue: TryFrom<V>, HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>, <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{ {
#[allow(clippy::match_wild_err_arm)] self.add((
match HeaderName::try_from(key) { HeaderName::try_from(key)
Ok(key) => match HeaderValue::try_from(value) { .map_err(Into::into)
Ok(value) => { .expect("Invalid header name"),
Rc::get_mut(&mut self.inner) HeaderValue::try_from(value)
.expect("Multiple copies exist") .map_err(Into::into)
.headers .expect("Invalid header value"),
.append(key, value); ))
}
Err(_) => panic!("Can not create header value"),
},
Err(_) => panic!("Can not create header name"),
}
self
} }
/// Adds a default *Content-Type* header if response does not contain one. /// Adds a default *Content-Type* header if response does not contain one.
/// ///
/// Default is `application/octet-stream`. /// Default is `application/octet-stream`.
pub fn add_content_type(mut self) -> Self { pub fn add_content_type(self) -> Self {
Rc::get_mut(&mut self.inner) self.add((
.expect("Multiple `Inner` copies exist.")
.headers
.insert(
CONTENT_TYPE, CONTENT_TYPE,
HeaderValue::from_static("application/octet-stream"), HeaderValue::from_static("application/octet-stream"),
); ))
self
} }
} }
@ -119,7 +121,7 @@ where
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(DefaultHeadersMiddleware { ready(Ok(DefaultHeadersMiddleware {
service, service,
inner: self.inner.clone(), inner: Rc::clone(&self.inner),
})) }))
} }
} }
@ -192,22 +194,27 @@ mod tests {
use crate::{ use crate::{
dev::ServiceRequest, dev::ServiceRequest,
http::header::CONTENT_TYPE, http::header::CONTENT_TYPE,
test::{ok_service, TestRequest}, test::{self, TestRequest},
HttpResponse, HttpResponse,
}; };
#[actix_rt::test] #[actix_rt::test]
async fn test_default_headers() { async fn adding_default_headers() {
let mw = DefaultHeaders::new() let mw = DefaultHeaders::new()
.header(CONTENT_TYPE, "0001") .add(("X-TEST", "0001"))
.new_transform(ok_service()) .add(("X-TEST-TWO", HeaderValue::from_static("123")))
.new_transform(test::ok_service())
.await .await
.unwrap(); .unwrap();
let req = TestRequest::default().to_srv_request(); let req = TestRequest::default().to_srv_request();
let resp = mw.call(req).await.unwrap(); let res = mw.call(req).await.unwrap();
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); 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 req = TestRequest::default().to_srv_request();
let srv = |req: ServiceRequest| { let srv = |req: ServiceRequest| {
ok(req.into_response( ok(req.into_response(
@ -217,7 +224,7 @@ mod tests {
)) ))
}; };
let mw = DefaultHeaders::new() let mw = DefaultHeaders::new()
.header(CONTENT_TYPE, "0001") .add((CONTENT_TYPE, "0001"))
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();
@ -226,11 +233,10 @@ mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_content_type() { async fn adding_content_type() {
let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish()));
let mw = DefaultHeaders::new() let mw = DefaultHeaders::new()
.add_content_type() .add_content_type()
.new_transform(srv.into_service()) .new_transform(test::ok_service())
.await .await
.unwrap(); .unwrap();
@ -241,4 +247,16 @@ mod tests {
"application/octet-stream" "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

@ -13,6 +13,7 @@ use futures_core::{future::LocalBoxFuture, ready};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::EitherBody,
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
http::StatusCode, http::StatusCode,
Error, Result, Error, Result,
@ -21,10 +22,10 @@ use crate::{
/// Return type for [`ErrorHandlers`] custom handlers. /// Return type for [`ErrorHandlers`] custom handlers.
pub enum ErrorHandlerResponse<B> { pub enum ErrorHandlerResponse<B> {
/// Immediate HTTP response. /// Immediate HTTP response.
Response(ServiceResponse<B>), Response(ServiceResponse<EitherBody<B>>),
/// A future that resolves to an HTTP response. /// A future that resolves to an HTTP response.
Future(LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>), Future(LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>),
} }
type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>; type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>>;
@ -44,7 +45,8 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
/// res.response_mut() /// res.response_mut()
/// .headers_mut() /// .headers_mut()
/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error")); /// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error"));
/// Ok(ErrorHandlerResponse::Response(res)) ///
/// Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
/// } /// }
/// ///
/// let app = App::new() /// let app = App::new()
@ -66,7 +68,7 @@ type Handlers<B> = Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>;
impl<B> Default for ErrorHandlers<B> { impl<B> Default for ErrorHandlers<B> {
fn default() -> Self { fn default() -> Self {
ErrorHandlers { ErrorHandlers {
handlers: Rc::new(AHashMap::default()), handlers: Default::default(),
} }
} }
} }
@ -95,7 +97,7 @@ where
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
type Response = ServiceResponse<B>; type Response = ServiceResponse<EitherBody<B>>;
type Error = Error; type Error = Error;
type Transform = ErrorHandlersMiddleware<S, B>; type Transform = ErrorHandlersMiddleware<S, B>;
type InitError = (); type InitError = ();
@ -119,7 +121,7 @@ where
S::Future: 'static, S::Future: 'static,
B: 'static, B: 'static,
{ {
type Response = ServiceResponse<B>; type Response = ServiceResponse<EitherBody<B>>;
type Error = Error; type Error = Error;
type Future = ErrorHandlersFuture<S::Future, B>; type Future = ErrorHandlersFuture<S::Future, B>;
@ -143,8 +145,8 @@ pin_project! {
fut: Fut, fut: Fut,
handlers: Handlers<B>, handlers: Handlers<B>,
}, },
HandlerFuture { ErrorHandlerFuture {
fut: LocalBoxFuture<'static, Fut::Output>, fut: LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>,
}, },
} }
} }
@ -153,25 +155,29 @@ impl<Fut, B> Future for ErrorHandlersFuture<Fut, B>
where where
Fut: Future<Output = Result<ServiceResponse<B>, Error>>, Fut: Future<Output = Result<ServiceResponse<B>, Error>>,
{ {
type Output = Fut::Output; type Output = Result<ServiceResponse<EitherBody<B>>, Error>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project() { match self.as_mut().project() {
ErrorHandlersProj::ServiceFuture { fut, handlers } => { ErrorHandlersProj::ServiceFuture { fut, handlers } => {
let res = ready!(fut.poll(cx))?; let res = ready!(fut.poll(cx))?;
match handlers.get(&res.status()) { match handlers.get(&res.status()) {
Some(handler) => match handler(res)? { Some(handler) => match handler(res)? {
ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)), ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)),
ErrorHandlerResponse::Future(fut) => { ErrorHandlerResponse::Future(fut) => {
self.as_mut() self.as_mut()
.set(ErrorHandlersFuture::HandlerFuture { fut }); .set(ErrorHandlersFuture::ErrorHandlerFuture { fut });
self.poll(cx) self.poll(cx)
} }
}, },
None => Poll::Ready(Ok(res)),
None => Poll::Ready(Ok(res.map_into_left_body())),
} }
} }
ErrorHandlersProj::HandlerFuture { fut } => fut.as_mut().poll(cx),
ErrorHandlersProj::ErrorHandlerFuture { fut } => fut.as_mut().poll(cx),
} }
} }
} }
@ -180,32 +186,33 @@ where
mod tests { mod tests {
use actix_service::IntoService; use actix_service::IntoService;
use actix_utils::future::ok; use actix_utils::future::ok;
use bytes::Bytes;
use futures_util::future::FutureExt as _; use futures_util::future::FutureExt as _;
use super::*; use super::*;
use crate::http::{ use crate::{
http::{
header::{HeaderValue, CONTENT_TYPE}, header::{HeaderValue, CONTENT_TYPE},
StatusCode, StatusCode,
},
test::{self, TestRequest},
}; };
use crate::test::{self, TestRequest};
use crate::HttpResponse;
#[actix_rt::test]
async fn add_header_error_handler() {
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> { fn error_handler<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut() res.response_mut()
.headers_mut() .headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001")); .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Response(res))
Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
} }
#[actix_rt::test] let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
async fn test_handler() {
let srv = |req: ServiceRequest| {
ok(req.into_response(HttpResponse::InternalServerError().finish()))
};
let mw = ErrorHandlers::new() let mw = ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, render_500) .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();
@ -214,24 +221,25 @@ mod tests {
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
} }
#[actix_rt::test]
async fn add_header_error_handler_async() {
#[allow(clippy::unnecessary_wraps)] #[allow(clippy::unnecessary_wraps)]
fn render_500_async<B: 'static>( fn error_handler<B: 'static>(
mut res: ServiceResponse<B>, mut res: ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>> { ) -> Result<ErrorHandlerResponse<B>> {
res.response_mut() res.response_mut()
.headers_mut() .headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001")); .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Future(ok(res).boxed_local()))
Ok(ErrorHandlerResponse::Future(
ok(res.map_into_left_body()).boxed_local(),
))
} }
#[actix_rt::test] let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
async fn test_handler_async() {
let srv = |req: ServiceRequest| {
ok(req.into_response(HttpResponse::InternalServerError().finish()))
};
let mw = ErrorHandlers::new() let mw = ErrorHandlers::new()
.handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async) .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
.new_transform(srv.into_service()) .new_transform(srv.into_service())
.await .await
.unwrap(); .unwrap();
@ -239,4 +247,34 @@ mod tests {
let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); 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

@ -322,13 +322,10 @@ pin_project! {
} }
} }
impl<B> MessageBody for StreamLog<B> impl<B: MessageBody> MessageBody for StreamLog<B> {
where type Error = B::Error;
B: MessageBody,
B::Error: Into<Error>,
{
type Error = Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.body.size() self.body.size()
} }
@ -344,7 +341,7 @@ where
*this.size += chunk.len(); *this.size += chunk.len();
Poll::Ready(Some(Ok(chunk))) Poll::Ready(Some(Ok(chunk)))
} }
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), Some(Err(err)) => Poll::Ready(Some(Err(err))),
None => Poll::Ready(None), None => Poll::Ready(None),
} }
} }

View File

@ -33,9 +33,9 @@ mod tests {
let _ = App::new() let _ = App::new()
.wrap(Compat::new(Logger::default())) .wrap(Compat::new(Logger::default()))
.wrap(Condition::new(true, DefaultHeaders::new())) .wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res)) Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
})) }))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(NormalizePath::new(TrailingSlash::Trim)); .wrap(NormalizePath::new(TrailingSlash::Trim));
@ -44,9 +44,9 @@ mod tests {
.wrap(NormalizePath::new(TrailingSlash::Trim)) .wrap(NormalizePath::new(TrailingSlash::Trim))
.wrap(Logger::default()) .wrap(Logger::default())
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res)) Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
})) }))
.wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
.wrap(Condition::new(true, DefaultHeaders::new())) .wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(Compat::new(Logger::default())); .wrap(Compat::new(Logger::default()));

View File

@ -349,7 +349,7 @@ impl Drop for HttpRequest {
fn drop(&mut self) { fn drop(&mut self) {
// if possible, contribute to current worker's HttpRequest allocation pool // if possible, contribute to current worker's HttpRequest allocation pool
// This relies on no Weak<HttpRequestInner> exists anywhere. (There is none.) // This relies on no weak references to inner existing anywhere within the codebase.
if let Some(inner) = Rc::get_mut(&mut self.inner) { if let Some(inner) = Rc::get_mut(&mut self.inner) {
if inner.app_state.pool().is_available() { if inner.app_state.pool().is_available() {
// clear additional app_data and keep the root one for reuse. // clear additional app_data and keep the root one for reuse.
@ -360,7 +360,7 @@ impl Drop for HttpRequest {
Rc::get_mut(&mut inner.req_data).unwrap().get_mut().clear(); Rc::get_mut(&mut inner.req_data).unwrap().get_mut().clear();
// a re-borrow of pool is necessary here. // a re-borrow of pool is necessary here.
let req = self.inner.clone(); let req = Rc::clone(&self.inner);
self.app_state().pool().push(req); self.app_state().pool().push(req);
} }
} }

View File

@ -17,7 +17,7 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H
/// # Mutating Request Data /// # Mutating Request Data
/// Note that since extractors must output owned data, only types that `impl Clone` can use this /// 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 /// 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 /// mutated in-place. To mutate request data, continue to use [`HttpRequest::req_data_mut`] or
/// re-insert the cloned data back into the extensions map. A `DerefMut` impl is intentionally not /// 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. /// provided to make this potential foot-gun more obvious.
/// ///

View File

@ -15,13 +15,12 @@ use crate::{
dev::{ensure_leading_slash, AppService, ResourceDef}, dev::{ensure_leading_slash, AppService, ResourceDef},
guard::Guard, guard::Guard,
handler::Handler, handler::Handler,
responder::Responder,
route::{Route, RouteService}, route::{Route, RouteService},
service::{ service::{
BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
ServiceResponse, ServiceResponse,
}, },
BoxError, Error, FromRequest, HttpResponse, BoxError, Error, FromRequest, HttpResponse, Responder,
}; };
/// *Resource* is an entry in resources table which corresponds to requested URL. /// *Resource* is an entry in resources table which corresponds to requested URL.
@ -526,7 +525,7 @@ mod tests {
.name("test") .name("test")
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
.header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
) )
.route(web::get().to(HttpResponse::Ok)), .route(web::get().to(HttpResponse::Ok)),
), ),

View File

@ -9,7 +9,7 @@ use std::{
use actix_http::{ use actix_http::{
body::{BodyStream, BoxBody, MessageBody}, body::{BodyStream, BoxBody, MessageBody},
error::HttpError, error::HttpError,
header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
ConnectionType, Extensions, Response, ResponseHead, StatusCode, ConnectionType, Extensions, Response, ResponseHead, StatusCode,
}; };
use bytes::Bytes; use bytes::Bytes;
@ -67,12 +67,9 @@ impl HttpResponseBuilder {
/// .insert_header(("X-TEST", "value")) /// .insert_header(("X-TEST", "value"))
/// .finish(); /// .finish();
/// ``` /// ```
pub fn insert_header<H>(&mut self, header: H) -> &mut Self pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match header.try_into_header_pair() { match header.try_into_pair() {
Ok((key, value)) => { Ok((key, value)) => {
parts.headers.insert(key, value); parts.headers.insert(key, value);
} }
@ -94,12 +91,9 @@ impl HttpResponseBuilder {
/// .append_header(("X-TEST", "value2")) /// .append_header(("X-TEST", "value2"))
/// .finish(); /// .finish();
/// ``` /// ```
pub fn append_header<H>(&mut self, header: H) -> &mut Self pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match header.try_into_header_pair() { match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value), Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.err = Some(e.into()), Err(e) => self.err = Some(e.into()),
}; };
@ -118,7 +112,7 @@ impl HttpResponseBuilder {
where where
K: TryInto<HeaderName>, K: TryInto<HeaderName>,
K::Error: Into<HttpError>, K::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if self.err.is_some() { if self.err.is_some() {
return self; return self;
@ -143,7 +137,7 @@ impl HttpResponseBuilder {
where where
K: TryInto<HeaderName>, K: TryInto<HeaderName>,
K::Error: Into<HttpError>, K::Error: Into<HttpError>,
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if self.err.is_some() { if self.err.is_some() {
return self; return self;
@ -180,7 +174,7 @@ impl HttpResponseBuilder {
#[inline] #[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Upgrade); parts.set_connection_type(ConnectionType::Upgrade);
@ -218,7 +212,7 @@ impl HttpResponseBuilder {
#[inline] #[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self pub fn content_type<V>(&mut self, value: V) -> &mut Self
where where
V: IntoHeaderValue, V: TryIntoHeaderValue,
{ {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match value.try_into_value() { match value.try_into_value() {

View File

@ -0,0 +1,245 @@
use actix_http::{
body::{EitherBody, MessageBody},
error::HttpError,
header::HeaderMap,
header::TryIntoHeaderPair,
StatusCode,
};
use crate::{BoxError, 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,
<T::Body as MessageBody>::Error: Into<BoxError>,
{
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

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

View File

@ -2,64 +2,58 @@ use std::borrow::Cow;
use actix_http::{ use actix_http::{
body::{BoxBody, EitherBody, MessageBody}, body::{BoxBody, EitherBody, MessageBody},
error::HttpError, header::TryIntoHeaderPair,
header::HeaderMap,
header::IntoHeaderPair,
StatusCode, StatusCode,
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder};
use super::CustomizeResponder;
/// Trait implemented by types that can be converted to an HTTP response. /// 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. /// 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 { pub trait Responder {
type Body: MessageBody + 'static; type Body: MessageBody + 'static;
/// Convert self to `HttpResponse`. /// Convert self to `HttpResponse`.
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>; fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>;
/// Override a status code for a Responder. /// Wraps responder to allow alteration of its response.
/// ///
/// ``` /// See [`CustomizeResponder`] docs for its capabilities.
/// use actix_web::{http::StatusCode, HttpRequest, Responder};
/// ///
/// fn index(req: HttpRequest) -> impl Responder { /// # Examples
/// "Welcome!".with_status(StatusCode::OK)
/// }
/// ``` /// ```
fn with_status(self, status: StatusCode) -> CustomResponder<Self> /// 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 where
Self: Sized, Self: Sized,
{ {
CustomResponder::new(self).with_status(status) CustomizeResponder::new(self)
} }
/// Insert header to the final response. #[doc(hidden)]
/// #[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")]
/// Overrides other headers with the same name. fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder<Self>
///
/// ```
/// use actix_web::{web, HttpRequest, Responder};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(req: HttpRequest) -> impl Responder {
/// web::Json(MyObj { name: "Name".to_owned() })
/// .with_header(("x-version", "1.2.3"))
/// }
/// ```
fn with_header<H>(self, header: H) -> CustomResponder<Self>
where where
Self: Sized, Self: Sized,
H: IntoHeaderPair,
{ {
CustomResponder::new(self).with_header(header) self.customize().insert_header(header)
} }
} }
@ -181,98 +175,6 @@ macro_rules! impl_into_string_responder {
impl_into_string_responder!(&'_ String); impl_into_string_responder!(&'_ String);
impl_into_string_responder!(Cow<'_, str>); impl_into_string_responder!(Cow<'_, str>);
/// Allows overriding status code and headers for a responder.
pub struct CustomResponder<T> {
responder: T,
status: Option<StatusCode>,
headers: Result<HeaderMap, HttpError>,
}
impl<T: Responder> CustomResponder<T> {
fn new(responder: T) -> Self {
CustomResponder {
responder,
status: None,
headers: Ok(HeaderMap::new()),
}
}
/// Override a status code for the Responder's response.
///
/// ```
/// use actix_web::{HttpRequest, Responder, http::StatusCode};
///
/// fn index(req: HttpRequest) -> impl Responder {
/// "Welcome!".with_status(StatusCode::OK)
/// }
/// ```
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = Some(status);
self
}
/// Insert header to the final response.
///
/// Overrides other headers with the same name.
///
/// ```
/// use actix_web::{web, HttpRequest, Responder};
/// use serde::Serialize;
///
/// #[derive(Serialize)]
/// struct MyObj {
/// name: String,
/// }
///
/// fn index(req: HttpRequest) -> impl Responder {
/// web::Json(MyObj { name: "Name".to_string() })
/// .with_header(("x-version", "1.2.3"))
/// .with_header(("x-version", "1.2.3"))
/// }
/// ```
pub fn with_header<H>(mut self, header: H) -> Self
where
H: IntoHeaderPair,
{
if let Ok(ref mut headers) = self.headers {
match header.try_into_header_pair() {
Ok((key, value)) => headers.append(key, value),
Err(e) => self.headers = Err(e.into()),
};
}
self
}
}
impl<T> Responder for CustomResponder<T>
where
T: Responder,
<T::Body as MessageBody>::Error: Into<BoxError>,
{
type Body = EitherBody<T::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
let headers = match self.headers {
Ok(headers) => headers,
Err(err) => return HttpResponse::from_error(err).map_into_right_body(),
};
let mut res = self.responder.respond_to(req);
if let Some(status) = self.status {
*res.status_mut() = status;
}
for (k, v) in headers {
// TODO: before v4, decide if this should be append instead
res.headers_mut().insert(k, v);
}
res.map_into_left_body()
}
}
#[cfg(test)] #[cfg(test)]
pub(crate) mod tests { pub(crate) mod tests {
use actix_service::Service; use actix_service::Service;
@ -440,59 +342,4 @@ pub(crate) mod tests {
assert_eq!(res.status(), StatusCode::BAD_REQUEST); assert_eq!(res.status(), StatusCode::BAD_REQUEST);
} }
#[actix_rt::test]
async fn test_custom_responder() {
let req = TestRequest::default().to_http_request();
let res = "test"
.to_string()
.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()
.with_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 test_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)
.with_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

@ -244,8 +244,7 @@ impl<B> HttpResponse<B> {
where where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
// TODO: avoid double boxing with down-casting, if it improves perf self.map_body(|_, body| body.boxed())
self.map_body(|_, body| BoxBody::new(body))
} }
/// Extract response body /// Extract response body
@ -314,7 +313,7 @@ impl Future for HttpResponse<BoxBody> {
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]
pub struct CookieIter<'a> { pub struct CookieIter<'a> {
iter: header::map::GetAll<'a>, iter: std::slice::Iter<'a, HeaderValue>,
} }
#[cfg(feature = "cookies")] #[cfg(feature = "cookies")]

View File

@ -935,7 +935,7 @@ mod tests {
web::scope("app") web::scope("app")
.wrap( .wrap(
DefaultHeaders::new() DefaultHeaders::new()
.header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
) )
.service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))),
), ),

View File

@ -451,7 +451,7 @@ impl<B> ServiceResponse<B> {
where where
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
self.map_body(|_, body| BoxBody::new(body)) self.map_body(|_, body| body.boxed())
} }
} }

View File

@ -1,915 +0,0 @@
//! Various helpers for Actix applications to use during testing.
use std::{borrow::Cow, net::SocketAddr, rc::Rc};
pub use actix_http::test::TestBuffer;
use actix_http::{
header::IntoHeaderPair, test::TestRequest as HttpTestRequest, Extensions, Method, Request,
StatusCode, Uri, Version,
};
use actix_router::{Path, ResourceDef, Url};
use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory};
use actix_utils::future::{ok, poll_fn};
use futures_core::Stream;
use futures_util::StreamExt as _;
use serde::{de::DeserializeOwned, Serialize};
#[cfg(feature = "cookies")]
use crate::cookie::{Cookie, CookieJar};
use crate::{
app_service::AppInitServiceState,
body::{self, BoxBody, MessageBody},
config::AppConfig,
data::Data,
dev::Payload,
http::header::ContentType,
rmap::ResourceMap,
service::{ServiceRequest, ServiceResponse},
web::{Bytes, BytesMut},
Error, HttpRequest, HttpResponse, HttpResponseBuilder,
};
/// Create service that always responds with `HttpResponse::Ok()` and no body.
pub fn ok_service(
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
default_service(StatusCode::OK)
}
/// Create service that always responds with given status code and no body.
pub fn default_service(
status_code: StatusCode,
) -> impl Service<ServiceRequest, Response = ServiceResponse<BoxBody>, Error = Error> {
(move |req: ServiceRequest| {
ok(req.into_response(HttpResponseBuilder::new(status_code).finish()))
})
.into_service()
}
/// Initialize service from application builder instance.
///
/// ```
/// 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 resp = app.call(req).await.unwrap();
/// assert_eq!(resp.status(), StatusCode::OK);
/// }
/// ```
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.
///
/// ```
/// 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 resp = test::call_service(&app, req).await;
/// assert_eq!(resp.status(), StatusCode::OK);
/// }
/// ```
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.unwrap()
}
/// Helper function that returns a response body of a TestRequest
///
/// ```
/// 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::read_response(&app, req).await;
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
/// }
/// ```
pub async fn read_response<S, B>(app: &S, req: Request) -> Bytes
where
S: Service<Request, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
let resp = app
.call(req)
.await
.unwrap_or_else(|e| panic!("read_response failed at application call: {}", e));
let body = resp.into_body();
let mut bytes = BytesMut::new();
actix_rt::pin!(body);
while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
bytes.extend_from_slice(&item.map_err(Into::into).unwrap());
}
bytes.freeze()
}
/// Helper function that returns a response body of a ServiceResponse.
///
/// ```
/// 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 resp = test::call_service(&app, req).await;
/// let result = test::read_body(resp).await;
/// assert_eq!(result, Bytes::from_static(b"welcome!"));
/// }
/// ```
pub async fn read_body<B>(res: ServiceResponse<B>) -> Bytes
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
let body = res.into_body();
let mut bytes = BytesMut::new();
actix_rt::pin!(body);
while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
bytes.extend_from_slice(&item.map_err(Into::into).unwrap());
}
bytes.freeze()
}
/// Helper function that returns a deserialized response body of a ServiceResponse.
///
/// ```
/// 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 resp = test::TestRequest::post()
/// .uri("/people")
/// .header(header::CONTENT_TYPE, "application/json")
/// .set_payload(payload)
/// .send_request(&mut app)
/// .await;
///
/// assert!(resp.status().is_success());
///
/// let result: Person = test::read_body_json(resp).await;
/// }
/// ```
pub async fn read_body_json<T, B>(res: ServiceResponse<B>) -> T
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
T: DeserializeOwned,
{
let body = read_body(res).await;
serde_json::from_slice(&body).unwrap_or_else(|e| {
panic!(
"read_response_json failed during deserialization of body: {:?}, {}",
body, e
)
})
}
pub async fn load_stream<S>(mut stream: S) -> Result<Bytes, Error>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
let mut data = BytesMut::new();
while let Some(item) = stream.next().await {
data.extend_from_slice(&item?);
}
Ok(data.freeze())
}
pub async fn load_body<B>(body: B) -> Result<Bytes, Error>
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
body::to_bytes(body).await.map_err(Into::into)
}
/// Helper function that returns a deserialized response body of a TestRequest
///
/// ```
/// 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::read_response_json(&mut app, req).await;
/// }
/// ```
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 + Unpin,
B::Error: Into<Error>,
T: DeserializeOwned,
{
let body = read_response(app, req).await;
serde_json::from_slice(&body).unwrap_or_else(|_| {
panic!(
"read_response_json failed during deserialization of body: {:?}",
body
)
})
}
/// 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<H>(mut self, header: H) -> Self
where
H: IntoHeaderPair,
{
self.req.insert_header(header);
self
}
/// Append a header, keeping any that were set with an equivalent field name.
pub fn append_header<H>(mut self, header: H) -> Self
where
H: IntoHeaderPair,
{
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<B: Into<Bytes>>(mut self, data: B) -> 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<T: Serialize>(mut self, data: &T) -> 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<T: Serialize>(mut self, data: &T) -> 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();
call_service(app, req).await
}
#[cfg(test)]
pub fn set_server_hostname(&mut self, host: &str) {
self.config.set_host(host)
}
}
/// Reduces boilerplate code when testing expected response payloads.
#[cfg(test)]
macro_rules! assert_body_eq {
($res:ident, $expected:expr) => {
assert_eq!(
::actix_http::body::to_bytes($res.into_body())
.await
.expect("body read should have succeeded"),
Bytes::from_static($expected),
)
};
}
#[cfg(test)]
pub(crate) use assert_body_eq;
#[cfg(test)]
mod tests {
use std::time::SystemTime;
use actix_http::HttpMessage;
use serde::{Deserialize, Serialize};
use super::*;
use crate::{http::header, web, App, 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_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 = read_response(&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 = read_response(&app, patch_req).await;
assert_eq!(result, Bytes::from_static(b"patch!"));
let delete_req = TestRequest::delete().uri("/index.html").to_request();
let result = read_response(&app, delete_req).await;
assert_eq!(result, Bytes::from_static(b"delete!"));
}
#[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 = read_response(&app, req).await;
assert_eq!(result, Bytes::from_static(b"welcome!"));
}
#[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 = read_body(resp).await;
assert_eq!(result, Bytes::from_static(b"welcome!"));
}
#[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 = read_response_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 resp = TestRequest::post()
.uri("/people")
.insert_header((header::CONTENT_TYPE, "application/json"))
.set_payload(payload)
.send_request(&app)
.await;
let result: Person = read_body_json(resp).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 = read_response_json(&app, req).await;
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[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 = read_response_json(&app, req).await;
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[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());
}
}

81
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");
}
}

431
src/test/test_request.rs Normal file
View File

@ -0,0 +1,431 @@
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<B: Into<Bytes>>(mut self, data: B) -> 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<T: Serialize>(mut self, data: &T) -> 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<T: Serialize>(mut self, data: &T) -> 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());
}
}

31
src/test/test_services.rs Normal file
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)
}

474
src/test/test_utils.rs Normal file
View File

@ -0,0 +1,474 @@
use std::fmt;
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,
B::Error: fmt::Debug,
{
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,
B::Error: fmt::Debug,
{
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,
B::Error: fmt::Debug,
{
let body = res.into_body();
body::to_bytes(body)
.await
.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,
B::Error: fmt::Debug,
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,
B::Error: fmt::Debug,
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,
B::Error: fmt::Debug,
T: DeserializeOwned,
{
call_and_read_body_json(app, req).await
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use super::*;
use crate::{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");
}
}

View File

@ -12,15 +12,14 @@ use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body, dev, body::EitherBody,
dev,
web::{Form, Json}, web::{Form, Json},
Error, FromRequest, HttpRequest, HttpResponse, Responder, Error, FromRequest, HttpRequest, HttpResponse, Responder,
}; };
/// Combines two extractor or responder types into a single type. /// Combines two extractor or responder types into a single type.
/// ///
/// Can be converted to and from an [`either::Either`].
///
/// # Extractor /// # Extractor
/// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for /// 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. /// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
@ -101,24 +100,6 @@ impl<T> Either<Json<T>, Form<T>> {
} }
} }
impl<L, R> From<either::Either<L, R>> for Either<L, R> {
fn from(val: either::Either<L, R>) -> Self {
match val {
either::Either::Left(l) => Either::Left(l),
either::Either::Right(r) => Either::Right(r),
}
}
}
impl<L, R> From<Either<L, R>> for either::Either<L, R> {
fn from(val: Either<L, R>) -> Self {
match val {
Either::Left(l) => either::Either::Left(l),
Either::Right(r) => either::Either::Right(r),
}
}
}
#[cfg(test)] #[cfg(test)]
impl<L, R> Either<L, R> { impl<L, R> Either<L, R> {
pub(self) fn unwrap_left(self) -> L { pub(self) fn unwrap_left(self) -> L {
@ -146,7 +127,7 @@ where
L: Responder, L: Responder,
R: Responder, R: Responder,
{ {
type Body = body::EitherBody<L::Body, R::Body>; type Body = EitherBody<L::Body, R::Body>;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> { fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
match self { match self {
@ -165,7 +146,7 @@ pub enum EitherExtractError<L, R> {
/// Error from payload buffering, such as exceeding payload max size limit. /// Error from payload buffering, such as exceeding payload max size limit.
Bytes(Error), Bytes(Error),
/// Error from primary extractor. /// Error from primary and fallback extractors.
Extract(L, R), Extract(L, R),
} }

View File

@ -449,12 +449,13 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
body,
error::InternalError, error::InternalError,
http::{ http::{
header::{self, CONTENT_LENGTH, CONTENT_TYPE}, header::{self, CONTENT_LENGTH, CONTENT_TYPE},
StatusCode, StatusCode,
}, },
test::{assert_body_eq, load_body, TestRequest}, test::{assert_body_eq, TestRequest},
}; };
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug)]
@ -517,7 +518,7 @@ mod tests {
let resp = HttpResponse::from_error(s.err().unwrap()); let resp = HttpResponse::from_error(s.err().unwrap());
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let body = load_body(resp.into_body()).await.unwrap(); let body = body::to_bytes(resp.into_body()).await.unwrap();
let msg: MyObject = serde_json::from_slice(&body).unwrap(); let msg: MyObject = serde_json::from_slice(&body).unwrap();
assert_eq!(msg.name, "invalid request"); assert_eq!(msg.name, "invalid request");
} }

View File

@ -8,7 +8,7 @@ pub use bytes::{Buf, BufMut, Bytes, BytesMut};
use crate::{ use crate::{
body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler,
resource::Resource, responder::Responder, route::Route, scope::Scope, service::WebService, resource::Resource, route::Route, scope::Scope, service::WebService, Responder,
}; };
pub use crate::config::ServiceConfig; pub use crate::config::ServiceConfig;

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