From b41b346c00260a164731bf484f930d492be61f67 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 11 Dec 2021 16:05:08 +0000
Subject: [PATCH 01/28] inline trivial body methods

---
 actix-http/src/body/body_stream.rs  |  2 ++
 actix-http/src/body/either.rs       |  7 +++++++
 actix-http/src/body/message_body.rs | 24 ++++++++++++++++++++++--
 actix-http/src/body/size.rs         | 11 ++++++++---
 actix-http/src/body/sized_stream.rs |  2 ++
 src/error/mod.rs                    |  1 +
 6 files changed, 42 insertions(+), 5 deletions(-)

diff --git a/actix-http/src/body/body_stream.rs b/actix-http/src/body/body_stream.rs
index 232d01590..cf4f488b2 100644
--- a/actix-http/src/body/body_stream.rs
+++ b/actix-http/src/body/body_stream.rs
@@ -27,6 +27,7 @@ where
     S: Stream<Item = Result<Bytes, E>>,
     E: Into<Box<dyn StdError>> + 'static,
 {
+    #[inline]
     pub fn new(stream: S) -> Self {
         BodyStream { stream }
     }
@@ -39,6 +40,7 @@ where
 {
     type Error = E;
 
+    #[inline]
     fn size(&self) -> BodySize {
         BodySize::Stream
     }
diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs
index 6135d834d..103b39c5d 100644
--- a/actix-http/src/body/either.rs
+++ b/actix-http/src/body/either.rs
@@ -23,6 +23,7 @@ pin_project! {
 
 impl<L> EitherBody<L, BoxBody> {
     /// Creates new `EitherBody` using left variant and boxed right variant.
+    #[inline]
     pub fn new(body: L) -> Self {
         Self::Left { body }
     }
@@ -30,11 +31,13 @@ impl<L> EitherBody<L, BoxBody> {
 
 impl<L, R> EitherBody<L, R> {
     /// Creates new `EitherBody` using left variant.
+    #[inline]
     pub fn left(body: L) -> Self {
         Self::Left { body }
     }
 
     /// Creates new `EitherBody` using right variant.
+    #[inline]
     pub fn right(body: R) -> Self {
         Self::Right { body }
     }
@@ -47,6 +50,7 @@ where
 {
     type Error = Error;
 
+    #[inline]
     fn size(&self) -> BodySize {
         match self {
             EitherBody::Left { body } => body.size(),
@@ -54,6 +58,7 @@ where
         }
     }
 
+    #[inline]
     fn poll_next(
         self: Pin<&mut Self>,
         cx: &mut Context<'_>,
@@ -68,6 +73,7 @@ where
         }
     }
 
+    #[inline]
     fn is_complete_body(&self) -> bool {
         match self {
             EitherBody::Left { body } => body.is_complete_body(),
@@ -75,6 +81,7 @@ where
         }
     }
 
+    #[inline]
     fn take_complete_body(&mut self) -> Bytes {
         match self {
             EitherBody::Left { body } => body.take_complete_body(),
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index e4020d2af..3e6c8d5cb 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -85,12 +85,10 @@ mod foreign_impls {
     impl MessageBody for Infallible {
         type Error = Infallible;
 
-        #[inline]
         fn size(&self) -> BodySize {
             match *self {}
         }
 
-        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -219,6 +217,7 @@ mod foreign_impls {
     impl MessageBody for &'static [u8] {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -234,10 +233,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             Bytes::from_static(mem::take(self))
         }
@@ -246,6 +247,7 @@ mod foreign_impls {
     impl MessageBody for Bytes {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -261,10 +263,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             mem::take(self)
         }
@@ -273,6 +277,7 @@ mod foreign_impls {
     impl MessageBody for BytesMut {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -288,10 +293,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             mem::take(self).freeze()
         }
@@ -300,6 +307,7 @@ mod foreign_impls {
     impl MessageBody for Vec<u8> {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -315,10 +323,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             Bytes::from(mem::take(self))
         }
@@ -327,6 +337,7 @@ mod foreign_impls {
     impl MessageBody for &'static str {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -344,10 +355,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             Bytes::from_static(mem::take(self).as_bytes())
         }
@@ -356,6 +369,7 @@ mod foreign_impls {
     impl MessageBody for String {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -372,10 +386,12 @@ mod foreign_impls {
             }
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             Bytes::from(mem::take(self))
         }
@@ -384,6 +400,7 @@ mod foreign_impls {
     impl MessageBody for bytestring::ByteString {
         type Error = Infallible;
 
+        #[inline]
         fn size(&self) -> BodySize {
             BodySize::Sized(self.len() as u64)
         }
@@ -396,10 +413,12 @@ mod foreign_impls {
             Poll::Ready(Some(Ok(string.into_bytes())))
         }
 
+        #[inline]
         fn is_complete_body(&self) -> bool {
             true
         }
 
+        #[inline]
         fn take_complete_body(&mut self) -> Bytes {
             mem::take(self).into_bytes()
         }
@@ -435,6 +454,7 @@ where
 {
     type Error = E;
 
+    #[inline]
     fn size(&self) -> BodySize {
         self.body.size()
     }
diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs
index d64af9d44..ec7873ca5 100644
--- a/actix-http/src/body/size.rs
+++ b/actix-http/src/body/size.rs
@@ -1,9 +1,11 @@
 /// Body size hint.
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
 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,
 
     /// Known size body.
@@ -18,6 +20,9 @@ pub enum 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.
     ///
     /// Streams will return false because it cannot be known without reading the stream.
diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs
index c8606897d..9c1727246 100644
--- a/actix-http/src/body/sized_stream.rs
+++ b/actix-http/src/body/sized_stream.rs
@@ -27,6 +27,7 @@ where
     S: Stream<Item = Result<Bytes, E>>,
     E: Into<Box<dyn StdError>> + 'static,
 {
+    #[inline]
     pub fn new(size: u64, stream: S) -> Self {
         SizedStream { size, stream }
     }
@@ -41,6 +42,7 @@ where
 {
     type Error = E;
 
+    #[inline]
     fn size(&self) -> BodySize {
         BodySize::Sized(self.size as u64)
     }
diff --git a/src/error/mod.rs b/src/error/mod.rs
index 4877358a4..64df9f553 100644
--- a/src/error/mod.rs
+++ b/src/error/mod.rs
@@ -1,4 +1,5 @@
 //! Error and Result module
+
 // This is meant to be a glob import of the whole error module except for `Error`. Rustdoc can't yet
 // correctly resolve the conflicting `Error` type defined in this module, so these re-exports are
 // expanded manually.

From cea44be67059a5270a892566323e9055809676de Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 11 Dec 2021 16:18:28 +0000
Subject: [PATCH 02/28] add test for returning App from function

---
 src/app.rs           | 21 +++++++++++++++++++++
 tests/test_server.rs | 13 +++++++------
 2 files changed, 28 insertions(+), 6 deletions(-)

diff --git a/src/app.rs b/src/app.rs
index ab2081c18..5323cb33a 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -706,4 +706,25 @@ mod tests {
         let body = read_body(resp).await;
         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();
+    }
 }
diff --git a/tests/test_server.rs b/tests/test_server.rs
index 51a78eb28..9b7ef6e1b 100644
--- a/tests/test_server.rs
+++ b/tests/test_server.rs
@@ -10,8 +10,13 @@ use std::{
     task::{Context, Poll},
 };
 
-use actix_http::header::{
-    ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING,
+use actix_web::{
+    dev::BodyEncoding,
+    http::header::{
+        ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING,
+    },
+    middleware::{Compress, NormalizePath, TrailingSlash},
+    web, App, Error, HttpResponse,
 };
 use brotli2::write::{BrotliDecoder, BrotliEncoder};
 use bytes::Bytes;
@@ -31,10 +36,6 @@ use openssl::{
 use rand::{distributions::Alphanumeric, Rng};
 use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder};
 
-use actix_web::dev::BodyEncoding;
-use actix_web::middleware::{Compress, NormalizePath, TrailingSlash};
-use actix_web::{web, App, Error, HttpResponse};
-
 const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
                    Hello World Hello World Hello World Hello World Hello World \
                    Hello World Hello World Hello World Hello World Hello World \

From 551a0d973cf50ff9a22665b0ca7b7f195b997ebf Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 13 Dec 2021 02:58:19 +0000
Subject: [PATCH 03/28] doc tweaks

---
 actix-http/src/payload.rs | 4 ++--
 src/app_service.rs        | 1 +
 src/request.rs            | 4 ++--
 3 files changed, 5 insertions(+), 4 deletions(-)

diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs
index 85bfc0b5a..5734af341 100644
--- a/actix-http/src/payload.rs
+++ b/actix-http/src/payload.rs
@@ -7,10 +7,10 @@ use h2::RecvStream;
 
 use crate::error::PayloadError;
 
-/// Type represent boxed payload
+/// A boxed payload.
 pub type PayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
 
-/// Type represent streaming payload
+/// A streaming payload.
 pub enum Payload<S = PayloadStream> {
     None,
     H1(crate::h1::Payload),
diff --git a/src/app_service.rs b/src/app_service.rs
index cc5100f04..515693db4 100644
--- a/src/app_service.rs
+++ b/src/app_service.rs
@@ -22,6 +22,7 @@ use crate::{
 type Guards = Vec<Box<dyn Guard>>;
 
 /// Service factory to convert `Request` to a `ServiceRequest<S>`.
+///
 /// It also executes data factories.
 pub struct AppInit<T, B>
 where
diff --git a/src/request.rs b/src/request.rs
index d84722d95..07fb4eb2d 100644
--- a/src/request.rs
+++ b/src/request.rs
@@ -349,7 +349,7 @@ impl Drop for HttpRequest {
     fn drop(&mut self) {
         // 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 inner.app_state.pool().is_available() {
                 // 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();
 
                 // 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);
             }
         }

From 11ee8ec3ab22da345b448c4cf2d8e51781815873 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Mon, 13 Dec 2021 16:08:08 +0000
Subject: [PATCH 04/28] align remaining header map terminology (#2510)

---
 CHANGES.md                                    |  11 +
 actix-files/src/named.rs                      |  62 ++---
 actix-http/CHANGES.md                         |  12 +-
 actix-http/src/h2/service.rs                  |  15 +-
 actix-http/src/header/as_name.rs              |   5 +
 actix-http/src/header/into_pair.rs            |  56 ++--
 actix-http/src/header/into_value.rs           |  30 +--
 actix-http/src/header/map.rs                  |   4 +-
 actix-http/src/header/mod.rs                  |   6 +-
 .../src/header/shared/content_encoding.rs     |   4 +-
 actix-http/src/header/shared/http_date.rs     |   5 +-
 actix-http/src/response.rs                    |   2 +-
 actix-http/src/response_builder.rs            |  20 +-
 actix-http/src/test.rs                        |  16 +-
 awc/CHANGES.md                                |   5 +-
 awc/src/builder.rs                            |  83 +++---
 awc/src/client/h1proto.rs                     |   2 +-
 awc/src/frozen.rs                             |   6 +-
 awc/src/lib.rs                                |  10 +-
 awc/src/middleware/redirect.rs                |   8 +-
 awc/src/request.rs                            |  27 +-
 awc/src/sender.rs                             |   4 +-
 awc/src/test.rs                               |  21 +-
 awc/src/ws.rs                                 |  10 +-
 examples/basic.rs                             |   4 +-
 examples/uds.rs                               |   4 +-
 src/app.rs                                    |   4 +-
 src/error/internal.rs                         |   2 +-
 src/error/response_error.rs                   |   2 +-
 src/http/header/content_disposition.rs        |   4 +-
 src/http/header/content_range.rs              |   4 +-
 src/http/header/entity.rs                     |   4 +-
 src/http/header/if_range.rs                   |   6 +-
 src/http/header/macros.rs                     |   8 +-
 src/http/header/range.rs                      |   4 +-
 src/lib.rs                                    |   7 +-
 src/middleware/default_headers.rs             | 127 +++++----
 src/middleware/mod.rs                         |   4 +-
 src/request_data.rs                           |   2 +-
 src/resource.rs                               |   5 +-
 src/response/builder.rs                       |  24 +-
 src/response/customize_responder.rs           | 245 ++++++++++++++++++
 src/response/mod.rs                           |   4 +
 src/{ => response}/responder.rs               | 205 ++-------------
 src/scope.rs                                  |   2 +-
 src/test.rs                                   |  14 +-
 src/web.rs                                    |   2 +-
 47 files changed, 608 insertions(+), 503 deletions(-)
 create mode 100644 src/response/customize_responder.rs
 rename src/{ => response}/responder.rs (63%)

diff --git a/CHANGES.md b/CHANGES.md
index 3e0b12d9e..2df820027 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,17 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+### 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]
+
+### Removed
+* Top-level `EitherExtractError` export. [#2510]
+
+[#2510]: https://github.com/actix/actix-web/pull/2510
 
 
 ## 4.0.0-beta.14 - 2021-12-11
diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs
index 0848543a8..810988f0c 100644
--- a/actix-files/src/named.rs
+++ b/actix-files/src/named.rs
@@ -2,14 +2,10 @@ use std::{
     fmt,
     fs::Metadata,
     io,
-    ops::{Deref, DerefMut},
     path::{Path, PathBuf},
     time::{SystemTime, UNIX_EPOCH},
 };
 
-#[cfg(unix)]
-use std::os::unix::fs::MetadataExt;
-
 use actix_service::{Service, ServiceFactory};
 use actix_web::{
     body::{self, BoxBody, SizedStream},
@@ -27,6 +23,7 @@ use actix_web::{
     Error, HttpMessage, HttpRequest, HttpResponse, Responder,
 };
 use bitflags::bitflags;
+use derive_more::{Deref, DerefMut};
 use futures_core::future::LocalBoxFuture;
 use mime_guess::from_path;
 
@@ -71,8 +68,11 @@ impl Default for Flags {
 ///     NamedFile::open_async("./static/index.html").await
 /// }
 /// ```
+#[derive(Deref, DerefMut)]
 pub struct NamedFile {
     path: PathBuf,
+    #[deref]
+    #[deref_mut]
     file: File,
     modified: Option<SystemTime>,
     pub(crate) md: Metadata,
@@ -364,14 +364,18 @@ impl NamedFile {
         self
     }
 
+    /// Creates a etag in a format is similar to Apache's.
     pub(crate) fn etag(&self) -> Option<header::EntityTag> {
-        // This etag format is similar to Apache's.
         self.modified.as_ref().map(|mtime| {
             let ino = {
                 #[cfg(unix)]
                 {
+                    #[cfg(unix)]
+                    use std::os::unix::fs::MetadataExt as _;
+
                     self.md.ino()
                 }
+
                 #[cfg(not(unix))]
                 {
                     0
@@ -472,17 +476,17 @@ impl NamedFile {
             false
         };
 
-        let mut resp = HttpResponse::build(self.status_code);
+        let mut res = HttpResponse::build(self.status_code);
 
         if self.flags.contains(Flags::PREFER_UTF8) {
             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 {
-            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) {
-            resp.insert_header((
+            res.insert_header((
                 header::CONTENT_DISPOSITION,
                 self.content_disposition.to_string(),
             ));
@@ -490,18 +494,18 @@ impl NamedFile {
 
         // default compressing
         if let Some(current_encoding) = self.encoding {
-            resp.encoding(current_encoding);
+            res.encoding(current_encoding);
         }
 
         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 {
-            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 offset = 0;
@@ -513,24 +517,24 @@ impl NamedFile {
                     length = ranges[0].length;
                     offset = ranges[0].start;
 
-                    resp.encoding(ContentEncoding::Identity);
-                    resp.insert_header((
+                    res.encoding(ContentEncoding::Identity);
+                    res.insert_header((
                         header::CONTENT_RANGE,
                         format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
                     ));
                 } else {
-                    resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
-                    return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
+                    res.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
+                    return res.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
                 };
             } else {
-                return resp.status(StatusCode::BAD_REQUEST).finish();
+                return res.status(StatusCode::BAD_REQUEST).finish();
             };
         };
 
         if precondition_failed {
-            return resp.status(StatusCode::PRECONDITION_FAILED).finish();
+            return res.status(StatusCode::PRECONDITION_FAILED).finish();
         } else if not_modified {
-            return resp
+            return res
                 .status(StatusCode::NOT_MODIFIED)
                 .body(body::None::new())
                 .map_into_boxed_body();
@@ -539,10 +543,10 @@ impl NamedFile {
         let reader = chunked::new_chunked_read(length, offset, self.file);
 
         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 {
     type Body = BoxBody;
 
diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index fe47902f7..011e2c608 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,6 +1,12 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+### Changed
+* Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510]
+* Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510]
+* Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510]
+
+[#2510]: https://github.com/actix/actix-web/pull/2510
 
 
 ## 3.0.0-beta.15 - 2021-12-11
@@ -260,7 +266,7 @@
 
 ## 3.0.0-beta.2 - 2021-02-10
 ### 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::append_header` method which allows using typed headers. [#1869]
 * `TestRequest::insert_header` method which allows using typed headers. [#1869]
@@ -271,9 +277,9 @@
 * `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
 
 ### 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]
-* 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]
 * `Extensions::insert` returns Option of replaced item. [#1904]
 * Remove `HttpResponseBuilder::json2()`. [#1903]
diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs
index f5821370a..469648054 100644
--- a/actix-http/src/h2/service.rs
+++ b/actix-http/src/h2/service.rs
@@ -270,10 +270,10 @@ where
     type Future = H2ServiceHandlerResponse<T, S, B>;
 
     fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        self.flow.service.poll_ready(cx).map_err(|e| {
-            let e = e.into();
-            error!("Service readiness error: {:?}", e);
-            DispatchError::Service(e)
+        self.flow.service.poll_ready(cx).map_err(|err| {
+            let err = err.into();
+            error!("Service readiness error: {:?}", err);
+            DispatchError::Service(err)
         })
     }
 
@@ -297,7 +297,6 @@ where
     T: AsyncRead + AsyncWrite + Unpin,
     S::Future: 'static,
 {
-    Incoming(Dispatcher<T, S, B, (), ()>),
     Handshake(
         Option<Rc<HttpFlow<S, (), ()>>>,
         Option<ServiceConfig>,
@@ -305,6 +304,7 @@ where
         OnConnectData,
         HandshakeWithTimeout<T>,
     ),
+    Established(Dispatcher<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> {
         match self.state {
-            State::Incoming(ref mut disp) => Pin::new(disp).poll(cx),
             State::Handshake(
                 ref mut srv,
                 ref mut config,
@@ -343,7 +342,7 @@ where
                 Ok((conn, timer)) => {
                     let on_connect_data = mem::take(conn_data);
 
-                    self.state = State::Incoming(Dispatcher::new(
+                    self.state = State::Established(Dispatcher::new(
                         conn,
                         srv.take().unwrap(),
                         config.take().unwrap(),
@@ -360,6 +359,8 @@ where
                     Poll::Ready(Err(err))
                 }
             },
+
+            State::Established(ref mut disp) => Pin::new(disp).poll(cx),
         }
     }
 }
diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs
index 17d007f2f..a895010b1 100644
--- a/actix-http/src/header/as_name.rs
+++ b/actix-http/src/header/as_name.rs
@@ -16,6 +16,7 @@ pub trait Sealed {
 }
 
 impl Sealed for HeaderName {
+    #[inline]
     fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
         Ok(Cow::Borrowed(self))
     }
@@ -23,6 +24,7 @@ impl Sealed for HeaderName {
 impl AsHeaderName for HeaderName {}
 
 impl Sealed for &HeaderName {
+    #[inline]
     fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
         Ok(Cow::Borrowed(*self))
     }
@@ -30,6 +32,7 @@ impl Sealed for &HeaderName {
 impl AsHeaderName for &HeaderName {}
 
 impl Sealed for &str {
+    #[inline]
     fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
         HeaderName::from_str(self).map(Cow::Owned)
     }
@@ -37,6 +40,7 @@ impl Sealed for &str {
 impl AsHeaderName for &str {}
 
 impl Sealed for String {
+    #[inline]
     fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
         HeaderName::from_str(self).map(Cow::Owned)
     }
@@ -44,6 +48,7 @@ impl Sealed for String {
 impl AsHeaderName for String {}
 
 impl Sealed for &String {
+    #[inline]
     fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
         HeaderName::from_str(self).map(Cow::Owned)
     }
diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs
index b4250e06e..91c3e6640 100644
--- a/actix-http/src/header/into_pair.rs
+++ b/actix-http/src/header/into_pair.rs
@@ -1,22 +1,20 @@
-//! [`IntoHeaderPair`] trait and implementations.
+//! [`TryIntoHeaderPair`] trait and implementations.
 
 use std::convert::TryFrom as _;
 
-use http::{
-    header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
-    Error as HttpError, HeaderValue,
+use super::{
+    Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue,
 };
+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`].
 ///
 /// [`HeaderMap`]: super::HeaderMap
-pub trait IntoHeaderPair: Sized {
+pub trait TryIntoHeaderPair: Sized {
     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)]
@@ -34,14 +32,14 @@ impl From<InvalidHeaderPart> for HttpError {
     }
 }
 
-impl<V> IntoHeaderPair for (HeaderName, V)
+impl<V> TryIntoHeaderPair for (HeaderName, V)
 where
-    V: IntoHeaderValue,
+    V: TryIntoHeaderValue,
     V::Error: Into<InvalidHeaderValue>,
 {
     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 value = value
             .try_into_value()
@@ -50,14 +48,14 @@ where
     }
 }
 
-impl<V> IntoHeaderPair for (&HeaderName, V)
+impl<V> TryIntoHeaderPair for (&HeaderName, V)
 where
-    V: IntoHeaderValue,
+    V: TryIntoHeaderValue,
     V::Error: Into<InvalidHeaderValue>,
 {
     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 value = value
             .try_into_value()
@@ -66,14 +64,14 @@ where
     }
 }
 
-impl<V> IntoHeaderPair for (&[u8], V)
+impl<V> TryIntoHeaderPair for (&[u8], V)
 where
-    V: IntoHeaderValue,
+    V: TryIntoHeaderValue,
     V::Error: Into<InvalidHeaderValue>,
 {
     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 = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
         let value = value
@@ -83,14 +81,14 @@ where
     }
 }
 
-impl<V> IntoHeaderPair for (&str, V)
+impl<V> TryIntoHeaderPair for (&str, V)
 where
-    V: IntoHeaderValue,
+    V: TryIntoHeaderValue,
     V::Error: Into<InvalidHeaderValue>,
 {
     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 = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
         let value = value
@@ -100,23 +98,25 @@ where
     }
 }
 
-impl<V> IntoHeaderPair for (String, V)
+impl<V> TryIntoHeaderPair for (String, V)
 where
-    V: IntoHeaderValue,
+    V: TryIntoHeaderValue,
     V::Error: Into<InvalidHeaderValue>,
 {
     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;
-        (name.as_str(), value).try_into_header_pair()
+        (name.as_str(), value).try_into_pair()
     }
 }
 
-impl<T: Header> IntoHeaderPair for T {
-    type Error = <T as IntoHeaderValue>::Error;
+impl<T: Header> TryIntoHeaderPair for T {
+    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()?))
     }
 }
diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs
index bad05db64..6d369ee65 100644
--- a/actix-http/src/header/into_value.rs
+++ b/actix-http/src/header/into_value.rs
@@ -1,4 +1,4 @@
-//! [`IntoHeaderValue`] trait and implementations.
+//! [`TryIntoHeaderValue`] trait and implementations.
 
 use std::convert::TryFrom as _;
 
@@ -7,7 +7,7 @@ use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
 use mime::Mime;
 
 /// 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.
     type Error: Into<HttpError>;
 
@@ -15,7 +15,7 @@ pub trait IntoHeaderValue: Sized {
     fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
 }
 
-impl IntoHeaderValue for HeaderValue {
+impl TryIntoHeaderValue for HeaderValue {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -24,7 +24,7 @@ impl IntoHeaderValue for HeaderValue {
     }
 }
 
-impl IntoHeaderValue for &HeaderValue {
+impl TryIntoHeaderValue for &HeaderValue {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -33,7 +33,7 @@ impl IntoHeaderValue for &HeaderValue {
     }
 }
 
-impl IntoHeaderValue for &str {
+impl TryIntoHeaderValue for &str {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -42,7 +42,7 @@ impl IntoHeaderValue for &str {
     }
 }
 
-impl IntoHeaderValue for &[u8] {
+impl TryIntoHeaderValue for &[u8] {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -51,7 +51,7 @@ impl IntoHeaderValue for &[u8] {
     }
 }
 
-impl IntoHeaderValue for Bytes {
+impl TryIntoHeaderValue for Bytes {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -60,7 +60,7 @@ impl IntoHeaderValue for Bytes {
     }
 }
 
-impl IntoHeaderValue for Vec<u8> {
+impl TryIntoHeaderValue for Vec<u8> {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -69,7 +69,7 @@ impl IntoHeaderValue for Vec<u8> {
     }
 }
 
-impl IntoHeaderValue for String {
+impl TryIntoHeaderValue for String {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -78,7 +78,7 @@ impl IntoHeaderValue for String {
     }
 }
 
-impl IntoHeaderValue for usize {
+impl TryIntoHeaderValue for usize {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -87,7 +87,7 @@ impl IntoHeaderValue for usize {
     }
 }
 
-impl IntoHeaderValue for i64 {
+impl TryIntoHeaderValue for i64 {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -96,7 +96,7 @@ impl IntoHeaderValue for i64 {
     }
 }
 
-impl IntoHeaderValue for u64 {
+impl TryIntoHeaderValue for u64 {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -105,7 +105,7 @@ impl IntoHeaderValue for u64 {
     }
 }
 
-impl IntoHeaderValue for i32 {
+impl TryIntoHeaderValue for i32 {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -114,7 +114,7 @@ impl IntoHeaderValue for i32 {
     }
 }
 
-impl IntoHeaderValue for u32 {
+impl TryIntoHeaderValue for u32 {
     type Error = InvalidHeaderValue;
 
     #[inline]
@@ -123,7 +123,7 @@ impl IntoHeaderValue for u32 {
     }
 }
 
-impl IntoHeaderValue for Mime {
+impl TryIntoHeaderValue for Mime {
     type Error = InvalidHeaderValue;
 
     #[inline]
diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs
index 12c8f9462..748410375 100644
--- a/actix-http/src/header/map.rs
+++ b/actix-http/src/header/map.rs
@@ -333,7 +333,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
     /// previous values are removed and returned as a `Removed` iterator. The key is not updated;
@@ -372,7 +372,7 @@ impl HeaderMap {
         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
     /// currently associated with the key. The key is not updated; this matters for types that can
diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs
index 5fe76381b..dd4f06106 100644
--- a/actix-http/src/header/mod.rs
+++ b/actix-http/src/header/mod.rs
@@ -37,8 +37,8 @@ mod shared;
 mod utils;
 
 pub use self::as_name::AsHeaderName;
-pub use self::into_pair::IntoHeaderPair;
-pub use self::into_value::IntoHeaderValue;
+pub use self::into_pair::TryIntoHeaderPair;
+pub use self::into_value::TryIntoHeaderValue;
 pub use self::map::HeaderMap;
 pub use self::shared::{
     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.
-pub trait Header: IntoHeaderValue {
+pub trait Header: TryIntoHeaderValue {
     /// Returns the name of the header field
     fn name() -> HeaderName;
 
diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs
index 073d90dce..a6e52138d 100644
--- a/actix-http/src/header/shared/content_encoding.rs
+++ b/actix-http/src/header/shared/content_encoding.rs
@@ -5,7 +5,7 @@ use http::header::InvalidHeaderValue;
 
 use crate::{
     error::ParseError,
-    header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue},
+    header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue},
     HttpMessage,
 };
 
@@ -96,7 +96,7 @@ impl TryFrom<&str> for ContentEncoding {
     }
 }
 
-impl IntoHeaderValue for ContentEncoding {
+impl TryIntoHeaderValue for ContentEncoding {
     type Error = InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs
index 228f6f00e..473d6cad0 100644
--- a/actix-http/src/header/shared/http_date.rs
+++ b/actix-http/src/header/shared/http_date.rs
@@ -4,7 +4,8 @@ use bytes::BytesMut;
 use http::header::{HeaderValue, InvalidHeaderValue};
 
 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.
@@ -29,7 +30,7 @@ impl fmt::Display for HttpDate {
     }
 }
 
-impl IntoHeaderValue for HttpDate {
+impl TryIntoHeaderValue for HttpDate {
     type Error = InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs
index 861cab2cb..9f799f669 100644
--- a/actix-http/src/response.rs
+++ b/actix-http/src/response.rs
@@ -11,7 +11,7 @@ use bytestring::ByteString;
 use crate::{
     body::{BoxBody, MessageBody},
     extensions::Extensions,
-    header::{self, HeaderMap, IntoHeaderValue},
+    header::{self, HeaderMap, TryIntoHeaderValue},
     message::{BoxedResponseHead, ResponseHead},
     Error, ResponseBuilder, StatusCode,
 };
diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs
index dfc2612fb..adbe86fca 100644
--- a/actix-http/src/response_builder.rs
+++ b/actix-http/src/response_builder.rs
@@ -8,7 +8,7 @@ use std::{
 use crate::{
     body::{EitherBody, MessageBody},
     error::{Error, HttpError},
-    header::{self, IntoHeaderPair, IntoHeaderValue},
+    header::{self, TryIntoHeaderPair, TryIntoHeaderValue},
     message::{BoxedResponseHead, ConnectionType, ResponseHead},
     Extensions, Response, StatusCode,
 };
@@ -90,12 +90,9 @@ impl ResponseBuilder {
     /// assert!(res.headers().contains_key("content-type"));
     /// assert!(res.headers().contains_key("x-test"));
     /// ```
-    pub fn insert_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
         if let Some(parts) = self.inner() {
-            match header.try_into_header_pair() {
+            match header.try_into_pair() {
                 Ok((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("x-test").count(), 2);
     /// ```
-    pub fn append_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
         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),
                 Err(e) => self.err = Some(e.into()),
             };
@@ -157,7 +151,7 @@ impl ResponseBuilder {
     #[inline]
     pub fn upgrade<V>(&mut self, value: V) -> &mut Self
     where
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if let Some(parts) = self.inner() {
             parts.set_connection_type(ConnectionType::Upgrade);
@@ -195,7 +189,7 @@ impl ResponseBuilder {
     #[inline]
     pub fn content_type<V>(&mut self, value: V) -> &mut Self
     where
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if let Some(parts) = self.inner() {
             match value.try_into_value() {
diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs
index ec781743d..7e26ee865 100644
--- a/actix-http/src/test.rs
+++ b/actix-http/src/test.rs
@@ -14,7 +14,7 @@ use bytes::{Bytes, BytesMut};
 use http::{Method, Uri, Version};
 
 use crate::{
-    header::{HeaderMap, IntoHeaderPair},
+    header::{HeaderMap, TryIntoHeaderPair},
     payload::Payload,
     Request,
 };
@@ -92,11 +92,8 @@ impl TestRequest {
     }
 
     /// Insert a header, replacing any that were set with an equivalent field name.
-    pub fn insert_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
-        match header.try_into_header_pair() {
+    pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
+        match header.try_into_pair() {
             Ok((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.
-    pub fn append_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
-        match header.try_into_header_pair() {
+    pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
+        match header.try_into_pair() {
             Ok((key, value)) => {
                 parts(&mut self.0).headers.append(key, value);
             }
diff --git a/awc/CHANGES.md b/awc/CHANGES.md
index 798b2ce6b..8a3fea46a 100644
--- a/awc/CHANGES.md
+++ b/awc/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+* 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
@@ -60,7 +63,7 @@
 * `ConnectorService` type is renamed to `BoxConnectorService`. [#2081]
 * Fix http/https encoding when enabling `compress` feature. [#2116]
 * 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
 [#2094]: https://github.com/actix/actix-web/pull/2094
diff --git a/awc/src/builder.rs b/awc/src/builder.rs
index 43e5c0def..30f203bb8 100644
--- a/awc/src/builder.rs
+++ b/awc/src/builder.rs
@@ -2,7 +2,7 @@ use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
 
 use actix_http::{
     error::HttpError,
-    header::{self, HeaderMap, HeaderName},
+    header::{self, HeaderMap, HeaderName, TryIntoHeaderPair},
     Uri,
 };
 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
 /// builder-like pattern.
 pub struct ClientBuilder<S = (), M = ()> {
-    default_headers: bool,
     max_http_version: Option<http::Version>,
     stream_window_size: Option<u32>,
     conn_window_size: Option<u32>,
-    headers: HeaderMap,
+    fundamental_headers: bool,
+    default_headers: HeaderMap,
     timeout: Option<Duration>,
     connector: Connector<S>,
     middleware: M,
@@ -44,15 +44,15 @@ impl 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,
             stream_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,
         }
     }
@@ -78,8 +78,8 @@ where
     {
         ClientBuilder {
             middleware: self.middleware,
+            fundamental_headers: self.fundamental_headers,
             default_headers: self.default_headers,
-            headers: self.headers,
             timeout: self.timeout,
             local_address: self.local_address,
             connector,
@@ -153,30 +153,46 @@ where
         self
     }
 
-    /// Do not add default request headers.
+    /// Do not add fundamental default request headers.
+    ///
     /// By default `Date` and `User-Agent` headers are set.
     pub fn no_default_headers(mut self) -> Self {
-        self.default_headers = false;
+        self.fundamental_headers = false;
         self
     }
 
-    /// Add default header. Headers added by this method
-    /// get added to every request.
+    /// Add default header.
+    ///
+    /// 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
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: fmt::Debug + Into<HttpError>,
-        V: header::IntoHeaderValue,
+        V: header::TryIntoHeaderValue,
         V::Error: fmt::Debug,
     {
         match HeaderName::try_from(key) {
             Ok(key) => match value.try_into_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
     }
@@ -190,10 +206,10 @@ where
             Some(password) => format!("{}:{}", username, password),
             None => format!("{}:", username),
         };
-        self.header(
+        self.add_default_header((
             header::AUTHORIZATION,
             format!("Basic {}", base64::encode(&auth)),
-        )
+        ))
     }
 
     /// Set client wide HTTP bearer authentication header
@@ -201,13 +217,12 @@ where
     where
         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),
-    /// that runs during inbound and/or outbound processing in the request
-    /// life-cycle (request -> response), modifying request/response as
-    /// necessary, across all requests managed by the Client.
+    /// Registers middleware, in the form of a middleware component (type), that runs during inbound
+    /// and/or outbound processing in the request life-cycle (request -> response),
+    /// modifying request/response as necessary, across all requests managed by the `Client`.
     pub fn wrap<S1, M1>(
         self,
         mw: M1,
@@ -218,11 +233,11 @@ where
     {
         ClientBuilder {
             middleware: NestTransform::new(self.middleware, mw),
-            default_headers: self.default_headers,
+            fundamental_headers: self.fundamental_headers,
             max_http_version: self.max_http_version,
             stream_window_size: self.stream_window_size,
             conn_window_size: self.conn_window_size,
-            headers: self.headers,
+            default_headers: self.default_headers,
             timeout: self.timeout,
             connector: self.connector,
             local_address: self.local_address,
@@ -237,10 +252,10 @@ where
         M::Transform:
             Service<ConnectRequest, Response = ConnectResponse, Error = SendRequestError>,
     {
-        let redirect_time = self.max_redirects;
+        let max_redirects = self.max_redirects;
 
-        if redirect_time > 0 {
-            self.wrap(Redirect::new().max_redirect_times(redirect_time))
+        if max_redirects > 0 {
+            self.wrap(Redirect::new().max_redirect_times(max_redirects))
                 ._finish()
         } else {
             self._finish()
@@ -272,7 +287,7 @@ where
         let connector = boxed::rc_service(self.middleware.new_transform(connector));
 
         Client(ClientConfig {
-            headers: Rc::new(self.headers),
+            default_headers: Rc::new(self.default_headers),
             timeout: self.timeout,
             connector,
         })
@@ -288,7 +303,7 @@ mod tests {
         let client = ClientBuilder::new().basic_auth("username", Some("password"));
         assert_eq!(
             client
-                .headers
+                .default_headers
                 .get(header::AUTHORIZATION)
                 .unwrap()
                 .to_str()
@@ -299,7 +314,7 @@ mod tests {
         let client = ClientBuilder::new().basic_auth("username", None);
         assert_eq!(
             client
-                .headers
+                .default_headers
                 .get(header::AUTHORIZATION)
                 .unwrap()
                 .to_str()
@@ -313,7 +328,7 @@ mod tests {
         let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n");
         assert_eq!(
             client
-                .headers
+                .default_headers
                 .get(header::AUTHORIZATION)
                 .unwrap()
                 .to_str()
diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs
index c8b9a3fae..1028a2178 100644
--- a/awc/src/client/h1proto.rs
+++ b/awc/src/client/h1proto.rs
@@ -9,7 +9,7 @@ use actix_http::{
     body::{BodySize, MessageBody},
     error::PayloadError,
     h1,
-    header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
+    header::{HeaderMap, TryIntoHeaderValue, EXPECT, HOST},
     Payload, RequestHeadType, ResponseHead, StatusCode,
 };
 use actix_utils::future::poll_fn;
diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs
index 7497f85c8..cd93a1d60 100644
--- a/awc/src/frozen.rs
+++ b/awc/src/frozen.rs
@@ -6,7 +6,7 @@ use serde::Serialize;
 
 use actix_http::{
     error::HttpError,
-    header::{HeaderMap, HeaderName, IntoHeaderValue},
+    header::{HeaderMap, HeaderName, TryIntoHeaderValue},
     Method, RequestHead, Uri,
 };
 
@@ -114,7 +114,7 @@ impl FrozenClientRequest {
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         self.extra_headers(HeaderMap::new())
             .extra_header(key, value)
@@ -142,7 +142,7 @@ impl FrozenSendBuilder {
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         match HeaderName::try_from(key) {
             Ok(key) => match value.try_into_value() {
diff --git a/awc/src/lib.rs b/awc/src/lib.rs
index 06fd33fac..00c559406 100644
--- a/awc/src/lib.rs
+++ b/awc/src/lib.rs
@@ -168,7 +168,7 @@ pub struct Client(ClientConfig);
 #[derive(Clone)]
 pub(crate) struct ClientConfig {
     pub(crate) connector: BoxConnectorService,
-    pub(crate) headers: Rc<HeaderMap>,
+    pub(crate) default_headers: Rc<HeaderMap>,
     pub(crate) timeout: Option<Duration>,
 }
 
@@ -204,7 +204,9 @@ impl Client {
     {
         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
@@ -297,7 +299,7 @@ impl Client {
         <Uri as TryFrom<U>>::Error: Into<HttpError>,
     {
         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
@@ -308,6 +310,6 @@ impl Client {
     /// Returns Some(&mut HeaderMap) when Client object is unique
     /// (No other clone of client exists at the same time).
     pub fn headers(&mut self) -> Option<&mut HeaderMap> {
-        Rc::get_mut(&mut self.0.headers)
+        Rc::get_mut(&mut self.0.default_headers)
     }
 }
diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs
index 0fde48074..704d2d79d 100644
--- a/awc/src/middleware/redirect.rs
+++ b/awc/src/middleware/redirect.rs
@@ -442,13 +442,15 @@ mod tests {
         });
 
         let client = ClientBuilder::new()
-            .header("custom", "value")
+            .add_default_header(("custom", "value"))
             .disable_redirects()
             .finish();
         let res = client.get(srv.url("/")).send().await.unwrap();
         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();
         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
         let client = ClientBuilder::new()
-            .header(header::AUTHORIZATION, "auth_key_value")
+            .add_default_header((header::AUTHORIZATION, "auth_key_value"))
             .finish();
         let res = client.get(srv1.url("/")).send().await.unwrap();
         assert_eq!(res.status().as_u16(), 200);
diff --git a/awc/src/request.rs b/awc/src/request.rs
index 3e1f83a82..9e37b2755 100644
--- a/awc/src/request.rs
+++ b/awc/src/request.rs
@@ -6,7 +6,7 @@ use serde::Serialize;
 
 use actix_http::{
     error::HttpError,
-    header::{self, HeaderMap, HeaderValue, IntoHeaderPair},
+    header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair},
     ConnectionType, Method, RequestHead, Uri, Version,
 };
 
@@ -147,11 +147,8 @@ impl ClientRequest {
     }
 
     /// 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,
-    {
-        match header.try_into_header_pair() {
+    pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
+        match header.try_into_pair() {
             Ok((key, value)) => {
                 self.head.headers.insert(key, value);
             }
@@ -162,11 +159,8 @@ impl ClientRequest {
     }
 
     /// Insert a header only if it is not yet set.
-    pub fn insert_header_if_none<H>(mut self, header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
-        match header.try_into_header_pair() {
+    pub fn insert_header_if_none(mut self, header: impl TryIntoHeaderPair) -> Self {
+        match header.try_into_pair() {
             Ok((key, value)) => {
                 if !self.head.headers.contains_key(&key) {
                     self.head.headers.insert(key, value);
@@ -192,11 +186,8 @@ impl ClientRequest {
     ///     .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON));
     /// # }
     /// ```
-    pub fn append_header<H>(mut self, header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
-        match header.try_into_header_pair() {
+    pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
+        match header.try_into_pair() {
             Ok((key, value)) => self.head.headers.append(key, value),
             Err(e) => self.err = Some(e.into()),
         };
@@ -588,7 +579,7 @@ mod tests {
     #[actix_rt::test]
     async fn test_client_header() {
         let req = Client::builder()
-            .header(header::CONTENT_TYPE, "111")
+            .add_default_header((header::CONTENT_TYPE, "111"))
             .finish()
             .get("/");
 
@@ -606,7 +597,7 @@ mod tests {
     #[actix_rt::test]
     async fn test_client_header_override() {
         let req = Client::builder()
-            .header(header::CONTENT_TYPE, "111")
+            .add_default_header((header::CONTENT_TYPE, "111"))
             .finish()
             .get("/")
             .insert_header((header::CONTENT_TYPE, "222"));
diff --git a/awc/src/sender.rs b/awc/src/sender.rs
index 1faf6140a..f83a70a9b 100644
--- a/awc/src/sender.rs
+++ b/awc/src/sender.rs
@@ -10,7 +10,7 @@ use std::{
 use actix_http::{
     body::BodyStream,
     error::HttpError,
-    header::{self, HeaderMap, HeaderName, IntoHeaderValue},
+    header::{self, HeaderMap, HeaderName, TryIntoHeaderValue},
     RequestHead, RequestHeadType,
 };
 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>
     where
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         match self {
             RequestSender::Owned(head) => {
diff --git a/awc/src/test.rs b/awc/src/test.rs
index 4a5c8e7ea..1b41efc93 100644
--- a/awc/src/test.rs
+++ b/awc/src/test.rs
@@ -1,6 +1,6 @@
 //! 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;
 
 #[cfg(feature = "cookies")]
@@ -28,10 +28,7 @@ impl Default for TestResponse {
 
 impl TestResponse {
     /// Create TestResponse and set header
-    pub fn with_header<H>(header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn with_header(header: impl TryIntoHeaderPair) -> Self {
         Self::default().insert_header(header)
     }
 
@@ -42,11 +39,8 @@ impl TestResponse {
     }
 
     /// Insert a header
-    pub fn insert_header<H>(mut self, header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
-        if let Ok((key, value)) = header.try_into_header_pair() {
+    pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self {
+        if let Ok((key, value)) = header.try_into_pair() {
             self.head.headers.insert(key, value);
             return self;
         }
@@ -54,11 +48,8 @@ impl TestResponse {
     }
 
     /// Append a header
-    pub fn append_header<H>(mut self, header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
-        if let Ok((key, value)) = header.try_into_header_pair() {
+    pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
+        if let Ok((key, value)) = header.try_into_pair() {
             self.head.headers.append(key, value);
             return self;
         }
diff --git a/awc/src/ws.rs b/awc/src/ws.rs
index f0d421dbc..06d54aadb 100644
--- a/awc/src/ws.rs
+++ b/awc/src/ws.rs
@@ -39,7 +39,7 @@ use crate::{
     connect::{BoxedSocket, ConnectRequest},
     error::{HttpError, InvalidUrl, SendRequestError, WsClientError},
     http::{
-        header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION},
+        header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION},
         ConnectionType, Method, StatusCode, Uri, Version,
     },
     response::ClientResponse,
@@ -171,7 +171,7 @@ impl WebsocketsRequest {
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         match HeaderName::try_from(key) {
             Ok(key) => match value.try_into_value() {
@@ -190,7 +190,7 @@ impl WebsocketsRequest {
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         match HeaderName::try_from(key) {
             Ok(key) => match value.try_into_value() {
@@ -209,7 +209,7 @@ impl WebsocketsRequest {
     where
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         match HeaderName::try_from(key) {
             Ok(key) => {
@@ -445,7 +445,7 @@ mod tests {
     #[actix_rt::test]
     async fn test_header_override() {
         let req = Client::builder()
-            .header(header::CONTENT_TYPE, "111")
+            .add_default_header((header::CONTENT_TYPE, "111"))
             .finish()
             .ws("/")
             .set_header(header::CONTENT_TYPE, "222");
diff --git a/examples/basic.rs b/examples/basic.rs
index d29546129..598d13a40 100644
--- a/examples/basic.rs
+++ b/examples/basic.rs
@@ -22,14 +22,14 @@ async fn main() -> std::io::Result<()> {
 
     HttpServer::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::Logger::default())
             .service(index)
             .service(no_params)
             .service(
                 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))
                     .route(web::get().to(index_async)),
             )
diff --git a/examples/uds.rs b/examples/uds.rs
index 1db252fef..cf0ffebde 100644
--- a/examples/uds.rs
+++ b/examples/uds.rs
@@ -26,14 +26,14 @@ async fn main() -> std::io::Result<()> {
 
     HttpServer::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::Logger::default())
             .service(index)
             .service(no_params)
             .service(
                 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))
                     .route(web::get().to(index_async)),
             )
diff --git a/src/app.rs b/src/app.rs
index 5323cb33a..feb35d7ae 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -602,7 +602,7 @@ mod tests {
             App::new()
                 .wrap(
                     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)),
         )
@@ -623,7 +623,7 @@ mod tests {
                 .route("/test", web::get().to(HttpResponse::Ok))
                 .wrap(
                     DefaultHeaders::new()
-                        .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")),
+                        .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
                 ),
         )
         .await;
diff --git a/src/error/internal.rs b/src/error/internal.rs
index b8e169018..37195dc2e 100644
--- a/src/error/internal.rs
+++ b/src/error/internal.rs
@@ -2,7 +2,7 @@ use std::{cell::RefCell, fmt, io::Write as _};
 
 use actix_http::{
     body::BoxBody,
-    header::{self, IntoHeaderValue as _},
+    header::{self, TryIntoHeaderValue as _},
     StatusCode,
 };
 use bytes::{BufMut as _, BytesMut};
diff --git a/src/error/response_error.rs b/src/error/response_error.rs
index 7260efa1a..e0b4af44c 100644
--- a/src/error/response_error.rs
+++ b/src/error/response_error.rs
@@ -8,7 +8,7 @@ use std::{
 
 use actix_http::{
     body::BoxBody,
-    header::{self, IntoHeaderValue},
+    header::{self, TryIntoHeaderValue},
     Response, StatusCode,
 };
 use bytes::BytesMut;
diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs
index 945a58f7f..26a9d8e76 100644
--- a/src/http/header/content_disposition.rs
+++ b/src/http/header/content_disposition.rs
@@ -14,7 +14,7 @@ use once_cell::sync::Lazy;
 use regex::Regex;
 use std::fmt::{self, Write};
 
-use super::{ExtendedValue, Header, IntoHeaderValue, Writer};
+use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer};
 use crate::http::header;
 
 /// Split at the index of the first `needle` if it exists or at the end.
@@ -454,7 +454,7 @@ impl ContentDisposition {
     }
 }
 
-impl IntoHeaderValue for ContentDisposition {
+impl TryIntoHeaderValue for ContentDisposition {
     type Error = header::InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
diff --git a/src/http/header/content_range.rs b/src/http/header/content_range.rs
index 90b3f7fe2..bcbe77e66 100644
--- a/src/http/header/content_range.rs
+++ b/src/http/header/content_range.rs
@@ -3,7 +3,7 @@ use std::{
     str::FromStr,
 };
 
-use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE};
+use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer, CONTENT_RANGE};
 use crate::error::ParseError;
 
 crate::http::header::common_header! {
@@ -196,7 +196,7 @@ impl Display for ContentRangeSpec {
     }
 }
 
-impl IntoHeaderValue for ContentRangeSpec {
+impl TryIntoHeaderValue for ContentRangeSpec {
     type Error = InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
diff --git a/src/http/header/entity.rs b/src/http/header/entity.rs
index 50b40b7b2..76fe39f23 100644
--- a/src/http/header/entity.rs
+++ b/src/http/header/entity.rs
@@ -3,7 +3,7 @@ use std::{
     str::FromStr,
 };
 
-use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
+use super::{HeaderValue, InvalidHeaderValue, TryIntoHeaderValue, Writer};
 
 /// check that each char in the slice is either:
 /// 1. `%x21`, or
@@ -159,7 +159,7 @@ impl FromStr for EntityTag {
     }
 }
 
-impl IntoHeaderValue for EntityTag {
+impl TryIntoHeaderValue for EntityTag {
     type Error = InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
diff --git a/src/http/header/if_range.rs b/src/http/header/if_range.rs
index 5af9255f6..b845fb3bf 100644
--- a/src/http/header/if_range.rs
+++ b/src/http/header/if_range.rs
@@ -1,8 +1,8 @@
 use std::fmt::{self, Display, Write};
 
 use super::{
-    from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue,
-    InvalidHeaderValue, Writer,
+    from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue,
+    TryIntoHeaderValue, Writer,
 };
 use crate::error::ParseError;
 use crate::http::header;
@@ -96,7 +96,7 @@ impl Display for IfRange {
     }
 }
 
-impl IntoHeaderValue for IfRange {
+impl TryIntoHeaderValue for IfRange {
     type Error = InvalidHeaderValue;
 
     fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs
index ca3792a37..25f40a52b 100644
--- a/src/http/header/macros.rs
+++ b/src/http/header/macros.rs
@@ -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;
 
             #[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;
 
             #[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;
 
             #[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;
 
             #[inline]
diff --git a/src/http/header/range.rs b/src/http/header/range.rs
index c1d60f1ee..68028f53a 100644
--- a/src/http/header/range.rs
+++ b/src/http/header/range.rs
@@ -6,7 +6,7 @@ use std::{
 
 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
 /// 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;
 
     fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
diff --git a/src/lib.rs b/src/lib.rs
index a44c9b3fb..171a2d101 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -86,7 +86,6 @@ pub mod middleware;
 mod request;
 mod request_data;
 mod resource;
-mod responder;
 mod response;
 mod rmap;
 mod route;
@@ -109,12 +108,10 @@ pub use crate::error::{Error, ResponseError, Result};
 pub use crate::extract::FromRequest;
 pub use crate::request::HttpRequest;
 pub use crate::resource::Resource;
-pub use crate::responder::Responder;
-pub use crate::response::{HttpResponse, HttpResponseBuilder};
+pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder};
 pub use crate::route::Route;
 pub use crate::scope::Scope;
 pub use crate::server::HttpServer;
-// TODO: is exposing the error directly really needed
-pub use crate::types::{Either, EitherExtractError};
+pub use crate::types::Either;
 
 pub(crate) type BoxError = Box<dyn std::error::Error>;
diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs
index dceca44c2..257467710 100644
--- a/src/middleware/default_headers.rs
+++ b/src/middleware/default_headers.rs
@@ -16,7 +16,7 @@ use pin_project_lite::pin_project;
 
 use crate::{
     dev::{Service, Transform},
-    http::header::{HeaderMap, HeaderName, HeaderValue, CONTENT_TYPE},
+    http::header::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair, CONTENT_TYPE},
     service::{ServiceRequest, ServiceResponse},
     Error,
 };
@@ -29,79 +29,81 @@ use crate::{
 /// ```
 /// use actix_web::{web, http, middleware, App, HttpResponse};
 ///
-/// fn main() {
-///     let app = App::new()
-///         .wrap(middleware::DefaultHeaders::new().header("X-Version", "0.2"))
-///         .service(
-///             web::resource("/test")
-///                 .route(web::get().to(|| HttpResponse::Ok()))
-///                 .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed()))
-///         );
-/// }
+/// let app = App::new()
+///     .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
+///     .service(
+///         web::resource("/test")
+///             .route(web::get().to(|| HttpResponse::Ok()))
+///             .route(web::method(http::Method::HEAD).to(|| HttpResponse::MethodNotAllowed()))
+///     );
 /// ```
-#[derive(Clone)]
+#[derive(Debug, Clone, Default)]
 pub struct DefaultHeaders {
     inner: Rc<Inner>,
 }
 
+#[derive(Debug, Default)]
 struct Inner {
     headers: HeaderMap,
 }
 
-impl Default for DefaultHeaders {
-    fn default() -> Self {
-        DefaultHeaders {
-            inner: Rc::new(Inner {
-                headers: HeaderMap::new(),
-            }),
-        }
-    }
-}
-
 impl DefaultHeaders {
     /// Constructs an empty `DefaultHeaders` middleware.
+    #[inline]
     pub fn new() -> DefaultHeaders {
         DefaultHeaders::default()
     }
 
     /// 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
         HeaderName: TryFrom<K>,
         <HeaderName as TryFrom<K>>::Error: Into<HttpError>,
         HeaderValue: TryFrom<V>,
         <HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
     {
-        #[allow(clippy::match_wild_err_arm)]
-        match HeaderName::try_from(key) {
-            Ok(key) => match HeaderValue::try_from(value) {
-                Ok(value) => {
-                    Rc::get_mut(&mut self.inner)
-                        .expect("Multiple copies exist")
-                        .headers
-                        .append(key, value);
-                }
-                Err(_) => panic!("Can not create header value"),
-            },
-            Err(_) => panic!("Can not create header name"),
-        }
-        self
+        self.add((
+            HeaderName::try_from(key)
+                .map_err(Into::into)
+                .expect("Invalid header name"),
+            HeaderValue::try_from(value)
+                .map_err(Into::into)
+                .expect("Invalid header value"),
+        ))
     }
 
     /// Adds a default *Content-Type* header if response does not contain one.
     ///
     /// Default is `application/octet-stream`.
-    pub fn add_content_type(mut self) -> Self {
-        Rc::get_mut(&mut self.inner)
-            .expect("Multiple `Inner` copies exist.")
-            .headers
-            .insert(
-                CONTENT_TYPE,
-                HeaderValue::from_static("application/octet-stream"),
-            );
-
-        self
+    pub fn add_content_type(self) -> Self {
+        self.add((
+            CONTENT_TYPE,
+            HeaderValue::from_static("application/octet-stream"),
+        ))
     }
 }
 
@@ -119,7 +121,7 @@ where
     fn new_transform(&self, service: S) -> Self::Future {
         ready(Ok(DefaultHeadersMiddleware {
             service,
-            inner: self.inner.clone(),
+            inner: Rc::clone(&self.inner),
         }))
     }
 }
@@ -197,17 +199,22 @@ mod tests {
     };
 
     #[actix_rt::test]
-    async fn test_default_headers() {
+    async fn adding_default_headers() {
         let mw = DefaultHeaders::new()
-            .header(CONTENT_TYPE, "0001")
+            .add(("X-TEST", "0001"))
+            .add(("X-TEST-TWO", HeaderValue::from_static("123")))
             .new_transform(ok_service())
             .await
             .unwrap();
 
         let req = TestRequest::default().to_srv_request();
-        let resp = mw.call(req).await.unwrap();
-        assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
+        let res = mw.call(req).await.unwrap();
+        assert_eq!(res.headers().get("x-test").unwrap(), "0001");
+        assert_eq!(res.headers().get("x-test-two").unwrap(), "123");
+    }
 
+    #[actix_rt::test]
+    async fn no_override_existing() {
         let req = TestRequest::default().to_srv_request();
         let srv = |req: ServiceRequest| {
             ok(req.into_response(
@@ -217,7 +224,7 @@ mod tests {
             ))
         };
         let mw = DefaultHeaders::new()
-            .header(CONTENT_TYPE, "0001")
+            .add((CONTENT_TYPE, "0001"))
             .new_transform(srv.into_service())
             .await
             .unwrap();
@@ -226,7 +233,7 @@ mod tests {
     }
 
     #[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()
             .add_content_type()
@@ -241,4 +248,16 @@ mod tests {
             "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"));
+    }
 }
diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs
index d19cb64e9..42d285580 100644
--- a/src/middleware/mod.rs
+++ b/src/middleware/mod.rs
@@ -33,7 +33,7 @@ mod tests {
         let _ = App::new()
             .wrap(Compat::new(Logger::default()))
             .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| {
                 Ok(ErrorHandlerResponse::Response(res))
             }))
@@ -46,7 +46,7 @@ mod tests {
             .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
                 Ok(ErrorHandlerResponse::Response(res))
             }))
-            .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2"))
+            .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
             .wrap(Condition::new(true, DefaultHeaders::new()))
             .wrap(Compat::new(Logger::default()));
 
diff --git a/src/request_data.rs b/src/request_data.rs
index 680f3e566..b685fd0d6 100644
--- a/src/request_data.rs
+++ b/src/request_data.rs
@@ -17,7 +17,7 @@ use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, H
 /// # Mutating Request Data
 /// Note that since extractors must output owned data, only types that `impl Clone` can use this
 /// extractor. A clone is taken of the required request data and can, therefore, not be directly
-/// mutated in-place. To mutate request data, continue to use [`HttpRequest::extensions_mut`] or
+/// 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
 /// provided to make this potential foot-gun more obvious.
 ///
diff --git a/src/resource.rs b/src/resource.rs
index 420374a86..53104930a 100644
--- a/src/resource.rs
+++ b/src/resource.rs
@@ -15,13 +15,12 @@ use crate::{
     dev::{ensure_leading_slash, AppService, ResourceDef},
     guard::Guard,
     handler::Handler,
-    responder::Responder,
     route::{Route, RouteService},
     service::{
         BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest,
         ServiceResponse,
     },
-    BoxError, Error, FromRequest, HttpResponse,
+    BoxError, Error, FromRequest, HttpResponse, Responder,
 };
 
 /// *Resource* is an entry in resources table which corresponds to requested URL.
@@ -526,7 +525,7 @@ mod tests {
                     .name("test")
                     .wrap(
                         DefaultHeaders::new()
-                            .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")),
+                            .add((header::CONTENT_TYPE, HeaderValue::from_static("0001"))),
                     )
                     .route(web::get().to(HttpResponse::Ok)),
             ),
diff --git a/src/response/builder.rs b/src/response/builder.rs
index 18a1c8a7f..b500ab331 100644
--- a/src/response/builder.rs
+++ b/src/response/builder.rs
@@ -9,7 +9,7 @@ use std::{
 use actix_http::{
     body::{BodyStream, BoxBody, MessageBody},
     error::HttpError,
-    header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue},
+    header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue},
     ConnectionType, Extensions, Response, ResponseHead, StatusCode,
 };
 use bytes::Bytes;
@@ -67,12 +67,9 @@ impl HttpResponseBuilder {
     ///     .insert_header(("X-TEST", "value"))
     ///     .finish();
     /// ```
-    pub fn insert_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn insert_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
         if let Some(parts) = self.inner() {
-            match header.try_into_header_pair() {
+            match header.try_into_pair() {
                 Ok((key, value)) => {
                     parts.headers.insert(key, value);
                 }
@@ -94,12 +91,9 @@ impl HttpResponseBuilder {
     ///     .append_header(("X-TEST", "value2"))
     ///     .finish();
     /// ```
-    pub fn append_header<H>(&mut self, header: H) -> &mut Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn append_header(&mut self, header: impl TryIntoHeaderPair) -> &mut Self {
         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),
                 Err(e) => self.err = Some(e.into()),
             };
@@ -118,7 +112,7 @@ impl HttpResponseBuilder {
     where
         K: TryInto<HeaderName>,
         K::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if self.err.is_some() {
             return self;
@@ -143,7 +137,7 @@ impl HttpResponseBuilder {
     where
         K: TryInto<HeaderName>,
         K::Error: Into<HttpError>,
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if self.err.is_some() {
             return self;
@@ -180,7 +174,7 @@ impl HttpResponseBuilder {
     #[inline]
     pub fn upgrade<V>(&mut self, value: V) -> &mut Self
     where
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if let Some(parts) = self.inner() {
             parts.set_connection_type(ConnectionType::Upgrade);
@@ -218,7 +212,7 @@ impl HttpResponseBuilder {
     #[inline]
     pub fn content_type<V>(&mut self, value: V) -> &mut Self
     where
-        V: IntoHeaderValue,
+        V: TryIntoHeaderValue,
     {
         if let Some(parts) = self.inner() {
             match value.try_into_value() {
diff --git a/src/response/customize_responder.rs b/src/response/customize_responder.rs
new file mode 100644
index 000000000..11f6b2916
--- /dev/null
+++ b/src/response/customize_responder.rs
@@ -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"),
+        );
+    }
+}
diff --git a/src/response/mod.rs b/src/response/mod.rs
index 8401db9d2..977147104 100644
--- a/src/response/mod.rs
+++ b/src/response/mod.rs
@@ -1,9 +1,13 @@
 mod builder;
+mod customize_responder;
 mod http_codes;
+mod responder;
 #[allow(clippy::module_inception)]
 mod response;
 
 pub use self::builder::HttpResponseBuilder;
+pub use self::customize_responder::CustomizeResponder;
+pub use self::responder::Responder;
 pub use self::response::HttpResponse;
 
 #[cfg(feature = "cookies")]
diff --git a/src/responder.rs b/src/response/responder.rs
similarity index 63%
rename from src/responder.rs
rename to src/response/responder.rs
index e72739a71..319b824f1 100644
--- a/src/responder.rs
+++ b/src/response/responder.rs
@@ -2,64 +2,58 @@ use std::borrow::Cow;
 
 use actix_http::{
     body::{BoxBody, EitherBody, MessageBody},
-    error::HttpError,
-    header::HeaderMap,
-    header::IntoHeaderPair,
+    header::TryIntoHeaderPair,
     StatusCode,
 };
 use bytes::{Bytes, BytesMut};
 
 use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder};
 
+use super::CustomizeResponder;
+
 /// Trait implemented by types that can be converted to an HTTP response.
 ///
 /// Any types that implement this trait can be used in the return type of a handler.
+// # TODO: more about implementation notes and foreign impls
 pub trait Responder {
     type Body: MessageBody + 'static;
 
     /// Convert self to `HttpResponse`.
     fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body>;
 
-    /// Override a status code for a Responder.
+    /// Wraps responder to allow alteration of its response.
     ///
-    /// ```
-    /// use actix_web::{http::StatusCode, HttpRequest, Responder};
+    /// See [`CustomizeResponder`] docs for its capabilities.
     ///
-    /// fn index(req: HttpRequest) -> impl Responder {
-    ///     "Welcome!".with_status(StatusCode::OK)
-    /// }
+    /// # Examples
     /// ```
-    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
         Self: Sized,
     {
-        CustomResponder::new(self).with_status(status)
+        CustomizeResponder::new(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_owned() })
-    ///         .with_header(("x-version", "1.2.3"))
-    /// }
-    /// ```
-    fn with_header<H>(self, header: H) -> CustomResponder<Self>
+    #[doc(hidden)]
+    #[deprecated(since = "4.0.0", note = "Prefer `.customize().insert_header(header)`.")]
+    fn with_header(self, header: impl TryIntoHeaderPair) -> CustomizeResponder<Self>
     where
         Self: Sized,
-        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!(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)]
 pub(crate) mod tests {
     use actix_service::Service;
@@ -440,59 +342,4 @@ pub(crate) mod tests {
 
         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"),
-        );
-    }
 }
diff --git a/src/scope.rs b/src/scope.rs
index 74523cd94..c35584770 100644
--- a/src/scope.rs
+++ b/src/scope.rs
@@ -935,7 +935,7 @@ mod tests {
                 web::scope("app")
                     .wrap(
                         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))),
             ),
diff --git a/src/test.rs b/src/test.rs
index cfb3ef8f2..5ef2343a8 100644
--- a/src/test.rs
+++ b/src/test.rs
@@ -4,8 +4,8 @@ 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,
+    header::TryIntoHeaderPair, test::TestRequest as HttpTestRequest, Extensions, Method,
+    Request, StatusCode, Uri, Version,
 };
 use actix_router::{Path, ResourceDef, Url};
 use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory};
@@ -445,19 +445,13 @@ impl TestRequest {
     }
 
     /// 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,
-    {
+    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<H>(mut self, header: H) -> Self
-    where
-        H: IntoHeaderPair,
-    {
+    pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self {
         self.req.append_header(header);
         self
     }
diff --git a/src/web.rs b/src/web.rs
index 16dbace60..042b8a008 100644
--- a/src/web.rs
+++ b/src/web.rs
@@ -8,7 +8,7 @@ pub use bytes::{Buf, BufMut, Bytes, BytesMut};
 
 use crate::{
     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;

From fb091b2b88f9590d449415f272bd763d3ad4df3c Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 14 Dec 2021 18:33:17 +0000
Subject: [PATCH 05/28] split up router pattern and resource_path modules

---
 actix-router/src/lib.rs           | 142 ++----------------------------
 actix-router/src/pattern.rs       |  92 +++++++++++++++++++
 actix-router/src/resource_path.rs |  36 ++++++++
 3 files changed, 137 insertions(+), 133 deletions(-)
 create mode 100644 actix-router/src/pattern.rs
 create mode 100644 actix-router/src/resource_path.rs

diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs
index f616f7fc6..03f464626 100644
--- a/actix-router/src/lib.rs
+++ b/actix-router/src/lib.rs
@@ -7,144 +7,20 @@
 
 mod de;
 mod path;
+mod pattern;
 mod resource;
+mod resource_path;
 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")]
 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")]
 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()
-        }
-    }
-}
diff --git a/actix-router/src/pattern.rs b/actix-router/src/pattern.rs
new file mode 100644
index 000000000..78a638a78
--- /dev/null
+++ b/actix-router/src/pattern.rs
@@ -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);
diff --git a/actix-router/src/resource_path.rs b/actix-router/src/resource_path.rs
new file mode 100644
index 000000000..91a7f2f55
--- /dev/null
+++ b/actix-router/src/resource_path.rs
@@ -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()
+    }
+}

From 05255c7f7c92d785bac919f39374efa9985eec57 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 14 Dec 2021 19:57:18 +0000
Subject: [PATCH 06/28] remove either crate conversions (#2516)

---
 CHANGES.md          |  2 ++
 Cargo.toml          |  1 -
 src/types/either.rs | 25 ++++---------------------
 3 files changed, 6 insertions(+), 22 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 2df820027..b8d3ce8de 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -10,8 +10,10 @@
 
 ### Removed
 * Top-level `EitherExtractError` export. [#2510]
+* Conversion implementations for `either` crate. [#2516]
 
 [#2510]: https://github.com/actix/actix-web/pull/2510
+[#2516]: https://github.com/actix/actix-web/pull/2516
 
 
 ## 4.0.0-beta.14 - 2021-12-11
diff --git a/Cargo.toml b/Cargo.toml
index 96e2dd797..e20529e1a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -86,7 +86,6 @@ bytes = "1"
 cfg-if = "1"
 cookie = { version = "0.15", features = ["percent-encode"], optional = true }
 derive_more = "0.99.5"
-either = "1.5.3"
 encoding_rs = "0.8"
 futures-core = { version = "0.3.7", default-features = false }
 futures-util = { version = "0.3.7", default-features = false }
diff --git a/src/types/either.rs b/src/types/either.rs
index 3c759736e..5b8e02525 100644
--- a/src/types/either.rs
+++ b/src/types/either.rs
@@ -12,7 +12,8 @@ use futures_core::ready;
 use pin_project_lite::pin_project;
 
 use crate::{
-    body, dev,
+    body::EitherBody,
+    dev,
     web::{Form, Json},
     Error, FromRequest, HttpRequest, HttpResponse, Responder,
 };
@@ -101,24 +102,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)]
 impl<L, R> Either<L, R> {
     pub(self) fn unwrap_left(self) -> L {
@@ -146,7 +129,7 @@ where
     L: 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> {
         match self {
@@ -165,7 +148,7 @@ pub enum EitherExtractError<L, R> {
     /// Error from payload buffering, such as exceeding payload max size limit.
     Bytes(Error),
 
-    /// Error from primary extractor.
+    /// Error from primary and fallback extractors.
     Extract(L, R),
 }
 

From dd4a372613339f6668dc248d2dbc414c3a6ccfad Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Tue, 14 Dec 2021 21:17:50 +0000
Subject: [PATCH 07/28] allow error handler middleware to return different body
 type (#2515)

---
 CHANGES.md                     |   3 +
 actix-router/src/url.rs        |  53 ++++++++------
 scripts/ci-test                |  32 ++++++---
 src/middleware/compat.rs       |  22 +++---
 src/middleware/condition.rs    |  13 ++--
 src/middleware/err_handlers.rs | 128 +++++++++++++++++++++------------
 src/middleware/mod.rs          |   4 +-
 7 files changed, 162 insertions(+), 93 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index b8d3ce8de..0c27aaa1c 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,12 +7,15 @@
 
 ### 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]
 
 ### Removed
 * Top-level `EitherExtractError` export. [#2510]
 * Conversion implementations for `either` crate. [#2516]
 
 [#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
 
 
diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs
index e08a7171a..10193dde8 100644
--- a/actix-router/src/url.rs
+++ b/actix-router/src/url.rs
@@ -2,22 +2,28 @@ use crate::ResourcePath;
 
 #[allow(dead_code)]
 const GEN_DELIMS: &[u8] = b":/?#[]@";
+
 #[allow(dead_code)]
 const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
+
 #[allow(dead_code)]
 const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
+
 #[allow(dead_code)]
 const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
+
 #[allow(dead_code)]
 const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
     ABCDEFGHIJKLMNOPQRSTUVWXYZ
     1234567890
     -._~";
+
 const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
     ABCDEFGHIJKLMNOPQRSTUVWXYZ
     1234567890
     -._~
     !$'()*,";
+
 const QS: &[u8] = b"+&=;b";
 
 #[inline]
@@ -34,19 +40,20 @@ thread_local! {
     static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
 }
 
-#[derive(Default, Clone, Debug)]
+#[derive(Debug, Clone, Default)]
 pub struct Url {
     uri: http::Uri,
     path: Option<String>,
 }
 
 impl Url {
+    #[inline]
     pub fn new(uri: http::Uri) -> Url {
         let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes()));
-
         Url { uri, path }
     }
 
+    #[inline]
     pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
         Url {
             path: quoter.requote(uri.path().as_bytes()),
@@ -54,15 +61,16 @@ impl Url {
         }
     }
 
+    #[inline]
     pub fn uri(&self) -> &http::Uri {
         &self.uri
     }
 
+    #[inline]
     pub fn path(&self) -> &str {
-        if let Some(ref s) = self.path {
-            s
-        } else {
-            self.uri.path()
+        match self.path {
+            Some(ref path) => path,
+            _ => self.uri.path(),
         }
     }
 
@@ -86,6 +94,7 @@ impl ResourcePath for Url {
     }
 }
 
+/// A quoter
 pub struct Quoter {
     safe_table: [u8; 16],
     protected_table: [u8; 16],
@@ -93,7 +102,7 @@ pub struct Quoter {
 
 impl Quoter {
     pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
-        let mut q = Quoter {
+        let mut quoter = Quoter {
             safe_table: [0; 16],
             protected_table: [0; 16],
         };
@@ -101,24 +110,24 @@ impl Quoter {
         // prepare safe table
         for i in 0..128 {
             if ALLOWED.contains(&i) {
-                set_bit(&mut q.safe_table, i);
+                set_bit(&mut quoter.safe_table, i);
             }
             if QS.contains(&i) {
-                set_bit(&mut q.safe_table, i);
+                set_bit(&mut quoter.safe_table, i);
             }
         }
 
         for ch in safe {
-            set_bit(&mut q.safe_table, *ch)
+            set_bit(&mut quoter.safe_table, *ch)
         }
 
         // prepare protected table
         for ch in protected {
-            set_bit(&mut q.safe_table, *ch);
-            set_bit(&mut q.protected_table, *ch);
+            set_bit(&mut quoter.safe_table, *ch);
+            set_bit(&mut quoter.protected_table, *ch);
         }
 
-        q
+        quoter
     }
 
     pub fn requote(&self, val: &[u8]) -> Option<String> {
@@ -215,7 +224,7 @@ mod tests {
     }
 
     #[test]
-    fn test_parse_url() {
+    fn parse_url() {
         let re = "/user/{id}/test";
 
         let path = match_url(re, "/user/2345/test");
@@ -231,24 +240,24 @@ mod tests {
     }
 
     #[test]
-    fn test_protected_chars() {
+    fn protected_chars() {
         let encoded = percent_encode(PROTECTED);
         let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
         assert_eq!(path.get("id").unwrap(), &encoded);
     }
 
     #[test]
-    fn test_non_protecteed_ascii() {
-        let nonprotected_ascii = ('\u{0}'..='\u{7F}')
+    fn non_protected_ascii() {
+        let non_protected_ascii = ('\u{0}'..='\u{7F}')
             .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8)))
             .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));
-        assert_eq!(path.get("id").unwrap(), &nonprotected_ascii);
+        assert_eq!(path.get("id").unwrap(), &non_protected_ascii);
     }
 
     #[test]
-    fn test_valid_utf8_multibyte() {
+    fn valid_utf8_multibyte() {
         let test = ('\u{FF00}'..='\u{FFFF}').collect::<String>();
         let encoded = percent_encode(test.as_bytes());
         let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded));
@@ -256,7 +265,7 @@ mod tests {
     }
 
     #[test]
-    fn test_invalid_utf8() {
+    fn invalid_utf8() {
         let invalid_utf8 = percent_encode((0x80..=0xff).collect::<Vec<_>>().as_slice());
         let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap();
         let path = Path::new(Url::new(uri));
@@ -266,7 +275,7 @@ mod tests {
     }
 
     #[test]
-    fn test_from_hex() {
+    fn hex_encoding() {
         let hex = b"0123456789abcdefABCDEF";
 
         for i in 0..256 {
diff --git a/scripts/ci-test b/scripts/ci-test
index 98e13927d..3ab229665 100755
--- a/scripts/ci-test
+++ b/scripts/ci-test
@@ -4,15 +4,25 @@
 
 set -x
 
-cargo test --lib --tests -p=actix-router --all-features
-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
+EXIT=0
 
-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
diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs
index ed441f7b9..d49c461c4 100644
--- a/src/middleware/compat.rs
+++ b/src/middleware/compat.rs
@@ -6,12 +6,15 @@ use std::{
     task::{Context, Poll},
 };
 
-use actix_http::body::MessageBody;
-use actix_service::{Service, Transform};
 use futures_core::{future::LocalBoxFuture, ready};
 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),
 /// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition).
@@ -52,7 +55,7 @@ where
     T::Response: MapServiceResponseBody,
     T::Error: Into<Error>,
 {
-    type Response = ServiceResponse;
+    type Response = ServiceResponse<BoxBody>;
     type Error = Error;
     type Transform = CompatMiddleware<T::Transform>;
     type InitError = T::InitError;
@@ -77,7 +80,7 @@ where
     S::Response: MapServiceResponseBody,
     S::Error: Into<Error>,
 {
-    type Response = ServiceResponse;
+    type Response = ServiceResponse<BoxBody>;
     type Error = Error;
     type Future = CompatMiddlewareFuture<S::Future>;
 
@@ -102,7 +105,7 @@ where
     T: MapServiceResponseBody,
     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> {
         let res = match ready!(self.project().fut.poll(cx)) {
@@ -116,14 +119,15 @@ where
 
 /// Convert `ServiceResponse`'s `ResponseBody<B>` generic type to `ResponseBody<Body>`.
 pub trait MapServiceResponseBody {
-    fn map_body(self) -> ServiceResponse;
+    fn map_body(self) -> ServiceResponse<BoxBody>;
 }
 
 impl<B> MapServiceResponseBody for ServiceResponse<B>
 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()
     }
 }
diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs
index a7777a96b..659f88bc9 100644
--- a/src/middleware/condition.rs
+++ b/src/middleware/condition.rs
@@ -106,7 +106,7 @@ mod tests {
             header::{HeaderValue, CONTENT_TYPE},
             StatusCode,
         },
-        middleware::err_handlers::*,
+        middleware::{err_handlers::*, Compat},
         test::{self, TestRequest},
         HttpResponse,
     };
@@ -116,7 +116,8 @@ mod tests {
         res.response_mut()
             .headers_mut()
             .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
-        Ok(ErrorHandlerResponse::Response(res))
+
+        Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
     }
 
     #[actix_rt::test]
@@ -125,7 +126,9 @@ mod tests {
             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)
             .new_transform(srv.into_service())
@@ -141,7 +144,9 @@ mod tests {
             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)
             .new_transform(srv.into_service())
diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs
index 756da30c3..fedefa6fa 100644
--- a/src/middleware/err_handlers.rs
+++ b/src/middleware/err_handlers.rs
@@ -13,6 +13,7 @@ use futures_core::{future::LocalBoxFuture, ready};
 use pin_project_lite::pin_project;
 
 use crate::{
+    body::EitherBody,
     dev::{ServiceRequest, ServiceResponse},
     http::StatusCode,
     Error, Result,
@@ -21,10 +22,10 @@ use crate::{
 /// Return type for [`ErrorHandlers`] custom handlers.
 pub enum ErrorHandlerResponse<B> {
     /// Immediate HTTP response.
-    Response(ServiceResponse<B>),
+    Response(ServiceResponse<EitherBody<B>>),
 
     /// 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>>;
@@ -44,7 +45,8 @@ type ErrorHandler<B> = dyn Fn(ServiceResponse<B>) -> Result<ErrorHandlerResponse
 ///     res.response_mut()
 ///        .headers_mut()
 ///        .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()
@@ -66,7 +68,7 @@ type Handlers<B> = Rc<AHashMap<StatusCode, Box<ErrorHandler<B>>>>;
 impl<B> Default for ErrorHandlers<B> {
     fn default() -> Self {
         ErrorHandlers {
-            handlers: Rc::new(AHashMap::default()),
+            handlers: Default::default(),
         }
     }
 }
@@ -95,7 +97,7 @@ where
     S::Future: 'static,
     B: 'static,
 {
-    type Response = ServiceResponse<B>;
+    type Response = ServiceResponse<EitherBody<B>>;
     type Error = Error;
     type Transform = ErrorHandlersMiddleware<S, B>;
     type InitError = ();
@@ -119,7 +121,7 @@ where
     S::Future: 'static,
     B: 'static,
 {
-    type Response = ServiceResponse<B>;
+    type Response = ServiceResponse<EitherBody<B>>;
     type Error = Error;
     type Future = ErrorHandlersFuture<S::Future, B>;
 
@@ -143,8 +145,8 @@ pin_project! {
             fut: Fut,
             handlers: Handlers<B>,
         },
-        HandlerFuture {
-            fut: LocalBoxFuture<'static, Fut::Output>,
+        ErrorHandlerFuture {
+            fut: LocalBoxFuture<'static, Result<ServiceResponse<EitherBody<B>>, Error>>,
         },
     }
 }
@@ -153,25 +155,29 @@ impl<Fut, B> Future for ErrorHandlersFuture<Fut, B>
 where
     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> {
         match self.as_mut().project() {
             ErrorHandlersProj::ServiceFuture { fut, handlers } => {
                 let res = ready!(fut.poll(cx))?;
+
                 match handlers.get(&res.status()) {
                     Some(handler) => match handler(res)? {
                         ErrorHandlerResponse::Response(res) => Poll::Ready(Ok(res)),
                         ErrorHandlerResponse::Future(fut) => {
                             self.as_mut()
-                                .set(ErrorHandlersFuture::HandlerFuture { fut });
+                                .set(ErrorHandlersFuture::ErrorHandlerFuture { fut });
+
                             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 {
     use actix_service::IntoService;
     use actix_utils::future::ok;
+    use bytes::Bytes;
     use futures_util::future::FutureExt as _;
 
     use super::*;
-    use crate::http::{
-        header::{HeaderValue, CONTENT_TYPE},
-        StatusCode,
+    use crate::{
+        http::{
+            header::{HeaderValue, CONTENT_TYPE},
+            StatusCode,
+        },
+        test::{self, TestRequest},
     };
-    use crate::test::{self, TestRequest};
-    use crate::HttpResponse;
-
-    #[allow(clippy::unnecessary_wraps)]
-    fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
-        res.response_mut()
-            .headers_mut()
-            .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
-        Ok(ErrorHandlerResponse::Response(res))
-    }
 
     #[actix_rt::test]
-    async fn test_handler() {
-        let srv = |req: ServiceRequest| {
-            ok(req.into_response(HttpResponse::InternalServerError().finish()))
-        };
+    async fn add_header_error_handler() {
+        #[allow(clippy::unnecessary_wraps)]
+        fn error_handler<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
+            res.response_mut()
+                .headers_mut()
+                .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
+
+            Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
+        }
+
+        let srv = test::default_service(StatusCode::INTERNAL_SERVER_ERROR);
 
         let mw = ErrorHandlers::new()
-            .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500)
+            .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
             .new_transform(srv.into_service())
             .await
             .unwrap();
@@ -214,24 +221,25 @@ mod tests {
         assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
     }
 
-    #[allow(clippy::unnecessary_wraps)]
-    fn render_500_async<B: 'static>(
-        mut res: ServiceResponse<B>,
-    ) -> Result<ErrorHandlerResponse<B>> {
-        res.response_mut()
-            .headers_mut()
-            .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
-        Ok(ErrorHandlerResponse::Future(ok(res).boxed_local()))
-    }
-
     #[actix_rt::test]
-    async fn test_handler_async() {
-        let srv = |req: ServiceRequest| {
-            ok(req.into_response(HttpResponse::InternalServerError().finish()))
-        };
+    async fn add_header_error_handler_async() {
+        #[allow(clippy::unnecessary_wraps)]
+        fn error_handler<B: 'static>(
+            mut res: ServiceResponse<B>,
+        ) -> Result<ErrorHandlerResponse<B>> {
+            res.response_mut()
+                .headers_mut()
+                .insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
+
+            Ok(ErrorHandlerResponse::Future(
+                ok(res.map_into_left_body()).boxed_local(),
+            ))
+        }
+
+        let srv = test::default_service(StatusCode::INTERNAL_SERVER_ERROR);
 
         let mw = ErrorHandlers::new()
-            .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async)
+            .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
             .new_transform(srv.into_service())
             .await
             .unwrap();
@@ -239,4 +247,34 @@ mod tests {
         let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await;
         assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
     }
+
+    #[actix_rt::test]
+    async fn changes_body_type() {
+        #[allow(clippy::unnecessary_wraps)]
+        fn error_handler<B: 'static>(
+            res: ServiceResponse<B>,
+        ) -> Result<ErrorHandlerResponse<B>> {
+            let (req, res) = res.into_parts();
+            let res = res.set_body(Bytes::from("sorry, that's no bueno"));
+
+            let res = ServiceResponse::new(req, res)
+                .map_into_boxed_body()
+                .map_into_right_body();
+
+            Ok(ErrorHandlerResponse::Response(res))
+        }
+
+        let srv = test::default_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
 }
diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs
index 42d285580..0da9b9b2e 100644
--- a/src/middleware/mod.rs
+++ b/src/middleware/mod.rs
@@ -35,7 +35,7 @@ mod tests {
             .wrap(Condition::new(true, DefaultHeaders::new()))
             .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
             .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
-                Ok(ErrorHandlerResponse::Response(res))
+                Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
             }))
             .wrap(Logger::default())
             .wrap(NormalizePath::new(TrailingSlash::Trim));
@@ -44,7 +44,7 @@ mod tests {
             .wrap(NormalizePath::new(TrailingSlash::Trim))
             .wrap(Logger::default())
             .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
-                Ok(ErrorHandlerResponse::Response(res))
+                Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
             }))
             .wrap(DefaultHeaders::new().add(("X-Test2", "X-Value2")))
             .wrap(Condition::new(true, DefaultHeaders::new()))

From 156cc20ac8af6455cb2438ba1b982265bac64521 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Wed, 15 Dec 2021 01:44:51 +0000
Subject: [PATCH 08/28] refactor testing utils (#2518)

---
 CHANGES.md                        |   6 +
 actix-http/src/test.rs            |   2 +-
 actix-test/CHANGES.md             |   4 +
 actix-test/src/lib.rs             |  13 +-
 src/middleware/default_headers.rs |   7 +-
 src/middleware/err_handlers.rs    |   6 +-
 src/test.rs                       | 909 ------------------------------
 src/test/mod.rs                   |  81 +++
 src/test/test_request.rs          | 431 ++++++++++++++
 src/test/test_services.rs         |  31 +
 src/test/test_utils.rs            | 474 ++++++++++++++++
 src/types/either.rs               |   2 -
 src/types/json.rs                 |   5 +-
 13 files changed, 1043 insertions(+), 928 deletions(-)
 delete mode 100644 src/test.rs
 create mode 100644 src/test/mod.rs
 create mode 100644 src/test/test_request.rs
 create mode 100644 src/test/test_services.rs
 create mode 100644 src/test/test_utils.rs

diff --git a/CHANGES.md b/CHANGES.md
index 0c27aaa1c..6494ba4f6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -9,14 +9,20 @@
 * 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.
 
 ### 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
diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs
index 7e26ee865..ea80345fe 100644
--- a/actix-http/src/test.rs
+++ b/actix-http/src/test.rs
@@ -264,7 +264,7 @@ impl TestSeqBuffer {
 
     /// Create new empty `TestBuffer` instance.
     pub fn empty() -> Self {
-        Self::new("")
+        Self::new(BytesMut::new())
     }
 
     pub fn read_buf(&self) -> Ref<'_, BytesMut> {
diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md
index ec7d3e8d1..b7107b44f 100644
--- a/actix-test/CHANGES.md
+++ b/actix-test/CHANGES.md
@@ -1,6 +1,10 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+* 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
diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs
index 934b8f3aa..3808ba69a 100644
--- a/actix-test/src/lib.rs
+++ b/actix-test/src/lib.rs
@@ -37,9 +37,14 @@ extern crate tls_rustls as rustls;
 use std::{fmt, net, thread, time::Duration};
 
 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};
+pub use actix_http_test::unused_addr;
 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::{
     body::MessageBody,
     dev::{AppConfig, Server, ServerHandle, Service},
@@ -48,12 +53,6 @@ use actix_web::{
 };
 use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
 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;
 
 /// Start default [`TestServer`].
diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs
index 257467710..89210b156 100644
--- a/src/middleware/default_headers.rs
+++ b/src/middleware/default_headers.rs
@@ -194,7 +194,7 @@ mod tests {
     use crate::{
         dev::ServiceRequest,
         http::header::CONTENT_TYPE,
-        test::{ok_service, TestRequest},
+        test::{self, TestRequest},
         HttpResponse,
     };
 
@@ -203,7 +203,7 @@ mod tests {
         let mw = DefaultHeaders::new()
             .add(("X-TEST", "0001"))
             .add(("X-TEST-TWO", HeaderValue::from_static("123")))
-            .new_transform(ok_service())
+            .new_transform(test::ok_service())
             .await
             .unwrap();
 
@@ -234,10 +234,9 @@ mod tests {
 
     #[actix_rt::test]
     async fn adding_content_type() {
-        let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish()));
         let mw = DefaultHeaders::new()
             .add_content_type()
-            .new_transform(srv.into_service())
+            .new_transform(test::ok_service())
             .await
             .unwrap();
 
diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs
index fedefa6fa..6d064372f 100644
--- a/src/middleware/err_handlers.rs
+++ b/src/middleware/err_handlers.rs
@@ -209,7 +209,7 @@ mod tests {
             Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
         }
 
-        let srv = test::default_service(StatusCode::INTERNAL_SERVER_ERROR);
+        let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
 
         let mw = ErrorHandlers::new()
             .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
@@ -236,7 +236,7 @@ mod tests {
             ))
         }
 
-        let srv = test::default_service(StatusCode::INTERNAL_SERVER_ERROR);
+        let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
 
         let mw = ErrorHandlers::new()
             .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
@@ -264,7 +264,7 @@ mod tests {
             Ok(ErrorHandlerResponse::Response(res))
         }
 
-        let srv = test::default_service(StatusCode::INTERNAL_SERVER_ERROR);
+        let srv = test::simple_service(StatusCode::INTERNAL_SERVER_ERROR);
 
         let mw = ErrorHandlers::new()
             .handler(StatusCode::INTERNAL_SERVER_ERROR, error_handler)
diff --git a/src/test.rs b/src/test.rs
deleted file mode 100644
index 5ef2343a8..000000000
--- a/src/test.rs
+++ /dev/null
@@ -1,909 +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::TryIntoHeaderPair, 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(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();
-        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());
-    }
-}
diff --git a/src/test/mod.rs b/src/test/mod.rs
new file mode 100644
index 000000000..a29dfc437
--- /dev/null
+++ b/src/test/mod.rs
@@ -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");
+    }
+}
diff --git a/src/test/test_request.rs b/src/test/test_request.rs
new file mode 100644
index 000000000..fd3355ef3
--- /dev/null
+++ b/src/test/test_request.rs
@@ -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());
+    }
+}
diff --git a/src/test/test_services.rs b/src/test/test_services.rs
new file mode 100644
index 000000000..b4810cfd8
--- /dev/null
+++ b/src/test/test_services.rs
@@ -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)
+}
diff --git a/src/test/test_utils.rs b/src/test/test_utils.rs
new file mode 100644
index 000000000..02d4c9bf3
--- /dev/null
+++ b/src/test/test_utils.rs
@@ -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");
+    }
+}
diff --git a/src/types/either.rs b/src/types/either.rs
index 5b8e02525..0eafb9e43 100644
--- a/src/types/either.rs
+++ b/src/types/either.rs
@@ -20,8 +20,6 @@ use crate::{
 
 /// Combines two extractor or responder types into a single type.
 ///
-/// Can be converted to and from an [`either::Either`].
-///
 /// # Extractor
 /// Provides a mechanism for trying two extractors, a primary and a fallback. Useful for
 /// "polymorphic payloads" where, for example, a form might be JSON or URL encoded.
diff --git a/src/types/json.rs b/src/types/json.rs
index 2b4d220e2..be6078b2b 100644
--- a/src/types/json.rs
+++ b/src/types/json.rs
@@ -449,12 +449,13 @@ mod tests {
 
     use super::*;
     use crate::{
+        body,
         error::InternalError,
         http::{
             header::{self, CONTENT_LENGTH, CONTENT_TYPE},
             StatusCode,
         },
-        test::{assert_body_eq, load_body, TestRequest},
+        test::{assert_body_eq, TestRequest},
     };
 
     #[derive(Serialize, Deserialize, PartialEq, Debug)]
@@ -517,7 +518,7 @@ mod tests {
         let resp = HttpResponse::from_error(s.err().unwrap());
         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();
         assert_eq!(msg.name, "invalid request");
     }

From a6d5776481eccf50819a2a64953ef4c954f1dcf9 Mon Sep 17 00:00:00 2001
From: Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>
Date: Fri, 17 Dec 2021 01:25:10 +0300
Subject: [PATCH 09/28] various fixes to MessageBody::complete_body (#2519)

---
 actix-http/src/body/boxed.rs        | 22 +---------------
 actix-http/src/body/message_body.rs | 39 ++++++++++++++++++++++++++---
 2 files changed, 36 insertions(+), 25 deletions(-)

diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs
index d2469e986..d4737aab8 100644
--- a/actix-http/src/body/boxed.rs
+++ b/actix-http/src/body/boxed.rs
@@ -57,27 +57,7 @@ impl MessageBody for BoxBody {
     }
 
     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",
-                );
-            }
-        }
+        self.0.take_complete_body()
     }
 }
 
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index 3e6c8d5cb..20263b3fb 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -134,7 +134,7 @@ mod foreign_impls {
 
     impl<B> MessageBody for Box<B>
     where
-        B: MessageBody + Unpin,
+        B: MessageBody + Unpin + ?Sized,
     {
         type Error = B::Error;
 
@@ -164,7 +164,7 @@ mod foreign_impls {
 
     impl<B> MessageBody for Pin<Box<B>>
     where
-        B: MessageBody,
+        B: MessageBody + ?Sized,
     {
         type Error = B::Error;
 
@@ -175,10 +175,10 @@ mod foreign_impls {
 
         #[inline]
         fn poll_next(
-            mut self: Pin<&mut Self>,
+            self: Pin<&mut Self>,
             cx: &mut Context<'_>,
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
-            self.as_mut().poll_next(cx)
+            self.get_mut().as_mut().poll_next(cx)
         }
 
         #[inline]
@@ -475,6 +475,16 @@ where
             None => Poll::Ready(None),
         }
     }
+
+    #[inline]
+    fn is_complete_body(&self) -> bool {
+        self.body.is_complete_body()
+    }
+
+    #[inline]
+    fn take_complete_body(&mut self) -> Bytes {
+        self.body.take_complete_body()
+    }
 }
 
 #[cfg(test)]
@@ -630,6 +640,27 @@ mod tests {
         assert_eq!(Pin::new(&mut data).poll_next(&mut cx), Poll::Ready(None));
     }
 
+    #[test]
+    fn complete_body_combinators() {
+        use crate::body::{BoxBody, EitherBody};
+
+        let body = Bytes::from_static(b"test");
+        let body = BoxBody::new(body);
+        let body = EitherBody::<_, ()>::left(body);
+        let body = EitherBody::<(), _>::right(body);
+        let body = Box::new(body);
+        let body = Box::pin(body);
+        let mut body = body;
+
+        assert!(body.is_complete_body());
+        assert_eq!(body.take_complete_body(), b"test".as_ref());
+
+        // subsequent poll_next returns None
+        let waker = futures_util::task::noop_waker();
+        let mut cx = Context::from_waker(&waker);
+        assert!(Pin::new(&mut body).poll_next(&mut cx).map_err(drop) == Poll::Ready(None));
+    }
+
     // down-casting used to be done with a method on MessageBody trait
     // test is kept to demonstrate equivalence of Any trait
     #[actix_rt::test]

From 44b7302845e163805fd12a445c34816d43cf98ef Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Thu, 16 Dec 2021 22:26:45 +0000
Subject: [PATCH 10/28] minimize futures-util dep in actix-http

---
 actix-http/Cargo.toml               | 3 ++-
 actix-http/src/body/message_body.rs | 4 ++--
 awc/Cargo.toml                      | 4 ++--
 3 files changed, 6 insertions(+), 5 deletions(-)

diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 374a55a62..2e8ec1dfc 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -55,7 +55,7 @@ bytestring = "1"
 derive_more = "0.99.5"
 encoding_rs = "0.8"
 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"
 http = "0.2.5"
 httparse = "1.5.1"
@@ -89,6 +89,7 @@ actix-web = "4.0.0-beta.14"
 async-stream = "0.3"
 criterion = { version = "0.3", features = ["html_reports"] }
 env_logger = "0.9"
+futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
 rcgen = "0.8"
 regex = "1.3"
 rustls-pemfile = "0.2"
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index 20263b3fb..10a7260f4 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -198,7 +198,7 @@ mod foreign_impls {
             // 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 waker = futures_task::noop_waker();
             let mut cx = Context::from_waker(&waker);
 
             match self.as_mut().poll_next(&mut cx) {
@@ -631,7 +631,7 @@ mod tests {
         // second call returns empty
         assert_eq!(data.take_complete_body(), b"".as_ref());
 
-        let waker = futures_util::task::noop_waker();
+        let waker = futures_task::noop_waker();
         let mut cx = Context::from_waker(&waker);
         let mut data = Bytes::from_static(b"test");
         // take returns whole chunk
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 48ae27df0..60a95871c 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -70,8 +70,8 @@ base64 = "0.13"
 bytes = "1"
 cfg-if = "1"
 derive_more = "0.99.5"
-futures-core = { version = "0.3.7", default-features = false }
-futures-util = { 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, features = ["alloc", "sink"] }
 h2 = "0.3.9"
 http = "0.2.5"
 itoa = "0.4"

From 3c0d059d92aa06be9e3a5b9216977dd3b7572f91 Mon Sep 17 00:00:00 2001
From: Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>
Date: Fri, 17 Dec 2021 03:43:40 +0300
Subject: [PATCH 11/28] MessageBody::boxed (#2520)

Co-authored-by: Rob Ede <robjtede@icloud.com>
---
 actix-http/CHANGES.md               |  2 ++
 actix-http/src/body/boxed.rs        | 16 +++++++++++++++-
 actix-http/src/body/either.rs       |  8 ++++++++
 actix-http/src/body/message_body.rs | 13 +++++++++++--
 actix-http/src/response.rs          |  2 +-
 awc/src/any_body.rs                 |  4 +---
 src/response/response.rs            |  3 +--
 src/service.rs                      |  2 +-
 8 files changed, 40 insertions(+), 10 deletions(-)

diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index 011e2c608..598ef9c0e 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -28,6 +28,7 @@
 * `Request::take_req_data()`. [#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 `boxed` method on `MessageBody` trait for wrapping body type. [#2520]
 
 ### Changed
 * Rename `body::BoxBody::{from_body => new}`. [#2468]
@@ -56,6 +57,7 @@
 [#2488]: https://github.com/actix/actix-web/pull/2488
 [#2491]: https://github.com/actix/actix-web/pull/2491
 [#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
diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs
index d4737aab8..7581bec88 100644
--- a/actix-http/src/body/boxed.rs
+++ b/actix-http/src/body/boxed.rs
@@ -14,7 +14,11 @@ use crate::Error;
 pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
 
 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
     where
         B: MessageBody + 'static,
@@ -24,6 +28,7 @@ impl BoxBody {
     }
 
     /// Returns a mutable pinned reference to the inner message body type.
+    #[inline]
     pub fn as_pin_mut(&mut self) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
         self.0.as_mut()
     }
@@ -38,10 +43,12 @@ impl fmt::Debug for BoxBody {
 impl MessageBody for BoxBody {
     type Error = Error;
 
+    #[inline]
     fn size(&self) -> BodySize {
         self.0.size()
     }
 
+    #[inline]
     fn poll_next(
         mut self: Pin<&mut Self>,
         cx: &mut Context<'_>,
@@ -52,13 +59,20 @@ impl MessageBody for BoxBody {
             .map_err(|err| Error::new_body().with_cause(err))
     }
 
+    #[inline]
     fn is_complete_body(&self) -> bool {
         self.0.is_complete_body()
     }
 
+    #[inline]
     fn take_complete_body(&mut self) -> Bytes {
         self.0.take_complete_body()
     }
+
+    #[inline]
+    fn boxed(self) -> BoxBody {
+        self
+    }
 }
 
 #[cfg(test)]
diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs
index 103b39c5d..3a4082dc9 100644
--- a/actix-http/src/body/either.rs
+++ b/actix-http/src/body/either.rs
@@ -88,6 +88,14 @@ where
             EitherBody::Right { body } => body.take_complete_body(),
         }
     }
+
+    #[inline]
+    fn boxed(self) -> BoxBody {
+        match self {
+            EitherBody::Left { body } => body.boxed(),
+            EitherBody::Right { body } => body.boxed(),
+        }
+    }
 }
 
 #[cfg(test)]
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index 10a7260f4..075ae7220 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -12,7 +12,7 @@ use bytes::{Bytes, BytesMut};
 use futures_core::ready;
 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.
 // TODO: examples
@@ -77,6 +77,15 @@ pub trait MessageBody {
             std::any::type_name::<Self>()
         );
     }
+
+    /// Converts this body into `BoxBody`.
+    #[inline]
+    fn boxed(self) -> BoxBody
+    where
+        Self: Sized + 'static,
+    {
+        BoxBody::new(self)
+    }
 }
 
 mod foreign_impls {
@@ -656,7 +665,7 @@ mod tests {
         assert_eq!(body.take_complete_body(), b"test".as_ref());
 
         // subsequent poll_next returns None
-        let waker = futures_util::task::noop_waker();
+        let waker = futures_task::noop_waker();
         let mut cx = Context::from_waker(&waker);
         assert!(Pin::new(&mut body).poll_next(&mut cx).map_err(drop) == Poll::Ready(None));
     }
diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs
index 9f799f669..aee9e80b4 100644
--- a/actix-http/src/response.rs
+++ b/actix-http/src/response.rs
@@ -194,7 +194,7 @@ impl<B> Response<B> {
     where
         B: MessageBody + 'static,
     {
-        self.map_body(|_, body| BoxBody::new(body))
+        self.map_body(|_, body| body.boxed())
     }
 
     /// Returns body, consuming this response.
diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs
index cb9038ff3..2ffeb5074 100644
--- a/awc/src/any_body.rs
+++ b/awc/src/any_body.rs
@@ -45,9 +45,7 @@ impl AnyBody {
     where
         B: MessageBody + 'static,
     {
-        Self::Body {
-            body: BoxBody::new(body),
-        }
+        Self::Body { body: body.boxed() }
     }
 
     /// Constructs new `AnyBody` instance from a slice of bytes by copying it.
diff --git a/src/response/response.rs b/src/response/response.rs
index 1900dd845..4fb4b44b6 100644
--- a/src/response/response.rs
+++ b/src/response/response.rs
@@ -244,8 +244,7 @@ impl<B> HttpResponse<B> {
     where
         B: MessageBody + 'static,
     {
-        // TODO: avoid double boxing with down-casting, if it improves perf
-        self.map_body(|_, body| BoxBody::new(body))
+        self.map_body(|_, body| body.boxed())
     }
 
     /// Extract response body
diff --git a/src/service.rs b/src/service.rs
index 36b3858e6..9ccf5274d 100644
--- a/src/service.rs
+++ b/src/service.rs
@@ -451,7 +451,7 @@ impl<B> ServiceResponse<B> {
     where
         B: MessageBody + 'static,
     {
-        self.map_body(|_, body| BoxBody::new(body))
+        self.map_body(|_, body| body.boxed())
     }
 }
 

From a2467718ac14d4ecf294ab1b2a398c47e97d47fa Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 01:27:27 +0000
Subject: [PATCH 12/28] passthrough StreamLog error type

---
 src/middleware/logger.rs | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs
index 74daa26d5..d7fdb234f 100644
--- a/src/middleware/logger.rs
+++ b/src/middleware/logger.rs
@@ -322,13 +322,10 @@ pin_project! {
     }
 }
 
-impl<B> MessageBody for StreamLog<B>
-where
-    B: MessageBody,
-    B::Error: Into<Error>,
-{
-    type Error = Error;
+impl<B: MessageBody> MessageBody for StreamLog<B> {
+    type Error = B::Error;
 
+    #[inline]
     fn size(&self) -> BodySize {
         self.body.size()
     }
@@ -344,7 +341,7 @@ where
                 *this.size += chunk.len();
                 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),
         }
     }

From 5359fa56c277362ae689f5170d36b74228291fb5 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 01:29:41 +0000
Subject: [PATCH 13/28] include source for dispatch body errors

---
 actix-http/src/error.rs         | 49 ++++++++++++++++-----------------
 actix-http/src/h1/dispatcher.rs |  2 +-
 actix-http/src/h1/service.rs    |  6 ++--
 actix-http/src/service.rs       |  6 ++--
 4 files changed, 31 insertions(+), 32 deletions(-)

diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs
index a04867ae1..3d2a918f4 100644
--- a/actix-http/src/error.rs
+++ b/actix-http/src/error.rs
@@ -332,31 +332,28 @@ impl From<PayloadError> for Error {
 }
 
 /// A set of errors that can occur during dispatching HTTP requests.
-#[derive(Debug, Display, Error, From)]
-#[non_exhaustive]
+#[derive(Debug, Display, From)]
 pub enum DispatchError {
-    /// Service error
-    // FIXME: display and error type
+    /// Service error.
     #[display(fmt = "Service Error")]
-    Service(#[error(not(source))] Response<BoxBody>),
+    Service(Response<BoxBody>),
 
-    /// Body error
-    // FIXME: display and error type
-    #[display(fmt = "Body Error")]
-    Body(#[error(not(source))] Box<dyn StdError>),
+    /// Body streaming error.
+    #[display(fmt = "Body error: {}", _0)]
+    Body(Box<dyn StdError>),
 
-    /// Upgrade service error
+    /// Upgrade service error.
     Upgrade,
 
     /// An `io::Error` that occurred while trying to read or write to a network stream.
     #[display(fmt = "IO error: {}", _0)]
     Io(io::Error),
 
-    /// Http request parse error.
-    #[display(fmt = "Parse error: {}", _0)]
+    /// Request parse error.
+    #[display(fmt = "Request parse error: {}", _0)]
     Parse(ParseError),
 
-    /// Http/2 error
+    /// HTTP/2 error.
     #[display(fmt = "{}", _0)]
     H2(h2::Error),
 
@@ -368,21 +365,23 @@ pub enum DispatchError {
     #[display(fmt = "Connection shutdown timeout")]
     DisconnectTimeout,
 
-    /// Payload is not consumed
-    #[display(fmt = "Task is completed but request's payload is not consumed")]
-    PayloadIsNotConsumed,
-
-    /// Malformed request
-    #[display(fmt = "Malformed request")]
-    MalformedRequest,
-
-    /// Internal error
+    /// Internal error.
     #[display(fmt = "Internal error")]
     InternalError,
+}
 
-    /// Unknown error
-    #[display(fmt = "Unknown error")]
-    Unknown,
+impl StdError for DispatchError {
+    fn source(&self) -> Option<&(dyn StdError + 'static)> {
+        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.
diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs
index 64bf83e03..16d7c3c11 100644
--- a/actix-http/src/h1/dispatcher.rs
+++ b/actix-http/src/h1/dispatcher.rs
@@ -458,7 +458,7 @@ where
                             }
 
                             Poll::Ready(Some(Err(err))) => {
-                                return Err(DispatchError::Service(err.into()))
+                                return Err(DispatchError::Body(err.into()))
                             }
 
                             Poll::Pending => return Ok(PollResponse::DoNothing),
diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs
index c4e6e7714..43b7919a7 100644
--- a/actix-http/src/h1/service.rs
+++ b/actix-http/src/h1/service.rs
@@ -356,9 +356,9 @@ where
     type Future = Dispatcher<T, S, B, X, U>;
 
     fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        self._poll_ready(cx).map_err(|e| {
-            log::error!("HTTP/1 service readiness error: {:?}", e);
-            DispatchError::Service(e)
+        self._poll_ready(cx).map_err(|err| {
+            log::error!("HTTP/1 service readiness error: {:?}", err);
+            DispatchError::Service(err)
         })
     }
 
diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs
index 93168749d..cd2efe678 100644
--- a/actix-http/src/service.rs
+++ b/actix-http/src/service.rs
@@ -493,9 +493,9 @@ where
     type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
 
     fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
-        self._poll_ready(cx).map_err(|e| {
-            log::error!("HTTP service readiness error: {:?}", e);
-            DispatchError::Service(e)
+        self._poll_ready(cx).map_err(|err| {
+            log::error!("HTTP service readiness error: {:?}", err);
+            DispatchError::Service(err)
         })
     }
 

From 2cf27863cb99a450d35a460c73a70919dfd29470 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 14:13:54 +0000
Subject: [PATCH 14/28] remove direct dep on pin-project in -http (#2524)

---
 actix-http/Cargo.toml           |   3 +-
 actix-http/src/h1/dispatcher.rs | 278 ++++++++++++++++++--------------
 2 files changed, 158 insertions(+), 123 deletions(-)

diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 2e8ec1dfc..515574ab1 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -45,7 +45,7 @@ __compress = []
 actix-service = "2.0.0"
 actix-codec = "0.4.1"
 actix-utils = "3.0.0"
-actix-rt = "2.2"
+actix-rt = { version = "2.2", default-features = false }
 
 ahash = "0.7"
 base64 = "0.13"
@@ -66,7 +66,6 @@ local-channel = "0.1"
 log = "0.4"
 mime = "0.3"
 percent-encoding = "2.1"
-pin-project = "1.0.0"
 pin-project-lite = "0.2"
 rand = "0.8"
 sha-1 = "0.9"
diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs
index 16d7c3c11..472845e65 100644
--- a/actix-http/src/h1/dispatcher.rs
+++ b/actix-http/src/h1/dispatcher.rs
@@ -15,7 +15,7 @@ use bitflags::bitflags;
 use bytes::{Buf, BytesMut};
 use futures_core::ready;
 use log::{error, trace};
-use pin_project::pin_project;
+use pin_project_lite::pin_project;
 
 use crate::{
     body::{BodySize, BoxBody, MessageBody},
@@ -46,79 +46,111 @@ bitflags! {
     }
 }
 
-#[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>>,
+// there's 2 versions of Dispatcher state because of:
+// https://github.com/taiki-e/pin-project-lite/issues/3
+//
+// tl;dr: pin-project-lite doesn't play well with other attribute macros
 
-    B: MessageBody,
+#[cfg(not(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>>,
 
-    X: Service<Request, Response = Request>,
-    X::Error: Into<Response<BoxBody>>,
+        B: MessageBody,
 
-    U: Service<(Request, Framed<T, Codec>), Response = ()>,
-    U::Error: fmt::Display,
-{
-    #[pin]
-    inner: DispatcherState<T, S, B, X, U>,
+        X: Service<Request, Response = Request>,
+        X::Error: Into<Response<BoxBody>>,
 
-    #[cfg(test)]
-    poll_count: u64,
+        U: Service<(Request, Framed<T, Codec>), Response = ()>,
+        U::Error: fmt::Display,
+    {
+        #[pin]
+        inner: DispatcherState<T, S, B, X, U>,
+    }
 }
 
-#[pin_project(project = DispatcherStateProj)]
-enum DispatcherState<T, S, B, X, U>
-where
-    S: Service<Request>,
-    S::Error: Into<Response<BoxBody>>,
+#[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,
+        B: MessageBody,
 
-    X: Service<Request, Response = Request>,
-    X::Error: Into<Response<BoxBody>>,
+        X: Service<Request, Response = Request>,
+        X::Error: Into<Response<BoxBody>>,
 
-    U: Service<(Request, Framed<T, Codec>), Response = ()>,
-    U::Error: fmt::Display,
-{
-    Normal(#[pin] InnerDispatcher<T, S, B, X, U>),
-    Upgrade(#[pin] U::Future),
+        U: Service<(Request, Framed<T, Codec>), Response = ()>,
+        U::Error: fmt::Display,
+    {
+        #[pin]
+        inner: DispatcherState<T, S, B, X, U>,
+
+        // used in tests
+        poll_count: u64,
+    }
 }
 
-#[pin_project(project = InnerDispatcherProj)]
-struct InnerDispatcher<T, S, B, X, U>
-where
-    S: Service<Request>,
-    S::Error: Into<Response<BoxBody>>,
+pin_project! {
+    #[project = DispatcherStateProj]
+    enum DispatcherState<T, S, B, X, U>
+    where
+        S: Service<Request>,
+        S::Error: Into<Response<BoxBody>>,
 
-    B: MessageBody,
+        B: MessageBody,
 
-    X: Service<Request, Response = Request>,
-    X::Error: Into<Response<BoxBody>>,
+        X: Service<Request, Response = Request>,
+        X::Error: Into<Response<BoxBody>>,
 
-    U: Service<(Request, Framed<T, Codec>), Response = ()>,
-    U::Error: fmt::Display,
-{
-    flow: Rc<HttpFlow<S, X, U>>,
-    flags: Flags,
-    peer_addr: Option<net::SocketAddr>,
-    conn_data: Option<Rc<Extensions>>,
-    error: Option<DispatchError>,
+        U: Service<(Request, Framed<T, Codec>), Response = ()>,
+        U::Error: fmt::Display,
+    {
+        Normal { #[pin] inner: InnerDispatcher<T, S, B, X, U> },
+        Upgrade { #[pin] fut: U::Future },
+    }
+}
 
-    #[pin]
-    state: State<S, B, X>,
-    payload: Option<PayloadSender>,
-    messages: VecDeque<DispatcherMessage>,
+pin_project! {
+    #[project = InnerDispatcherProj]
+    struct InnerDispatcher<T, S, B, X, U>
+    where
+        S: Service<Request>,
+        S::Error: Into<Response<BoxBody>>,
 
-    ka_expire: Instant,
-    #[pin]
-    ka_timer: Option<Sleep>,
+        B: MessageBody,
 
-    io: Option<T>,
-    read_buf: BytesMut,
-    write_buf: BytesMut,
-    codec: Codec,
+        X: Service<Request, Response = Request>,
+        X::Error: Into<Response<BoxBody>>,
+
+        U: Service<(Request, Framed<T, Codec>), Response = ()>,
+        U::Error: fmt::Display,
+    {
+        flow: Rc<HttpFlow<S, X, U>>,
+        flags: Flags,
+        peer_addr: Option<net::SocketAddr>,
+        conn_data: Option<Rc<Extensions>>,
+        error: Option<DispatchError>,
+
+        #[pin]
+        state: State<S, B, X>,
+        payload: Option<PayloadSender>,
+        messages: VecDeque<DispatcherMessage>,
+
+        ka_expire: Instant,
+        #[pin]
+        ka_timer: Option<Sleep>,
+
+        io: Option<T>,
+        read_buf: BytesMut,
+        write_buf: BytesMut,
+        codec: Codec,
+    }
 }
 
 enum DispatcherMessage {
@@ -127,19 +159,21 @@ enum DispatcherMessage {
     Error(Response<()>),
 }
 
-#[pin_project(project = StateProj)]
-enum State<S, B, X>
-where
-    S: Service<Request>,
-    X: Service<Request, Response = Request>,
+pin_project! {
+    #[project = StateProj]
+    enum State<S, B, X>
+    where
+        S: Service<Request>,
+        X: Service<Request, Response = Request>,
 
-    B: MessageBody,
-{
-    None,
-    ExpectCall(#[pin] X::Future),
-    ServiceCall(#[pin] S::Future),
-    SendPayload(#[pin] B),
-    SendErrorPayload(#[pin] BoxBody),
+        B: MessageBody,
+    {
+        None,
+        ExpectCall { #[pin] fut: X::Future },
+        ServiceCall { #[pin] fut: S::Future },
+        SendPayload { #[pin] body: B },
+        SendErrorPayload { #[pin] body: BoxBody },
+    }
 }
 
 impl<S, B, X> State<S, B, X>
@@ -198,25 +232,27 @@ where
         };
 
         Dispatcher {
-            inner: DispatcherState::Normal(InnerDispatcher {
-                flow,
-                flags,
-                peer_addr,
-                conn_data: conn_data.0.map(Rc::new),
-                error: None,
+            inner: DispatcherState::Normal {
+                inner: InnerDispatcher {
+                    flow,
+                    flags,
+                    peer_addr,
+                    conn_data: conn_data.0.map(Rc::new),
+                    error: None,
 
-                state: State::None,
-                payload: None,
-                messages: VecDeque::new(),
+                    state: State::None,
+                    payload: None,
+                    messages: VecDeque::new(),
 
-                ka_expire,
-                ka_timer,
+                    ka_expire,
+                    ka_timer,
 
-                io: Some(io),
-                read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
-                write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
-                codec: Codec::new(config),
-            }),
+                    io: Some(io),
+                    read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
+                    write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
+                    codec: Codec::new(config),
+                },
+            },
 
             #[cfg(test)]
             poll_count: 0,
@@ -316,7 +352,7 @@ where
         let size = self.as_mut().send_response_inner(message, &body)?;
         let state = match size {
             BodySize::None | BodySize::Sized(0) => State::None,
-            _ => State::SendPayload(body),
+            _ => State::SendPayload { body },
         };
         self.project().state.set(state);
         Ok(())
@@ -330,7 +366,7 @@ where
         let size = self.as_mut().send_response_inner(message, &body)?;
         let state = match size {
             BodySize::None | BodySize::Sized(0) => State::None,
-            _ => State::SendErrorPayload(body),
+            _ => State::SendErrorPayload { body },
         };
         self.project().state.set(state);
         Ok(())
@@ -356,12 +392,12 @@ where
                         // Handle `EXPECT: 100-Continue` header
                         if req.head().expect() {
                             // set InnerDispatcher state and continue loop to poll it.
-                            let task = this.flow.expect.call(req);
-                            this.state.set(State::ExpectCall(task));
+                            let fut = this.flow.expect.call(req);
+                            this.state.set(State::ExpectCall { fut });
                         } else {
                             // the same as expect call.
-                            let task = this.flow.service.call(req);
-                            this.state.set(State::ServiceCall(task));
+                            let fut = this.flow.service.call(req);
+                            this.state.set(State::ServiceCall { fut });
                         };
                     }
 
@@ -381,7 +417,7 @@ where
                     // all messages are dealt with.
                     None => return Ok(PollResponse::DoNothing),
                 },
-                StateProj::ServiceCall(fut) => match fut.poll(cx) {
+                StateProj::ServiceCall { fut } => match fut.poll(cx) {
                     // service call resolved. send response.
                     Poll::Ready(Ok(res)) => {
                         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,
                     // get blocked or finished.
                     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))) => {
                                 this.codec
                                     .encode(Message::Chunk(Some(item)), this.write_buf)?;
@@ -437,13 +473,13 @@ where
                     return Ok(PollResponse::DrainWriteBuf);
                 }
 
-                StateProj::SendErrorPayload(mut stream) => {
+                StateProj::SendErrorPayload { mut body } => {
                     // TODO: de-dupe impl with SendPayload
 
                     // keep populate writer buffer until buffer size limit hit,
                     // get blocked or finished.
                     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))) => {
                                 this.codec
                                     .encode(Message::Chunk(Some(item)), this.write_buf)?;
@@ -469,14 +505,14 @@ where
                     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
                     // to service call.
                     Poll::Ready(Ok(req)) => {
                         this.write_buf
                             .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n");
                         let fut = this.flow.service.call(req);
-                        this.state.set(State::ServiceCall(fut));
+                        this.state.set(State::ServiceCall { fut });
                     }
 
                     // send expect error as response
@@ -502,25 +538,25 @@ where
         let mut this = self.as_mut().project();
         if req.head().expect() {
             // set dispatcher state so the future is pinned.
-            let task = this.flow.expect.call(req);
-            this.state.set(State::ExpectCall(task));
+            let fut = this.flow.expect.call(req);
+            this.state.set(State::ExpectCall { fut });
         } else {
             // the same as above.
-            let task = this.flow.service.call(req);
-            this.state.set(State::ServiceCall(task));
+            let fut = this.flow.service.call(req);
+            this.state.set(State::ServiceCall { fut });
         };
 
         // eagerly poll the future for once(or twice if expect is resolved immediately).
         loop {
             match self.as_mut().project().state.project() {
-                StateProj::ExpectCall(fut) => {
+                StateProj::ExpectCall { fut } => {
                     match fut.poll(cx) {
                         // expect is resolved. continue loop and poll the service call branch.
                         Poll::Ready(Ok(req)) => {
                             self.as_mut().send_continue();
                             let mut this = self.as_mut().project();
-                            let task = this.flow.service.call(req);
-                            this.state.set(State::ServiceCall(task));
+                            let fut = this.flow.service.call(req);
+                            this.state.set(State::ServiceCall { fut });
                             continue;
                         }
                         // future is pending. return Ok(()) to notify that a new state is
@@ -536,7 +572,7 @@ where
                         }
                     }
                 }
-                StateProj::ServiceCall(fut) => {
+                StateProj::ServiceCall { fut } => {
                     // return no matter the service call future's result.
                     return match fut.poll(cx) {
                         // future is resolved. send response and return a result. On success
@@ -901,7 +937,7 @@ where
         }
 
         match this.inner.project() {
-            DispatcherStateProj::Normal(mut inner) => {
+            DispatcherStateProj::Normal { mut inner } => {
                 inner.as_mut().poll_keepalive(cx)?;
 
                 if inner.flags.contains(Flags::SHUTDOWN) {
@@ -941,7 +977,7 @@ where
                                 self.as_mut()
                                     .project()
                                     .inner
-                                    .set(DispatcherState::Upgrade(upgrade));
+                                    .set(DispatcherState::Upgrade { fut: upgrade });
                                 return self.poll(cx);
                             }
                         };
@@ -993,8 +1029,8 @@ where
                     }
                 }
             }
-            DispatcherStateProj::Upgrade(fut) => fut.poll(cx).map_err(|e| {
-                error!("Upgrade handler error: {}", e);
+            DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
+                error!("Upgrade handler error: {}", err);
                 DispatchError::Upgrade
             }),
         }
@@ -1088,7 +1124,7 @@ mod tests {
                 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_eq!(
                     &inner.project().io.take().unwrap().write_buf[..26],
@@ -1123,7 +1159,7 @@ mod tests {
 
             actix_rt::pin!(h1);
 
-            assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
+            assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
 
             match h1.as_mut().poll(cx) {
                 Poll::Pending => panic!("first poll should not be pending"),
@@ -1133,7 +1169,7 @@ mod tests {
             // polls: initial => shutdown
             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[..];
                 stabilize_date_header(res);
 
@@ -1177,7 +1213,7 @@ mod tests {
 
             actix_rt::pin!(h1);
 
-            assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
+            assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
 
             match h1.as_mut().poll(cx) {
                 Poll::Pending => panic!("first poll should not be pending"),
@@ -1187,7 +1223,7 @@ mod tests {
             // polls: initial => shutdown
             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[..];
                 stabilize_date_header(res);
 
@@ -1237,13 +1273,13 @@ mod tests {
             actix_rt::pin!(h1);
 
             assert!(h1.as_mut().poll(cx).is_pending());
-            assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
+            assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
 
             // polls: manual
             assert_eq!(h1.poll_count, 1);
             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 res = &io.write_buf()[..];
                 assert_eq!(
@@ -1258,7 +1294,7 @@ mod tests {
             // polls: manual manual shutdown
             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 mut res = (&io.write_buf()[..]).to_owned();
                 stabilize_date_header(&mut res);
@@ -1309,12 +1345,12 @@ mod tests {
             actix_rt::pin!(h1);
 
             assert!(h1.as_mut().poll(cx).is_ready());
-            assert!(matches!(&h1.inner, DispatcherState::Normal(_)));
+            assert!(matches!(&h1.inner, DispatcherState::Normal { .. }));
 
             // polls: manual shutdown
             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 mut res = (&io.write_buf()[..]).to_owned();
                 stabilize_date_header(&mut res);
@@ -1386,7 +1422,7 @@ mod tests {
             actix_rt::pin!(h1);
 
             assert!(h1.as_mut().poll(cx).is_ready());
-            assert!(matches!(&h1.inner, DispatcherState::Upgrade(_)));
+            assert!(matches!(&h1.inner, DispatcherState::Upgrade { .. }));
 
             // polls: manual shutdown
             assert_eq!(h1.poll_count, 2);

From 57ea322ce5acefba4f17cb090d9dc5b7da4c1de1 Mon Sep 17 00:00:00 2001
From: Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>
Date: Fri, 17 Dec 2021 22:09:08 +0300
Subject: [PATCH 15/28] simplify MessageBody::complete_body interface (#2522)

---
 actix-http/src/body/boxed.rs        |  57 ++++--
 actix-http/src/body/either.rs       |  18 +-
 actix-http/src/body/message_body.rs | 289 +++++++---------------------
 actix-http/src/body/none.rs         |   9 +-
 actix-http/src/encoding/encoder.rs  |  54 +++---
 actix-http/src/h1/dispatcher.rs     |   6 +-
 src/dev.rs                          |   8 +
 7 files changed, 149 insertions(+), 292 deletions(-)

diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs
index 7581bec88..a2d7540c4 100644
--- a/actix-http/src/body/boxed.rs
+++ b/actix-http/src/body/boxed.rs
@@ -8,10 +8,16 @@ use std::{
 use bytes::Bytes;
 
 use super::{BodySize, MessageBody, MessageBodyMapErr};
-use crate::Error;
+use crate::body;
 
 /// A boxed message body with boxed errors.
-pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
+pub struct BoxBody(BoxBodyInner);
+
+enum BoxBodyInner {
+    None(body::None),
+    Bytes(Bytes),
+    Stream(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>),
+}
 
 impl BoxBody {
     /// Same as `MessageBody::boxed`.
@@ -23,29 +29,42 @@ impl BoxBody {
     where
         B: MessageBody + 'static,
     {
-        let body = MessageBodyMapErr::new(body, Into::into);
-        Self(Box::pin(body))
+        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);
+                    Self(BoxBodyInner::Stream(Box::pin(body)))
+                }
+            },
+        }
     }
 
     /// Returns a mutable pinned reference to the inner message body type.
     #[inline]
-    pub fn as_pin_mut(&mut self) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
-        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 {
+        // TODO show BoxBodyInner
         f.write_str("BoxBody(dyn MessageBody)")
     }
 }
 
 impl MessageBody for BoxBody {
-    type Error = Error;
+    type Error = Box<dyn StdError>;
 
     #[inline]
     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]
@@ -53,20 +72,20 @@ impl MessageBody for BoxBody {
         mut self: Pin<&mut Self>,
         cx: &mut Context<'_>,
     ) -> Poll<Option<Result<Bytes, Self::Error>>> {
-        self.0
-            .as_mut()
-            .poll_next(cx)
-            .map_err(|err| Error::new_body().with_cause(err))
+        match &mut self.0 {
+            BoxBodyInner::None(_) => Poll::Ready(None),
+            BoxBodyInner::Bytes(bytes) => Pin::new(bytes).poll_next(cx).map_err(Into::into),
+            BoxBodyInner::Stream(stream) => Pin::new(stream).poll_next(cx),
+        }
     }
 
     #[inline]
-    fn is_complete_body(&self) -> bool {
-        self.0.is_complete_body()
-    }
-
-    #[inline]
-    fn take_complete_body(&mut self) -> Bytes {
-        self.0.take_complete_body()
+    fn try_into_bytes(self) -> Result<Bytes, Self> {
+        match self.0 {
+            BoxBodyInner::None(none) => Ok(none.try_into_bytes().unwrap()),
+            BoxBodyInner::Bytes(bytes) => Ok(bytes.try_into_bytes().unwrap()),
+            _ => Err(self),
+        }
     }
 
     #[inline]
diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs
index 3a4082dc9..add1eab7c 100644
--- a/actix-http/src/body/either.rs
+++ b/actix-http/src/body/either.rs
@@ -74,18 +74,14 @@ where
     }
 
     #[inline]
-    fn is_complete_body(&self) -> bool {
+    fn try_into_bytes(self) -> Result<Bytes, Self> {
         match self {
-            EitherBody::Left { body } => body.is_complete_body(),
-            EitherBody::Right { body } => body.is_complete_body(),
-        }
-    }
-
-    #[inline]
-    fn take_complete_body(&mut self) -> Bytes {
-        match self {
-            EitherBody::Left { body } => body.take_complete_body(),
-            EitherBody::Right { body } => body.take_complete_body(),
+            EitherBody::Left { body } => body
+                .try_into_bytes()
+                .map_err(|body| EitherBody::Left { body }),
+            EitherBody::Right { body } => body
+                .try_into_bytes()
+                .map_err(|body| EitherBody::Right { body }),
         }
     }
 
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index 075ae7220..bd13e75ec 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -31,51 +31,14 @@ pub trait MessageBody {
         cx: &mut Context<'_>,
     ) -> Poll<Option<Result<Bytes, Self::Error>>>;
 
-    /// Returns true if entire body bytes chunk is obtainable in one call to `poll_next`.
+    /// Convert this body into `Bytes`.
     ///
-    /// This method's implementation should agree with [`take_complete_body`] and should always be
-    /// checked before taking the body.
-    ///
-    /// The default implementation returns `false.
-    ///
-    /// [`take_complete_body`]: MessageBody::take_complete_body
-    fn is_complete_body(&self) -> bool {
-        false
-    }
-
-    /// Returns the complete chunk of body bytes.
-    ///
-    /// Implementors of this method should note the following:
-    /// - It is acceptable to skip the omit checks of [`is_complete_body`]. The responsibility of
-    ///   performing this check is delegated to the caller.
-    /// - If the result of [`is_complete_body`] is conditional, that condition should be given
-    ///   equivalent attention here.
-    /// - 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>()
-        );
+    /// Bodies with `BodySize::None` are allowed to return empty `Bytes`.
+    fn try_into_bytes(self) -> Result<Bytes, Self>
+    where
+        Self: Sized,
+    {
+        Err(self)
     }
 
     /// Converts this body into `BoxBody`.
@@ -104,14 +67,6 @@ mod foreign_impls {
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             match *self {}
         }
-
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        fn take_complete_body(&mut self) -> Bytes {
-            match *self {}
-        }
     }
 
     impl MessageBody for () {
@@ -131,13 +86,8 @@ mod foreign_impls {
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            Bytes::new()
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(Bytes::new())
         }
     }
 
@@ -159,16 +109,6 @@ mod foreign_impls {
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             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>>
@@ -189,38 +129,6 @@ mod foreign_impls {
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             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_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] {
@@ -232,24 +140,19 @@ mod foreign_impls {
         }
 
         fn poll_next(
-            mut self: Pin<&mut Self>,
+            self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             if self.is_empty() {
                 Poll::Ready(None)
             } else {
-                Poll::Ready(Some(Ok(self.take_complete_body())))
+                Poll::Ready(Some(Ok(Bytes::from_static(mem::take(self.get_mut())))))
             }
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            Bytes::from_static(mem::take(self))
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(Bytes::from_static(self))
         }
     }
 
@@ -262,24 +165,19 @@ mod foreign_impls {
         }
 
         fn poll_next(
-            mut self: Pin<&mut Self>,
+            self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             if self.is_empty() {
                 Poll::Ready(None)
             } else {
-                Poll::Ready(Some(Ok(self.take_complete_body())))
+                Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
             }
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            mem::take(self)
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(self)
         }
     }
 
@@ -292,24 +190,19 @@ mod foreign_impls {
         }
 
         fn poll_next(
-            mut self: Pin<&mut Self>,
+            self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             if self.is_empty() {
                 Poll::Ready(None)
             } else {
-                Poll::Ready(Some(Ok(self.take_complete_body())))
+                Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
             }
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            mem::take(self).freeze()
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(self.freeze())
         }
     }
 
@@ -322,24 +215,19 @@ mod foreign_impls {
         }
 
         fn poll_next(
-            mut self: Pin<&mut Self>,
+            self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
         ) -> Poll<Option<Result<Bytes, Self::Error>>> {
             if self.is_empty() {
                 Poll::Ready(None)
             } else {
-                Poll::Ready(Some(Ok(self.take_complete_body())))
+                Poll::Ready(Some(Ok(mem::take(self.get_mut()).into())))
             }
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            Bytes::from(mem::take(self))
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(Bytes::from(self))
         }
     }
 
@@ -365,13 +253,8 @@ mod foreign_impls {
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            Bytes::from_static(mem::take(self).as_bytes())
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(Bytes::from_static(self.as_bytes()))
         }
     }
 
@@ -396,13 +279,8 @@ mod foreign_impls {
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            Bytes::from(mem::take(self))
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(Bytes::from(self))
         }
     }
 
@@ -423,13 +301,8 @@ mod foreign_impls {
         }
 
         #[inline]
-        fn is_complete_body(&self) -> bool {
-            true
-        }
-
-        #[inline]
-        fn take_complete_body(&mut self) -> Bytes {
-            mem::take(self).into_bytes()
+        fn try_into_bytes(self) -> Result<Bytes, Self> {
+            Ok(self.into_bytes())
         }
     }
 }
@@ -486,13 +359,9 @@ where
     }
 
     #[inline]
-    fn is_complete_body(&self) -> bool {
-        self.body.is_complete_body()
-    }
-
-    #[inline]
-    fn take_complete_body(&mut self) -> Bytes {
-        self.body.take_complete_body()
+    fn try_into_bytes(self) -> Result<Bytes, Self> {
+        let Self { body, mapper } = self;
+        body.try_into_bytes().map_err(|body| Self { body, mapper })
     }
 }
 
@@ -503,6 +372,7 @@ mod tests {
     use bytes::{Bytes, BytesMut};
 
     use super::*;
+    use crate::body::{self, EitherBody};
 
     macro_rules! assert_poll_next {
         ($pin:expr, $exp:expr) => {
@@ -604,70 +474,45 @@ mod tests {
         assert_poll_next!(pl, Bytes::from("test"));
     }
 
-    #[test]
-    fn take_string() {
-        let mut data = "test".repeat(2);
-        let data_bytes = Bytes::from(data.clone());
-        assert!(data.is_complete_body());
-        assert_eq!(data.take_complete_body(), data_bytes);
-
-        let mut big_data = "test".repeat(64 * 1024);
-        let data_bytes = Bytes::from(big_data.clone());
-        assert!(big_data.is_complete_body());
-        assert_eq!(big_data.take_complete_body(), data_bytes);
-    }
-
-    #[test]
-    fn take_boxed_equivalence() {
-        let mut data = Bytes::from_static(b"test");
-        assert!(data.is_complete_body());
-        assert_eq!(data.take_complete_body(), b"test".as_ref());
-
-        let mut data = Box::new(Bytes::from_static(b"test"));
-        assert!(data.is_complete_body());
-        assert_eq!(data.take_complete_body(), b"test".as_ref());
-
-        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]
-    fn take_policy() {
-        let mut data = Bytes::from_static(b"test");
-        // first call returns chunk
-        assert_eq!(data.take_complete_body(), b"test".as_ref());
-        // second call returns empty
-        assert_eq!(data.take_complete_body(), b"".as_ref());
-
-        let waker = futures_task::noop_waker();
-        let mut cx = Context::from_waker(&waker);
-        let mut data = Bytes::from_static(b"test");
-        // 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));
-    }
-
-    #[test]
-    fn complete_body_combinators() {
-        use crate::body::{BoxBody, EitherBody};
-
+    #[actix_rt::test]
+    async fn complete_body_combinators() {
+        let body = Bytes::from_static(b"test");
+        let body = BoxBody::new(body);
+        let body = EitherBody::<_, ()>::left(body);
+        let body = EitherBody::<(), _>::right(body);
+        // Do not support try_into_bytes:
+        // let body = Box::new(body);
+        // let body = Box::pin(body);
+
+        assert_eq!(body.try_into_bytes().unwrap(), Bytes::from("test"));
+    }
+
+    #[actix_rt::test]
+    async fn complete_body_combinators_poll() {
         let body = Bytes::from_static(b"test");
         let body = BoxBody::new(body);
         let body = EitherBody::<_, ()>::left(body);
         let body = EitherBody::<(), _>::right(body);
-        let body = Box::new(body);
-        let body = Box::pin(body);
         let mut body = body;
 
-        assert!(body.is_complete_body());
-        assert_eq!(body.take_complete_body(), b"test".as_ref());
+        assert_eq!(body.size(), BodySize::Sized(4));
+        assert_poll_next!(Pin::new(&mut body), Bytes::from("test"));
+        assert_poll_next_none!(Pin::new(&mut body));
+    }
 
-        // subsequent poll_next returns None
-        let waker = futures_task::noop_waker();
-        let mut cx = Context::from_waker(&waker);
-        assert!(Pin::new(&mut body).poll_next(&mut cx).map_err(drop) == Poll::Ready(None));
+    #[actix_rt::test]
+    async fn none_body_combinators() {
+        fn none_body() -> BoxBody {
+            let body = body::None;
+            let body = BoxBody::new(body);
+            let body = EitherBody::<_, ()>::left(body);
+            let body = EitherBody::<(), _>::right(body);
+            body.boxed()
+        }
+
+        assert_eq!(none_body().size(), BodySize::None);
+        assert_eq!(none_body().try_into_bytes().unwrap(), Bytes::new());
+        assert_poll_next_none!(Pin::new(&mut none_body()));
     }
 
     // down-casting used to be done with a method on MessageBody trait
diff --git a/actix-http/src/body/none.rs b/actix-http/src/body/none.rs
index bb494078f..0e7bbe5a9 100644
--- a/actix-http/src/body/none.rs
+++ b/actix-http/src/body/none.rs
@@ -42,12 +42,7 @@ impl MessageBody for None {
     }
 
     #[inline]
-    fn is_complete_body(&self) -> bool {
-        true
-    }
-
-    #[inline]
-    fn take_complete_body(&mut self) -> Bytes {
-        Bytes::new()
+    fn try_into_bytes(self) -> Result<Bytes, Self> {
+        Ok(Bytes::new())
     }
 }
diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs
index fa294ab0d..70448a115 100644
--- a/actix-http/src/encoding/encoder.rs
+++ b/actix-http/src/encoding/encoder.rs
@@ -53,7 +53,7 @@ impl<B: MessageBody> Encoder<B> {
         }
     }
 
-    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)
             || head.status == StatusCode::SWITCHING_PROTOCOLS
             || head.status == StatusCode::NO_CONTENT
@@ -65,11 +65,9 @@ impl<B: MessageBody> Encoder<B> {
             return Self::none();
         }
 
-        let body = if body.is_complete_body() {
-            let body = body.take_complete_body();
-            EncoderBody::Full { body }
-        } else {
-            EncoderBody::Stream { body }
+        let body = match body.try_into_bytes() {
+            Ok(body) => EncoderBody::Full { body },
+            Err(body) => EncoderBody::Stream { body },
         };
 
         if can_encode {
@@ -133,21 +131,14 @@ where
         }
     }
 
-    fn is_complete_body(&self) -> bool {
+    fn try_into_bytes(self) -> Result<Bytes, Self>
+    where
+        Self: Sized,
+    {
         match self {
-            EncoderBody::None => true,
-            EncoderBody::Full { .. } => true,
-            EncoderBody::Stream { .. } => false,
-        }
-    }
-
-    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")
-            }
+            EncoderBody::None => Ok(Bytes::new()),
+            EncoderBody::Full { body } => Ok(body),
+            _ => Err(self),
         }
     }
 }
@@ -234,19 +225,20 @@ where
         }
     }
 
-    fn is_complete_body(&self) -> bool {
+    fn try_into_bytes(mut self) -> Result<Bytes, Self>
+    where
+        Self: Sized,
+    {
         if self.encoder.is_some() {
-            false
+            Err(self)
         } else {
-            self.body.is_complete_body()
-        }
-    }
-
-    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()
+            match self.body.try_into_bytes() {
+                Ok(body) => Ok(body),
+                Err(body) => {
+                    self.body = body;
+                    Err(self)
+                }
+            }
         }
     }
 }
diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs
index 472845e65..5c0cb64af 100644
--- a/actix-http/src/h1/dispatcher.rs
+++ b/actix-http/src/h1/dispatcher.rs
@@ -22,7 +22,7 @@ use crate::{
     config::ServiceConfig,
     error::{DispatchError, ParseError, PayloadError},
     service::HttpFlow,
-    Extensions, OnConnectData, Request, Response, StatusCode,
+    Error, Extensions, OnConnectData, Request, Response, StatusCode,
 };
 
 use super::{
@@ -494,7 +494,9 @@ where
                             }
 
                             Poll::Ready(Some(Err(err))) => {
-                                return Err(DispatchError::Body(err.into()))
+                                return Err(DispatchError::Body(
+                                    Error::new_body().with_cause(err).into(),
+                                ))
                             }
 
                             Poll::Pending => return Ok(PollResponse::DoNothing),
diff --git a/src/dev.rs b/src/dev.rs
index d4a64985c..edcc158f8 100644
--- a/src/dev.rs
+++ b/src/dev.rs
@@ -139,4 +139,12 @@ impl crate::body::MessageBody for AnyBody {
             AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx),
         }
     }
+
+    fn try_into_bytes(self) -> Result<crate::web::Bytes, Self> {
+        match self {
+            AnyBody::None => Ok(crate::web::Bytes::new()),
+            AnyBody::Full { body } => Ok(body),
+            _ => Err(self),
+        }
+    }
 }

From aa31086af5ce0c67eeeec3fc8858cc35770d8a40 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 19:16:42 +0000
Subject: [PATCH 16/28] improve BoxBody Debug impl

---
 actix-http/src/body/boxed.rs | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs
index a2d7540c4..0c6950a1f 100644
--- a/actix-http/src/body/boxed.rs
+++ b/actix-http/src/body/boxed.rs
@@ -11,6 +11,7 @@ use super::{BodySize, MessageBody, MessageBodyMapErr};
 use crate::body;
 
 /// A boxed message body with boxed errors.
+#[derive(Debug)]
 pub struct BoxBody(BoxBodyInner);
 
 enum BoxBodyInner {
@@ -19,6 +20,16 @@ enum BoxBodyInner {
     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 {
     /// Same as `MessageBody::boxed`.
     ///
@@ -48,13 +59,6 @@ impl BoxBody {
     }
 }
 
-impl fmt::Debug for BoxBody {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        // TODO show BoxBodyInner
-        f.write_str("BoxBody(dyn MessageBody)")
-    }
-}
-
 impl MessageBody for BoxBody {
     type Error = Box<dyn StdError>;
 

From 1d6f5ba6d696515d8b8eb71f0e353b2fc9a797b8 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 19:19:21 +0000
Subject: [PATCH 17/28] improve codegen on BoxBody poll_next

---
 actix-http/src/body/boxed.rs        | 14 +++++++++-----
 actix-http/src/body/message_body.rs | 10 +++++++++-
 actix-http/src/encoding/encoder.rs  |  4 ++++
 3 files changed, 22 insertions(+), 6 deletions(-)

diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs
index 0c6950a1f..d109a6a74 100644
--- a/actix-http/src/body/boxed.rs
+++ b/actix-http/src/body/boxed.rs
@@ -77,17 +77,21 @@ impl MessageBody for BoxBody {
         cx: &mut Context<'_>,
     ) -> Poll<Option<Result<Bytes, Self::Error>>> {
         match &mut self.0 {
-            BoxBodyInner::None(_) => Poll::Ready(None),
-            BoxBodyInner::Bytes(bytes) => Pin::new(bytes).poll_next(cx).map_err(Into::into),
-            BoxBodyInner::Stream(stream) => Pin::new(stream).poll_next(cx),
+            BoxBodyInner::None(body) => {
+                Pin::new(body).poll_next(cx).map_err(|err| match err {})
+            }
+            BoxBodyInner::Bytes(body) => {
+                Pin::new(body).poll_next(cx).map_err(|err| match err {})
+            }
+            BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
         }
     }
 
     #[inline]
     fn try_into_bytes(self) -> Result<Bytes, Self> {
         match self.0 {
-            BoxBodyInner::None(none) => Ok(none.try_into_bytes().unwrap()),
-            BoxBodyInner::Bytes(bytes) => Ok(bytes.try_into_bytes().unwrap()),
+            BoxBodyInner::None(body) => Ok(body.try_into_bytes().unwrap()),
+            BoxBodyInner::Bytes(body) => Ok(body.try_into_bytes().unwrap()),
             _ => Err(self),
         }
     }
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index bd13e75ec..cf65bac2d 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -33,7 +33,8 @@ pub trait MessageBody {
 
     /// Convert this body into `Bytes`.
     ///
-    /// Bodies with `BodySize::None` are allowed to return empty `Bytes`.
+    /// Body types with `BodySize::None` are allowed to return empty `Bytes`.
+    #[inline]
     fn try_into_bytes(self) -> Result<Bytes, Self>
     where
         Self: Sized,
@@ -139,6 +140,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -164,6 +166,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -189,6 +192,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -214,6 +218,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -239,6 +244,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -266,6 +272,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
@@ -292,6 +299,7 @@ mod foreign_impls {
             BodySize::Sized(self.len() as u64)
         }
 
+        #[inline]
         fn poll_next(
             self: Pin<&mut Self>,
             _cx: &mut Context<'_>,
diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs
index 70448a115..d06d6b4d8 100644
--- a/actix-http/src/encoding/encoder.rs
+++ b/actix-http/src/encoding/encoder.rs
@@ -108,6 +108,7 @@ where
 {
     type Error = EncoderError;
 
+    #[inline]
     fn size(&self) -> BodySize {
         match self {
             EncoderBody::None => BodySize::None,
@@ -131,6 +132,7 @@ where
         }
     }
 
+    #[inline]
     fn try_into_bytes(self) -> Result<Bytes, Self>
     where
         Self: Sized,
@@ -149,6 +151,7 @@ where
 {
     type Error = EncoderError;
 
+    #[inline]
     fn size(&self) -> BodySize {
         if self.encoder.is_some() {
             BodySize::Stream
@@ -225,6 +228,7 @@ where
         }
     }
 
+    #[inline]
     fn try_into_bytes(mut self) -> Result<Bytes, Self>
     where
         Self: Sized,

From 5842a3279daff2a34bdf1151d317ebbe813288c7 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 19:35:08 +0000
Subject: [PATCH 18/28] update messagebody documentation

---
 actix-http/CHANGES.md               |  9 ++++++++-
 actix-http/src/body/message_body.rs | 20 ++++++++++++++++----
 actix-http/src/response.rs          |  2 +-
 3 files changed, 25 insertions(+), 6 deletions(-)

diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index 598ef9c0e..806e32cc0 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,12 +1,19 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+### 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
@@ -27,7 +34,7 @@
 * `Request::take_conn_data()`. [#2491]
 * `Request::take_req_data()`. [#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
diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs
index cf65bac2d..0a605a69a 100644
--- a/actix-http/src/body/message_body.rs
+++ b/actix-http/src/body/message_body.rs
@@ -17,11 +17,15 @@ use super::{BodySize, BoxBody};
 /// An interface types that can converted to bytes and used as response bodies.
 // TODO: examples
 pub trait MessageBody {
-    // TODO: consider this bound to only fmt::Display since the error type is not really used
-    // and there is an impl for Into<Box<StdError>> on String
+    /// The type of error that will be returned if streaming body fails.
+    ///
+    /// 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>>;
 
     /// Body size hint.
+    ///
+    /// If [`BodySize::None`] is returned, optimizations that skip reading the body are allowed.
     fn size(&self) -> BodySize;
 
     /// Attempt to pull out the next chunk of body bytes.
@@ -31,9 +35,17 @@ pub trait MessageBody {
         cx: &mut Context<'_>,
     ) -> Poll<Option<Result<Bytes, Self::Error>>>;
 
-    /// Convert this body into `Bytes`.
+    /// Try to convert into the complete chunk of body bytes.
     ///
-    /// Body types with `BodySize::None` are allowed to return empty `Bytes`.
+    /// Implement this method if the entire body can be trivially extracted. This is useful for
+    /// optimizations where `poll_next` calls can be avoided.
+    ///
+    /// 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.
+    ///
+    /// # Errors
+    /// The default implementation will error and return the original type back to the caller for
+    /// further use.
     #[inline]
     fn try_into_bytes(self) -> Result<Bytes, Self>
     where
diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs
index aee9e80b4..a0e6d9b7c 100644
--- a/actix-http/src/response.rs
+++ b/actix-http/src/response.rs
@@ -170,7 +170,7 @@ impl<B> Response<B> {
     /// Returns split head and body.
     ///
     /// # 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.
     pub fn into_parts(self) -> (Response<()>, B) {
         self.replace_body(())

From ae47d96fc6831d6f27a05fb05e47c464fe4f45e1 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 20:56:54 +0000
Subject: [PATCH 19/28] use body::None in encoder body

---
 actix-http/src/encoding/encoder.rs | 18 +++++++++++-------
 1 file changed, 11 insertions(+), 7 deletions(-)

diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs
index d06d6b4d8..b565bb2b5 100644
--- a/actix-http/src/encoding/encoder.rs
+++ b/actix-http/src/encoding/encoder.rs
@@ -25,7 +25,7 @@ use zstd::stream::write::Encoder as ZstdEncoder;
 
 use super::Writer;
 use crate::{
-    body::{BodySize, MessageBody},
+    body::{self, BodySize, MessageBody},
     error::BlockingError,
     header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
     ResponseHead, StatusCode,
@@ -46,7 +46,9 @@ pin_project! {
 impl<B: MessageBody> Encoder<B> {
     fn none() -> Self {
         Encoder {
-            body: EncoderBody::None,
+            body: EncoderBody::None {
+                body: body::None::new(),
+            },
             encoder: None,
             fut: None,
             eof: true,
@@ -96,7 +98,7 @@ impl<B: MessageBody> Encoder<B> {
 pin_project! {
     #[project = EncoderBodyProj]
     enum EncoderBody<B> {
-        None,
+        None { body: body::None },
         Full { body: Bytes },
         Stream { #[pin] body: B },
     }
@@ -111,7 +113,7 @@ where
     #[inline]
     fn size(&self) -> BodySize {
         match self {
-            EncoderBody::None => BodySize::None,
+            EncoderBody::None { body } => body.size(),
             EncoderBody::Full { body } => body.size(),
             EncoderBody::Stream { body } => body.size(),
         }
@@ -122,7 +124,9 @@ where
         cx: &mut Context<'_>,
     ) -> Poll<Option<Result<Bytes, Self::Error>>> {
         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 } => {
                 Pin::new(body).poll_next(cx).map_err(|err| match err {})
             }
@@ -138,8 +142,8 @@ where
         Self: Sized,
     {
         match self {
-            EncoderBody::None => Ok(Bytes::new()),
-            EncoderBody::Full { body } => Ok(body),
+            EncoderBody::None { body } => Ok(body.try_into_bytes().unwrap()),
+            EncoderBody::Full { body } => Ok(body.try_into_bytes().unwrap()),
             _ => Err(self),
         }
     }

From 7bf47967cc77090dad5dd247af55bd00c7808260 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 20:57:51 +0000
Subject: [PATCH 20/28] prepare actix-http release 3.0.0-beta.16

---
 Cargo.toml                  | 2 +-
 actix-files/Cargo.toml      | 2 +-
 actix-http-test/Cargo.toml  | 2 +-
 actix-http/CHANGES.md       | 3 +++
 actix-http/Cargo.toml       | 2 +-
 actix-http/README.md        | 4 ++--
 actix-multipart/Cargo.toml  | 2 +-
 actix-test/Cargo.toml       | 2 +-
 actix-web-actors/Cargo.toml | 2 +-
 awc/Cargo.toml              | 4 ++--
 10 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index e20529e1a..020fb03b5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -77,7 +77,7 @@ actix-service = "2.0.0"
 actix-utils = "3.0.0"
 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-web-codegen = "0.5.0-beta.6"
 
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
index edb7cfab4..792f479d0 100644
--- a/actix-files/Cargo.toml
+++ b/actix-files/Cargo.toml
@@ -22,7 +22,7 @@ path = "src/lib.rs"
 experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
 
 [dependencies]
-actix-http = "3.0.0-beta.15"
+actix-http = "3.0.0-beta.16"
 actix-service = "2"
 actix-utils = "3"
 actix-web = { version = "4.0.0-beta.14", default-features = false }
diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml
index 449fa342e..841a9a241 100644
--- a/actix-http-test/Cargo.toml
+++ b/actix-http-test/Cargo.toml
@@ -52,4 +52,4 @@ tokio = { version = "1.2", features = ["sync"] }
 
 [dev-dependencies]
 actix-web = { version = "4.0.0-beta.14", default-features = false, features = ["cookies"] }
-actix-http = "3.0.0-beta.15"
+actix-http = "3.0.0-beta.16"
diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index 806e32cc0..218ed5a6e 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+
+
+## 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]
 
diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 515574ab1..03ee1409e 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-http"
-version = "3.0.0-beta.15"
+version = "3.0.0-beta.16"
 authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
 description = "HTTP primitives for the Actix ecosystem"
 keywords = ["actix", "http", "framework", "async", "futures"]
diff --git a/actix-http/README.md b/actix-http/README.md
index a2aa41333..731d7a48e 100644
--- a/actix-http/README.md
+++ b/actix-http/README.md
@@ -3,11 +3,11 @@
 > HTTP primitives for the Actix ecosystem.
 
 [![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)
 ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
 <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)
 [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
 
diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml
index 6fd1211d9..8c3c86f08 100644
--- a/actix-multipart/Cargo.toml
+++ b/actix-multipart/Cargo.toml
@@ -28,7 +28,7 @@ twoway = "0.2"
 
 [dev-dependencies]
 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"] }
 tokio = { version = "1", features = ["sync"] }
 tokio-stream = "0.1"
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index 71f99f791..67a0a087d 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -29,7 +29,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
 
 [dependencies]
 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-rt = "2.1"
 actix-service = "2.0.0"
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index 128d68c15..e8145ee82 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -16,7 +16,7 @@ path = "src/lib.rs"
 [dependencies]
 actix = { version = "0.12.0", default-features = false }
 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 }
 
 bytes = "1"
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 60a95871c..4b1e00dde 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -60,7 +60,7 @@ dangerous-h2c = []
 [dependencies]
 actix-codec = "0.4.1"
 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-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] }
 actix-utils = "3.0.0"
@@ -93,7 +93,7 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
 trust-dns-resolver = { version = "0.20.0", optional = true }
 
 [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-server = "2.0.0-rc.1"
 actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] }

From 6c2c7b68e2d6eb5ee1c5cf879e8874928e420e72 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 20:59:01 +0000
Subject: [PATCH 21/28] prepare actix-web release 4.0.0-beta.15

---
 CHANGES.md                   | 3 +++
 Cargo.toml                   | 2 +-
 README.md                    | 4 ++--
 actix-files/Cargo.toml       | 4 ++--
 actix-http-test/Cargo.toml   | 2 +-
 actix-http/Cargo.toml        | 2 +-
 actix-multipart/Cargo.toml   | 2 +-
 actix-test/Cargo.toml        | 2 +-
 actix-web-actors/Cargo.toml  | 2 +-
 actix-web-codegen/Cargo.toml | 2 +-
 awc/Cargo.toml               | 2 +-
 11 files changed, 15 insertions(+), 12 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 6494ba4f6..1c0691efe 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## 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]
diff --git a/Cargo.toml b/Cargo.toml
index 020fb03b5..3c4ddd1b0 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-web"
-version = "4.0.0-beta.14"
+version = "4.0.0-beta.15"
 authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
 description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
 keywords = ["actix", "http", "web", "framework", "async"]
diff --git a/README.md b/README.md
index 4a1671905..5cce9f3b9 100644
--- a/README.md
+++ b/README.md
@@ -6,10 +6,10 @@
   <p>
 
 [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
-[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-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)
 ![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 />
 [![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) 
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
index 792f479d0..3e7c377bf 100644
--- a/actix-files/Cargo.toml
+++ b/actix-files/Cargo.toml
@@ -25,7 +25,7 @@ experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
 actix-http = "3.0.0-beta.16"
 actix-service = "2"
 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"
 bitflags = "1"
@@ -44,4 +44,4 @@ tokio-uring = { version = "0.1", optional = true }
 [dev-dependencies]
 actix-rt = "2.2"
 actix-test = "0.1.0-beta.8"
-actix-web = "4.0.0-beta.14"
+actix-web = "4.0.0-beta.15"
diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml
index 841a9a241..70855b5e6 100644
--- a/actix-http-test/Cargo.toml
+++ b/actix-http-test/Cargo.toml
@@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
 tokio = { version = "1.2", features = ["sync"] }
 
 [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.16"
diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml
index 03ee1409e..9f93bf6d2 100644
--- a/actix-http/Cargo.toml
+++ b/actix-http/Cargo.toml
@@ -83,7 +83,7 @@ zstd = { version = "0.9", optional = true }
 actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
 actix-server = "2.0.0-rc.1"
 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"
 criterion = { version = "0.3", features = ["html_reports"] }
diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml
index 8c3c86f08..c7145e542 100644
--- a/actix-multipart/Cargo.toml
+++ b/actix-multipart/Cargo.toml
@@ -15,7 +15,7 @@ path = "src/lib.rs"
 
 [dependencies]
 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"
 derive_more = "0.99.5"
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index 67a0a087d..c2a7a3211 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -34,7 +34,7 @@ actix-http-test = "3.0.0-beta.9"
 actix-rt = "2.1"
 actix-service = "2.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"] }
 
 futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index e8145ee82..a2a69153d 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -17,7 +17,7 @@ path = "src/lib.rs"
 actix = { version = "0.12.0", default-features = false }
 actix-codec = "0.4.1"
 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"
 bytestring = "1"
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 211f19da6..6571e2a24 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -25,7 +25,7 @@ actix-macros = "0.2.3"
 actix-rt = "2.2"
 actix-test = "0.1.0-beta.8"
 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"] }
 trybuild = "1"
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 4b1e00dde..9ca6acb75 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -99,7 +99,7 @@ actix-server = "2.0.0-rc.1"
 actix-test = { version = "0.1.0-beta.8", features = ["openssl", "rustls"] }
 actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] }
 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"
 env_logger = "0.9"

From 8340b63b7b18009b006671ff9f1fbb790d67bf19 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 20:59:59 +0000
Subject: [PATCH 22/28] prepare awc release 3.0.0-beta.14

---
 Cargo.toml                  | 2 +-
 actix-http-test/Cargo.toml  | 2 +-
 actix-test/Cargo.toml       | 2 +-
 actix-web-actors/Cargo.toml | 2 +-
 awc/CHANGES.md              | 3 +++
 awc/Cargo.toml              | 2 +-
 awc/README.md               | 4 ++--
 7 files changed, 10 insertions(+), 7 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 3c4ddd1b0..c46211821 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -107,7 +107,7 @@ url = "2.1"
 
 [dev-dependencies]
 actix-test = { version = "0.1.0-beta.8", 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"
 criterion = { version = "0.3", features = ["html_reports"] }
diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml
index 70855b5e6..0c205fc2a 100644
--- a/actix-http-test/Cargo.toml
+++ b/actix-http-test/Cargo.toml
@@ -35,7 +35,7 @@ actix-tls = "3.0.0-rc.1"
 actix-utils = "3.0.0"
 actix-rt = "2.2"
 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"
 bytes = "1"
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index c2a7a3211..9f409019b 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -35,7 +35,7 @@ actix-rt = "2.1"
 actix-service = "2.0.0"
 actix-utils = "3.0.0"
 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-util = { version = "0.3.7", default-features = false, features = [] }
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index a2a69153d..6c45a5479 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -28,7 +28,7 @@ tokio = { version = "1", features = ["sync"] }
 [dev-dependencies]
 actix-rt = "2.2"
 actix-test = "0.1.0-beta.8"
-awc = { version = "3.0.0-beta.13", default-features = false }
+awc = { version = "3.0.0-beta.14", default-features = false }
 
 env_logger = "0.9"
 futures-util = { version = "0.3.7", default-features = false }
diff --git a/awc/CHANGES.md b/awc/CHANGES.md
index 8a3fea46a..ef0f9faaa 100644
--- a/awc/CHANGES.md
+++ b/awc/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+
+
+## 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
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index 9ca6acb75..dc9e503b1 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "awc"
-version = "3.0.0-beta.13"
+version = "3.0.0-beta.14"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "fakeshadow <24548779@qq.com>",
diff --git a/awc/README.md b/awc/README.md
index f3c5452fc..3fbdd903a 100644
--- a/awc/README.md
+++ b/awc/README.md
@@ -3,9 +3,9 @@
 > Async HTTP and WebSocket client library.
 
 [![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)
-[![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)
 
 ## Documentation & Resources

From 73bbe56971bff3cdcf8e6c0098351c4daf76b74e Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 21:00:15 +0000
Subject: [PATCH 23/28] prepare actix-test release 0.1.0-beta.9

---
 Cargo.toml                   | 2 +-
 actix-files/Cargo.toml       | 2 +-
 actix-test/CHANGES.md        | 3 +++
 actix-test/Cargo.toml        | 2 +-
 actix-web-actors/Cargo.toml  | 2 +-
 actix-web-codegen/Cargo.toml | 2 +-
 awc/Cargo.toml               | 2 +-
 7 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index c46211821..af6b5909a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -106,7 +106,7 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
 url = "2.1"
 
 [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.14", features = ["openssl"] }
 
 brotli2 = "0.3.2"
diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml
index 3e7c377bf..c74a8e9a6 100644
--- a/actix-files/Cargo.toml
+++ b/actix-files/Cargo.toml
@@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true }
 
 [dev-dependencies]
 actix-rt = "2.2"
-actix-test = "0.1.0-beta.8"
+actix-test = "0.1.0-beta.9"
 actix-web = "4.0.0-beta.15"
diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md
index b7107b44f..ef78ac54a 100644
--- a/actix-test/CHANGES.md
+++ b/actix-test/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## 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]
 
diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml
index 9f409019b..7957b3a9c 100644
--- a/actix-test/Cargo.toml
+++ b/actix-test/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-test"
-version = "0.1.0-beta.8"
+version = "0.1.0-beta.9"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "Rob Ede <robjtede@icloud.com>",
diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml
index 6c45a5479..3f213f378 100644
--- a/actix-web-actors/Cargo.toml
+++ b/actix-web-actors/Cargo.toml
@@ -27,7 +27,7 @@ tokio = { version = "1", features = ["sync"] }
 
 [dev-dependencies]
 actix-rt = "2.2"
-actix-test = "0.1.0-beta.8"
+actix-test = "0.1.0-beta.9"
 awc = { version = "3.0.0-beta.14", default-features = false }
 
 env_logger = "0.9"
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 6571e2a24..502483599 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -23,7 +23,7 @@ actix-router = "0.5.0-beta.2"
 [dev-dependencies]
 actix-macros = "0.2.3"
 actix-rt = "2.2"
-actix-test = "0.1.0-beta.8"
+actix-test = "0.1.0-beta.9"
 actix-utils = "3.0.0"
 actix-web = "4.0.0-beta.15"
 
diff --git a/awc/Cargo.toml b/awc/Cargo.toml
index dc9e503b1..4cab20d05 100644
--- a/awc/Cargo.toml
+++ b/awc/Cargo.toml
@@ -96,7 +96,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true }
 actix-http = { version = "3.0.0-beta.16", features = ["openssl"] }
 actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] }
 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-utils = "3.0.0"
 actix-web = { version = "4.0.0-beta.15", features = ["openssl"] }

From 9cd8526085b5aa4538d17495f8e21313d7c53527 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 21:23:00 +0000
Subject: [PATCH 24/28] prepare actix-router release 0.5.0-beta.3

---
 Cargo.toml                   |  2 +-
 actix-router/CHANGES.md      |  3 +++
 actix-router/Cargo.toml      |  2 +-
 actix-web-codegen/Cargo.toml |  2 +-
 scripts/unreleased           | 41 ++++++++++++++++++++++++++++++++++++
 5 files changed, 47 insertions(+), 3 deletions(-)
 create mode 100755 scripts/unreleased

diff --git a/Cargo.toml b/Cargo.toml
index af6b5909a..02bef3af6 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -78,7 +78,7 @@ actix-utils = "3.0.0"
 actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
 
 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"
 
 ahash = "0.7"
diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md
index c2858f2ba..d0ed55c88 100644
--- a/actix-router/CHANGES.md
+++ b/actix-router/CHANGES.md
@@ -1,6 +1,9 @@
 # Changes
 
 ## Unreleased - 2021-xx-xx
+
+
+## 0.5.0-beta.3 - 2021-12-17
 * Minimum supported Rust version (MSRV) is now 1.52.
 
 
diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml
index b95bca505..afd39dfd3 100644
--- a/actix-router/Cargo.toml
+++ b/actix-router/Cargo.toml
@@ -1,6 +1,6 @@
 [package]
 name = "actix-router"
-version = "0.5.0-beta.2"
+version = "0.5.0-beta.3"
 authors = [
     "Nikolay Kim <fafhrd91@gmail.com>",
     "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml
index 502483599..8d42137e7 100644
--- a/actix-web-codegen/Cargo.toml
+++ b/actix-web-codegen/Cargo.toml
@@ -18,7 +18,7 @@ proc-macro = true
 quote = "1"
 syn = { version = "1", features = ["full", "parsing"] }
 proc-macro2 = "1"
-actix-router = "0.5.0-beta.2"
+actix-router = "0.5.0-beta.3"
 
 [dev-dependencies]
 actix-macros = "0.2.3"
diff --git a/scripts/unreleased b/scripts/unreleased
new file mode 100755
index 000000000..4dfa2d9ae
--- /dev/null
+++ b/scripts/unreleased
@@ -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

From 0bd5ccc43285ff871c1ea908a74e1b1d9e0f1409 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Fri, 17 Dec 2021 21:39:15 +0000
Subject: [PATCH 25/28] update changelog

---
 CHANGES.md | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/CHANGES.md b/CHANGES.md
index 1c0691efe..857974d3f 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -15,7 +15,7 @@
 * 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.
+* Relax body type and error bounds on test utilities. [#2518]
 
 ### Removed
 * Top-level `EitherExtractError` export. [#2510]

From 84ea9e7e8849d98108a66990f8508fc014d78d19 Mon Sep 17 00:00:00 2001
From: Thales <thales.fragosoz@gmail.com>
Date: Fri, 17 Dec 2021 21:05:12 -0300
Subject: [PATCH 26/28] http: Replace header::map::GetAll with std::slice::Iter
 (#2527)

---
 actix-http/CHANGES.md        |  4 +++
 actix-http/src/header/map.rs | 55 ++++--------------------------------
 src/response/response.rs     |  2 +-
 3 files changed, 11 insertions(+), 50 deletions(-)

diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md
index 218ed5a6e..c5e57e1a4 100644
--- a/actix-http/CHANGES.md
+++ b/actix-http/CHANGES.md
@@ -1,6 +1,10 @@
 # Changes
 
 ## 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
diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs
index 748410375..478867ed0 100644
--- a/actix-http/src/header/map.rs
+++ b/actix-http/src/header/map.rs
@@ -306,8 +306,11 @@ impl HeaderMap {
     /// assert_eq!(set_cookies_iter.next().unwrap(), "two=2");
     /// assert!(set_cookies_iter.next().is_none());
     /// ```
-    pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> {
-        GetAll::new(self.get_value(key))
+    pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
+        match self.get_value(key) {
+            Some(value) => value.iter(),
+            None => (&[]).iter(),
+        }
     }
 
     // TODO: get_all_mut ?
@@ -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.
 ///
 /// 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!(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!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
     assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
diff --git a/src/response/response.rs b/src/response/response.rs
index 4fb4b44b6..6fa2082e7 100644
--- a/src/response/response.rs
+++ b/src/response/response.rs
@@ -313,7 +313,7 @@ impl Future for HttpResponse<BoxBody> {
 
 #[cfg(feature = "cookies")]
 pub struct CookieIter<'a> {
-    iter: header::map::GetAll<'a>,
+    iter: std::slice::Iter<'a, HeaderValue>,
 }
 
 #[cfg(feature = "cookies")]

From 5c53db1e4ddb37e1e81ff5bb20e86d1ec87d6a44 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 18 Dec 2021 01:48:16 +0000
Subject: [PATCH 27/28] remove hidden anybody

---
 src/dev.rs | 46 ----------------------------------------------
 1 file changed, 46 deletions(-)

diff --git a/src/dev.rs b/src/dev.rs
index edcc158f8..23a40f292 100644
--- a/src/dev.rs
+++ b/src/dev.rs
@@ -102,49 +102,3 @@ impl<B> BodyEncoding for crate::HttpResponse<B> {
         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),
-        }
-    }
-
-    fn try_into_bytes(self) -> Result<crate::web::Bytes, Self> {
-        match self {
-            AnyBody::None => Ok(crate::web::Bytes::new()),
-            AnyBody::Full { body } => Ok(body),
-            _ => Err(self),
-        }
-    }
-}

From d2b97240109b0f09e55900ad645c3cf6d5102956 Mon Sep 17 00:00:00 2001
From: Rob Ede <robjtede@icloud.com>
Date: Sat, 18 Dec 2021 03:27:32 +0000
Subject: [PATCH 28/28] update bump script to detect prerelease versions

---
 scripts/bump | 14 ++++++++++++--
 src/app.rs   | 18 ++++++++++--------
 2 files changed, 22 insertions(+), 10 deletions(-)

diff --git a/scripts/bump b/scripts/bump
index 0c360569d..43cd8b8c7 100755
--- a/scripts/bump
+++ b/scripts/bump
@@ -55,6 +55,11 @@ else
     read -p "Update version to: " NEW_VERSION
 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")"
 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)"
 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 "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
 GH_RELEASE="${GH_RELEASE:-n}"
 
 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
 
 echo
diff --git a/src/app.rs b/src/app.rs
index feb35d7ae..6bccc1ff1 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -486,19 +486,21 @@ where
 
 #[cfg(test)]
 mod tests {
-    use actix_service::Service;
+    use actix_service::Service as _;
     use actix_utils::future::{err, ok};
     use bytes::Bytes;
 
     use super::*;
-    use crate::http::{
-        header::{self, HeaderValue},
-        Method, StatusCode,
+    use crate::{
+        http::{
+            header::{self, HeaderValue},
+            Method, StatusCode,
+        },
+        middleware::DefaultHeaders,
+        service::ServiceRequest,
+        test::{call_service, init_service, read_body, try_init_service, TestRequest},
+        web, HttpRequest, HttpResponse,
     };
-    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]
     async fn test_default_resource() {