From cd025f5c0ba7774263cbae38fd6b4c652b4d54a3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 22 Dec 2021 15:00:32 +0000 Subject: [PATCH 01/66] allow any body type in Resource (#2526) --- CHANGES.md | 4 +++ actix-http/Cargo.toml | 1 - src/middleware/compat.rs | 9 +++++ src/middleware/mod.rs | 4 +++ src/middleware/noop.rs | 37 +++++++++++++++++++++ src/resource.rs | 72 +++++++++++++++++++++++++++++----------- src/scope.rs | 10 +++--- 7 files changed, 112 insertions(+), 25 deletions(-) create mode 100644 src/middleware/noop.rs diff --git a/CHANGES.md b/CHANGES.md index 8e030819f..07c247554 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +- No longer require `Resource` service body type to be boxed. [#2526] + +[#2526]: https://github.com/actix/actix-web/pull/2526 ## 4.0.0-beta.15 - 2021-12-17 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 2958a1c77..c15f5ee28 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -55,7 +55,6 @@ bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } -futures-task = { version = "0.3.7", default-features = false, features = ["alloc"] } h2 = "0.3.9" http = "0.2.5" httparse = "1.5.1" diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index d49c461c4..3386240b7 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -38,6 +38,15 @@ pub struct Compat { transform: T, } +#[cfg(test)] +impl Compat { + pub(crate) fn noop() -> Self { + Self { + transform: super::Noop, + } + } +} + impl Compat { /// Wrap a middleware to give it broader compatibility. pub fn new(middleware: T) -> Self { diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index 0da9b9b2e..a781052a6 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -5,6 +5,8 @@ mod condition; mod default_headers; mod err_handlers; mod logger; +#[cfg(test)] +mod noop; mod normalize; pub use self::compat::Compat; @@ -12,6 +14,8 @@ pub use self::condition::Condition; pub use self::default_headers::DefaultHeaders; pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; +#[cfg(test)] +pub(crate) use self::noop::Noop; pub use self::normalize::{NormalizePath, TrailingSlash}; #[cfg(feature = "__compress")] diff --git a/src/middleware/noop.rs b/src/middleware/noop.rs new file mode 100644 index 000000000..ae7da1d81 --- /dev/null +++ b/src/middleware/noop.rs @@ -0,0 +1,37 @@ +//! A no-op middleware. See [Noop] for docs. + +use actix_utils::future::{ready, Ready}; + +use crate::dev::{Service, Transform}; + +/// A no-op middleware that passes through request and response untouched. +pub(crate) struct Noop; + +impl, Req> Transform for Noop { + type Response = S::Response; + type Error = S::Error; + type Transform = NoopService; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(NoopService { service })) + } +} + +#[doc(hidden)] +pub(crate) struct NoopService { + service: S, +} + +impl, Req> Service for NoopService { + type Response = S::Response; + type Error = S::Error; + type Future = S::Future; + + crate::dev::forward_ready!(service); + + fn call(&self, req: Req) -> Self::Future { + self.service.call(req) + } +} diff --git a/src/resource.rs b/src/resource.rs index c13544063..d94d2a464 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,6 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::Extensions; +use actix_http::{body::BoxBody, Extensions}; use actix_router::{IntoPatterns, Patterns}; use actix_service::{ apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -45,7 +45,7 @@ use crate::{ /// /// If no matching route could be found, *405* response code get returned. /// Default behavior could be overridden with `default_resource()` method. -pub struct Resource { +pub struct Resource { endpoint: T, rdef: Patterns, name: Option, @@ -54,6 +54,7 @@ pub struct Resource { guards: Vec>, default: BoxedHttpServiceFactory, factory_ref: Rc>>, + _phantom: PhantomData, } impl Resource { @@ -71,19 +72,21 @@ impl Resource { default: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::MethodNotAllowed())) })), + _phantom: PhantomData, } } } -impl Resource +impl Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B: MessageBody, { /// Set resource name. /// @@ -252,26 +255,28 @@ where /// type (i.e modify response's body). /// /// **Note**: middlewares get called in opposite order of middlewares registration. - pub fn wrap( + pub fn wrap( self, mw: M, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where M: Transform< T::Service, ServiceRequest, - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1: MessageBody, { Resource { endpoint: apply(mw, self.endpoint), @@ -282,6 +287,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -319,21 +325,23 @@ where /// .route(web::get().to(index))); /// } /// ``` - pub fn wrap_fn( + pub fn wrap_fn( self, mw: F, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, + B1, > where F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future>, + R: Future, Error>>, + B1: MessageBody, { Resource { endpoint: apply_fn_factory(self.endpoint, mw), @@ -344,6 +352,7 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, + _phantom: PhantomData, } } @@ -371,15 +380,16 @@ where } } -impl HttpServiceFactory for Resource +impl HttpServiceFactory for Resource where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { let guards = if self.guards.is_empty() { @@ -411,7 +421,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); config.register_service(rdef, guards, endpoint, None) @@ -534,11 +546,11 @@ mod tests { >, > { web::resource("/test-compat") - // .wrap_fn(|req, srv| { - // let fut = srv.call(req); - // async { Ok(fut.await?.map_into_right_body::<()>()) } - // }) - .wrap(Compat::new(DefaultHeaders::new())) + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) .route(web::get().to(|| async { "hello" })) } @@ -801,4 +813,26 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + + #[actix_rt::test] + async fn test_middleware_body_type() { + let srv = init_service( + App::new().service( + web::resource("/test") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .route(web::get().to(|| async { "hello" })), + ), + ) + .await; + + // test if `try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } } diff --git a/src/scope.rs b/src/scope.rs index 35bbb50ba..7f9a94875 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -616,11 +616,11 @@ mod tests { >, > { web::scope("/test-compat") - // .wrap_fn(|req, srv| { - // let fut = srv.call(req); - // async { Ok(fut.await?.map_into_right_body::<()>()) } - // }) - .wrap(Compat::new(DefaultHeaders::new())) + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .wrap(Compat::noop()) .service(web::resource("").route(web::get().to(|| async { "hello" }))) } From 7b1512d863ed63cb7a1fab51ff358b24b33c5f19 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 22 Dec 2021 18:48:59 +0300 Subject: [PATCH 02/66] allow any body type in Scope (#2523) --- CHANGES.md | 2 ++ src/middleware/compat.rs | 2 +- src/scope.rs | 37 +++++++++++++++++++++++++++++++++---- 3 files changed, 36 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 07c247554..a43b3ee41 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,8 +2,10 @@ ## Unreleased - 2021-xx-xx ### Changed +- No longer require `Scope` service body type to be boxed. [#2523] - No longer require `Resource` service body type to be boxed. [#2526] +[#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 3386240b7..18c9ff6a7 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -17,7 +17,7 @@ use crate::{ }; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), -/// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). +/// and [`Condition`](super::Condition). /// /// # Examples /// ``` diff --git a/src/scope.rs b/src/scope.rs index 7f9a94875..1fd282f61 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,6 +1,9 @@ use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc}; -use actix_http::{body::BoxBody, Extensions}; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, +}; use actix_router::{ResourceDef, Router}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, @@ -399,15 +402,16 @@ where } } -impl HttpServiceFactory for Scope +impl HttpServiceFactory for Scope where T: ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), > + 'static, + B: MessageBody + 'static, { fn register(mut self, config: &mut AppService) { // update default resource if needed @@ -457,7 +461,9 @@ where req.add_data_container(Rc::clone(data)); } - srv.call(req) + let fut = srv.call(req); + + async { Ok(fut.await?.map_into_boxed_body()) } }); // register final service @@ -980,6 +986,29 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_body_type() { + // Compile test that Scope accepts any body type; test for `EitherBody` + let srv = init_service( + App::new().service( + web::scope("app") + .wrap_fn(|req, srv| { + let fut = srv.call(req); + async { Ok(fut.await?.map_into_right_body::<()>()) } + }) + .service(web::resource("/test").route(web::get().to(|| async { "hello" }))), + ), + ) + .await; + + // test if `MessageBody::try_into_bytes()` is preserved across scope layer + use actix_http::body::MessageBody as _; + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + let body = resp.into_body(); + assert_eq!(body.try_into_bytes().unwrap(), b"hello".as_ref()); + } + #[actix_rt::test] async fn test_middleware_fn() { let srv = init_service( From 1296e07c4830f0ab2e2864a6fc9faa93972e5935 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 24 Dec 2021 17:47:47 +0000 Subject: [PATCH 03/66] relax unpin bounds on payload types (#2545) --- actix-http/CHANGES.md | 9 ++++ actix-http/src/encoding/decoder.rs | 39 ++++++++------- actix-http/src/h1/dispatcher.rs | 7 +-- actix-http/src/h1/payload.rs | 76 +++++++++++++++++------------- actix-http/src/h1/utils.rs | 4 +- actix-http/src/h2/dispatcher.rs | 4 +- actix-http/src/h2/mod.rs | 11 +++++ actix-http/src/lib.rs | 3 +- actix-http/src/payload.rs | 73 +++++++++++++++++----------- actix-http/src/requests/request.rs | 11 +++-- actix-http/src/test.rs | 2 +- actix-http/tests/test_rustls.rs | 33 +++++++++---- actix-multipart/src/server.rs | 2 +- awc/src/client/connection.rs | 4 +- awc/src/client/h1proto.rs | 16 +++++-- awc/src/response.rs | 6 +-- awc/src/sender.rs | 15 +++--- awc/src/test.rs | 5 +- src/dev.rs | 2 +- src/response/builder.rs | 36 ++++++-------- src/service.rs | 4 +- src/test/test_request.rs | 21 +++++---- 22 files changed, 229 insertions(+), 154 deletions(-) diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 3b45e934f..adc4c35c7 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -3,8 +3,17 @@ ## Unreleased - 2021-xx-xx ### Changes - `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] +- `Payload` inner fields are now named. [#2545] +- `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] +- `impl Future` for `h1::SendResponse` no longer requires the body type be `Unpin`. [#2545] +- `impl Stream` for `encoding::Decoder` no longer requires the stream type be `Unpin`. [#2545] +- Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] + +### Removed +- `h1::Payload::readany`. [#2545] [#2527]: https://github.com/actix/actix-web/pull/2527 +[#2545]: https://github.com/actix/actix-web/pull/2545 ## 3.0.0-beta.16 - 2021-12-17 diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index a46e330c9..0f519637a 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -28,11 +28,14 @@ use crate::{ const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; -pub struct Decoder { - decoder: Option, - stream: S, - eof: bool, - fut: Option, ContentDecoder), io::Error>>>, +pin_project_lite::pin_project! { + pub struct Decoder { + decoder: Option, + #[pin] + stream: S, + eof: bool, + fut: Option, ContentDecoder), io::Error>>>, + } } impl Decoder @@ -89,42 +92,44 @@ where impl Stream for Decoder where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + loop { - if let Some(ref mut fut) = self.fut { + if let Some(ref mut fut) = this.fut { let (chunk, decoder) = ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; - self.decoder = Some(decoder); - self.fut.take(); + *this.decoder = Some(decoder); + this.fut.take(); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } - if self.eof { + if *this.eof { return Poll::Ready(None); } - match ready!(Pin::new(&mut self.stream).poll_next(cx)) { + match ready!(this.stream.as_mut().poll_next(cx)) { Some(Err(err)) => return Poll::Ready(Some(Err(err))), Some(Ok(chunk)) => { - if let Some(mut decoder) = self.decoder.take() { + if let Some(mut decoder) = this.decoder.take() { if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE { let chunk = decoder.feed_data(chunk)?; - self.decoder = Some(decoder); + *this.decoder = Some(decoder); if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } } else { - self.fut = Some(spawn_blocking(move || { + *this.fut = Some(spawn_blocking(move || { let chunk = decoder.feed_data(chunk)?; Ok((chunk, decoder)) })); @@ -137,9 +142,9 @@ where } None => { - self.eof = true; + *this.eof = true; - return if let Some(mut decoder) = self.decoder.take() { + return if let Some(mut decoder) = this.decoder.take() { match decoder.feed_eof() { Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(None) => Poll::Ready(None), diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 5c0cb64af..13055f08a 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -646,10 +646,11 @@ where Payload is attached to Request and passed to Service::call where the state can be collected and consumed. */ - let (ps, pl) = Payload::create(false); - let (req1, _) = req.replace_payload(crate::Payload::H1(pl)); + let (sender, payload) = Payload::create(false); + let (req1, _) = + req.replace_payload(crate::Payload::H1 { payload }); req = req1; - *this.payload = Some(ps); + *this.payload = Some(sender); } // Request has no payload. diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index f912e0ba3..4d031c15a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -1,9 +1,12 @@ //! Payload stream -use std::cell::RefCell; -use std::collections::VecDeque; -use std::pin::Pin; -use std::rc::{Rc, Weak}; -use std::task::{Context, Poll, Waker}; + +use std::{ + cell::RefCell, + collections::VecDeque, + pin::Pin, + rc::{Rc, Weak}, + task::{Context, Poll, Waker}, +}; use bytes::Bytes; use futures_core::Stream; @@ -22,39 +25,32 @@ pub enum PayloadStatus { /// Buffered stream of bytes chunks /// -/// Payload stores chunks in a vector. First chunk can be received with -/// `.readany()` method. Payload stream is not thread safe. Payload does not -/// notify current task when new data is available. +/// Payload stores chunks in a vector. First chunk can be received with `poll_next`. Payload does +/// not notify current task when new data is available. /// -/// Payload stream can be used as `Response` body stream. +/// Payload can be used as `Response` body stream. #[derive(Debug)] pub struct Payload { inner: Rc>, } impl Payload { - /// Create payload stream. + /// Creates a payload stream. /// - /// This method construct two objects responsible for bytes stream - /// generation. - /// - /// * `PayloadSender` - *Sender* side of the stream - /// - /// * `Payload` - *Receiver* side of the stream + /// This method construct two objects responsible for bytes stream generation: + /// - `PayloadSender` - *Sender* side of the stream + /// - `Payload` - *Receiver* side of the stream pub fn create(eof: bool) -> (PayloadSender, Payload) { let shared = Rc::new(RefCell::new(Inner::new(eof))); ( - PayloadSender { - inner: Rc::downgrade(&shared), - }, + PayloadSender::new(Rc::downgrade(&shared)), Payload { inner: shared }, ) } - /// Create empty payload - #[doc(hidden)] - pub fn empty() -> Payload { + /// Creates an empty payload. + pub(crate) fn empty() -> Payload { Payload { inner: Rc::new(RefCell::new(Inner::new(true))), } @@ -77,14 +73,6 @@ impl Payload { pub fn unread_data(&mut self, data: Bytes) { self.inner.borrow_mut().unread_data(data); } - - #[inline] - pub fn readany( - &mut self, - cx: &mut Context<'_>, - ) -> Poll>> { - self.inner.borrow_mut().readany(cx) - } } impl Stream for Payload { @@ -94,7 +82,7 @@ impl Stream for Payload { self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll>> { - self.inner.borrow_mut().readany(cx) + Pin::new(&mut *self.inner.borrow_mut()).poll_next(cx) } } @@ -104,6 +92,10 @@ pub struct PayloadSender { } impl PayloadSender { + fn new(inner: Weak>) -> Self { + Self { inner } + } + #[inline] pub fn set_error(&mut self, err: PayloadError) { if let Some(shared) = self.inner.upgrade() { @@ -227,7 +219,10 @@ impl Inner { self.len } - fn readany(&mut self, cx: &mut Context<'_>) -> Poll>> { + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { if let Some(data) = self.items.pop_front() { self.len -= data.len(); self.need_read = self.len < MAX_BUFFER_SIZE; @@ -257,8 +252,18 @@ impl Inner { #[cfg(test)] mod tests { - use super::*; + use std::panic::{RefUnwindSafe, UnwindSafe}; + use actix_utils::future::poll_fn; + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); + + assert_impl_all!(Inner: Unpin, Send, Sync); + assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe); #[actix_rt::test] async fn test_unread_data() { @@ -270,7 +275,10 @@ mod tests { assert_eq!( Bytes::from("data"), - poll_fn(|cx| payload.readany(cx)).await.unwrap().unwrap() + poll_fn(|cx| Pin::new(&mut payload).poll_next(cx)) + .await + .unwrap() + .unwrap() ); } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 131c7f1ed..5c11b1dab 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -45,7 +45,7 @@ where impl Future for SendResponse where T: AsyncRead + AsyncWrite + Unpin, - B: MessageBody + Unpin, + B: MessageBody, B::Error: Into, { type Output = Result, Error>; @@ -81,7 +81,7 @@ where // body is done when item is None body_done = item.is_none(); if body_done { - let _ = this.body.take(); + this.body.set(None); } let framed = this.framed.as_mut().as_pin_mut().unwrap(); framed diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 8fbefe6de..7f0f15ee6 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -108,8 +108,8 @@ where match Pin::new(&mut this.connection).poll_accept(cx)? { Poll::Ready(Some((req, tx))) => { let (parts, body) = req.into_parts(); - let pl = crate::h2::Payload::new(body); - let pl = Payload::H2(pl); + let payload = crate::h2::Payload::new(body); + let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); let head = req.head_mut(); diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index cbcb6d0fc..47d51b420 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -98,3 +98,14 @@ where } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::assert_impl_all; + + use super::*; + + assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 2b7bc730b..f2b415790 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -58,7 +58,8 @@ pub use self::header::ContentEncoding; pub use self::http_message::HttpMessage; pub use self::message::ConnectionType; pub use self::message::Message; -pub use self::payload::{Payload, PayloadStream}; +#[allow(deprecated)] +pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream}; pub use self::requests::{Request, RequestHead, RequestHeadType}; pub use self::responses::{Response, ResponseBuilder, ResponseHead}; pub use self::service::HttpService; diff --git a/actix-http/src/payload.rs b/actix-http/src/payload.rs index 69840e7c1..c9f338c7d 100644 --- a/actix-http/src/payload.rs +++ b/actix-http/src/payload.rs @@ -1,70 +1,89 @@ use std::{ + mem, pin::Pin, task::{Context, Poll}, }; use bytes::Bytes; use futures_core::Stream; -use h2::RecvStream; use crate::error::PayloadError; -// TODO: rename to boxed payload -/// A boxed payload. -pub type PayloadStream = Pin>>>; +/// A boxed payload stream. +pub type BoxedPayloadStream = Pin>>>; -/// A streaming payload. -pub enum Payload { - None, - H1(crate::h1::Payload), - H2(crate::h2::Payload), - Stream(S), +#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")] +pub type PayloadStream = BoxedPayloadStream; + +pin_project_lite::pin_project! { + /// A streaming payload. + #[project = PayloadProj] + pub enum Payload { + None, + H1 { payload: crate::h1::Payload }, + H2 { payload: crate::h2::Payload }, + Stream { #[pin] payload: S }, + } } impl From for Payload { - fn from(v: crate::h1::Payload) -> Self { - Payload::H1(v) + fn from(payload: crate::h1::Payload) -> Self { + Payload::H1 { payload } } } impl From for Payload { - fn from(v: crate::h2::Payload) -> Self { - Payload::H2(v) + fn from(payload: crate::h2::Payload) -> Self { + Payload::H2 { payload } } } -impl From for Payload { - fn from(v: RecvStream) -> Self { - Payload::H2(crate::h2::Payload::new(v)) +impl From for Payload { + fn from(stream: h2::RecvStream) -> Self { + Payload::H2 { + payload: crate::h2::Payload::new(stream), + } } } -impl From for Payload { - fn from(pl: PayloadStream) -> Self { - Payload::Stream(pl) +impl From for Payload { + fn from(payload: BoxedPayloadStream) -> Self { + Payload::Stream { payload } } } impl Payload { /// Takes current payload and replaces it with `None` value pub fn take(&mut self) -> Payload { - std::mem::replace(self, Payload::None) + mem::replace(self, Payload::None) } } impl Stream for Payload where - S: Stream> + Unpin, + S: Stream>, { type Item = Result; #[inline] fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.get_mut() { - Payload::None => Poll::Ready(None), - Payload::H1(ref mut pl) => pl.readany(cx), - Payload::H2(ref mut pl) => Pin::new(pl).poll_next(cx), - Payload::Stream(ref mut pl) => Pin::new(pl).poll_next(cx), + match self.project() { + PayloadProj::None => Poll::Ready(None), + PayloadProj::H1 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::H2 { payload } => Pin::new(payload).poll_next(cx), + PayloadProj::Stream { payload } => payload.poll_next(cx), } } } + +#[cfg(test)] +mod tests { + use std::panic::{RefUnwindSafe, UnwindSafe}; + + use static_assertions::{assert_impl_all, assert_not_impl_any}; + + use super::*; + + assert_impl_all!(Payload: Unpin); + assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe); +} diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs index 0254a8f11..4eaaba8e1 100644 --- a/actix-http/src/requests/request.rs +++ b/actix-http/src/requests/request.rs @@ -10,11 +10,12 @@ use std::{ use http::{header, Method, Uri, Version}; use crate::{ - header::HeaderMap, Extensions, HttpMessage, Message, Payload, PayloadStream, RequestHead, + header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, + RequestHead, }; /// An HTTP request. -pub struct Request

{ +pub struct Request

{ pub(crate) payload: Payload

, pub(crate) head: Message, pub(crate) conn_data: Option>, @@ -46,7 +47,7 @@ impl

HttpMessage for Request

{ } } -impl From> for Request { +impl From> for Request { fn from(head: Message) -> Self { Request { head, @@ -57,10 +58,10 @@ impl From> for Request { } } -impl Request { +impl Request { /// Create new Request instance #[allow(clippy::new_without_default)] - pub fn new() -> Request { + pub fn new() -> Request { Request { head: Message::new(), payload: Payload::None, diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index ea80345fe..1f76498ef 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -120,7 +120,7 @@ impl TestRequest { } /// Set request payload. - pub fn set_payload>(&mut self, data: B) -> &mut Self { + pub fn set_payload(&mut self, data: impl Into) -> &mut Self { let mut payload = crate::h1::Payload::empty(); payload.unread_data(data.into()); parts(&mut self.0).payload = Some(payload.into()); diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 42ff0dba1..51fefae72 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -7,6 +7,7 @@ use std::{ io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, + task::Poll, }; use actix_http::{ @@ -16,25 +17,37 @@ use actix_http::{ Error, HttpService, Method, Request, Response, StatusCode, Version, }; use actix_http_test::test_server; +use actix_rt::pin; use actix_service::{fn_factory_with_config, fn_service}; use actix_tls::connect::rustls::webpki_roots_cert_store; -use actix_utils::future::{err, ok}; +use actix_utils::future::{err, ok, poll_fn}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; -use futures_core::Stream; -use futures_util::stream::{once, StreamExt as _}; +use futures_core::{ready, Stream}; +use futures_util::stream::once; use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; use rustls_pemfile::{certs, pkcs8_private_keys}; -async fn load_body(mut stream: S) -> Result +async fn load_body(stream: S) -> Result where - S: Stream> + Unpin, + S: Stream>, { - let mut body = BytesMut::new(); - while let Some(item) = stream.next().await { - body.extend_from_slice(&item?) - } - Ok(body) + let mut buf = BytesMut::new(); + + pin!(stream); + + poll_fn(|cx| loop { + let body = stream.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf) } fn tls_config() -> RustlsServerConfig { diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 8eabcee10..239f7f905 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1233,7 +1233,7 @@ mod tests { // and should not consume the payload match payload { - actix_web::dev::Payload::H1(_) => {} //expected + actix_web::dev::Payload::H1 { .. } => {} //expected _ => unreachable!(), } } diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 0e1f0bfec..456f119aa 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -267,7 +267,9 @@ where Connection::Tls(ConnectionType::H2(conn)) => { h2proto::send_request(conn, head.into(), body).await } - _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), + _ => { + unreachable!("Plain TCP connection can be used only with HTTP/1.1 protocol") + } } }) } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 1028a2178..cf716db72 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -13,16 +13,17 @@ use actix_http::{ Payload, RequestHeadType, ResponseHead, StatusCode, }; use actix_utils::future::poll_fn; -use bytes::buf::BufMut; -use bytes::{Bytes, BytesMut}; +use bytes::{buf::BufMut, Bytes, BytesMut}; use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; use crate::BoxError; -use super::connection::{ConnectionIo, H1Connection}; -use super::error::{ConnectError, SendRequestError}; +use super::{ + connection::{ConnectionIo, H1Connection}, + error::{ConnectError, SendRequestError}, +}; pub(crate) async fn send_request( io: H1Connection, @@ -123,7 +124,12 @@ where Ok((head, Payload::None)) } - _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))), + _ => Ok(( + head, + Payload::Stream { + payload: Box::pin(PlStream::new(framed)), + }, + )), } } diff --git a/awc/src/response.rs b/awc/src/response.rs index fefebd0a0..78cc339b4 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -10,8 +10,8 @@ use std::{ }; use actix_http::{ - error::PayloadError, header, header::HeaderMap, Extensions, HttpMessage, Payload, - PayloadStream, ResponseHead, StatusCode, Version, + error::PayloadError, header, header::HeaderMap, BoxedPayloadStream, Extensions, + HttpMessage, Payload, ResponseHead, StatusCode, Version, }; use actix_rt::time::{sleep, Sleep}; use bytes::{Bytes, BytesMut}; @@ -23,7 +23,7 @@ use crate::cookie::{Cookie, ParseError as CookieParseError}; use crate::error::JsonPayloadError; /// Client Response -pub struct ClientResponse { +pub struct ClientResponse { pub(crate) head: ResponseHead, pub(crate) payload: Payload, pub(crate) timeout: ResponseTimeout, diff --git a/awc/src/sender.rs b/awc/src/sender.rs index f83a70a9b..29c814531 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -20,7 +20,7 @@ use futures_core::Stream; use serde::Serialize; #[cfg(feature = "__compress")] -use actix_http::{encoding::Decoder, header::ContentEncoding, Payload, PayloadStream}; +use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; use crate::{ any_body::AnyBody, @@ -91,7 +91,7 @@ impl SendClientRequest { #[cfg(feature = "__compress")] impl Future for SendClientRequest { - type Output = Result>>, SendRequestError>; + type Output = Result>, SendRequestError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); @@ -108,12 +108,13 @@ impl Future for SendClientRequest { res.into_client_response()._timeout(delay.take()).map_body( |head, payload| { if *response_decompress { - Payload::Stream(Decoder::from_headers(payload, &head.headers)) + Payload::Stream { + payload: Decoder::from_headers(payload, &head.headers), + } } else { - Payload::Stream(Decoder::new( - payload, - ContentEncoding::Identity, - )) + Payload::Stream { + payload: Decoder::new(payload, ContentEncoding::Identity), + } } }, ) diff --git a/awc/src/test.rs b/awc/src/test.rs index 1b41efc93..96ae1f0a1 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -65,7 +65,7 @@ impl TestResponse { /// Set response's payload pub fn set_payload>(mut self, data: B) -> Self { - let mut payload = h1::Payload::empty(); + let (_, mut payload) = h1::Payload::create(true); payload.unread_data(data.into()); self.payload = Some(payload.into()); self @@ -90,7 +90,8 @@ impl TestResponse { if let Some(pl) = self.payload { ClientResponse::new(head, pl) } else { - ClientResponse::new(head, h1::Payload::empty().into()) + let (_, payload) = h1::Payload::create(true); + ClientResponse::new(head, payload.into()) } } } diff --git a/src/dev.rs b/src/dev.rs index 23a40f292..6e1970467 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -14,7 +14,7 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; +pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::{Server, ServerHandle}; pub use actix_service::{ diff --git a/src/response/builder.rs b/src/response/builder.rs index b500ab331..93d8ab567 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -429,9 +429,12 @@ mod tests { use actix_http::body; use super::*; - use crate::http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, + use crate::{ + http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, }; #[test] @@ -472,32 +475,23 @@ mod tests { #[actix_rt::test] async fn test_json() { - let resp = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); - let resp = HttpResponse::Ok().json(&["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let res = HttpResponse::Ok().json(&["v1", "v2", "v3"]); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); // content type override - let resp = HttpResponse::Ok() + let res = HttpResponse::Ok() .insert_header((CONTENT_TYPE, "text/json")) .json(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + let ct = res.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!( - body::to_bytes(resp.into_body()).await.unwrap().as_ref(), - br#"["v1","v2","v3"]"# - ); + assert_body_eq!(res, br#"["v1","v2","v3"]"#); } #[actix_rt::test] diff --git a/src/service.rs b/src/service.rs index 9ccf5274d..d5c381fa4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -7,7 +7,7 @@ use std::{ use actix_http::{ body::{BoxBody, EitherBody, MessageBody}, header::HeaderMap, - Extensions, HttpMessage, Method, Payload, PayloadStream, RequestHead, Response, + BoxedPayloadStream, Extensions, HttpMessage, Method, Payload, RequestHead, Response, ResponseHead, StatusCode, Uri, Version, }; use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; @@ -293,7 +293,7 @@ impl Resource for ServiceRequest { } impl HttpMessage for ServiceRequest { - type Stream = PayloadStream; + type Stream = BoxedPayloadStream; #[inline] /// Returns Request's headers. diff --git a/src/test/test_request.rs b/src/test/test_request.rs index fd3355ef3..5c4de9084 100644 --- a/src/test/test_request.rs +++ b/src/test/test_request.rs @@ -174,25 +174,28 @@ impl TestRequest { } /// Set request payload. - pub fn set_payload>(mut self, data: B) -> Self { + pub fn set_payload(mut self, data: impl Into) -> Self { self.req.set_payload(data); self } - /// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type` - /// header is set to `application/x-www-form-urlencoded`. - pub fn set_form(mut self, data: &T) -> Self { - let bytes = serde_urlencoded::to_string(data) + /// Serialize `data` to a URL encoded form and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/x-www-form-urlencoded`. + pub fn set_form(mut self, data: impl Serialize) -> Self { + let bytes = serde_urlencoded::to_string(&data) .expect("Failed to serialize test data as a urlencoded form"); self.req.set_payload(bytes); self.req.insert_header(ContentType::form_url_encoded()); self } - /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is - /// set to `application/json`. - pub fn set_json(mut self, data: &T) -> Self { - let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json"); + /// Serialize `data` to JSON and set it as the request payload. + /// + /// The `Content-Type` header is set to `application/json`. + pub fn set_json(mut self, data: impl Serialize) -> Self { + let bytes = + serde_json::to_string(&data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self From d2590fd46cbab9cf96b3e6864430f675f4512835 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 02:33:37 +0000 Subject: [PATCH 04/66] `ClientRequest::send_body` takes `impl MessageBody` (#2546) --- .github/workflows/ci-master.yml | 66 ++++ .github/workflows/ci.yml | 62 ---- awc/CHANGES.md | 6 + awc/src/any_body.rs | 19 +- awc/src/connect.rs | 2 +- awc/src/frozen.rs | 6 +- awc/src/lib.rs | 5 +- awc/src/middleware/redirect.rs | 4 +- awc/src/request.rs | 74 ++-- awc/src/response.rs | 556 ----------------------------- awc/src/responses/json_body.rs | 192 ++++++++++ awc/src/responses/mod.rs | 49 +++ awc/src/responses/read_body.rs | 61 ++++ awc/src/responses/response.rs | 257 +++++++++++++ awc/src/responses/response_body.rs | 144 ++++++++ awc/src/sender.rs | 22 +- awc/src/ws.rs | 3 +- src/guard.rs | 12 +- 18 files changed, 853 insertions(+), 687 deletions(-) create mode 100644 .github/workflows/ci-master.yml delete mode 100644 awc/src/response.rs create mode 100644 awc/src/responses/json_body.rs create mode 100644 awc/src/responses/mod.rs create mode 100644 awc/src/responses/read_body.rs create mode 100644 awc/src/responses/response.rs create mode 100644 awc/src/responses/response_body.rs diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml new file mode 100644 index 000000000..548ec21b7 --- /dev/null +++ b/.github/workflows/ci-master.yml @@ -0,0 +1,66 @@ +name: CI (master only) + +on: + push: + branches: [master] + +jobs: + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset } + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset-linux } + + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose + - name: Upload to Codecov + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9b98a7b8..fe464bf27 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,68 +96,6 @@ jobs: cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo-cache - ci_feature_powerset_check: - name: Verify Feature Combinations - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Install cargo-hack - uses: actions-rs/cargo@v1 - with: - command: install - args: cargo-hack - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset } - - - name: check feature combinations - uses: actions-rs/cargo@v1 - with: { command: ci-check-all-feature-powerset-linux } - - coverage: - name: coverage - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - name: Install stable - uses: actions-rs/toolchain@v1 - with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Generate coverage file - if: github.ref == 'refs/heads/master' - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - - name: Upload to Codecov - if: github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: { file: cobertura.xml } - rustdoc: name: doc tests runs-on: ubuntu-latest diff --git a/awc/CHANGES.md b/awc/CHANGES.md index b5144b7a2..e1a059481 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,8 +3,14 @@ ## Unreleased - 2021-xx-xx - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] +- `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] +- Rename `MessageBody => ResponseBody` to avoid conflicts with `MessageBody` trait. [#2546] +- `impl Future` for `ResponseBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Future` for `JsonBody` no longer requires the body type be `Unpin`. [#2546] +- `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] [#2503]: https://github.com/actix/actix-web/pull/2503 +[#2546]: https://github.com/actix/actix-web/pull/2546 ## 3.0.0-beta.14 - 2021-12-17 diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index 2ffeb5074..437216313 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -77,10 +77,27 @@ impl AnyBody where B: MessageBody + 'static, { + /// Converts a [`MessageBody`] type into the best possible representation. + /// + /// Checks size for `None` and tries to convert to `Bytes`. Otherwise, uses the `Body` variant. + pub fn from_message_body(body: B) -> Self + where + B: MessageBody, + { + if matches!(body.size(), BodySize::None) { + return Self::None; + } + + match body.try_into_bytes() { + Ok(body) => Self::Bytes { body }, + Err(body) => Self::new(body), + } + } + pub fn into_boxed(self) -> AnyBody { match self { Self::None => AnyBody::None, - Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Bytes { body } => AnyBody::Bytes { body }, Self::Body { body } => AnyBody::new_boxed(body), } } diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 19870b069..f93014a67 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -16,7 +16,7 @@ use crate::{ client::{ Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, }, - response::ClientResponse, + ClientResponse, }; pub type BoxConnectorService = Rc< diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index cd93a1d60..b98d8d5e1 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -5,13 +5,13 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, header::{HeaderMap, HeaderName, TryIntoHeaderValue}, Method, RequestHead, Uri, }; use crate::{ - any_body::AnyBody, sender::{RequestSender, SendClientRequest}, BoxError, ClientConfig, }; @@ -46,7 +46,7 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, @@ -159,7 +159,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { if let Some(e) = self.err { return e.into(); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 00c559406..cef8e03dc 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -113,7 +113,7 @@ pub mod error; mod frozen; pub mod middleware; mod request; -mod response; +mod responses; mod sender; pub mod test; pub mod ws; @@ -128,7 +128,8 @@ pub use self::client::Connector; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; -pub use self::response::{ClientResponse, JsonBody, MessageBody}; +#[allow(deprecated)] +pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody}; pub use self::sender::SendClientRequest; use std::{convert::TryFrom, rc::Rc, time::Duration}; diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 704d2d79d..0ee969eee 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -190,9 +190,7 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => AnyBody::Bytes { - body: bytes.clone(), - }, + Some(ref bytes) => AnyBody::from(bytes.clone()), // TODO: should this be AnyBody::Empty or AnyBody::None. _ => AnyBody::empty(), } diff --git a/awc/src/request.rs b/awc/src/request.rs index 9e37b2755..3eb76e3f6 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -5,13 +5,13 @@ use futures_core::Stream; use serde::Serialize; use actix_http::{ + body::MessageBody, error::HttpError, header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair}, ConnectionType, Method, RequestHead, Uri, Version, }; use crate::{ - any_body::AnyBody, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, @@ -26,20 +26,20 @@ use crate::cookie::{Cookie, CookieJar}; /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. /// -/// ``` -/// #[actix_rt::main] -/// async fn main() { -/// let response = awc::Client::new() -/// .get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send HTTP request -/// .await; +/// ```no_run +/// # #[actix_rt::main] +/// # async fn main() { +/// let response = awc::Client::new() +/// .get("http://www.rust-lang.org") // <- Create request builder +/// .insert_header(("User-Agent", "Actix-web")) +/// .send() // <- Send HTTP request +/// .await; /// -/// response.and_then(|response| { // <- server HTTP response -/// println!("Response: {:?}", response); -/// Ok(()) -/// }); -/// } +/// response.and_then(|response| { // <- server HTTP response +/// println!("Response: {:?}", response); +/// Ok(()) +/// }); +/// # } /// ``` pub struct ClientRequest { pub(crate) head: RequestHead, @@ -174,17 +174,13 @@ impl ClientRequest { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ``` - /// # #[actix_rt::main] - /// # async fn main() { - /// # use awc::Client; - /// use awc::http::header::CONTENT_TYPE; + /// ```no_run + /// use awc::{http::header, Client}; /// /// Client::new() /// .get("http://www.rust-lang.org") /// .insert_header(("X-TEST", "value")) - /// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)); - /// # } + /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); /// ``` pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { match header.try_into_pair() { @@ -252,23 +248,25 @@ impl ClientRequest { /// Set a cookie /// - /// ``` - /// #[actix_rt::main] - /// async fn main() { - /// let resp = awc::Client::new().get("https://www.rust-lang.org") - /// .cookie( - /// awc::cookie::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .send() - /// .await; + /// ```no_run + /// use awc::{cookie, Client}; /// - /// println!("Response: {:?}", resp); - /// } + /// # #[actix_rt::main] + /// # async fn main() { + /// let resp = Client::new().get("https://www.rust-lang.org") + /// .cookie( + /// awc::cookie::Cookie::build("name", "value") + /// .domain("www.rust-lang.org") + /// .path("/") + /// .secure(true) + /// .http_only(true) + /// .finish(), + /// ) + /// .send() + /// .await; + /// + /// println!("Response: {:?}", resp); + /// # } /// ``` #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { @@ -340,7 +338,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, diff --git a/awc/src/response.rs b/awc/src/response.rs deleted file mode 100644 index 78cc339b4..000000000 --- a/awc/src/response.rs +++ /dev/null @@ -1,556 +0,0 @@ -use std::{ - cell::{Ref, RefMut}, - fmt, - future::Future, - io, - marker::PhantomData, - pin::Pin, - task::{Context, Poll}, - time::{Duration, Instant}, -}; - -use actix_http::{ - error::PayloadError, header, header::HeaderMap, BoxedPayloadStream, Extensions, - HttpMessage, Payload, ResponseHead, StatusCode, Version, -}; -use actix_rt::time::{sleep, Sleep}; -use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; -use serde::de::DeserializeOwned; - -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, ParseError as CookieParseError}; -use crate::error::JsonPayloadError; - -/// Client Response -pub struct ClientResponse { - pub(crate) head: ResponseHead, - pub(crate) payload: Payload, - pub(crate) timeout: ResponseTimeout, -} - -/// helper enum with reusable sleep passed from `SendClientResponse`. -/// See `ClientResponse::_timeout` for reason. -pub(crate) enum ResponseTimeout { - Disabled(Option>>), - Enabled(Pin>), -} - -impl Default for ResponseTimeout { - fn default() -> Self { - Self::Disabled(None) - } -} - -impl ResponseTimeout { - fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { - match *self { - Self::Enabled(ref mut timeout) => { - if timeout.as_mut().poll(cx).is_ready() { - Err(PayloadError::Io(io::Error::new( - io::ErrorKind::TimedOut, - "Response Payload IO timed out", - ))) - } else { - Ok(()) - } - } - Self::Disabled(_) => Ok(()), - } - } -} - -impl HttpMessage for ClientResponse { - type Stream = S; - - fn headers(&self) -> &HeaderMap { - &self.head.headers - } - - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } - - fn extensions(&self) -> Ref<'_, Extensions> { - self.head.extensions() - } - - fn extensions_mut(&self) -> RefMut<'_, Extensions> { - self.head.extensions_mut() - } -} - -impl ClientResponse { - /// Create new Request instance - pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { - ClientResponse { - head, - payload, - timeout: ResponseTimeout::default(), - } - } - - #[inline] - pub(crate) fn head(&self) -> &ResponseHead { - &self.head - } - - /// Read the Request Version. - #[inline] - pub fn version(&self) -> Version { - self.head().version - } - - /// Get the status from the server. - #[inline] - pub fn status(&self) -> StatusCode { - self.head().status - } - - #[inline] - /// Returns request's headers. - pub fn headers(&self) -> &HeaderMap { - &self.head().headers - } - - /// Set a body and return previous body value - pub fn map_body(mut self, f: F) -> ClientResponse - where - F: FnOnce(&mut ResponseHead, Payload) -> Payload, - { - let payload = f(&mut self.head, self.payload); - - ClientResponse { - payload, - head: self.head, - timeout: self.timeout, - } - } - - /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). - /// - /// This duration covers the duration of processing the response body stream - /// and would end it as timeout error when deadline met. - /// - /// Disabled by default. - pub fn timeout(self, dur: Duration) -> Self { - let timeout = match self.timeout { - ResponseTimeout::Disabled(Some(mut timeout)) - | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { - Some(deadline) => { - timeout.as_mut().reset(deadline.into()); - ResponseTimeout::Enabled(timeout) - } - None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }, - _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), - }; - - Self { - payload: self.payload, - head: self.head, - timeout, - } - } - - /// This method does not enable timeout. It's used to pass the boxed `Sleep` from - /// `SendClientRequest` and reuse it's heap allocation together with it's slot in - /// timer wheel. - pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { - self.timeout = ResponseTimeout::Disabled(timeout); - self - } - - /// Load request cookies. - #[cfg(feature = "cookies")] - pub fn cookies(&self) -> Result>>, CookieParseError> { - struct Cookies(Vec>); - - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(&header::SET_COOKIE) { - let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[cfg(feature = "cookies")] - pub fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } -} - -impl ClientResponse -where - S: Stream>, -{ - /// Loads HTTP response's body. - pub fn body(&mut self) -> MessageBody { - MessageBody::new(self) - } - - /// Loads and parse `application/json` encoded body. - /// Return `JsonBody` future. It resolves to a `T` value. - /// - /// Returns error: - /// - /// * content type is not `application/json` - /// * content length is greater than 256k - pub fn json(&mut self) -> JsonBody { - JsonBody::new(self) - } -} - -impl Stream for ClientResponse -where - S: Stream> + Unpin, -{ - type Item = Result; - - fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let this = self.get_mut(); - this.timeout.poll_timeout(cx)?; - - Pin::new(&mut this.payload).poll_next(cx) - } -} - -impl fmt::Debug for ClientResponse { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; - writeln!(f, " headers:")?; - for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; - } - Ok(()) - } -} - -const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; - -/// Future that resolves to a complete HTTP message body. -pub struct MessageBody { - length: Option, - timeout: ResponseTimeout, - body: Result, Option>, -} - -impl MessageBody -where - S: Stream>, -{ - /// Create `MessageBody` for request. - pub fn new(res: &mut ClientResponse) -> MessageBody { - let length = match res.headers().get(&header::CONTENT_LENGTH) { - Some(value) => { - let len = value.to_str().ok().and_then(|s| s.parse::().ok()); - - match len { - None => return Self::err(PayloadError::UnknownLength), - len => len, - } - } - None => None, - }; - - MessageBody { - length, - timeout: std::mem::take(&mut res.timeout), - body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), - } - } - - /// Change max size of payload. By default max size is 2048kB - pub fn limit(mut self, limit: usize) -> Self { - if let Ok(ref mut body) = self.body { - body.limit = limit; - } - self - } - - fn err(e: PayloadError) -> Self { - MessageBody { - length: None, - timeout: ResponseTimeout::default(), - body: Err(Some(e)), - } - } -} - -impl Future for MessageBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - match this.body { - Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())), - Ok(ref mut body) => { - if let Some(len) = this.length.take() { - if len > body.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - } - - this.timeout.poll_timeout(cx)?; - - Pin::new(body).poll(cx) - } - } - } -} - -/// Response's payload json parser, it resolves to a deserialized `T` value. -/// -/// Returns error: -/// -/// * content type is not `application/json` -/// * content length is greater than 64k -pub struct JsonBody { - length: Option, - err: Option, - timeout: ResponseTimeout, - fut: Option>, - _phantom: PhantomData, -} - -impl JsonBody -where - S: Stream>, - U: DeserializeOwned, -{ - /// Create `JsonBody` for request. - pub fn new(res: &mut ClientResponse) -> Self { - // check content-type - let json = if let Ok(Some(mime)) = res.mime_type() { - mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - } else { - false - }; - if !json { - return JsonBody { - length: None, - fut: None, - timeout: ResponseTimeout::default(), - err: Some(JsonPayloadError::ContentType), - _phantom: PhantomData, - }; - } - - let mut len = None; - - if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } - } - } - - JsonBody { - length: len, - err: None, - timeout: std::mem::take(&mut res.timeout), - fut: Some(ReadBody::new(res.take_payload(), 65536)), - _phantom: PhantomData, - } - } - - /// Change max size of payload. By default max size is 64kB - pub fn limit(mut self, limit: usize) -> Self { - if let Some(ref mut fut) = self.fut { - fut.limit = limit; - } - self - } -} - -impl Unpin for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ -} - -impl Future for JsonBody -where - T: Stream> + Unpin, - U: DeserializeOwned, -{ - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - if let Some(err) = self.err.take() { - return Poll::Ready(Err(err)); - } - - if let Some(len) = self.length.take() { - if len > self.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); - } - } - - self.timeout - .poll_timeout(cx) - .map_err(JsonPayloadError::Payload)?; - - let body = ready!(Pin::new(&mut self.get_mut().fut.as_mut().unwrap()).poll(cx))?; - Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) - } -} - -struct ReadBody { - stream: Payload, - buf: BytesMut, - limit: usize, -} - -impl ReadBody { - fn new(stream: Payload, limit: usize) -> Self { - Self { - stream, - buf: BytesMut::new(), - limit, - } - } -} - -impl Future for ReadBody -where - S: Stream> + Unpin, -{ - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) { - if (this.buf.len() + chunk.len()) > this.limit { - return Poll::Ready(Err(PayloadError::Overflow)); - } - this.buf.extend_from_slice(&chunk); - } - - Poll::Ready(Ok(this.buf.split().freeze())) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::{Deserialize, Serialize}; - - use crate::{http::header, test::TestResponse}; - - #[actix_rt::test] - async fn test_body() { - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); - match req.body().await.err().unwrap() { - PayloadError::UnknownLength => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); - match req.body().await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"test")) - .finish(); - assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); - - let mut req = TestResponse::default() - .set_payload(Bytes::from_static(b"11111111111111")) - .finish(); - match req.body().limit(5).await.err().unwrap() { - PayloadError::Overflow => {} - _ => unreachable!("error"), - } - } - - #[derive(Serialize, Deserialize, PartialEq, Debug)] - struct MyObject { - name: String, - } - - fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { - match err { - JsonPayloadError::Payload(PayloadError::Overflow) => { - matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) - } - JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), - _ => false, - } - } - - #[actix_rt::test] - async fn test_json_body() { - let mut req = TestResponse::default().finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/text"), - )) - .finish(); - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("10000"), - )) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; - assert!(json_eq( - json.err().unwrap(), - JsonPayloadError::Payload(PayloadError::Overflow) - )); - - let mut req = TestResponse::default() - .insert_header(( - header::CONTENT_TYPE, - header::HeaderValue::from_static("application/json"), - )) - .insert_header(( - header::CONTENT_LENGTH, - header::HeaderValue::from_static("16"), - )) - .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) - .finish(); - - let json = JsonBody::<_, MyObject>::new(&mut req).await; - assert_eq!( - json.ok().unwrap(), - MyObject { - name: "test".to_owned() - } - ); - } -} diff --git a/awc/src/responses/json_body.rs b/awc/src/responses/json_body.rs new file mode 100644 index 000000000..3912324b6 --- /dev/null +++ b/awc/src/responses/json_body.rs @@ -0,0 +1,192 @@ +use std::{ + future::Future, + marker::PhantomData, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::{error::JsonPayloadError, ClientResponse}; + +pin_project! { + /// A `Future` that reads a body stream, parses JSON, resolving to a deserialized `T`. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct JsonBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + _phantom: PhantomData, + } +} + +impl JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + /// Creates a JSON body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = res.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + + if !json { + return JsonBody { + length: None, + body: None, + timeout: ResponseTimeout::default(), + err: Some(JsonPayloadError::ContentType), + _phantom: PhantomData, + }; + } + + let length = res + .headers() + .get(&header::CONTENT_LENGTH) + .and_then(|len_hdr| len_hdr.to_str().ok()) + .and_then(|len_str| len_str.parse::().ok()); + + JsonBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + _phantom: PhantomData, + } + } + + /// Change max size of payload. Default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut fut) = self.body { + fut.limit = limit; + } + + self + } +} + +impl Future for JsonBody +where + S: Stream>, + T: DeserializeOwned, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); + } + } + + this.timeout + .poll_timeout(cx) + .map_err(JsonPayloadError::Payload)?; + + let body = ready!(this.body.as_pin_mut().unwrap().poll(cx))?; + Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) + } +} + +#[cfg(test)] +mod tests { + use actix_http::BoxedPayloadStream; + use serde::{Deserialize, Serialize}; + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(JsonBody: Unpin); + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Payload(PayloadError::Overflow) => { + matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) + } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), + _ => false, + } + } + + #[actix_rt::test] + async fn read_json_body() { + let mut req = TestResponse::default().finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + )) + .finish(); + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + )) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Payload(PayloadError::Overflow) + )); + + let mut req = TestResponse::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + + let json = JsonBody::<_, MyObject>::new(&mut req).await; + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } +} diff --git a/awc/src/responses/mod.rs b/awc/src/responses/mod.rs new file mode 100644 index 000000000..588ce014c --- /dev/null +++ b/awc/src/responses/mod.rs @@ -0,0 +1,49 @@ +use std::{future::Future, io, pin::Pin, task::Context}; + +use actix_http::error::PayloadError; +use actix_rt::time::Sleep; + +mod json_body; +mod read_body; +mod response; +mod response_body; + +pub use self::json_body::JsonBody; +pub use self::response::ClientResponse; +#[allow(deprecated)] +pub use self::response_body::{MessageBody, ResponseBody}; + +/// Default body size limit: 2 MiB +const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; + +/// Helper enum with reusable sleep passed from `SendClientResponse`. +/// +/// See [`ClientResponse::_timeout`] for reason. +pub(crate) enum ResponseTimeout { + Disabled(Option>>), + Enabled(Pin>), +} + +impl Default for ResponseTimeout { + fn default() -> Self { + Self::Disabled(None) + } +} + +impl ResponseTimeout { + fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { + match *self { + Self::Enabled(ref mut timeout) => { + if timeout.as_mut().poll(cx).is_ready() { + Err(PayloadError::Io(io::Error::new( + io::ErrorKind::TimedOut, + "Response Payload IO timed out", + ))) + } else { + Ok(()) + } + } + Self::Disabled(_) => Ok(()), + } + } +} diff --git a/awc/src/responses/read_body.rs b/awc/src/responses/read_body.rs new file mode 100644 index 000000000..a32bbb984 --- /dev/null +++ b/awc/src/responses/read_body.rs @@ -0,0 +1,61 @@ +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, Payload}; +use bytes::{Bytes, BytesMut}; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +pin_project! { + pub(crate) struct ReadBody { + #[pin] + pub(crate) stream: Payload, + pub(crate) buf: BytesMut, + pub(crate) limit: usize, + } +} + +impl ReadBody { + pub(crate) fn new(stream: Payload, limit: usize) -> Self { + Self { + stream, + buf: BytesMut::new(), + limit, + } + } +} + +impl Future for ReadBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + + while let Some(chunk) = ready!(this.stream.as_mut().poll_next(cx)?) { + if (this.buf.len() + chunk.len()) > *this.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + + this.buf.extend_from_slice(&chunk); + } + + Poll::Ready(Ok(this.buf.split().freeze())) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ReadBody<()>: Unpin); + assert_impl_all!(ReadBody: Unpin); +} diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs new file mode 100644 index 000000000..6385aea19 --- /dev/null +++ b/awc/src/responses/response.rs @@ -0,0 +1,257 @@ +use std::{ + cell::{Ref, RefMut}, + fmt, mem, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +use actix_http::{ + error::PayloadError, header, header::HeaderMap, BoxedPayloadStream, Extensions, + HttpMessage, Payload, ResponseHead, StatusCode, Version, +}; +use actix_rt::time::{sleep, Sleep}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; +use serde::de::DeserializeOwned; + +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, ParseError as CookieParseError}; + +use super::{JsonBody, ResponseBody, ResponseTimeout}; + +pin_project! { + /// Client Response + pub struct ClientResponse { + pub(crate) head: ResponseHead, + #[pin] + pub(crate) payload: Payload, + pub(crate) timeout: ResponseTimeout, + } +} + +impl ClientResponse { + /// Create new Request instance + pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { + ClientResponse { + head, + payload, + timeout: ResponseTimeout::default(), + } + } + + #[inline] + pub(crate) fn head(&self) -> &ResponseHead { + &self.head + } + + /// Read the Request Version. + #[inline] + pub fn version(&self) -> Version { + self.head().version + } + + /// Get the status from the server. + #[inline] + pub fn status(&self) -> StatusCode { + self.head().status + } + + #[inline] + /// Returns request's headers. + pub fn headers(&self) -> &HeaderMap { + &self.head().headers + } + + /// Set a body and return previous body value + pub fn map_body(mut self, f: F) -> ClientResponse + where + F: FnOnce(&mut ResponseHead, Payload) -> Payload, + { + let payload = f(&mut self.head, self.payload); + + ClientResponse { + payload, + head: self.head, + timeout: self.timeout, + } + } + + /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). + /// + /// This duration covers the duration of processing the response body stream + /// and would end it as timeout error when deadline met. + /// + /// Disabled by default. + pub fn timeout(self, dur: Duration) -> Self { + let timeout = match self.timeout { + ResponseTimeout::Disabled(Some(mut timeout)) + | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { + Some(deadline) => { + timeout.as_mut().reset(deadline.into()); + ResponseTimeout::Enabled(timeout) + } + None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }, + _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }; + + Self { + payload: self.payload, + head: self.head, + timeout, + } + } + + /// This method does not enable timeout. It's used to pass the boxed `Sleep` from + /// `SendClientRequest` and reuse it's heap allocation together with it's slot in + /// timer wheel. + pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { + self.timeout = ResponseTimeout::Disabled(timeout); + self + } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + struct Cookies(Vec>); + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(&header::SET_COOKIE) { + let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + cookies.push(Cookie::parse_encoded(s)?.into_owned()); + } + self.extensions_mut().insert(Cookies(cookies)); + } + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } +} + +impl ClientResponse +where + S: Stream>, +{ + /// Returns a [`Future`] that consumes the body stream and resolves to [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json` + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB) + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # use bytes::Bytes; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/robots.txt").send().await?; + /// let body: Bytes = res.body().await?; + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn body(&mut self) -> ResponseBody { + ResponseBody::new(self) + } + + /// Returns a [`Future`] consumes the body stream, parses JSON, and resolves to a deserialized + /// `T` value. + /// + /// # Errors + /// Future returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + /// + /// # Examples + /// ```no_run + /// # use awc::Client; + /// # #[actix_rt::main] + /// # async fn async_ctx() -> Result<(), Box> { + /// let client = Client::default(); + /// let mut res = client.get("https://httpbin.org/json").send().await?; + /// let val = res.json::().await?; + /// assert!(val.is_object()); + /// # Ok(()) + /// # } + /// ``` + /// + /// [`Future`]: std::future::Future + pub fn json(&mut self) -> JsonBody { + JsonBody::new(self) + } +} + +impl fmt::Debug for ClientResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "\nClientResponse {:?} {}", self.version(), self.status(),)?; + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { + writeln!(f, " {:?}: {:?}", key, val)?; + } + Ok(()) + } +} + +impl HttpMessage for ClientResponse { + type Stream = S; + + fn headers(&self) -> &HeaderMap { + &self.head.headers + } + + fn take_payload(&mut self) -> Payload { + mem::replace(&mut self.payload, Payload::None) + } + + fn extensions(&self) -> Ref<'_, Extensions> { + self.head.extensions() + } + + fn extensions_mut(&self) -> RefMut<'_, Extensions> { + self.head.extensions_mut() + } +} + +impl Stream for ClientResponse +where + S: Stream> + Unpin, +{ + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.project(); + this.timeout.poll_timeout(cx)?; + this.payload.poll_next(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::any_body::AnyBody; + + assert_impl_all!(ClientResponse: Unpin); + assert_impl_all!(ClientResponse<()>: Unpin); + assert_impl_all!(ClientResponse: Unpin); +} diff --git a/awc/src/responses/response_body.rs b/awc/src/responses/response_body.rs new file mode 100644 index 000000000..8d9d1274a --- /dev/null +++ b/awc/src/responses/response_body.rs @@ -0,0 +1,144 @@ +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{error::PayloadError, header, HttpMessage}; +use bytes::Bytes; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use super::{read_body::ReadBody, ResponseTimeout, DEFAULT_BODY_LIMIT}; +use crate::ClientResponse; + +pin_project! { + /// A `Future` that reads a body stream, resolving as [`Bytes`]. + /// + /// # Errors + /// `Future` implementation returns error if: + /// - content type is not `application/json`; + /// - content length is greater than [limit](JsonBody::limit) (default: 2 MiB). + pub struct ResponseBody { + #[pin] + body: Option>, + length: Option, + timeout: ResponseTimeout, + err: Option, + } +} + +#[deprecated(since = "3.0.0", note = "Renamed to `ResponseBody`.")] +pub type MessageBody = ResponseBody; + +impl ResponseBody +where + S: Stream>, +{ + /// Creates a body stream reader from a response by taking its payload. + pub fn new(res: &mut ClientResponse) -> ResponseBody { + let length = match res.headers().get(&header::CONTENT_LENGTH) { + Some(value) => { + let len = value.to_str().ok().and_then(|s| s.parse::().ok()); + + match len { + None => return Self::err(PayloadError::UnknownLength), + len => len, + } + } + None => None, + }; + + ResponseBody { + body: Some(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), + length, + timeout: mem::take(&mut res.timeout), + err: None, + } + } + + /// Change max size limit of payload. + /// + /// The default limit is 2 MiB. + pub fn limit(mut self, limit: usize) -> Self { + if let Some(ref mut body) = self.body { + body.limit = limit; + } + + self + } + + fn err(err: PayloadError) -> Self { + ResponseBody { + body: None, + length: None, + timeout: ResponseTimeout::default(), + err: Some(err), + } + } +} + +impl Future for ResponseBody +where + S: Stream>, +{ + type Output = Result; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + + if let Some(err) = this.err.take() { + return Poll::Ready(Err(err)); + } + + if let Some(len) = this.length.take() { + let body = Option::as_ref(&this.body).unwrap(); + if len > body.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + } + + this.timeout.poll_timeout(cx)?; + + this.body.as_pin_mut().unwrap().poll(cx) + } +} + +#[cfg(test)] +mod tests { + use static_assertions::assert_impl_all; + + use super::*; + use crate::{http::header, test::TestResponse}; + + assert_impl_all!(ResponseBody<()>: Unpin); + + #[actix_rt::test] + async fn read_body() { + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); + match req.body().await.err().unwrap() { + PayloadError::UnknownLength => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); + match req.body().await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"test")) + .finish(); + assert_eq!(req.body().await.ok().unwrap(), Bytes::from_static(b"test")); + + let mut req = TestResponse::default() + .set_payload(Bytes::from_static(b"11111111111111")) + .finish(); + match req.body().limit(5).await.err().unwrap() { + PayloadError::Overflow => {} + _ => unreachable!("error"), + } + } +} diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 29c814531..71d705d38 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -8,7 +8,7 @@ use std::{ }; use actix_http::{ - body::BodyStream, + body::{BodyStream, MessageBody}, error::HttpError, header::{self, HeaderMap, HeaderName, TryIntoHeaderValue}, RequestHead, RequestHeadType, @@ -189,15 +189,17 @@ impl RequestSender { body: B, ) -> SendClientRequest where - B: Into, + B: MessageBody + 'static, { let req = match self { - RequestSender::Owned(head) => { - ConnectRequest::Client(RequestHeadType::Owned(head), body.into(), addr) - } + RequestSender::Owned(head) => ConnectRequest::Client( + RequestHeadType::Owned(head), + AnyBody::from_message_body(body).into_boxed(), + addr, + ), RequestSender::Rc(head, extra_headers) => ConnectRequest::Client( RequestHeadType::Rc(head, extra_headers), - body.into(), + AnyBody::from_message_body(body).into_boxed(), addr, ), }; @@ -229,9 +231,7 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes { - body: Bytes::from(body), - }, + AnyBody::from_message_body(body.into_bytes()), ) } @@ -260,9 +260,7 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::Bytes { - body: Bytes::from(body), - }, + AnyBody::from_message_body(body.into_bytes()), ) } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 06d54aadb..c63e22969 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -42,8 +42,7 @@ use crate::{ header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, ConnectionType, Method, StatusCode, Uri, Version, }, - response::ClientResponse, - ClientConfig, + ClientConfig, ClientResponse, }; #[cfg(feature = "cookies")] diff --git a/src/guard.rs b/src/guard.rs index a5770df89..d5c585c1b 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -270,13 +270,11 @@ impl Guard for HeaderGuard { /// ``` /// use actix_web::{web, guard::Host, App, HttpResponse}; /// -/// fn main() { -/// App::new().service( -/// web::resource("/index.html") -/// .guard(Host("www.rust-lang.org")) -/// .to(|| HttpResponse::MethodNotAllowed()) -/// ); -/// } +/// App::new().service( +/// web::resource("/index.html") +/// .guard(Host("www.rust-lang.org")) +/// .to(|| HttpResponse::MethodNotAllowed()) +/// ); /// ``` pub fn Host>(host: H) -> HostGuard { HostGuard(host.as_ref().to_string(), None) From 3756dfc2cea2049c393e2944cffeb84075a982e4 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 02:23:22 +0000 Subject: [PATCH 05/66] move client to own module --- awc/src/builder.rs | 6 +- awc/src/client/mod.rs | 188 ++++++++++++++++++++++++++++++++++++++++-- awc/src/frozen.rs | 3 +- awc/src/lib.rs | 183 +--------------------------------------- awc/src/request.rs | 22 ++--- awc/src/sender.rs | 3 +- awc/src/ws.rs | 5 +- 7 files changed, 203 insertions(+), 207 deletions(-) diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 30f203bb8..16a4e9cb5 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -9,11 +9,13 @@ use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; use crate::{ - client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, + client::{ + ClientConfig, ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection, + }, connect::DefaultConnector, error::SendRequestError, middleware::{NestTransform, Redirect, Transform}, - Client, ClientConfig, ConnectRequest, ConnectResponse, + Client, ConnectRequest, ConnectResponse, }; /// An HTTP Client builder diff --git a/awc/src/client/mod.rs b/awc/src/client/mod.rs index 0d5c899bc..d5854d83e 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -1,6 +1,15 @@ //! HTTP client. -use http::Uri; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; +use actix_rt::net::TcpStream; +use actix_service::Service; +pub use actix_tls::connect::{ + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, +}; + +use crate::{ws, BoxConnectorService, ClientBuilder, ClientRequest}; mod config; mod connection; @@ -10,10 +19,6 @@ mod h1proto; mod h2proto; mod pool; -pub use actix_tls::connect::{ - ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, -}; - pub use self::connection::{Connection, ConnectionIo}; pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; @@ -23,3 +28,176 @@ pub struct Connect { pub uri: Uri, pub addr: Option, } + +/// An asynchronous HTTP and WebSocket client. +/// +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. +/// +/// # Examples +/// ``` +/// use awc::Client; +/// +/// #[actix_rt::main] +/// async fn main() { +/// let mut client = Client::default(); +/// +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; +/// +/// println!("Response: {:?}", res); +/// } +/// ``` +#[derive(Clone)] +pub struct Client(pub(crate) ClientConfig); + +#[derive(Clone)] +pub(crate) struct ClientConfig { + pub(crate) connector: BoxConnectorService, + pub(crate) default_headers: Rc, + pub(crate) timeout: Option, +} + +impl Default for Client { + fn default() -> Self { + ClientBuilder::new().finish() + } +} + +impl Client { + /// Create new client instance with default settings. + pub fn new() -> Client { + Client::default() + } + + /// Create `Client` builder. + /// This function is equivalent of `ClientBuilder::new()`. + pub fn builder() -> ClientBuilder< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + > { + ClientBuilder::new() + } + + /// Construct HTTP request. + pub fn request(&self, method: Method, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ClientRequest::new(method, url, self.0.clone()); + + for header in self.0.default_headers.iter() { + // header map is empty + // TODO: probably append instead + req = req.insert_header_if_none(header); + } + req + } + + /// Create `ClientRequest` from `RequestHead` + /// + /// It is useful for proxy requests. This implementation + /// copies all headers and the method. + pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = self.request(head.method.clone(), url); + for header in head.headers.iter() { + req = req.insert_header_if_none(header); + } + req + } + + /// Construct HTTP *GET* request. + pub fn get(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::GET, url) + } + + /// Construct HTTP *HEAD* request. + pub fn head(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::HEAD, url) + } + + /// Construct HTTP *PUT* request. + pub fn put(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PUT, url) + } + + /// Construct HTTP *POST* request. + pub fn post(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::POST, url) + } + + /// Construct HTTP *PATCH* request. + pub fn patch(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::PATCH, url) + } + + /// Construct HTTP *DELETE* request. + pub fn delete(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::DELETE, url) + } + + /// Construct HTTP *OPTIONS* request. + pub fn options(&self, url: U) -> ClientRequest + where + Uri: TryFrom, + >::Error: Into, + { + self.request(Method::OPTIONS, url) + } + + /// Initialize a WebSocket connection. + /// Returns a WebSocket connection builder. + pub fn ws(&self, url: U) -> ws::WebsocketsRequest + where + Uri: TryFrom, + >::Error: Into, + { + let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); + for (key, value) in self.0.default_headers.iter() { + req.head.headers.insert(key.clone(), value.clone()); + } + req + } + + /// Get default HeaderMap of 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.default_headers) + } +} diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index b98d8d5e1..14ecf9f32 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -12,8 +12,9 @@ use actix_http::{ }; use crate::{ + client::ClientConfig, sender::{RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; /// `FrozenClientRequest` struct represents cloneable client request. diff --git a/awc/src/lib.rs b/awc/src/lib.rs index cef8e03dc..348d9312b 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -124,7 +124,7 @@ pub use actix_http as http; pub use cookie; pub use self::builder::ClientBuilder; -pub use self::client::Connector; +pub use self::client::{Client, Connector}; pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; @@ -132,185 +132,4 @@ pub use self::request::ClientRequest; pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody}; pub use self::sender::SendClientRequest; -use std::{convert::TryFrom, rc::Rc, time::Duration}; - -use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; -use actix_rt::net::TcpStream; -use actix_service::Service; - -use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; - pub(crate) type BoxError = Box; - -/// An asynchronous HTTP and WebSocket client. -/// -/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU -/// and memory usage. -/// -/// # Examples -/// ``` -/// use awc::Client; -/// -/// #[actix_rt::main] -/// async fn main() { -/// let mut client = Client::default(); -/// -/// let res = client.get("http://www.rust-lang.org") -/// .insert_header(("User-Agent", "my-app/1.2")) -/// .send() -/// .await; -/// -/// println!("Response: {:?}", res); -/// } -/// ``` -#[derive(Clone)] -pub struct Client(ClientConfig); - -#[derive(Clone)] -pub(crate) struct ClientConfig { - pub(crate) connector: BoxConnectorService, - pub(crate) default_headers: Rc, - pub(crate) timeout: Option, -} - -impl Default for Client { - fn default() -> Self { - ClientBuilder::new().finish() - } -} - -impl Client { - /// Create new client instance with default settings. - pub fn new() -> Client { - Client::default() - } - - /// Create `Client` builder. - /// This function is equivalent of `ClientBuilder::new()`. - pub fn builder() -> ClientBuilder< - impl Service< - ConnectInfo, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone, - > { - ClientBuilder::new() - } - - /// Construct HTTP request. - pub fn request(&self, method: Method, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ClientRequest::new(method, url, self.0.clone()); - - for header in self.0.default_headers.iter() { - // header map is empty - // TODO: probably append instead - req = req.insert_header_if_none(header); - } - req - } - - /// Create `ClientRequest` from `RequestHead` - /// - /// It is useful for proxy requests. This implementation - /// copies all headers and the method. - pub fn request_from(&self, url: U, head: &RequestHead) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = self.request(head.method.clone(), url); - for header in head.headers.iter() { - req = req.insert_header_if_none(header); - } - req - } - - /// Construct HTTP *GET* request. - pub fn get(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::GET, url) - } - - /// Construct HTTP *HEAD* request. - pub fn head(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::HEAD, url) - } - - /// Construct HTTP *PUT* request. - pub fn put(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PUT, url) - } - - /// Construct HTTP *POST* request. - pub fn post(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::POST, url) - } - - /// Construct HTTP *PATCH* request. - pub fn patch(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::PATCH, url) - } - - /// Construct HTTP *DELETE* request. - pub fn delete(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::DELETE, url) - } - - /// Construct HTTP *OPTIONS* request. - pub fn options(&self, url: U) -> ClientRequest - where - Uri: TryFrom, - >::Error: Into, - { - self.request(Method::OPTIONS, url) - } - - /// Initialize a WebSocket connection. - /// Returns a WebSocket connection builder. - pub fn ws(&self, url: U) -> ws::WebsocketsRequest - where - Uri: TryFrom, - >::Error: Into, - { - let mut req = ws::WebsocketsRequest::new(url, self.0.clone()); - for (key, value) in self.0.default_headers.iter() { - req.head.headers.insert(key.clone(), value.clone()); - } - req - } - - /// Get default HeaderMap of 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.default_headers) - } -} diff --git a/awc/src/request.rs b/awc/src/request.rs index 3eb76e3f6..8824dd08a 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -12,10 +12,11 @@ use actix_http::{ }; use crate::{ + client::ClientConfig, error::{FreezeRequestError, InvalidUrl}, frozen::FrozenClientRequest, sender::{PrepForSendingError, RequestSender, SendClientRequest}, - BoxError, ClientConfig, + BoxError, }; #[cfg(feature = "cookies")] @@ -249,23 +250,16 @@ impl ClientRequest { /// Set a cookie /// /// ```no_run - /// use awc::{cookie, Client}; + /// use awc::{cookie::Cookie, Client}; /// /// # #[actix_rt::main] /// # async fn main() { - /// let resp = Client::new().get("https://www.rust-lang.org") - /// .cookie( - /// awc::cookie::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .send() - /// .await; + /// let res = Client::new().get("https://httpbin.org/cookies") + /// .cookie(Cookie::new("name", "value")) + /// .send() + /// .await; /// - /// println!("Response: {:?}", resp); + /// println!("Response: {:?}", res); /// # } /// ``` #[cfg(feature = "cookies")] diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 71d705d38..edf41163d 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -24,8 +24,9 @@ use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; use crate::{ any_body::AnyBody, + client::ClientConfig, error::{FreezeRequestError, InvalidUrl, SendRequestError}, - BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, + BoxError, ClientResponse, ConnectRequest, ConnectResponse, }; #[derive(Debug, From)] diff --git a/awc/src/ws.rs b/awc/src/ws.rs index c63e22969..96f8cf893 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -31,18 +31,19 @@ use std::{convert::TryFrom, fmt, net::SocketAddr, str}; use actix_codec::Framed; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; -use actix_service::Service; +use actix_service::Service as _; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::{ + client::ClientConfig, connect::{BoxedSocket, ConnectRequest}, error::{HttpError, InvalidUrl, SendRequestError, WsClientError}, http::{ header::{self, HeaderName, HeaderValue, TryIntoHeaderValue, AUTHORIZATION}, ConnectionType, Method, StatusCode, Uri, Version, }, - ClientConfig, ClientResponse, + ClientResponse, }; #[cfg(feature = "cookies")] From 01cbfc57244bc7c0528d158dbc61492ed65a64a3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 02:28:23 +0000 Subject: [PATCH 06/66] reduce -http re-exports in awc --- awc/src/error.rs | 1 + awc/src/lib.rs | 17 +++++++++++++---- src/http/mod.rs | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/awc/src/error.rs b/awc/src/error.rs index c1d855053..aa9dc4d99 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,5 +1,6 @@ //! HTTP client errors +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{ error::{HttpError, PayloadError}, header::HeaderValue, diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 348d9312b..970ca2d92 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -105,6 +105,11 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] +pub use actix_http::body; + +#[cfg(feature = "cookies")] +pub use cookie; + mod any_body; mod builder; mod client; @@ -118,10 +123,14 @@ mod sender; pub mod test; pub mod ws; -// TODO: hmmmmmm -pub use actix_http as http; -#[cfg(feature = "cookies")] -pub use cookie; +pub mod http { + //! Various HTTP related types. + + // TODO: figure out how best to expose http::Error vs actix_http::Error + pub use actix_http::{ + header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version, + }; +} pub use self::builder::ClientBuilder; pub use self::client::{Client, Connector}; diff --git a/src/http/mod.rs b/src/http/mod.rs index bbd94a60f..2581532cd 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -2,4 +2,5 @@ pub mod header; +// TODO: figure out how best to expose http::Error vs actix_http::Error pub use actix_http::{uri, ConnectionType, Error, Method, StatusCode, Uri, Version}; From 34e5c7c799b88dcd18ebaf8c4b8d75132e19bf25 Mon Sep 17 00:00:00 2001 From: Mark Lodato Date: Fri, 24 Dec 2021 21:35:19 -0500 Subject: [PATCH 07/66] Improve module docs for error handler middleware (#2543) --- src/middleware/err_handlers.rs | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 6d064372f..bde054330 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -37,27 +37,21 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result(mut res: dev::ServiceResponse) -> Result> { -/// res.response_mut() -/// .headers_mut() -/// .insert(header::CONTENT_TYPE, header::HeaderValue::from_static("Error")); +/// use actix_web::http::{header, StatusCode}; +/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; +/// use actix_web::{dev, web, App, HttpResponse, Result}; /// +/// fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// res.response_mut().headers_mut().insert( +/// header::CONTENT_TYPE, +/// header::HeaderValue::from_static("Error"), +/// ); /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// /// let app = App::new() -/// .wrap( -/// ErrorHandlers::new() -/// .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500), -/// ) -/// .service(web::resource("/test") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// .route(web::head().to(|| HttpResponse::MethodNotAllowed()) -/// )); +/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) +/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` pub struct ErrorHandlers { handlers: Handlers, From adf993584124f44fc07835fcd7e467184291ab38 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 03:44:09 +0000 Subject: [PATCH 08/66] improve scope documentation closes #2389 --- actix-router/src/resource.rs | 54 +++++++-------- src/resource.rs | 27 ++++---- src/route.rs | 6 +- src/scope.rs | 126 ++++++++++++++++------------------- src/web.rs | 13 +++- 5 files changed, 109 insertions(+), 117 deletions(-) diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index fa77b1e7b..f1eb9caf5 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -29,26 +29,25 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// /// /// # Pattern Format and Matching Behavior -/// /// Resource pattern is defined as a string of zero or more _segments_ where each segment is /// preceded by a slash `/`. /// -/// This means that pattern string __must__ either be empty or begin with a slash (`/`). -/// This also implies that a trailing slash in pattern defines an empty segment. -/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also +/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern +/// `"/user/"` has two segments: `["user", ""]` /// -/// A key point to underhand is that `ResourceDef` matches segments, not strings. -/// It matches segments individually. -/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, -/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are +/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path +/// `/user/123/456`, because the second segment doesn't match: `["user", ""]` +/// vs `["user", "123", "456"]`. /// /// This definition is consistent with the definition of absolute URL path in -/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// [RFC 3986 §3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) /// /// /// # Static Resources -/// A static resource is the most basic type of definition. Pass a pattern to -/// [new][Self::new]. Conforming paths must match the pattern exactly. +/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new]. +/// Conforming paths must match the pattern exactly. /// /// ## Examples /// ``` @@ -63,7 +62,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/search")); /// ``` /// -/// /// # Dynamic Segments /// Also known as "path parameters". Resources can define sections of a pattern that be extracted /// from a conforming path, if it conforms to (one of) the resource pattern(s). @@ -102,15 +100,15 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("id").unwrap(), "123"); /// ``` /// -/// /// # Prefix Resources /// A prefix resource is defined as pattern that can match just the start of a path, up to a /// segment boundary. /// /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. -/// They define and therefore require an empty segment in order to match. Examples are given below. +/// They define and therefore require an empty segment in order to match. It is easier to understand +/// this behavior after reading the [matching behavior section]. Examples are given below. /// -/// Empty pattern matches any path as a prefix. +/// The empty pattern (`""`), as a prefix, matches any path. /// /// Prefix resources can contain dynamic segments. /// @@ -130,7 +128,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/123")); /// ``` /// -/// /// # Custom Regex Segments /// Dynamic segments can be customised to only match a specific regular expression. It can be /// helpful to do this if resource definitions would otherwise conflict and cause one to @@ -158,7 +155,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!resource.is_match("/user/abc")); /// ``` /// -/// /// # Tail Segments /// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those /// up until a `/` character), there is a special pattern to match (and capture) the remaining @@ -179,7 +175,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); /// ``` /// -/// /// # Multi-Pattern Resources /// For resources that can map to multiple distinct paths, it may be suitable to use /// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined @@ -198,7 +193,6 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(resource.is_match("/index")); /// ``` /// -/// /// # Trailing Slashes /// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. /// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if @@ -212,6 +206,8 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// assert!(!ResourceDef::new("/root/").is_match("/root")); /// assert!(!ResourceDef::prefix("/root/").is_match("/root")); /// ``` +/// +/// [matching behavior section]: #pattern-format-and-matching-behavior #[derive(Clone, Debug)] pub struct ResourceDef { id: u16, @@ -279,7 +275,7 @@ impl ResourceDef { /// ``` pub fn new(paths: T) -> Self { profile_method!(new); - Self::new2(paths, false) + Self::construct(paths, false) } /// Constructs a new resource definition using a pattern that performs prefix matching. @@ -292,7 +288,7 @@ impl ResourceDef { /// resource definition with a tail segment; use [`new`][Self::new] in this case. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -307,14 +303,14 @@ impl ResourceDef { /// ``` pub fn prefix(paths: T) -> Self { profile_method!(prefix); - ResourceDef::new2(paths, true) + ResourceDef::construct(paths, true) } /// Constructs a new resource definition using a string pattern that performs prefix matching, - /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// ensuring a leading `/` if pattern is not empty. /// /// # Panics - /// Panics if path regex pattern is malformed. + /// Panics if path pattern is malformed. /// /// # Examples /// ``` @@ -515,8 +511,8 @@ impl ResourceDef { .collect::>(); match patterns.len() { - 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), - _ => ResourceDef::new2(patterns, other.is_prefix()), + 1 => ResourceDef::construct(&patterns[0], other.is_prefix()), + _ => ResourceDef::construct(patterns, other.is_prefix()), } } @@ -881,8 +877,8 @@ impl ResourceDef { } } - fn new2(paths: T, is_prefix: bool) -> Self { - profile_method!(new2); + fn construct(paths: T, is_prefix: bool) -> Self { + profile_method!(construct); let patterns = paths.patterns(); let (pat_type, segments) = match &patterns { @@ -1814,7 +1810,7 @@ mod tests { #[test] #[should_panic] - fn prefix_plus_tail_match_is_allowed() { + fn prefix_plus_tail_match_disallowed() { ResourceDef::prefix("/user/{id}*"); } } diff --git a/src/resource.rs b/src/resource.rs index d94d2a464..0d82bb004 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -23,28 +23,25 @@ use crate::{ BoxError, Error, FromRequest, HttpResponse, Responder, }; -/// *Resource* is an entry in resources table which corresponds to requested URL. +/// A collection of [`Route`]s that respond to the same path pattern. /// -/// Resource in turn has at least one route. -/// Route consists of an handlers objects and list of guards -/// (objects that implement `Guard` trait). -/// Resources and routes uses builder-like pattern for configuration. -/// During request handling, resource object iterate through all routes -/// and check guards for specific route, if request matches all -/// guards, route considered matched and route handler get called. +/// Resource in turn has at least one route. Route consists of an handlers objects and list of +/// guards (objects that implement `Guard` trait). Resources and routes uses builder-like pattern +/// for configuration. During request handling, resource object iterate through all routes and check +/// guards for specific route, if request matches all guards, route considered matched and route +/// handler get called. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/") -/// .route(web::get().to(|| HttpResponse::Ok()))); -/// } +/// let app = App::new().service( +/// web::resource("/") +/// .route(web::get().to(|| HttpResponse::Ok()))); /// ``` /// -/// If no matching route could be found, *405* response code get returned. -/// Default behavior could be overridden with `default_resource()` method. +/// If no matching route could be found, *405* response code get returned. Default behavior could be +/// overridden with `default_resource()` method. pub struct Resource { endpoint: T, rdef: Patterns, diff --git a/src/route.rs b/src/route.rs index 4447bff50..16c01275b 100644 --- a/src/route.rs +++ b/src/route.rs @@ -15,10 +15,10 @@ use crate::{ BoxError, Error, FromRequest, HttpResponse, Responder, }; -/// Resource route definition +/// A request handler with [guards](guard). /// -/// Route uses builder-like pattern for configuration. -/// If handler is not explicitly set, default *404 Not Found* handler is used. +/// Route uses a builder-like pattern for configuration. If handler is not set, a `404 Not Found` +/// handler is used. pub struct Route { service: BoxedHttpServiceFactory, guards: Rc>>, diff --git a/src/scope.rs b/src/scope.rs index 1fd282f61..c3bab8f7e 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -27,34 +27,36 @@ use crate::{ type Guards = Vec>; -/// Resources scope. +/// A collection of [`Route`]s, [`Resource`]s, or other services that share a common path prefix. /// -/// Scope is a set of resources with common root path. -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. -/// Scope prefix is always complete path segment, i.e `/app` would -/// be converted to a `/app/` and it would not match `/app` path. +/// The `Scope`'s path can contain [dynamic segments]. The dynamic segments can be extracted from +/// requests using the [`Path`](crate::web::Path) extractor or +/// with [`HttpRequest::match_info()`](crate::HttpRequest::match_info). /// -/// You can get variable path segments from `HttpRequest::match_info()`. -/// `Path` extractor also is able to extract scope level variable segments. +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. /// +/// # Examples /// ``` /// use actix_web::{web, App, HttpResponse}; /// -/// fn main() { -/// let app = App::new().service( -/// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { "OK" })) -/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) -/// ); -/// } +/// let app = App::new().service( +/// web::scope("/{project_id}/") +/// .service(web::resource("/path1").to(|| async { "OK" })) +/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) +/// ); /// ``` /// /// In the above example three routes get registered: -/// * /{project_id}/path1 - responds to all http method -/// * /{project_id}/path2 - `GET` requests -/// * /{project_id}/path3 - `HEAD` requests +/// - /{project_id}/path1 - responds to all HTTP methods +/// - /{project_id}/path2 - responds to `GET` requests +/// - /{project_id}/path3 - responds to `HEAD` requests +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources +/// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments pub struct Scope { endpoint: T, rdef: String, @@ -106,16 +108,14 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .guard(guard::Header("content-type", "text/plain")) - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|r: HttpRequest| { - /// HttpResponse::MethodNotAllowed() - /// })) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .guard(guard::Header("content-type", "text/plain")) + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|r: HttpRequest| { + /// HttpResponse::MethodNotAllowed() + /// })) + /// ); /// ``` pub fn guard(mut self, guard: G) -> Self { self.guards.push(Box::new(guard)); @@ -186,15 +186,13 @@ where /// ); /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap(middleware::Logger::default()) - /// .service( - /// web::scope("/api") - /// .configure(config) - /// ) - /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); - /// } + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .service( + /// web::scope("/api") + /// .configure(config) + /// ) + /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// ``` pub fn configure(mut self, cfg_fn: F) -> Self where @@ -233,13 +231,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app").service( - /// web::scope("/v1") - /// .service(web::resource("/test1").to(index))) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app").service( + /// web::scope("/v1") + /// .service(web::resource("/test1").to(index))) + /// ); /// ``` pub fn service(mut self, factory: F) -> Self where @@ -263,13 +259,11 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) - /// ); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())) + /// ); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -355,21 +349,19 @@ where /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index))); - /// } + /// let app = App::new().service( + /// web::scope("/app") + /// .wrap_fn(|req, srv| { + /// let fut = srv.call(req); + /// async { + /// let mut res = fut.await?; + /// res.headers_mut().insert( + /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), + /// ); + /// Ok(res) + /// } + /// }) + /// .route("/index.html", web::get().to(index))); /// ``` pub fn wrap_fn( self, diff --git a/src/web.rs b/src/web.rs index 042b8a008..22877692d 100644 --- a/src/web.rs +++ b/src/web.rs @@ -52,11 +52,16 @@ pub fn resource(path: T) -> Resource { /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic /// path segments. /// +/// # Avoid Trailing Slashes +/// Avoid using trailing slashes in the scope prefix (e.g., `web::scope("/scope/")`). It will almost +/// certainly not have the expected behavior. See the [documentation on resource definitions][pat] +/// to understand why this is the case and how to correctly construct scope/prefix definitions. +/// /// # Examples /// In this example, three routes are set up (and will handle any method): -/// * `/{project_id}/path1` -/// * `/{project_id}/path2` -/// * `/{project_id}/path3` +/// - `/{project_id}/path1` +/// - `/{project_id}/path2` +/// - `/{project_id}/path3` /// /// ``` /// use actix_web::{web, App, HttpResponse}; @@ -68,6 +73,8 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` +/// +/// [pat]: crate::dev::ResourceDef#prefix-resources pub fn scope(path: &str) -> Scope { Scope::new(path) } From 5860fe53814ede54d3f6939af1a457a9b9549613 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 04:43:59 +0000 Subject: [PATCH 09/66] expose Handler trait --- src/handler.rs | 99 ++++++++++++++++++++++++++++++++++++++++--------- src/lib.rs | 1 + src/resource.rs | 6 +-- src/route.rs | 6 +-- src/web.rs | 6 +-- 5 files changed, 91 insertions(+), 27 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e543ecc7f..647606890 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -8,26 +8,88 @@ use crate::{ BoxError, FromRequest, HttpResponse, Responder, }; -/// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted -/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). +/// The interface for request handlers. /// -/// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See for more information. +/// # What Is A Request Handler +/// A request handler has three requirements: +/// 1. It is an async function (or a function/closure that returns an appropriate future); +/// 1. The function accepts zero or more parameters that implement [`FromRequest`]; +/// 1. The async function (or future) resolves to a type that can be converted into an +/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// -/// [`impl FromRequest`]: crate::FromRequest +/// # Compiler Errors +/// If you get the error `the trait Handler<_, _, _> is not implemented`, then your handler does not +/// fulfill one or more of the above requirements. +/// +/// Unfortunately we cannot provide a better compile error message (while keeping the trait's +/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added +/// to Rust. +/// +/// # How Do Handlers Receive Variable Numbers Of Arguments +/// Rest assured there is no macro magic here; it's just traits. +/// +/// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). +/// +/// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way +/// that aligns their parameter positions with a corresponding tuple of types (becoming the `T` type +/// parameter in this trait's implementation). +/// +/// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the +/// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on +/// that tuple, and the `Handler::call` implementation for that particular function arity +/// destructures the tuple into it's component types and calls your handler function with them. +/// +/// In pseudo-code the process looks something like this: +/// ```ignore +/// async fn my_handler(body: String, state: web::Data) -> impl Responder { +/// ... +/// } +/// +/// // the function params above described as a tuple, names do not matter, only position +/// type InferredMyHandlerArgs = (String, web::Data); +/// +/// // create tuple of arguments to be passed to handler +/// let args = InferredMyHandlerArgs::from_request(&request, &payload).await; +/// +/// // call handler with argument tuple +/// let response = Handler::call(&my_handler, args).await; +/// +/// // which is effectively... +/// +/// let (body, state) = args; +/// let response = my_handler(body, state).await; +/// ``` +/// +/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the +/// bounds of the handler call after argument extraction: +/// ```ignore +/// impl Handler<(Arg1, Arg2), R> for Func +/// where +/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static, +/// R: Future, +/// R::Output: Responder, +/// { +/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R { +/// (self)(arg1, arg2) +/// } +/// } +/// ``` +/// +/// [arity]: https://en.wikipedia.org/wiki/Arity +/// [`from_request`]: FromRequest::from_request +/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 pub trait Handler: Clone + 'static where R: Future, R::Output: Responder, { - fn call(&self, param: T) -> R; + fn call(&self, args: T) -> R; } -pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where - F: Handler, - T: FromRequest, + F: Handler, + Args: FromRequest, R: Future, R::Output: Responder, ::Body: MessageBody, @@ -39,7 +101,7 @@ where async move { let (req, mut payload) = req.into_parts(); - let res = match T::from_request(&req, &mut payload).await { + let res = match Args::from_request(&req, &mut payload).await { Err(err) => HttpResponse::from_error(err), Ok(data) => handler @@ -59,17 +121,18 @@ where /// /// # Examples /// ```ignore -/// factory_tuple! {} // implements Handler for types: fn() -> Res -/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// factory_tuple! {} // implements Handler for types: fn() -> R +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { - impl Handler<($($param,)*), Res> for Func - where Func: Fn($($param),*) -> Res + Clone + 'static, - Res: Future, - Res::Output: Responder, + impl Handler<($($param,)*), R> for Func + where Func: Fn($($param),*) -> R + Clone + 'static, + R: Future, + R::Output: Responder, { + #[inline] #[allow(non_snake_case)] - fn call(&self, ($($param,)*): ($($param,)*)) -> Res { + fn call(&self, ($($param,)*): ($($param,)*)) -> R { (self)($($param,)*) } } diff --git a/src/lib.rs b/src/lib.rs index 171a2d101..3462ae90b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -106,6 +106,7 @@ pub use cookie; pub use crate::app::App; pub use crate::error::{Error, ResponseError, Result}; pub use crate::extract::FromRequest; +pub use crate::handler::Handler; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; diff --git a/src/resource.rs b/src/resource.rs index 0d82bb004..c6c8a5b89 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -232,10 +232,10 @@ where /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - I: FromRequest + 'static, + F: Handler, + Args: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, ::Body: MessageBody, diff --git a/src/route.rs b/src/route.rs index 16c01275b..6396c4286 100644 --- a/src/route.rs +++ b/src/route.rs @@ -176,10 +176,10 @@ impl Route { /// ); /// } /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, - T: FromRequest + 'static, + F: Handler, + Args: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, ::Body: MessageBody, diff --git a/src/web.rs b/src/web.rs index 22877692d..46e7704b6 100644 --- a/src/web.rs +++ b/src/web.rs @@ -146,10 +146,10 @@ pub fn method(method: Method) -> Route { /// web::to(index)) /// ); /// ``` -pub fn to(handler: F) -> Route +pub fn to(handler: F) -> Route where - F: Handler, - I: FromRequest + 'static, + F: Handler, + Args: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, ::Body: MessageBody + 'static, From 2e493cf7915587f157f6da42b1a0dfaa34d7f097 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sat, 25 Dec 2021 04:53:51 +0000 Subject: [PATCH 10/66] remove crate level clippy allows --- src/app.rs | 42 +++++++++++++-------------- src/app_service.rs | 1 + src/config.rs | 2 ++ src/guard.rs | 58 ++++++++++++++++---------------------- src/handler.rs | 8 +++--- src/lib.rs | 1 - src/middleware/compress.rs | 1 + src/resource.rs | 16 +++++------ src/scope.rs | 1 + src/server.rs | 1 + 10 files changed, 61 insertions(+), 70 deletions(-) diff --git a/src/app.rs b/src/app.rs index b4b952734..10868d18d 100644 --- a/src/app.rs +++ b/src/app.rs @@ -122,9 +122,10 @@ impl App { self.app_data(Data::new(data)) } - /// Add application data factory. This function is similar to `.data()` but it accepts a - /// "data factory". Data values are constructed asynchronously during application - /// initialization, before the server starts accepting requests. + /// Add application data factory that resolves asynchronously. + /// + /// Data items are constructed during application initialization, before the server starts + /// accepting requests. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -150,6 +151,7 @@ impl App { } .boxed_local() })); + self } @@ -200,11 +202,9 @@ impl App { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .route("/test1", web::get().to(index)) - /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); - /// } + /// let app = App::new() + /// .route("/test1", web::get().to(index)) + /// .route("/test2", web::post().to(|| HttpResponse::MethodNotAllowed())); /// ``` pub fn route(self, path: &str, mut route: Route) -> Self { self.service( @@ -243,13 +243,11 @@ impl App { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").route(web::get().to(index))) - /// .default_service( - /// web::route().to(|| HttpResponse::NotFound())); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").route(web::get().to(index))) + /// .default_service( + /// web::route().to(|| HttpResponse::NotFound())); /// ``` /// /// It is also possible to use static files as default service. @@ -257,14 +255,12 @@ impl App { /// ``` /// use actix_web::{web, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new() - /// .service( - /// web::resource("/index.html").to(|| HttpResponse::Ok())) - /// .default_service( - /// web::to(|| HttpResponse::NotFound()) - /// ); - /// } + /// let app = App::new() + /// .service( + /// web::resource("/index.html").to(|| HttpResponse::Ok())) + /// .default_service( + /// web::to(|| HttpResponse::NotFound()) + /// ); /// ``` pub fn default_service(mut self, svc: F) -> Self where diff --git a/src/app_service.rs b/src/app_service.rs index 4e84cb201..e0d424390 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -236,6 +236,7 @@ where } pub struct AppRoutingFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, diff --git a/src/config.rs b/src/config.rs index 9e77c0f96..cfa9a4ca3 100644 --- a/src/config.rs +++ b/src/config.rs @@ -24,6 +24,7 @@ pub struct AppService { config: AppConfig, root: bool, default: Rc, + #[allow(clippy::type_complexity)] services: Vec<( ResourceDef, HttpNewService, @@ -48,6 +49,7 @@ impl AppService { self.root } + #[allow(clippy::type_complexity)] pub(crate) fn into_services( self, ) -> ( diff --git a/src/guard.rs b/src/guard.rs index d5c585c1b..db7f06987 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -15,14 +15,12 @@ //! ``` //! use actix_web::{web, http, dev, guard, App, HttpResponse}; //! -//! fn main() { -//! App::new().service(web::resource("/index.html").route( -//! web::route() -//! .guard(guard::Post()) -//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) -//! .to(|| HttpResponse::MethodNotAllowed())) -//! ); -//! } +//! App::new().service(web::resource("/index.html").route( +//! web::route() +//! .guard(guard::Post()) +//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) +//! .to(|| HttpResponse::MethodNotAllowed())) +//! ); //! ``` #![allow(non_snake_case)] @@ -53,16 +51,14 @@ impl Guard for Rc { /// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::fn_guard( -/// |req| req.headers() -/// .contains_key("content-type"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard( +/// guard::fn_guard( +/// |req| req.headers() +/// .contains_key("content-type"))) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); /// ``` pub fn fn_guard(f: F) -> impl Guard where @@ -96,13 +92,11 @@ where /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard(guard::Any(guard::Get()).or(guard::Post())) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); /// ``` pub fn Any(guard: F) -> AnyGuard { AnyGuard(vec![Box::new(guard)]) @@ -135,14 +129,12 @@ impl Guard for AnyGuard { /// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// -/// fn main() { -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); -/// } +/// App::new().service(web::resource("/index.html").route( +/// web::route() +/// .guard( +/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) +/// .to(|| HttpResponse::MethodNotAllowed())) +/// ); /// ``` pub fn All(guard: F) -> AllGuard { AllGuard(vec![Box::new(guard)]) diff --git a/src/handler.rs b/src/handler.rs index 647606890..ea6855c7f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -31,8 +31,8 @@ use crate::{ /// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). /// /// Secondly, the `Handler` trait is implemented for functions (up to an [arity] of 12) in a way -/// that aligns their parameter positions with a corresponding tuple of types (becoming the `T` type -/// parameter in this trait's implementation). +/// that aligns their parameter positions with a corresponding tuple of types (becoming the `Args` +/// type parameter for this trait). /// /// Thanks to Rust's type system, Actix Web can infer the function parameter types. During the /// extraction step, the parameter types are described as a tuple type, [`from_request`] is run on @@ -78,12 +78,12 @@ use crate::{ /// [arity]: https://en.wikipedia.org/wiki/Arity /// [`from_request`]: FromRequest::from_request /// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 -pub trait Handler: Clone + 'static +pub trait Handler: Clone + 'static where R: Future, R::Output: Responder, { - fn call(&self, args: T) -> R; + fn call(&self, args: Args) -> R; } pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory diff --git a/src/lib.rs b/src/lib.rs index 3462ae90b..5f5b915b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,7 +66,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::needless_doctest_main, clippy::type_complexity)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index af4a107e3..d3cdf5763 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -113,6 +113,7 @@ where { type Response = ServiceResponse>>; type Error = Error; + #[allow(clippy::type_complexity)] type Future = Either, Ready>>; actix_service::forward_ready!(service); diff --git a/src/resource.rs b/src/resource.rs index c6c8a5b89..f0c6f6d7c 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -131,15 +131,13 @@ where /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/").route( - /// web::route() - /// .guard(guard::Any(guard::Get()).or(guard::Put())) - /// .guard(guard::Header("Content-Type", "text/plain")) - /// .to(|| HttpResponse::Ok())) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/").route( + /// web::route() + /// .guard(guard::Any(guard::Get()).or(guard::Put())) + /// .guard(guard::Header("Content-Type", "text/plain")) + /// .to(|| HttpResponse::Ok())) + /// ); /// ``` /// /// Multiple routes could be added to a resource. Resource object uses diff --git a/src/scope.rs b/src/scope.rs index c3bab8f7e..176e0d5a0 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -469,6 +469,7 @@ where } pub struct ScopeFactory { + #[allow(clippy::type_complexity)] services: Rc< [( ResourceDef, diff --git a/src/server.rs b/src/server.rs index b2ff423f1..ed0c965b3 100644 --- a/src/server.rs +++ b/src/server.rs @@ -63,6 +63,7 @@ where backlog: u32, sockets: Vec, builder: ServerBuilder, + #[allow(clippy::type_complexity)] on_connect_fn: Option>, _phantom: PhantomData<(S, B)>, } From ac0c4eb68434a975beacfca20e26cf2596999f5f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Sun, 26 Dec 2021 21:24:03 +0000 Subject: [PATCH 11/66] update actix-tls references to stable 3.0.0 --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-http/Cargo.toml | 4 ++-- awc/Cargo.toml | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d15f26172..7b095af91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -75,7 +75,7 @@ actix-rt = "2.3" actix-server = "2.0.0-rc.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-http = "3.0.0-beta.16" actix-router = "0.5.0-beta.3" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index e1c875a1f..2ad620b08 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -31,7 +31,7 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0" actix-codec = "0.4.1" -actix-tls = "3.0.0-rc.1" +actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-rc.1" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index c15f5ee28..3fa437562 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -71,7 +71,7 @@ sha-1 = "0.10" smallvec = "1.6.1" # tls -actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } +actix-tls = { version = "3.0.0", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } @@ -81,7 +81,7 @@ zstd = { version = "0.9", optional = true } [dev-dependencies] 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-tls = { version = "3.0.0", features = ["openssl"] } actix-web = "4.0.0-beta.15" async-stream = "0.3" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 4b29aac16..d0494e023 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -62,7 +62,7 @@ actix-codec = "0.4.1" actix-service = "2.0.0" actix-http = "3.0.0-beta.16" actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3.0.0-rc.2", features = ["connect", "uri"] } +actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-utils = "3.0.0" ahash = "0.7" @@ -97,7 +97,7 @@ 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.9", features = ["openssl", "rustls"] } -actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" actix-web = { version = "4.0.0-beta.15", features = ["openssl"] } From 554ae7a868b169b9210c9c24002ca1afd96f0c71 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Mon, 27 Dec 2021 03:44:30 +0300 Subject: [PATCH 12/66] rework Handler trait (#2549) --- src/handler.rs | 37 +++++++++++++++++-------------------- src/resource.rs | 11 ++++------- src/route.rs | 14 +++++--------- src/web.rs | 15 ++++++--------- 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index ea6855c7f..d458e22e1 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,9 +3,8 @@ use std::future::Future; use actix_service::{boxed, fn_service}; use crate::{ - body::MessageBody, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, FromRequest, HttpResponse, Responder, + FromRequest, HttpResponse, Responder, }; /// The interface for request handlers. @@ -18,7 +17,7 @@ use crate::{ /// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// /// # Compiler Errors -/// If you get the error `the trait Handler<_, _, _> is not implemented`, then your handler does not +/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not /// fulfill one or more of the above requirements. /// /// Unfortunately we cannot provide a better compile error message (while keeping the trait's @@ -78,22 +77,18 @@ use crate::{ /// [arity]: https://en.wikipedia.org/wiki/Arity /// [`from_request`]: FromRequest::from_request /// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 -pub trait Handler: Clone + 'static -where - R: Future, - R::Output: Responder, -{ - fn call(&self, args: Args) -> R; +pub trait Handler: Clone + 'static { + type Output; + type Future: Future; + + fn call(&self, args: Args) -> Self::Future; } -pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where - F: Handler, + F: Handler, Args: FromRequest, - R: Future, - R::Output: Responder, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F::Output: Responder, { boxed::factory(fn_service(move |req: ServiceRequest| { let handler = handler.clone(); @@ -125,14 +120,16 @@ where /// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> R /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { - impl Handler<($($param,)*), R> for Func - where Func: Fn($($param),*) -> R + Clone + 'static, - R: Future, - R::Output: Responder, + impl Handler<($($param,)*)> for Func + where Func: Fn($($param),*) -> Fut + Clone + 'static, + Fut: Future, { + type Output = Fut::Output; + type Future = Fut; + #[inline] #[allow(non_snake_case)] - fn call(&self, ($($param,)*): ($($param,)*)) -> R { + fn call(&self, ($($param,)*): ($($param,)*)) -> Self::Future { (self)($($param,)*) } } diff --git a/src/resource.rs b/src/resource.rs index f0c6f6d7c..564c4d3ef 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -20,7 +20,7 @@ use crate::{ BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, ServiceResponse, }, - BoxError, Error, FromRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; /// A collection of [`Route`]s that respond to the same path pattern. @@ -230,14 +230,11 @@ where /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, + F: Handler, Args: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F::Output: Responder + 'static, { self.routes.push(Route::new().to(handler)); self diff --git a/src/route.rs b/src/route.rs index 6396c4286..6d6fca4b7 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,4 @@ -use std::{future::Future, mem, rc::Rc}; +use std::{mem, rc::Rc}; use actix_http::Method; use actix_service::{ @@ -8,11 +8,10 @@ use actix_service::{ use futures_core::future::LocalBoxFuture; use crate::{ - body::MessageBody, guard::{self, Guard}, handler::{handler_service, Handler}, service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, - BoxError, Error, FromRequest, HttpResponse, Responder, + Error, FromRequest, HttpResponse, Responder, }; /// A request handler with [guards](guard). @@ -176,14 +175,11 @@ impl Route { /// ); /// } /// ``` - pub fn to(mut self, handler: F) -> Self + pub fn to(mut self, handler: F) -> Self where - F: Handler, + F: Handler, Args: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody, - <::Body as MessageBody>::Error: Into, + F::Output: Responder + 'static, { self.service = handler_service(handler); self diff --git a/src/web.rs b/src/web.rs index 46e7704b6..47bff36a3 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,14 +1,14 @@ //! Essentials helper functions and types for application registration. -use std::{error::Error as StdError, future::Future}; +use std::future::Future; use actix_http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, - resource::Resource, route::Route, scope::Scope, service::WebService, Responder, + error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, + route::Route, scope::Scope, service::WebService, Responder, }; pub use crate::config::ServiceConfig; @@ -146,14 +146,11 @@ pub fn method(method: Method) -> Route { /// web::to(index)) /// ); /// ``` -pub fn to(handler: F) -> Route +pub fn to(handler: F) -> Route where - F: Handler, + F: Handler, Args: FromRequest + 'static, - R: Future + 'static, - R::Output: Responder + 'static, - ::Body: MessageBody + 'static, - <::Body as MessageBody>::Error: Into>, + F::Output: Responder + 'static, { Route::new().to(handler) } From 2308f8afa4b71dad726d5a3534e7eb4b1469098a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 27 Dec 2021 16:15:20 +0000 Subject: [PATCH 13/66] use const header values where possible --- actix-http/src/h2/dispatcher.rs | 8 +++++--- actix-http/src/header/shared/content_encoding.rs | 4 ++-- actix-http/src/ws/mod.rs | 5 +++-- awc/src/client/h2proto.rs | 8 +++++--- awc/src/ws.rs | 13 ++++++++----- src/middleware/default_headers.rs | 7 +++---- 6 files changed, 26 insertions(+), 19 deletions(-) diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 7f0f15ee6..a90eb3466 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -288,9 +288,11 @@ fn prepare_response( let _ = match size { BodySize::None | BodySize::Stream => None, - BodySize::Sized(0) => res - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index a6e52138d..68511a8ee 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -45,13 +45,13 @@ pub enum ContentEncoding { impl ContentEncoding { /// Is the content compressed? #[inline] - pub fn is_compression(self) -> bool { + pub const fn is_compression(self) -> bool { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } /// Convert content encoding to string. #[inline] - pub fn as_str(self) -> &'static str { + pub const fn as_str(self) -> &'static str { match self { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 6491da149..568d801a2 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -99,8 +99,9 @@ impl From for Response { match err { HandshakeError::GetMethodRequired => { let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); - res.headers_mut() - .insert(header::ALLOW, HeaderValue::from_static("GET")); + #[allow(clippy::declare_interior_mutable_const)] + const HV_GET: HeaderValue = HeaderValue::from_static("GET"); + res.headers_mut().insert(header::ALLOW, HV_GET); res } diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index 9ced5776b..709896ddd 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -52,9 +52,11 @@ where let _ = match length { BodySize::None => None, - BodySize::Sized(0) => req - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + BodySize::Sized(0) => { + #[allow(clippy::declare_interior_mutable_const)] + const HV_ZERO: HeaderValue = HeaderValue::from_static("0"); + req.headers_mut().insert(CONTENT_LENGTH, HV_ZERO) + } BodySize::Sized(len) => { let mut buf = itoa::Buffer::new(); diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 96f8cf893..f3ee02d43 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -300,13 +300,16 @@ impl WebsocketsRequest { } self.head.set_connection_type(ConnectionType::Upgrade); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_WEBSOCKET: HeaderValue = HeaderValue::from_static("websocket"); + self.head.headers.insert(header::UPGRADE, HV_WEBSOCKET); + + #[allow(clippy::declare_interior_mutable_const)] + const HV_THIRTEEN: HeaderValue = HeaderValue::from_static("13"); self.head .headers - .insert(header::UPGRADE, HeaderValue::from_static("websocket")); - self.head.headers.insert( - header::SEC_WEBSOCKET_VERSION, - HeaderValue::from_static("13"), - ); + .insert(header::SEC_WEBSOCKET_VERSION, HV_THIRTEEN); if let Some(protocols) = self.protocols.take() { self.head.headers.insert( diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index 89210b156..003abd40d 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -100,10 +100,9 @@ impl DefaultHeaders { /// /// Default is `application/octet-stream`. pub fn add_content_type(self) -> Self { - self.add(( - CONTENT_TYPE, - HeaderValue::from_static("application/octet-stream"), - )) + #[allow(clippy::declare_interior_mutable_const)] + const HV_MIME: HeaderValue = HeaderValue::from_static("application/octet-stream"); + self.add((CONTENT_TYPE, HV_MIME)) } } From 76684a786ed82a4e39acb81eedf798de942e4b61 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 27 Dec 2021 18:45:31 +0000 Subject: [PATCH 14/66] update server dep to rc2 (#2550) --- CHANGES.md | 3 +++ Cargo.toml | 10 +++++----- README.md | 4 ++-- actix-files/CHANGES.md | 4 ++++ actix-files/Cargo.toml | 10 +++++----- actix-files/README.md | 4 ++-- actix-http-test/CHANGES.md | 6 ++++++ actix-http-test/Cargo.toml | 10 +++++----- actix-http-test/README.md | 4 ++-- actix-http-test/src/lib.rs | 6 +++--- actix-http/CHANGES.md | 3 +++ actix-http/Cargo.toml | 8 ++++---- actix-http/README.md | 4 ++-- actix-multipart/CHANGES.md | 4 ++++ actix-multipart/Cargo.toml | 6 +++--- actix-multipart/README.md | 4 ++-- actix-test/CHANGES.md | 4 ++++ actix-test/Cargo.toml | 10 +++++----- actix-web-actors/CHANGES.md | 4 ++++ actix-web-actors/Cargo.toml | 10 +++++----- actix-web-actors/README.md | 4 ++-- actix-web-codegen/Cargo.toml | 4 ++-- awc/CHANGES.md | 3 +++ awc/Cargo.toml | 14 +++++++------- awc/README.md | 4 ++-- 25 files changed, 89 insertions(+), 58 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a43b3ee41..e1317a7a5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.16 - 2021-12-27 ### Changed - No longer require `Scope` service body type to be boxed. [#2523] - No longer require `Resource` service body type to be boxed. [#2526] diff --git a/Cargo.toml b/Cargo.toml index 7b095af91..b6ef184e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.15" +version = "4.0.0-beta.16" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -72,12 +72,12 @@ experimental-io-uring = ["actix-server/io-uring"] actix-codec = "0.4.1" actix-macros = "0.2.3" actix-rt = "2.3" -actix-server = "2.0.0-rc.1" +actix-server = "2.0.0-rc.2" actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0", default-features = false, optional = true } -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-router = "0.5.0-beta.3" actix-web-codegen = "0.5.0-beta.6" @@ -106,8 +106,8 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.9", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.14", features = ["openssl"] } +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.15", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index a9afbf386..f9d388f8b 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![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.15)](https://docs.rs/actix-web/4.0.0-beta.15) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.16)](https://docs.rs/actix-web/4.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-web.svg) -[![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) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.16/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.16)
[![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/CHANGES.md b/actix-files/CHANGES.md index ef8eba0fc..af6dcb415 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.6.0-beta.11 - 2021-12-27 +* No significant changes since `0.6.0-beta.10`. + + ## 0.6.0-beta.10 - 2021-12-11 - No significant changes since `0.6.0-beta.9`. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c74a8e9a6..bbd4fee22 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.10" +version = "0.6.0-beta.11" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -22,10 +22,10 @@ path = "src/lib.rs" experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] [dependencies] -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.15", default-features = false } +actix-web = { version = "4.0.0-beta.16", default-features = false } askama_escape = "0.10" bitflags = "1" @@ -43,5 +43,5 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.9" -actix-web = "4.0.0-beta.15" +actix-test = "0.1.0-beta.10" +actix-web = "4.0.0-beta.16" diff --git a/actix-files/README.md b/actix-files/README.md index d686e255c..db5c94d1e 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.10)](https://docs.rs/actix-files/0.6.0-beta.10) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.11)](https://docs.rs/actix-files/0.6.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.10/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.11/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 4e86e20e8..8c6a63b72 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,12 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.10 - 2021-12-27 +- Update `actix-server` to `2.0.0-rc.2`. [#2550] + +[#2550]: https://github.com/actix/actix-web/pull/2550 + + ## 3.0.0-beta.9 - 2021-12-11 - No significant changes since `3.0.0-beta.8`. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 2ad620b08..f071678a4 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.9" +version = "3.0.0-beta.10" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] @@ -34,8 +34,8 @@ actix-codec = "0.4.1" actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" -actix-server = "2.0.0-rc.1" -awc = { version = "3.0.0-beta.14", default-features = false } +actix-server = "2.0.0-rc.2" +awc = { version = "3.0.0-beta.15", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tokio = { version = "1.8", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.15", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.16" +actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.17" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index a0a8f1cd8..589c54c23 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.9)](https://docs.rs/actix-http-test/3.0.0-beta.9) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) [![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-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.9) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 03239ece6..8636ef9c4 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -12,7 +12,7 @@ use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; -use actix_server::{Server, ServiceFactory}; +use actix_server::{Server, ServerServiceFactory}; use awc::{ error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector, @@ -51,13 +51,13 @@ use tokio::sync::mpsc; /// assert!(response.status().is_success()); /// } /// ``` -pub async fn test_server>(factory: F) -> TestServer { +pub async fn test_server>(factory: F) -> TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); test_server_with_addr(tcp, factory).await } /// Start [`test server`](test_server()) on an existing address binding. -pub async fn test_server_with_addr>( +pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index adc4c35c7..d74a754ac 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.17 - 2021-12-27 ### Changes - `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] - `Payload` inner fields are now named. [#2545] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 3fa437562..b27cb4053 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.16" +version = "3.0.0-beta.17" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] @@ -79,10 +79,10 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.9", optional = true } [dev-dependencies] -actix-http-test = { version = "3.0.0-beta.9", features = ["openssl"] } -actix-server = "2.0.0-rc.1" +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.15" +actix-web = "4.0.0-beta.16" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-http/README.md b/actix-http/README.md index 05edffd2c..223e18ceb 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.16)](https://docs.rs/actix-http/3.0.0-beta.16) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.17)](https://docs.rs/actix-http/3.0.0-beta.17) [![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)
-[![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) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) [![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/CHANGES.md b/actix-multipart/CHANGES.md index e58c3ee24..a9a1e8784 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.11 - 2021-12-27 +* No significant changes since `0.4.0-beta.10`. + + ## 0.4.0-beta.10 - 2021-12-11 - No significant changes since `0.4.0-beta.9`. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 595c14d7e..8338eb952 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.10" +version = "0.4.0-beta.11" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] @@ -15,7 +15,7 @@ path = "src/lib.rs" [dependencies] actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.15", default-features = false } +actix-web = { version = "4.0.0-beta.16", default-features = false } bytes = "1" derive_more = "0.99.5" @@ -28,7 +28,7 @@ twoway = "0.2" [dev-dependencies] actix-rt = "2.2" -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } tokio = { version = "1.8", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 647796557..a9ee325ba 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.10)](https://docs.rs/actix-multipart/0.4.0-beta.10) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) [![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-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.10/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.10) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index e3deeb3f4..2de0a69d6 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.10 - 2021-12-27 +* No significant changes since `0.1.0-beta.9`. + + ## 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 4a4615820..b34d8e6ee 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.9" +version = "0.1.0-beta.10" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" -actix-http = "3.0.0-beta.16" -actix-http-test = "3.0.0-beta.9" +actix-http = "3.0.0-beta.17" +actix-http-test = "3.0.0-beta.10" 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.14", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.15", 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/CHANGES.md b/actix-web-actors/CHANGES.md index 6abfe2c61..2fbbe7444 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.9 - 2021-12-27 +* No significant changes since `4.0.0-beta.8`. + + ## 4.0.0-beta.8 - 2021-12-11 - Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] - Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index d57f139f6..9dba2dfbd 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.8" +version = "4.0.0-beta.9" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -16,8 +16,8 @@ path = "src/lib.rs" [dependencies] 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.15", default-features = false } +actix-http = "3.0.0-beta.17" +actix-web = { version = "4.0.0-beta.16", default-features = false } bytes = "1" bytestring = "1" @@ -27,8 +27,8 @@ tokio = { version = "1.8", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.9" -awc = { version = "3.0.0-beta.14", default-features = false } +actix-test = "0.1.0-beta.10" +awc = { version = "3.0.0-beta.15", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 954f8273b..232c81eac 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web-actors/4.0.0-beta.8) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.8) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 8d42137e7..5250baa90 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.3" [dev-dependencies] actix-macros = "0.2.3" actix-rt = "2.2" -actix-test = "0.1.0-beta.9" +actix-test = "0.1.0-beta.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.15" +actix-web = "4.0.0-beta.16" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e1a059481..200ad846a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.15 - 2021-12-27 - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] - `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index d0494e023..b77662de0 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.14" +version = "3.0.0-beta.15" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -60,7 +60,7 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.4.1" actix-service = "2.0.0" -actix-http = "3.0.0-beta.16" +actix-http = "3.0.0-beta.17" actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.0.0", features = ["connect", "uri"] } actix-utils = "3.0.0" @@ -93,13 +93,13 @@ 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.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.9", features = ["openssl", "rustls"] } +actix-http = { version = "3.0.0-beta.17", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-server = "2.0.0-rc.2" +actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.15", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.16", features = ["openssl"] } brotli2 = "0.3.2" env_logger = "0.9" diff --git a/awc/README.md b/awc/README.md index 3fbdd903a..582ecb18f 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.14)](https://docs.rs/awc/3.0.0-beta.14) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.15)](https://docs.rs/awc/3.0.0-beta.15) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.14/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.14) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.15/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.15) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 36193b0a50d7453fb55d0c4d72b3409fa767426e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 27 Dec 2021 18:54:10 +0000 Subject: [PATCH 15/66] specify tokio dep to avoid RUSTSEC-2021-0124 warning --- 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 +- awc/Cargo.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index f071678a4..5c58978ea 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -48,7 +48,7 @@ serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tokio = { version = "1.8", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index b27cb4053..9e587890b 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -96,7 +96,7 @@ serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.8", features = ["net", "rt", "macros"] } +tokio = { version = "1.8.4", features = ["net", "rt", "macros"] } [[example]] name = "ws" diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 8338eb952..de13133a1 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -30,5 +30,5 @@ twoway = "0.2" actix-rt = "2.2" actix-http = "3.0.0-beta.17" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } -tokio = { version = "1.8", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index b34d8e6ee..c7177a38c 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -45,4 +45,4 @@ serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true } -tokio = { version = "1.8", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 9dba2dfbd..3b792093a 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -23,7 +23,7 @@ bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1.8", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index b77662de0..8777ffa74 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -83,7 +83,7 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tokio = { version = "1.8", features = ["sync"] } +tokio = { version = "1.8.4", features = ["sync"] } cookie = { version = "0.15", features = ["percent-encode"], optional = true } From 4616ca8ee65ca1e14e784b8b46dc1a1306b422a5 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 28 Dec 2021 02:37:13 +0000 Subject: [PATCH 16/66] rework `Guard` trait (#2552) --- CHANGES.md | 11 + actix-files/src/service.rs | 8 +- actix-http/src/message.rs | 6 +- src/app_service.rs | 13 +- src/dev.rs | 23 +- src/guard.rs | 742 +++++++++++++++++++++--------------- src/middleware/normalize.rs | 12 +- src/route.rs | 38 +- src/scope.rs | 7 +- src/service.rs | 34 +- src/test/test_request.rs | 2 +- 11 files changed, 545 insertions(+), 351 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e1317a7a5..f925f3b94 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,17 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- `guard::GuardContext` for use with the `Guard` trait. [#2552] +- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] + +### Changed +- `Guard` trait now receives a `&GuardContext`. [#2552] +- `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] +- Some guards now return `impl Guard` and their concrete types are made private: `guard::{Header}` and all the method guards. [#2552] +- The `Not` guard is now generic over the type of guard it wraps. [#2552] + +[#2552]: https://github.com/actix/actix-web/pull/2552 ## 4.0.0-beta.16 - 2021-12-27 diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index f6e1c2e11..057dbe5a3 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,8 +1,8 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; -use actix_service::Service; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + body::BoxBody, + dev::{Service, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::{header, Method}, @@ -94,7 +94,7 @@ impl fmt::Debug for FilesService { } impl Service for FilesService { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -103,7 +103,7 @@ impl Service for FilesService { fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards - (**guard).check(req.head()) + (**guard).check(&req.guard_ctx()) } else { // default behavior matches!(*req.method(), Method::HEAD | Method::GET) diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 34213f68a..ecd08fbb3 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -1,4 +1,4 @@ -use std::{cell::RefCell, rc::Rc}; +use std::{cell::RefCell, ops, rc::Rc}; use bitflags::bitflags; @@ -49,7 +49,7 @@ impl Message { } } -impl std::ops::Deref for Message { +impl ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { @@ -57,7 +57,7 @@ impl std::ops::Deref for Message { } } -impl std::ops::DerefMut for Message { +impl ops::DerefMut for Message { fn deref_mut(&mut self) -> &mut Self::Target { Rc::get_mut(&mut self.head).expect("Multiple copies exist") } diff --git a/src/app_service.rs b/src/app_service.rs index e0d424390..56b24f0d8 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,14 +1,16 @@ use std::{cell::RefCell, mem, rc::Rc}; -use actix_http::{Extensions, Request}; +use actix_http::Request; use actix_router::{Path, ResourceDef, Router, Url}; use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; use crate::{ + body::BoxBody, config::{AppConfig, AppService}, data::FnDataFactory, + dev::Extensions, guard::Guard, request::{HttpRequest, HttpRequestPool}, rmap::ResourceMap, @@ -297,7 +299,7 @@ pub struct AppRouting { } impl Service for AppRouting { - type Response = ServiceResponse; + type Response = ServiceResponse; type Error = Error; type Future = LocalBoxFuture<'static, Result>; @@ -306,12 +308,15 @@ impl Service for AppRouting { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); diff --git a/src/dev.rs b/src/dev.rs index 6e1970467..bb1385bde 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -3,6 +3,16 @@ //! Most users will not have to interact with the types in this module, but it is useful for those //! writing extractors, middleware, libraries, or interacting with the service API directly. +pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; +pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; +pub use actix_server::{Server, ServerHandle}; +pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, +}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; @@ -14,16 +24,6 @@ pub use crate::types::form::UrlEncoded; pub use crate::types::json::JsonBody; pub use crate::types::readlines::Readlines; -pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; -pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; -pub use actix_server::{Server, ServerHandle}; -pub use actix_service::{ - always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, -}; - -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; - use crate::http::header::ContentEncoding; use actix_router::Patterns; @@ -46,7 +46,6 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { patterns } -struct Enc(ContentEncoding); /// Helper trait that allows to set specific encoding for response. pub trait BodyEncoding { @@ -70,6 +69,8 @@ impl BodyEncoding for actix_http::ResponseBuilder { } } +struct Enc(ContentEncoding); + impl BodyEncoding for actix_http::Response { fn get_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) diff --git a/src/guard.rs b/src/guard.rs index db7f06987..fb3e4f243 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,160 +1,248 @@ -//! Route match guards. +//! Route guards. //! -//! Guards are one of the ways how actix-web router chooses a -//! handler service. In essence it is just a function that accepts a -//! reference to a `RequestHead` instance and returns a boolean. -//! It is possible to add guards to *scopes*, *resources* -//! and *routes*. Actix provide several guards by default, like various -//! http methods, header, etc. To become a guard, type must implement `Guard` -//! trait. Simple functions could be guards as well. +//! Guards are used during routing to help select a matching service or handler using some aspect of +//! the request; though guards should not be used for path matching since it is a built-in function +//! of the Actix Web router. //! -//! Guards can not modify the request object. But it is possible -//! to store extra attributes on a request by using the `Extensions` container. -//! Extensions containers are available via the `RequestHead::extensions()` method. +//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services. //! +//! Fundamentally, a guard is a predicate function that receives a reference to a request context +//! object and returns a boolean; true if the request _should_ be handled by the guarded service +//! or handler. This interface is defined by the [`Guard`] trait. +//! +//! Commonly-used guards are provided in this module as well as way of creating a guard from a +//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be +//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on +//! services multiple times (which might have different combining behavior than you want). +//! +//! Guards can not modify anything about the request. However, it is possible to store extra +//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`]. +//! +//! Guards can prevent resource definitions from overlapping (resulting in some inaccessible routes) +//! where they otherwise would when only considering paths. See the virtual hosting example below. +//! +//! # Examples +//! In the following code, the `/guarded` resource has one defined route whose handler will only be +//! called if the request method is `POST` and there is a request header with name and value equal +//! to `x-guarded` and `secret`, respectively. //! ``` -//! use actix_web::{web, http, dev, guard, App, HttpResponse}; +//! use actix_web::{web, http::Method, guard, HttpResponse}; //! -//! App::new().service(web::resource("/index.html").route( +//! web::resource("/guarded").route( //! web::route() -//! .guard(guard::Post()) -//! .guard(guard::fn_guard(|head| head.method == http::Method::GET)) -//! .to(|| HttpResponse::MethodNotAllowed())) +//! .guard(guard::Any(guard::Get()).or(guard::Post())) +//! .guard(guard::Header("x-guarded", "secret")) +//! .to(|| HttpResponse::Ok()) //! ); //! ``` +//! +//! Guards can be used to set up some form of [virtual hosting] within a single app. +//! Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +//! definitions they become safe to use in this way. Without these host guards, only routes under +//! the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +//! and `localhost` as the `Host` guards. +//! ``` +//! use actix_web::{web, http::Method, guard, App, HttpResponse}; +//! +//! App::new() +//! .service( +//! web::scope("") +//! .guard(guard::Host("www.rust-lang.org")) +//! .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))), +//! ) +//! .service( +//! web::scope("") +//! .guard(guard::Host("play.rust-lang.org")) +//! .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))), +//! ); +//! ``` +//! +//! [`Scope`]: crate::Scope::guard() +//! [`Resource`]: crate::Resource::guard() +//! [`Route`]: crate::Route::guard() +//! [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting -#![allow(non_snake_case)] +use std::{ + cell::{Ref, RefMut}, + convert::TryFrom, + rc::Rc, +}; -use std::rc::Rc; -use std::{convert::TryFrom, ops::Deref}; +use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; -use actix_http::{header, uri::Uri, Method as HttpMethod, RequestHead}; +use crate::service::ServiceRequest; -/// Trait defines resource guards. Guards are used for route selection. -/// -/// Guards can not modify the request object. But it is possible -/// to store extra attributes on a request by using the `Extensions` container. -/// Extensions containers are available via the `RequestHead::extensions()` method. -pub trait Guard { - /// Check if request matches predicate - fn check(&self, request: &RequestHead) -> bool; +/// Provides access to request parts that are useful during routing. +#[derive(Debug)] +pub struct GuardContext<'a> { + pub(crate) req: &'a ServiceRequest, } -impl Guard for Rc { - fn check(&self, request: &RequestHead) -> bool { - self.deref().check(request) +impl<'a> GuardContext<'a> { + /// Returns reference to the request head. + #[inline] + pub fn head(&self) -> &RequestHead { + self.req.head() + } + + /// Returns reference to the request-local data container. + #[inline] + pub fn req_data(&self) -> Ref<'a, Extensions> { + self.req.req_data() + } + + /// Returns mutable reference to the request-local data container. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { + self.req.req_data_mut() } } -/// Create guard object for supplied function. +/// Interface for routing guards. /// +/// See [module level documentation](self) for more. +pub trait Guard { + /// Returns true if predicate condition is met for a given request. + fn check(&self, ctx: &GuardContext<'_>) -> bool; +} + +impl Guard for Rc { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (**self).check(ctx) + } +} + +/// Creates a guard using the given function. +/// +/// # Examples /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::fn_guard( -/// |req| req.headers() -/// .contains_key("content-type"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); +/// web::route() +/// .guard(guard::fn_guard(|ctx| { +/// ctx.head().headers().contains_key("content-type") +/// })) +/// .to(|| HttpResponse::Ok()); /// ``` pub fn fn_guard(f: F) -> impl Guard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { FnGuard(f) } -struct FnGuard bool>(F); +struct FnGuard) -> bool>(F); impl Guard for FnGuard where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self.0)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self.0)(ctx) } } impl Guard for F where - F: Fn(&RequestHead) -> bool, + F: Fn(&GuardContext<'_>) -> bool, { - fn check(&self, head: &RequestHead) -> bool { - (self)(head) + fn check(&self, ctx: &GuardContext<'_>) -> bool { + (self)(ctx) } } -/// Return guard that matches if any of supplied guards. +/// Creates a guard that matches if any added guards match. /// +/// # Examples +/// The handler below will be called for either request method `GET` or `POST`. /// ``` -/// use actix_web::{web, guard, App, HttpResponse}; +/// use actix_web::{web, guard, HttpResponse}; /// -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard(guard::Any(guard::Get()).or(guard::Post())) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); +/// web::route() +/// .guard( +/// guard::Any(guard::Get()) +/// .or(guard::Post())) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn Any(guard: F) -> AnyGuard { - AnyGuard(vec![Box::new(guard)]) + AnyGuard { + guards: vec![Box::new(guard)], + } } -/// Matches any of supplied guards. -pub struct AnyGuard(Vec>); +/// A collection of guards that match if the disjunction of their `check` outcomes is true. +/// +/// That is, only one contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AnyGuard` using [`Any`]. +pub struct AnyGuard { + guards: Vec>, +} impl AnyGuard { - /// Add guard to a list of guards to check + /// Adds new guard to the collection of guards to check. pub fn or(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AnyGuard { - fn check(&self, req: &RequestHead) -> bool { - for p in &self.0 { - if p.check(req) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if guard.check(ctx) { return true; } } + false } } -/// Return guard that matches if all of the supplied guards. +/// Creates a guard that matches if all added guards match. /// +/// # Examples +/// The handler below will only be called if the request method is `GET` **and** the specified +/// header name and value match exactly. /// ``` -/// use actix_web::{guard, web, App, HttpResponse}; +/// use actix_web::{guard, web, HttpResponse}; /// -/// App::new().service(web::resource("/index.html").route( -/// web::route() -/// .guard( -/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain"))) -/// .to(|| HttpResponse::MethodNotAllowed())) -/// ); +/// web::route() +/// .guard( +/// guard::All(guard::Get()) +/// .and(guard::Header("accept", "text/plain")) +/// ) +/// .to(|| HttpResponse::Ok()); /// ``` +#[allow(non_snake_case)] pub fn All(guard: F) -> AllGuard { - AllGuard(vec![Box::new(guard)]) + AllGuard { + guards: vec![Box::new(guard)], + } } -/// Matches if all of supplied guards. -pub struct AllGuard(Vec>); +/// A collection of guards that match if the conjunction of their `check` outcomes is true. +/// +/// That is, **all** contained guard needs to match in order for the aggregate guard to match. +/// +/// Construct an `AllGuard` using [`All`]. +pub struct AllGuard { + guards: Vec>, +} impl AllGuard { - /// Add new guard to the list of guards to check + /// Adds new guard to the collection of guards to check. pub fn and(mut self, guard: F) -> Self { - self.0.push(Box::new(guard)); + self.guards.push(Box::new(guard)); self } } impl Guard for AllGuard { - fn check(&self, request: &RequestHead) -> bool { - for p in &self.0 { - if !p.check(request) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + for guard in &self.guards { + if !guard.check(ctx) { return false; } } @@ -162,159 +250,189 @@ impl Guard for AllGuard { } } -/// Return guard that matches if supplied guard does not match. -pub fn Not(guard: F) -> NotGuard { - NotGuard(Box::new(guard)) -} +/// Wraps a guard and inverts the outcome of it's `Guard` implementation. +/// +/// # Examples +/// The handler below will be called for any request method apart from `GET`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Not(guard::Get())) +/// .to(|| HttpResponse::Ok()); +/// ``` +pub struct Not(pub G); -#[doc(hidden)] -pub struct NotGuard(Box); - -impl Guard for NotGuard { - fn check(&self, request: &RequestHead) -> bool { - !self.0.check(request) +impl Guard for Not { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + !self.0.check(ctx) } } -/// HTTP method guard. -#[doc(hidden)] -pub struct MethodGuard(HttpMethod); - -impl Guard for MethodGuard { - fn check(&self, request: &RequestHead) -> bool { - request.method == self.0 - } -} - -/// Guard to match *GET* HTTP method. -pub fn Get() -> MethodGuard { - MethodGuard(HttpMethod::GET) -} - -/// Predicate to match *POST* HTTP method. -pub fn Post() -> MethodGuard { - MethodGuard(HttpMethod::POST) -} - -/// Predicate to match *PUT* HTTP method. -pub fn Put() -> MethodGuard { - MethodGuard(HttpMethod::PUT) -} - -/// Predicate to match *DELETE* HTTP method. -pub fn Delete() -> MethodGuard { - MethodGuard(HttpMethod::DELETE) -} - -/// Predicate to match *HEAD* HTTP method. -pub fn Head() -> MethodGuard { - MethodGuard(HttpMethod::HEAD) -} - -/// Predicate to match *OPTIONS* HTTP method. -pub fn Options() -> MethodGuard { - MethodGuard(HttpMethod::OPTIONS) -} - -/// Predicate to match *CONNECT* HTTP method. -pub fn Connect() -> MethodGuard { - MethodGuard(HttpMethod::CONNECT) -} - -/// Predicate to match *PATCH* HTTP method. -pub fn Patch() -> MethodGuard { - MethodGuard(HttpMethod::PATCH) -} - -/// Predicate to match *TRACE* HTTP method. -pub fn Trace() -> MethodGuard { - MethodGuard(HttpMethod::TRACE) -} - -/// Predicate to match specified HTTP method. -pub fn Method(method: HttpMethod) -> MethodGuard { +/// Creates a guard that matches a specified HTTP method. +#[allow(non_snake_case)] +pub fn Method(method: HttpMethod) -> impl Guard { MethodGuard(method) } -/// Return predicate that matches if request contains specified header and -/// value. -pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard { +/// HTTP method guard. +struct MethodGuard(HttpMethod); + +impl Guard for MethodGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + ctx.head().method == self.0 + } +} + +macro_rules! method_guard { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = " Creates a guard that matches the `" $method_const "` request method."] + /// + /// # Examples + #[doc = " The route in this example will only respond to `" $method_const "` requests."] + /// ``` + /// use actix_web::{guard, web, HttpResponse}; + /// + /// web::route() + #[doc = " .guard(guard::" $method_fn "())"] + /// .to(|| HttpResponse::Ok()); + /// ``` + #[allow(non_snake_case)] + pub fn $method_fn() -> impl Guard { + MethodGuard(HttpMethod::$method_const) + } + } + }; +} + +method_guard!(Get, GET); +method_guard!(Post, POST); +method_guard!(Put, PUT); +method_guard!(Delete, DELETE); +method_guard!(Head, HEAD); +method_guard!(Options, OPTIONS); +method_guard!(Connect, CONNECT); +method_guard!(Patch, PATCH); +method_guard!(Trace, TRACE); + +/// Creates a guard that matches if request contains given header name and value. +/// +/// # Examples +/// The handler below will be called when the request contains an `x-guarded` header with value +/// equal to `secret`. +/// ``` +/// use actix_web::{guard, web, HttpResponse}; +/// +/// web::route() +/// .guard(guard::Header("x-guarded", "secret")) +/// .to(|| HttpResponse::Ok()); +/// ``` +#[allow(non_snake_case)] +pub fn Header(name: &'static str, value: &'static str) -> impl Guard { HeaderGuard( header::HeaderName::try_from(name).unwrap(), header::HeaderValue::from_static(value), ) } -#[doc(hidden)] -pub struct HeaderGuard(header::HeaderName, header::HeaderValue); +struct HeaderGuard(header::HeaderName, header::HeaderValue); impl Guard for HeaderGuard { - fn check(&self, req: &RequestHead) -> bool { - if let Some(val) = req.headers.get(&self.0) { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + if let Some(val) = ctx.head().headers.get(&self.0) { return val == self.1; } + false } } -/// Return predicate that matches if request contains specified Host name. +/// Creates a guard that matches requests targetting a specific host. /// -/// ``` -/// use actix_web::{web, guard::Host, App, HttpResponse}; +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; /// -/// App::new().service( -/// web::resource("/index.html") -/// .guard(Host("www.rust-lang.org")) -/// .to(|| HttpResponse::MethodNotAllowed()) -/// ); +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. /// ``` -pub fn Host>(host: H) -> HostGuard { - HostGuard(host.as_ref().to_string(), None) +/// use actix_web::{web, guard::Host, HttpResponse}; +/// +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure"))); +/// ``` +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } } fn get_host_uri(req: &RequestHead) -> Option { - use core::str::FromStr; req.headers .get(header::HOST) .and_then(|host_value| host_value.to_str().ok()) .or_else(|| req.uri.host()) - .map(|host: &str| Uri::from_str(host).ok()) - .and_then(|host_success| host_success) + .and_then(|host| host.parse().ok()) } #[doc(hidden)] -pub struct HostGuard(String, Option); +pub struct HostGuard { + host: String, + scheme: Option, +} impl HostGuard { /// Set request scheme to match pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.1 = Some(scheme.as_ref().to_string()); + self.scheme = Some(scheme.as_ref().to_string()); self } } impl Guard for HostGuard { - fn check(&self, req: &RequestHead) -> bool { - let req_host_uri = if let Some(uri) = get_host_uri(req) { - uri - } else { - return false; + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, }; - if let Some(uri_host) = req_host_uri.host() { - if self.0 != uri_host { - return false; - } - } else { - return false; + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, } - if let Some(ref scheme) = self.1 { + if let Some(ref scheme) = self.scheme { if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { return scheme == req_host_uri_scheme; } + + // TODO: is the the correct behavior? + // falls through if scheme cannot be determined } + // all conditions passed true } } @@ -327,171 +445,201 @@ mod tests { use crate::test::TestRequest; #[test] - fn test_header() { + fn header_match() { let req = TestRequest::default() .insert_header((header::TRANSFER_ENCODING, "chunked")) - .to_http_request(); + .to_srv_request(); - let pred = Header("transfer-encoding", "chunked"); - assert!(pred.check(req.head())); + let hdr = Header("transfer-encoding", "chunked"); + assert!(hdr.check(&req.guard_ctx())); - let pred = Header("transfer-encoding", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("transfer-encoding", "other"); + assert!(!hdr.check(&req.guard_ctx())); - let pred = Header("content-type", "other"); - assert!(!pred.check(req.head())); + let hdr = Header("content-type", "chunked"); + assert!(!hdr.check(&req.guard_ctx())); + + let hdr = Header("content-type", "other"); + assert!(!hdr.check(&req.guard_ctx())); } #[test] - fn test_host() { + fn host_from_header() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_scheme() { + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { let req = TestRequest::default() .insert_header(( header::HOST, header::HeaderValue::from_static("https://www.rust-lang.org"), )) - .to_http_request(); + .to_srv_request(); - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); - let pred = Host("www.rust-lang.org").scheme("http"); - assert!(!pred.check(req.head())); + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("crates.io").scheme("https"); - assert!(!pred.check(req.head())); + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); - let pred = Host("localhost"); - assert!(!pred.check(req.head())); + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); } #[test] - fn test_host_without_header() { + fn method_guards() { + let get_req = TestRequest::get().to_srv_request(); + let post_req = TestRequest::post().to_srv_request(); + + assert!(Get().check(&get_req.guard_ctx())); + assert!(!Get().check(&post_req.guard_ctx())); + + assert!(Post().check(&post_req.guard_ctx())); + assert!(!Post().check(&get_req.guard_ctx())); + + let req = TestRequest::put().to_srv_request(); + assert!(Put().check(&req.guard_ctx())); + assert!(!Put().check(&get_req.guard_ctx())); + + let req = TestRequest::patch().to_srv_request(); + assert!(Patch().check(&req.guard_ctx())); + assert!(!Patch().check(&get_req.guard_ctx())); + + let r = TestRequest::delete().to_srv_request(); + assert!(Delete().check(&r.guard_ctx())); + assert!(!Delete().check(&get_req.guard_ctx())); + + let req = TestRequest::default().method(Method::HEAD).to_srv_request(); + assert!(Head().check(&req.guard_ctx())); + assert!(!Head().check(&get_req.guard_ctx())); + let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_http_request(); - - let pred = Host("www.rust-lang.org"); - assert!(pred.check(req.head())); - - let pred = Host("www.rust-lang.org").scheme("https"); - assert!(pred.check(req.head())); - - let pred = Host("blog.rust-lang.org"); - assert!(!pred.check(req.head())); - - let pred = Host("blog.rust-lang.org").scheme("https"); - assert!(!pred.check(req.head())); - - let pred = Host("crates.io"); - assert!(!pred.check(req.head())); - - let pred = Host("localhost"); - assert!(!pred.check(req.head())); - } - - #[test] - fn test_methods() { - let req = TestRequest::default().to_http_request(); - let req2 = TestRequest::default() - .method(Method::POST) - .to_http_request(); - - assert!(Get().check(req.head())); - assert!(!Get().check(req2.head())); - assert!(Post().check(req2.head())); - assert!(!Post().check(req.head())); - - let r = TestRequest::default().method(Method::PUT).to_http_request(); - assert!(Put().check(r.head())); - assert!(!Put().check(req.head())); - - let r = TestRequest::default() - .method(Method::DELETE) - .to_http_request(); - assert!(Delete().check(r.head())); - assert!(!Delete().check(req.head())); - - let r = TestRequest::default() - .method(Method::HEAD) - .to_http_request(); - assert!(Head().check(r.head())); - assert!(!Head().check(req.head())); - - let r = TestRequest::default() .method(Method::OPTIONS) - .to_http_request(); - assert!(Options().check(r.head())); - assert!(!Options().check(req.head())); + .to_srv_request(); + assert!(Options().check(&req.guard_ctx())); + assert!(!Options().check(&get_req.guard_ctx())); - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::CONNECT) - .to_http_request(); - assert!(Connect().check(r.head())); - assert!(!Connect().check(req.head())); + .to_srv_request(); + assert!(Connect().check(&req.guard_ctx())); + assert!(!Connect().check(&get_req.guard_ctx())); - let r = TestRequest::default() - .method(Method::PATCH) - .to_http_request(); - assert!(Patch().check(r.head())); - assert!(!Patch().check(req.head())); - - let r = TestRequest::default() + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); - assert!(Trace().check(r.head())); - assert!(!Trace().check(req.head())); + .to_srv_request(); + assert!(Trace().check(&req.guard_ctx())); + assert!(!Trace().check(&get_req.guard_ctx())); } #[test] - fn test_preds() { - let r = TestRequest::default() + fn aggregate_any() { + let req = TestRequest::default() .method(Method::TRACE) - .to_http_request(); + .to_srv_request(); - assert!(Not(Get()).check(r.head())); - assert!(!Not(Trace()).check(r.head())); + assert!(Any(Trace()).check(&req.guard_ctx())); + assert!(Any(Trace()).or(Get()).check(&req.guard_ctx())); + assert!(!Any(Get()).or(Get()).check(&req.guard_ctx())); + } - assert!(All(Trace()).and(Trace()).check(r.head())); - assert!(!All(Get()).and(Trace()).check(r.head())); + #[test] + fn aggregate_all() { + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); - assert!(Any(Get()).or(Trace()).check(r.head())); - assert!(!Any(Get()).or(Get()).check(r.head())); + assert!(All(Trace()).check(&req.guard_ctx())); + assert!(All(Trace()).and(Trace()).check(&req.guard_ctx())); + assert!(!All(Trace()).and(Get()).check(&req.guard_ctx())); + } + + #[test] + fn nested_not() { + let req = TestRequest::default().to_srv_request(); + + let get = Get(); + assert!(get.check(&req.guard_ctx())); + + let not_get = Not(get); + assert!(!not_get.check(&req.guard_ctx())); + + let not_not_get = Not(not_get); + assert!(not_not_get.check(&req.guard_ctx())); + } + + #[test] + fn function_guard() { + let domain = "rust-lang.org".to_owned(); + let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain)); + + let req = TestRequest::default() + .uri("blog.rust-lang.org") + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); + + let req = TestRequest::default().uri("crates.io").to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); } } diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 18dcaeefa..3ab908481 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -225,7 +225,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -261,7 +261,7 @@ mod tests { .service(web::resource("/v1/something").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -294,7 +294,7 @@ mod tests { let app = init_service( App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -318,7 +318,7 @@ mod tests { .service(web::resource("/v1/something/").to(HttpResponse::Ok)) .service( web::resource("/v2/something/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -353,7 +353,7 @@ mod tests { .wrap(NormalizePath(TrailingSlash::Always)) .service( web::resource("/") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) @@ -378,7 +378,7 @@ mod tests { .service(web::resource("/v1/").to(HttpResponse::Ok)) .service( web::resource("/v2/something") - .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .guard(fn_guard(|ctx| ctx.head().uri.query() == Some("query=test"))) .to(HttpResponse::Ok), ), ) diff --git a/src/route.rs b/src/route.rs index 6d6fca4b7..0410b99dd 100644 --- a/src/route.rs +++ b/src/route.rs @@ -65,9 +65,12 @@ pub struct RouteService { } impl RouteService { + // TODO: does this need to take &mut ? pub fn check(&self, req: &mut ServiceRequest) -> bool { - for f in self.guards.iter() { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in self.guards.iter() { + if !guard.check(&guard_ctx) { return false; } } @@ -90,6 +93,7 @@ impl Service for RouteService { impl Route { /// Add method guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -110,6 +114,7 @@ impl Route { /// Add guard to the route. /// + /// # Examples /// ``` /// # use actix_web::*; /// # fn main() { @@ -143,16 +148,13 @@ impl Route { /// format!("Welcome {}!", info.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) // <- register handler - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) // <- register handler + /// ); /// ``` /// /// It is possible to use multiple extractors for one handler function. - /// /// ``` /// # use std::collections::HashMap; /// # use serde::Deserialize; @@ -164,16 +166,18 @@ impl Route { /// } /// /// /// extract path info using serde - /// async fn index(path: web::Path, query: web::Query>, body: web::Json) -> String { + /// async fn index( + /// path: web::Path, + /// query: web::Query>, + /// body: web::Json + /// ) -> String { /// format!("Welcome {}!", path.username) /// } /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/{username}/index.html") // <- define path parameters - /// .route(web::get().to(index)) - /// ); - /// } + /// let app = App::new().service( + /// web::resource("/{username}/index.html") // <- define path parameters + /// .route(web::get().to(index)) + /// ); /// ``` pub fn to(mut self, handler: F) -> Self where @@ -199,7 +203,7 @@ impl Route { /// type Error = Infallible; /// type Future = LocalBoxFuture<'static, Result>; /// - /// always_ready!(); + /// dev::always_ready!(); /// /// fn call(&self, req: ServiceRequest) -> Self::Future { /// let (req, _) = req.into_parts(); diff --git a/src/scope.rs b/src/scope.rs index 176e0d5a0..b4618bb6c 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -538,12 +538,15 @@ impl Service for ScopeService { fn call(&self, mut req: ServiceRequest) -> Self::Future { let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { - for f in guards { - if !f.check(req.head()) { + let guard_ctx = req.guard_ctx(); + + for guard in guards { + if !guard.check(&guard_ctx) { return false; } } } + true }); diff --git a/src/service.rs b/src/service.rs index d5c381fa4..975556197 100644 --- a/src/service.rs +++ b/src/service.rs @@ -21,7 +21,7 @@ use cookie::{Cookie, ParseError as CookieParseError}; use crate::{ config::{AppConfig, AppService}, dev::ensure_leading_slash, - guard::Guard, + guard::{Guard, GuardContext}, info::ConnectionInfo, rmap::ResourceMap, Error, HttpRequest, HttpResponse, @@ -172,7 +172,7 @@ impl ServiceRequest { self.head().uri.path() } - /// Counterpart to [`HttpRequest::query_string`](super::HttpRequest::query_string()). + /// Counterpart to [`HttpRequest::query_string`]. #[inline] pub fn query_string(&self) -> &str { self.req.query_string() @@ -208,13 +208,13 @@ impl ServiceRequest { self.req.match_info() } - /// Counterpart to [`HttpRequest::match_name`](super::HttpRequest::match_name()). + /// Counterpart to [`HttpRequest::match_name`]. #[inline] pub fn match_name(&self) -> Option<&str> { self.req.match_name() } - /// Counterpart to [`HttpRequest::match_pattern`](super::HttpRequest::match_pattern()). + /// Counterpart to [`HttpRequest::match_pattern`]. #[inline] pub fn match_pattern(&self) -> Option { self.req.match_pattern() @@ -238,7 +238,7 @@ impl ServiceRequest { self.req.app_config() } - /// Counterpart to [`HttpRequest::app_data`](super::HttpRequest::app_data()). + /// Counterpart to [`HttpRequest::app_data`]. #[inline] pub fn app_data(&self) -> Option<&T> { for container in self.req.inner.app_data.iter().rev() { @@ -250,19 +250,33 @@ impl ServiceRequest { None } - /// Counterpart to [`HttpRequest::conn_data`](super::HttpRequest::conn_data()). + /// Counterpart to [`HttpRequest::conn_data`]. #[inline] pub fn conn_data(&self) -> Option<&T> { self.req.conn_data() } + /// Counterpart to [`HttpRequest::req_data`]. + #[inline] + pub fn req_data(&self) -> Ref<'_, Extensions> { + self.req.req_data() + } + + /// Counterpart to [`HttpRequest::req_data_mut`]. + #[inline] + pub fn req_data_mut(&self) -> RefMut<'_, Extensions> { + self.req.req_data_mut() + } + #[cfg(feature = "cookies")] + #[inline] pub fn cookies(&self) -> Result>>, CookieParseError> { self.req.cookies() } /// Return request cookie. #[cfg(feature = "cookies")] + #[inline] pub fn cookie(&self, name: &str) -> Option> { self.req.cookie(name) } @@ -283,6 +297,14 @@ impl ServiceRequest { .app_data .push(extensions); } + + /// Creates a context object for use with a [guard](crate::guard). + /// + /// Useful if you are implementing + #[inline] + pub fn guard_ctx(&self) -> GuardContext<'_> { + GuardContext { req: self } + } } impl Resource for ServiceRequest { diff --git a/src/test/test_request.rs b/src/test/test_request.rs index 5c4de9084..fc42253d7 100644 --- a/src/test/test_request.rs +++ b/src/test/test_request.rs @@ -124,7 +124,7 @@ impl TestRequest { self } - /// Set HTTP Uri of this request + /// Set HTTP URI of this request pub fn uri(mut self, path: &str) -> Self { self.req.uri(path); self From 96a4dc9decf4947b60eaf9d33f29c328265eec36 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 28 Dec 2021 03:22:22 +0000 Subject: [PATCH 17/66] use modern signatures for awc `send_*` and `header` methods (#2553) --- awc/CHANGES.md | 5 ++ awc/src/any_body.rs | 93 +--------------------------------- awc/src/frozen.rs | 58 ++++++++------------- awc/src/middleware/redirect.rs | 4 +- awc/src/request.rs | 2 +- awc/src/responses/response.rs | 7 +-- awc/src/sender.rs | 45 ++++++---------- 7 files changed, 52 insertions(+), 162 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 200ad846a..06e94292a 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] +- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] +- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] + +[#2553]: https://github.com/actix/actix-web/pull/2553 ## 3.0.0-beta.15 - 2021-12-27 diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs index 437216313..d09a943ab 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -1,17 +1,13 @@ use std::{ - borrow::Cow, fmt, mem, pin::Pin, task::{Context, Poll}, }; -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; +use bytes::Bytes; use pin_project_lite::pin_project; -use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; - -use crate::BoxError; +use actix_http::body::{BodySize, BoxBody, MessageBody}; pin_project! { /// Represents various types of HTTP message body. @@ -160,91 +156,6 @@ impl fmt::Debug for AnyBody { } } -impl From<&'static str> for AnyBody { - fn from(string: &'static str) -> Self { - Self::Bytes { - body: Bytes::from_static(string.as_ref()), - } - } -} - -impl From<&'static [u8]> for AnyBody { - fn from(bytes: &'static [u8]) -> Self { - Self::Bytes { - body: Bytes::from_static(bytes), - } - } -} - -impl From> for AnyBody { - fn from(vec: Vec) -> Self { - Self::Bytes { - body: Bytes::from(vec), - } - } -} - -impl From for AnyBody { - fn from(string: String) -> Self { - Self::Bytes { - body: Bytes::from(string), - } - } -} - -impl From<&'_ String> for AnyBody { - fn from(string: &String) -> Self { - Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), - } - } -} - -impl From> for AnyBody { - fn from(string: Cow<'_, str>) -> Self { - match string { - Cow::Owned(s) => Self::from(s), - Cow::Borrowed(s) => Self::Bytes { - body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), - }, - } - } -} - -impl From for AnyBody { - fn from(bytes: Bytes) -> Self { - Self::Bytes { body: bytes } - } -} - -impl From for AnyBody { - fn from(bytes: BytesMut) -> Self { - Self::Bytes { - body: bytes.freeze(), - } - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: SizedStream) -> Self { - AnyBody::new_boxed(stream) - } -} - -impl From> for AnyBody -where - S: Stream> + 'static, - E: Into + 'static, -{ - fn from(stream: BodyStream) -> Self { - AnyBody::new_boxed(stream) - } -} - #[cfg(test)] mod tests { use std::marker::PhantomPinned; diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 14ecf9f32..4023bd1c8 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, net, rc::Rc, time::Duration}; +use std::{net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; @@ -7,7 +7,7 @@ use serde::Serialize; use actix_http::{ body::MessageBody, error::HttpError, - header::{HeaderMap, HeaderName, TryIntoHeaderValue}, + header::{HeaderMap, TryIntoHeaderPair}, Method, RequestHead, Uri, }; @@ -18,6 +18,7 @@ use crate::{ }; /// `FrozenClientRequest` struct represents cloneable client request. +/// /// It could be used to send same request multiple times. #[derive(Clone)] pub struct FrozenClientRequest { @@ -83,7 +84,7 @@ impl FrozenClientRequest { /// Send a streaming body. pub fn send_stream(&self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( @@ -105,20 +106,14 @@ impl FrozenClientRequest { ) } - /// Create a `FrozenSendBuilder` with extra headers + /// Clones this `FrozenClientRequest`, returning a new one with extra headers added. pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder { FrozenSendBuilder::new(self.clone(), extra_headers) } - /// Create a `FrozenSendBuilder` with an extra header - pub fn extra_header(&self, key: K, value: V) -> FrozenSendBuilder - where - HeaderName: TryFrom, - >::Error: Into, - V: TryIntoHeaderValue, - { - self.extra_headers(HeaderMap::new()) - .extra_header(key, value) + /// Clones this `FrozenClientRequest`, returning a new one with the extra header added. + pub fn extra_header(&self, header: impl TryIntoHeaderPair) -> FrozenSendBuilder { + self.extra_headers(HeaderMap::new()).extra_header(header) } } @@ -139,29 +134,20 @@ impl FrozenSendBuilder { } /// Insert a header, it overrides existing header in `FrozenClientRequest`. - pub fn extra_header(mut self, key: K, value: V) -> Self - where - HeaderName: TryFrom, - >::Error: Into, - V: TryIntoHeaderValue, - { - match HeaderName::try_from(key) { - Ok(key) => match value.try_into_value() { - Ok(value) => { - self.extra_headers.insert(key, value); - } - Err(e) => self.err = Some(e.into()), - }, - Err(e) => self.err = Some(e.into()), + pub fn extra_header(mut self, header: impl TryIntoHeaderPair) -> Self { + match header.try_into_pair() { + Ok((key, value)) => { + self.extra_headers.insert(key, value); + } + + Err(err) => self.err = Some(err.into()), } + self } /// Complete request construction and send a body. - pub fn send_body(self, body: B) -> SendClientRequest - where - B: MessageBody + 'static, - { + pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -176,9 +162,9 @@ impl FrozenSendBuilder { } /// Complete request construction and send a json body. - pub fn send_json(self, value: &T) -> SendClientRequest { - if let Some(e) = self.err { - return e.into(); + pub fn send_json(self, value: impl Serialize) -> SendClientRequest { + if let Some(err) = self.err { + return err.into(); } RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json( @@ -191,7 +177,7 @@ impl FrozenSendBuilder { } /// Complete request construction and send an urlencoded body. - pub fn send_form(self, value: &T) -> SendClientRequest { + pub fn send_form(self, value: impl Serialize) -> SendClientRequest { if let Some(e) = self.err { return e.into(); } @@ -208,7 +194,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a streaming body. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { if let Some(e) = self.err { diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 0ee969eee..704d2d79d 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -190,7 +190,9 @@ where let body_new = if is_redirect { // try to reuse body match body { - Some(ref bytes) => AnyBody::from(bytes.clone()), + Some(ref bytes) => AnyBody::Bytes { + body: bytes.clone(), + }, // TODO: should this be AnyBody::Empty or AnyBody::None. _ => AnyBody::empty(), } diff --git a/awc/src/request.rs b/awc/src/request.rs index 8824dd08a..8bcf1ee01 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -385,7 +385,7 @@ impl ClientRequest { /// Set an streaming body and generate `ClientRequest`. pub fn send_stream(self, stream: S) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { let slf = match self.prep_for_sending() { diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs index 6385aea19..0197265f1 100644 --- a/awc/src/responses/response.rs +++ b/awc/src/responses/response.rs @@ -7,8 +7,8 @@ use std::{ }; use actix_http::{ - error::PayloadError, header, header::HeaderMap, BoxedPayloadStream, Extensions, - HttpMessage, Payload, ResponseHead, StatusCode, Version, + error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, + Payload, ResponseHead, StatusCode, Version, }; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; @@ -119,12 +119,13 @@ impl ClientResponse { if self.extensions().get::().is_none() { let mut cookies = Vec::new(); - for hdr in self.headers().get_all(&header::SET_COOKIE) { + for hdr in self.headers().get_all(&actix_http::header::SET_COOKIE) { let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; cookies.push(Cookie::parse_encoded(s)?.into_owned()); } self.extensions_mut().insert(Cookies(cookies)); } + Ok(Ref::map(self.extensions(), |ext| { &ext.get::().unwrap().0 })) diff --git a/awc/src/sender.rs b/awc/src/sender.rs index edf41163d..cd30e571d 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -181,17 +181,14 @@ pub(crate) enum RequestSender { } impl RequestSender { - pub(crate) fn send_body( + pub(crate) fn send_body( self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - body: B, - ) -> SendClientRequest - where - B: MessageBody + 'static, - { + body: impl MessageBody + 'static, + ) -> SendClientRequest { let req = match self { RequestSender::Owned(head) => ConnectRequest::Client( RequestHeadType::Owned(head), @@ -210,15 +207,15 @@ impl RequestSender { SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } - pub(crate) fn send_json( + pub(crate) fn send_json( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { - let body = match serde_json::to_string(value) { + let body = match serde_json::to_string(&value) { Ok(body) => body, Err(err) => return PrepForSendingError::Json(err).into(), }; @@ -227,22 +224,16 @@ impl RequestSender { return e.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::from_message_body(body.into_bytes()), - ) + self.send_body(addr, response_decompress, timeout, config, body) } - pub(crate) fn send_form( + pub(crate) fn send_form( mut self, addr: Option, response_decompress: bool, timeout: Option, config: &ClientConfig, - value: &T, + value: impl Serialize, ) -> SendClientRequest { let body = match serde_urlencoded::to_string(value) { Ok(body) => body, @@ -250,19 +241,13 @@ impl RequestSender { }; // set content-type - if let Err(e) = + if let Err(err) = self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded") { - return e.into(); + return err.into(); } - self.send_body( - addr, - response_decompress, - timeout, - config, - AnyBody::from_message_body(body.into_bytes()), - ) + self.send_body(addr, response_decompress, timeout, config, body) } pub(crate) fn send_stream( @@ -274,7 +259,7 @@ impl RequestSender { stream: S, ) -> SendClientRequest where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, E: Into + 'static, { self.send_body( @@ -282,7 +267,7 @@ impl RequestSender { response_decompress, timeout, config, - AnyBody::new_boxed(BodyStream::new(stream)), + BodyStream::new(stream), ) } @@ -293,7 +278,7 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) + self.send_body(addr, response_decompress, timeout, config, ()) } fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> From 0f5c876c6bb764635f27280056c47f90382bccf6 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 28 Dec 2021 14:50:48 +0000 Subject: [PATCH 18/66] tweak guard docs --- src/guard.rs | 70 +++++++++++++++++++++++++++++++++------------------- 1 file changed, 45 insertions(+), 25 deletions(-) diff --git a/src/guard.rs b/src/guard.rs index fb3e4f243..ebda69cb9 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -10,16 +10,22 @@ //! object and returns a boolean; true if the request _should_ be handled by the guarded service //! or handler. This interface is defined by the [`Guard`] trait. //! -//! Commonly-used guards are provided in this module as well as way of creating a guard from a +//! Commonly-used guards are provided in this module as well as a way of creating a guard from a //! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be //! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on //! services multiple times (which might have different combining behavior than you want). //! +//! There are shortcuts for routes with method guards in the [`web`](crate::web) module: +//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by +//! the following calls are equivalent: +//! - `web::get()` (recommended form) +//! - `web::route().guard(guard::Get())` +//! //! Guards can not modify anything about the request. However, it is possible to store extra //! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`]. //! -//! Guards can prevent resource definitions from overlapping (resulting in some inaccessible routes) -//! where they otherwise would when only considering paths. See the virtual hosting example below. +//! Guards can prevent resource definitions from overlapping which, when only considering paths, +//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting. //! //! # Examples //! In the following code, the `/guarded` resource has one defined route whose handler will only be @@ -36,31 +42,9 @@ //! ); //! ``` //! -//! Guards can be used to set up some form of [virtual hosting] within a single app. -//! Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard -//! definitions they become safe to use in this way. Without these host guards, only routes under -//! the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` -//! and `localhost` as the `Host` guards. -//! ``` -//! use actix_web::{web, http::Method, guard, App, HttpResponse}; -//! -//! App::new() -//! .service( -//! web::scope("") -//! .guard(guard::Host("www.rust-lang.org")) -//! .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))), -//! ) -//! .service( -//! web::scope("") -//! .guard(guard::Host("play.rust-lang.org")) -//! .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))), -//! ); -//! ``` -//! //! [`Scope`]: crate::Scope::guard() //! [`Resource`]: crate::Resource::guard() //! [`Route`]: crate::Route::guard() -//! [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting use std::{ cell::{Ref, RefMut}, @@ -373,6 +357,29 @@ impl Guard for HeaderGuard { /// .guard(Host("admin.rust-lang.org").scheme("https")) /// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure"))); /// ``` +/// +/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))), +/// ); +/// ``` +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting #[allow(non_snake_case)] pub fn Host(host: impl AsRef) -> HostGuard { HostGuard { @@ -642,4 +649,17 @@ mod tests { let req = TestRequest::default().uri("crates.io").to_srv_request(); assert!(!guard.check(&req.guard_ctx())); } + + #[test] + fn mega_nesting() { + let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx)); + + let req = TestRequest::default().to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + + let req = TestRequest::default() + .method(Method::TRACE) + .to_srv_request(); + assert!(guard.check(&req.guard_ctx())); + } } From 2b2de298001c7718952a7e8647f6b1a2736c7259 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 28 Dec 2021 14:52:43 +0000 Subject: [PATCH 19/66] never return port in `realip_remote_addr` (#2554) --- CHANGES.md | 6 ++ src/config.rs | 4 +- src/info.rs | 125 ++++++++++++++++++++++++--------------- src/middleware/logger.rs | 2 +- src/request.rs | 19 +++--- 5 files changed, 97 insertions(+), 59 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index f925f3b94..b6d3b103d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -11,7 +11,13 @@ - Some guards now return `impl Guard` and their concrete types are made private: `guard::{Header}` and all the method guards. [#2552] - The `Not` guard is now generic over the type of guard it wraps. [#2552] +### Fixed +- Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] +- `ConnectionInfo::peer_addr` will not return the port number. [#2554] +- `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554] + [#2552]: https://github.com/actix/actix-web/pull/2552 +[#2554]: https://github.com/actix/actix-web/pull/2554 ## 4.0.0-beta.16 - 2021-12-27 diff --git a/src/config.rs b/src/config.rs index cfa9a4ca3..d68374387 100644 --- a/src/config.rs +++ b/src/config.rs @@ -128,7 +128,7 @@ impl AppConfig { /// Server host name. /// - /// Host name is used by application router as a hostname for url generation. + /// Host name is used by application router as a hostname for URL generation. /// Check [ConnectionInfo](super::dev::ConnectionInfo::host()) /// documentation for more information. /// @@ -137,7 +137,7 @@ impl AppConfig { &self.host } - /// Returns true if connection is secure(https) + /// Returns true if connection is secure (i.e., running over `https:`). pub fn secure(&self) -> bool { self.secure } diff --git a/src/info.rs b/src/info.rs index 71194b24d..ce1ef97c6 100644 --- a/src/info.rs +++ b/src/info.rs @@ -67,7 +67,7 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option< pub struct ConnectionInfo { host: String, scheme: String, - remote_addr: Option, + peer_addr: Option, realip_remote_addr: Option, } @@ -134,67 +134,70 @@ impl ConnectionInfo { .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) .map(str::to_owned); - let remote_addr = req.peer_addr.map(|addr| addr.to_string()); + let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string()); ConnectionInfo { host, scheme, - remote_addr, + peer_addr, realip_remote_addr, } } + /// Real IP (remote address) of client that initiated request. + /// + /// The address is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-For` header + /// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr)) + /// + /// # Security + /// Do not use this function for security purposes unless you can be sure that the `Forwarded` + /// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a + /// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate. + #[inline] + pub fn realip_remote_addr(&self) -> Option<&str> { + self.realip_remote_addr + .as_deref() + .or_else(|| self.peer_addr.as_deref()) + } + + /// Returns serialized IP address of the peer connection. + /// + /// See [`HttpRequest::peer_addr`] for more details. + #[inline] + pub fn peer_addr(&self) -> Option<&str> { + self.peer_addr.as_deref() + } + + /// Hostname of the request. + /// + /// Hostname is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Host` header + /// - `Host` header + /// - request target / URI + /// - configured server hostname + #[inline] + pub fn host(&self) -> &str { + &self.host + } + /// Scheme of the request. /// - /// Scheme is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Proto - /// - Uri + /// Scheme is resolved through the following, in order: + /// - `Forwarded` header + /// - `X-Forwarded-Proto` header + /// - request target / URI #[inline] pub fn scheme(&self) -> &str { &self.scheme } - /// Hostname of the request. - /// - /// Hostname is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-Host - /// - Host - /// - Uri - /// - Server hostname - pub fn host(&self) -> &str { - &self.host - } - - /// Remote address of the connection. - /// - /// Get remote_addr address from socket address. + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")] pub fn remote_addr(&self) -> Option<&str> { - self.remote_addr.as_deref() - } - - /// Real IP (remote address) of client that initiated request. - /// - /// The address is resolved through the following headers, in this order: - /// - /// - Forwarded - /// - X-Forwarded-For - /// - remote_addr name of opened socket - /// - /// # Security - /// Do not use this function for security purposes, unless you can ensure the Forwarded and - /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket - /// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead. - /// - /// [peer_addr]: crate::web::HttpRequest::peer_addr() - #[inline] - pub fn realip_remote_addr(&self) -> Option<&str> { - self.realip_remote_addr - .as_deref() - .or_else(|| self.remote_addr.as_deref()) + self.peer_addr() } } @@ -209,7 +212,7 @@ impl FromRequest for ConnectionInfo { /// Extractor for peer's socket address. /// -/// Also see [`HttpRequest::peer_addr`]. +/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`]. /// /// # Examples /// ``` @@ -432,13 +435,37 @@ mod tests { #[actix_rt::test] async fn peer_addr_extract() { + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + let addr = "127.0.0.1:8080".parse().unwrap(); let req = TestRequest::default().peer_addr(addr).to_http_request(); let peer_addr = PeerAddr::extract(&req).await.unwrap(); assert_eq!(peer_addr, PeerAddr(addr)); + } + #[actix_rt::test] + async fn remote_address() { let req = TestRequest::default().to_http_request(); - let res = PeerAddr::extract(&req).await; - assert!(res.is_err()); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.peer_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1"); + } + + #[actix_rt::test] + async fn real_ip_from_socket_addr() { + let req = TestRequest::default().to_http_request(); + let res = ConnectionInfo::extract(&req).await.unwrap(); + assert!(res.realip_remote_addr().is_none()); + + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1"); } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index d7fdb234f..969cb0c10 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -547,7 +547,7 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().peer_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) diff --git a/src/request.rs b/src/request.rs index 07fb4eb2d..b59369317 100644 --- a/src/request.rs +++ b/src/request.rs @@ -228,23 +228,28 @@ impl HttpRequest { self.app_state().rmap() } - /// Peer socket address. + /// Returns peer socket address. /// /// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// the Actix Web server, then it would be address of this proxy. /// - /// To get client connection information `.connection_info()` should be used. + /// For expanded client connection information, use [`connection_info`] instead. /// - /// Will only return None when called in unit tests. + /// Will only return None when called in unit tests unless [`TestRequest::peer_addr`] is used. + /// + /// [`TestRequest::peer_addr`]: crate::test::TestRequest::peer_addr + /// [`connection_info`]: Self::connection_info #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr } - /// Get *ConnectionInfo* for the current request. + /// Returns connection info for the current request. /// - /// This method panics if request's extensions container is already - /// borrowed. + /// The return type, [`ConnectionInfo`], can also be used as an extractor. + /// + /// # Panics + /// Panics if request's extensions container is already borrowed. #[inline] pub fn connection_info(&self) -> Ref<'_, ConnectionInfo> { if !self.extensions().contains::() { @@ -252,7 +257,7 @@ impl HttpRequest { self.extensions_mut().insert(info); } - Ref::map(self.extensions(), |e| e.get().unwrap()) + Ref::map(self.extensions(), |data| data.get().unwrap()) } /// App config From 798e9911e948857e15df695a7691df2a146b1120 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 07:07:46 +0000 Subject: [PATCH 20/66] prepare awc release 3.0.0-beta.16 --- awc/CHANGES.md | 3 +++ awc/Cargo.toml | 4 ++-- awc/README.md | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 06e94292a..212469873 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.16 - 2021-12-29 - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] - `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] - Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 8777ffa74..676a10895 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.15" +version = "3.0.0-beta.16" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", @@ -99,7 +99,7 @@ actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.16", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.17", features = ["openssl"] } brotli2 = "0.3.2" env_logger = "0.9" diff --git a/awc/README.md b/awc/README.md index 582ecb18f..4916210e4 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.15)](https://docs.rs/awc/3.0.0-beta.15) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.16)](https://docs.rs/awc/3.0.0-beta.16) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.15/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.15) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.16/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.16) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 11d50d792b6569f190f9873f3cee5c5569b75e0f Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 07:07:51 +0000 Subject: [PATCH 21/66] prepare actix-web release 4.0.0-beta.17 --- CHANGES.md | 5 ++++- Cargo.toml | 4 ++-- README.md | 4 ++-- actix-files/Cargo.toml | 4 ++-- actix-http-test/Cargo.toml | 4 ++-- actix-http/Cargo.toml | 2 +- actix-multipart/Cargo.toml | 2 +- actix-test/Cargo.toml | 4 ++-- actix-web-actors/Cargo.toml | 4 ++-- actix-web-codegen/Cargo.toml | 2 +- 10 files changed, 19 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b6d3b103d..c870f10e7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.17 - 2021-12-29 ### Added - `guard::GuardContext` for use with the `Guard` trait. [#2552] - `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] @@ -8,7 +11,7 @@ ### Changed - `Guard` trait now receives a `&GuardContext`. [#2552] - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] -- Some guards now return `impl Guard` and their concrete types are made private: `guard::{Header}` and all the method guards. [#2552] +- Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] - The `Not` guard is now generic over the type of guard it wraps. [#2552] ### Fixed diff --git a/Cargo.toml b/Cargo.toml index b6ef184e0..1b85e8e75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.16" +version = "4.0.0-beta.17" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] @@ -107,7 +107,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.15", features = ["openssl"] } +awc = { version = "3.0.0-beta.16", features = ["openssl"] } brotli2 = "0.3.2" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/README.md b/README.md index f9d388f8b..afe6b1f8e 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![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.16)](https://docs.rs/actix-web/4.0.0-beta.16) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.17)](https://docs.rs/actix-web/4.0.0-beta.17) [![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.16/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.16) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.17)
[![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 bbd4fee22..b7bb3fd07 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.17" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.16", default-features = false } +actix-web = { version = "4.0.0-beta.17", 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.10" -actix-web = "4.0.0-beta.16" +actix-web = "4.0.0-beta.17" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 5c58978ea..2883a8f7e 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-rc.2" -awc = { version = "3.0.0-beta.15", default-features = false } +awc = { version = "3.0.0-beta.16", default-features = false } base64 = "0.13" bytes = "1" @@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } actix-http = "3.0.0-beta.17" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9e587890b..9575f55e7 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -82,7 +82,7 @@ zstd = { version = "0.9", optional = true } actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.16" +actix-web = "4.0.0-beta.17" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index de13133a1..4beddd0b8 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.16", default-features = false } +actix-web = { version = "4.0.0-beta.17", default-features = false } bytes = "1" derive_more = "0.99.5" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index c7177a38c..c523a6566 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -34,8 +34,8 @@ actix-http-test = "3.0.0-beta.10" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.15", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.16", 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 3b792093a..719c563cb 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.17" -actix-web = { version = "4.0.0-beta.16", default-features = false } +actix-web = { version = "4.0.0-beta.17", default-features = false } bytes = "1" bytestring = "1" @@ -28,7 +28,7 @@ tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.10" -awc = { version = "3.0.0-beta.15", default-features = false } +awc = { version = "3.0.0-beta.16", default-features = false } env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 5250baa90..b014a47ae 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.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.16" +actix-web = "4.0.0-beta.17" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" From 9779010a5af14a7ccbe3668280f117de5345769a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 07:08:10 +0000 Subject: [PATCH 22/66] prepare actix-files release 0.6.0-beta.12 --- actix-files/CHANGES.md | 4 ++++ actix-files/Cargo.toml | 2 +- actix-files/README.md | 4 ++-- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index af6dcb415..65007c955 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.6.0-beta.12 - 2021-12-29 +* No significant changes since `0.6.0-beta.11`. + + ## 0.6.0-beta.11 - 2021-12-27 * No significant changes since `0.6.0-beta.10`. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index b7bb3fd07..adf4408ba 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.11" +version = "0.6.0-beta.12" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/actix-files/README.md b/actix-files/README.md index db5c94d1e..3f310a607 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.11)](https://docs.rs/actix-files/0.6.0-beta.11) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.11/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.11) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From a87e01f0d139836e1d738529f67da8ec9e1059ed Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 08:59:15 +0000 Subject: [PATCH 23/66] bump msrv to 1.54 --- .github/workflows/ci.yml | 2 +- CHANGES.md | 2 ++ Cargo.toml | 1 - README.md | 4 +-- actix-files/CHANGES.md | 5 +-- actix-files/README.md | 4 +-- actix-http-test/CHANGES.md | 1 + actix-http-test/README.md | 4 +-- actix-http/CHANGES.md | 1 + actix-http/README.md | 4 +-- actix-multipart/CHANGES.md | 3 +- actix-multipart/README.md | 4 +-- actix-router/CHANGES.md | 1 + actix-test/CHANGES.md | 3 +- actix-web-actors/CHANGES.md | 3 +- actix-web-actors/README.md | 4 +-- actix-web-codegen/CHANGES.md | 1 + actix-web-codegen/README.md | 4 +-- actix-web-codegen/tests/trybuild.rs | 2 +- .../trybuild/route-missing-method-fail.stderr | 6 ++-- awc/README.md | 2 +- clippy.toml | 2 +- scripts/bump | 2 +- src/error/internal.rs | 16 ++++------ src/guard.rs | 30 ++++++++--------- src/lib.rs | 2 +- src/web.rs | 32 +++++++++---------- 27 files changed, 74 insertions(+), 71 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe464bf27..2689804f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } version: - - 1.52.0 # MSRV + - 1.54.0 # MSRV - stable - nightly diff --git a/CHANGES.md b/CHANGES.md index c870f10e7..83efa6f3b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,8 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +- Minimum supported Rust version (MSRV) is now 1.54. ## 4.0.0-beta.17 - 2021-12-29 diff --git a/Cargo.toml b/Cargo.toml index 1b85e8e75..44c58e494 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,7 +94,6 @@ language-tags = "0.3" once_cell = "1.5" log = "0.4" mime = "0.3" -paste = "1" pin-project-lite = "0.2.7" regex = "1.4" serde = { version = "1.0", features = ["derive"] } diff --git a/README.md b/README.md index afe6b1f8e..aac7818bd 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [![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.17)](https://docs.rs/actix-web/4.0.0-beta.17) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.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.17/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.17)
@@ -32,7 +32,7 @@ - SSL support using OpenSSL or Rustls - Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) - Includes an async [HTTP client](https://docs.rs/awc/) -- Runs on stable Rust 1.52+ +- Runs on stable Rust 1.54+ ## Documentation diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 65007c955..c626fd3fb 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,14 +1,15 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 0.6.0-beta.12 - 2021-12-29 -* No significant changes since `0.6.0-beta.11`. +- No significant changes since `0.6.0-beta.11`. ## 0.6.0-beta.11 - 2021-12-27 -* No significant changes since `0.6.0-beta.10`. +- No significant changes since `0.6.0-beta.10`. ## 0.6.0-beta.10 - 2021-12-11 diff --git a/actix-files/README.md b/actix-files/README.md index 3f310a607..41dd714d3 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12) @@ -15,4 +15,4 @@ - [API Documentation](https://docs.rs/actix-files/) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 8c6a63b72..e83e95bd3 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 3.0.0-beta.10 - 2021-12-27 diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 589c54c23..e2cdc0ba2 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index d74a754ac..6911d9969 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 3.0.0-beta.17 - 2021-12-27 diff --git a/actix-http/README.md b/actix-http/README.md index 223e18ceb..084753ac9 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -4,7 +4,7 @@ [![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.17)](https://docs.rs/actix-http/3.0.0-beta.17) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 ## Example diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index a9a1e8784..65fe51d44 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,10 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 0.4.0-beta.11 - 2021-12-27 -* No significant changes since `0.4.0-beta.10`. +- No significant changes since `0.4.0-beta.10`. ## 0.4.0-beta.10 - 2021-12-11 diff --git a/actix-multipart/README.md b/actix-multipart/README.md index a9ee325ba..a773f5d52 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-multipart) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 0a6a56359..c85d10e2a 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 0.5.0-beta.3 - 2021-12-17 diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 2de0a69d6..9838c6f5f 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,10 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 0.1.0-beta.10 - 2021-12-27 -* No significant changes since `0.1.0-beta.9`. +- No significant changes since `0.1.0-beta.9`. ## 0.1.0-beta.9 - 2021-12-17 diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 2fbbe7444..5c8091fbb 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,10 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 4.0.0-beta.9 - 2021-12-27 -* No significant changes since `4.0.0-beta.8`. +- No significant changes since `4.0.0-beta.8`. ## 4.0.0-beta.8 - 2021-12-11 diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 232c81eac..0bd007e6a 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) @@ -14,4 +14,4 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-actors) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 0d881d303..5f8c0f259 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,7 @@ # Changes ## Unreleased - 2021-xx-xx +- Minimum supported Rust version (MSRV) is now 1.54. ## 0.5.0-beta.6 - 2021-12-11 diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index f05d3f22c..abb638cee 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -4,7 +4,7 @@ [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.6)](https://docs.rs/actix-web-codegen/0.5.0-beta.6) -[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6) @@ -14,7 +14,7 @@ ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 ## Compile Testing diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index dd70cb7ca..b2d9ce186 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.52)] // MSRV +#[rustversion::stable(1.54)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index c36b090c0..b1cefafde 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -1,13 +1,13 @@ error: The #[route(..)] macro requires at least one `method` attribute - --> $DIR/route-missing-method-fail.rs:3:1 + --> tests/trybuild/route-missing-method-fail.rs:3:1 | 3 | #[route("/")] | ^^^^^^^^^^^^^ | - = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the attribute macro `route` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> $DIR/route-missing-method-fail.rs:12:55 + --> tests/trybuild/route-missing-method-fail.rs:12:55 | 12 | let srv = actix_test::start(|| App::new().service(index)); | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/awc/README.md b/awc/README.md index 4916210e4..2f8562181 100644 --- a/awc/README.md +++ b/awc/README.md @@ -12,7 +12,7 @@ - [API Documentation](https://docs.rs/awc) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) -- Minimum Supported Rust Version (MSRV): 1.52 +- Minimum Supported Rust Version (MSRV): 1.54 ## Example diff --git a/clippy.toml b/clippy.toml index cef91fde7..ece14b8d2 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1 +1 @@ -msrv = "1.52" +msrv = "1.54" diff --git a/scripts/bump b/scripts/bump index 43cd8b8c7..1cd190e03 100755 --- a/scripts/bump +++ b/scripts/bump @@ -40,7 +40,7 @@ cat "$CHANGELOG_FILE" | # if word count of changelog chunk is 0 then insert filler changelog chunk if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then - echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" + echo "- No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" echo >>"$CHANGE_CHUNK_FILE" echo >>"$CHANGE_CHUNK_FILE" fi diff --git a/src/error/internal.rs b/src/error/internal.rs index 37195dc2e..1c6e343e3 100644 --- a/src/error/internal.rs +++ b/src/error/internal.rs @@ -118,15 +118,13 @@ where macro_rules! error_helper { ($name:ident, $status:ident) => { - paste::paste! { - #[doc = "Helper function that wraps any error and generates a `" $status "` response."] - #[allow(non_snake_case)] - pub fn $name(err: T) -> Error - where - T: fmt::Debug + fmt::Display + 'static, - { - InternalError::new(err, StatusCode::$status).into() - } + #[doc = concat!("Helper function that wraps any error and generates a `", stringify!($status), "` response.")] + #[allow(non_snake_case)] + pub fn $name(err: T) -> Error + where + T: fmt::Debug + fmt::Display + 'static, + { + InternalError::new(err, StatusCode::$status).into() } }; } diff --git a/src/guard.rs b/src/guard.rs index ebda69cb9..7a015d2da 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -270,22 +270,20 @@ impl Guard for MethodGuard { macro_rules! method_guard { ($method_fn:ident, $method_const:ident) => { - paste::paste! { - #[doc = " Creates a guard that matches the `" $method_const "` request method."] - /// - /// # Examples - #[doc = " The route in this example will only respond to `" $method_const "` requests."] - /// ``` - /// use actix_web::{guard, web, HttpResponse}; - /// - /// web::route() - #[doc = " .guard(guard::" $method_fn "())"] - /// .to(|| HttpResponse::Ok()); - /// ``` - #[allow(non_snake_case)] - pub fn $method_fn() -> impl Guard { - MethodGuard(HttpMethod::$method_const) - } + #[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")] + /// + /// # Examples + #[doc = concat!("The route in this example will only respond to `", stringify!($method_const), "` requests.")] + /// ``` + /// use actix_web::{guard, web, HttpResponse}; + /// + /// web::route() + #[doc = concat!(" .guard(guard::", stringify!($method_fn), "())")] + /// .to(|| HttpResponse::Ok()); + /// ``` + #[allow(non_snake_case)] + pub fn $method_fn() -> impl Guard { + MethodGuard(HttpMethod::$method_const) } }; } diff --git a/src/lib.rs b/src/lib.rs index 5f5b915b7..18f0d581d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -53,7 +53,7 @@ //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Includes an async [HTTP client](https://docs.rs/awc/) -//! * Runs on stable Rust 1.52+ +//! * Runs on stable Rust 1.54+ //! //! # Crate Features //! * `cookies` - cookies support (enabled by default) diff --git a/src/web.rs b/src/web.rs index 47bff36a3..e4339352b 100644 --- a/src/web.rs +++ b/src/web.rs @@ -86,23 +86,21 @@ pub fn route() -> Route { macro_rules! method_route { ($method_fn:ident, $method_const:ident) => { - paste::paste! { - #[doc = " Creates a new route with `" $method_const "` method guard."] - /// - /// # Examples - #[doc = " In this example, one `" $method_const " /{project_id}` route is set up:"] - /// ``` - /// use actix_web::{web, App, HttpResponse}; - /// - /// let app = App::new().service( - /// web::resource("/{project_id}") - #[doc = " .route(web::" $method_fn "().to(|| HttpResponse::Ok()))"] - /// - /// ); - /// ``` - pub fn $method_fn() -> Route { - method(Method::$method_const) - } + #[doc = concat!(" Creates a new route with `", stringify!($method_const), "` method guard.")] + /// + /// # Examples + #[doc = concat!(" In this example, one `", stringify!($method_const), " /{project_id}` route is set up:")] + /// ``` + /// use actix_web::{web, App, HttpResponse}; + /// + /// let app = App::new().service( + /// web::resource("/{project_id}") + #[doc = concat!(" .route(web::", stringify!($method_fn), "().to(|| HttpResponse::Ok()))")] + /// + /// ); + /// ``` + pub fn $method_fn() -> Route { + method(Method::$method_const) } }; } From 74738c63a728043f3d138f8f5bbbbb4e8c406d77 Mon Sep 17 00:00:00 2001 From: Luca Palmieri Date: Wed, 29 Dec 2021 11:03:25 +0100 Subject: [PATCH 24/66] Upgrade time dependency (via `cookie`) (#2555) --- CHANGES.md | 2 +- Cargo.toml | 2 +- awc/CHANGES.md | 3 ++- awc/Cargo.toml | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 83efa6f3b..d7b8045c7 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,9 @@ ## Unreleased - 2021-xx-xx ### Changed +- `actix-web` has upgraded to `cookie` 0.16. This removes `actix-web`'s dependency on a version of `time` that was affected by RUSTSEC-2020-0071. `actix-web` still depends on a vulnerable version of `chrono` via `rcgen`, but `rcgen` is only used as a dev dependency therefore this does not affect end users. - Minimum supported Rust version (MSRV) is now 1.54. - ## 4.0.0-beta.17 - 2021-12-29 ### Added - `guard::GuardContext` for use with the `Guard` trait. [#2552] diff --git a/Cargo.toml b/Cargo.toml index 44c58e494..3f91c6f9f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ actix-web-codegen = "0.5.0-beta.6" ahash = "0.7" bytes = "1" cfg-if = "1" -cookie = { version = "0.15", features = ["percent-encode"], optional = true } +cookie = { version = "0.16", features = ["percent-encode"], optional = true } derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 212469873..0b344b96c 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,7 +1,8 @@ # Changes ## Unreleased - 2021-xx-xx - +### Changed +- `awc` has upgraded to `cookie` 0.16. This removes `awc`'s dependency on a version of `time` that was affected by RUSTSEC-2020-0071. `awc` still depends on a vulnerable version of `chrono` via `rcgen`, but `rcgen` is only used as a dev dependency therefore this does not affect end users. ## 3.0.0-beta.16 - 2021-12-29 - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 676a10895..e3a7346b8 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -85,7 +85,7 @@ serde_json = "1.0" serde_urlencoded = "0.7" tokio = { version = "1.8.4", features = ["sync"] } -cookie = { version = "0.15", features = ["percent-encode"], optional = true } +cookie = { version = "0.16", features = ["percent-encode"], optional = true } tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } From 542c92c9a7f0107ce5e6d6a3f71f34a80eac4b19 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 10:06:36 +0000 Subject: [PATCH 25/66] tweak changelogs --- CHANGES.md | 9 ++++++++- awc/CHANGES.md | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index d7b8045c7..9b2a4747a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,9 +2,16 @@ ## Unreleased - 2021-xx-xx ### Changed -- `actix-web` has upgraded to `cookie` 0.16. This removes `actix-web`'s dependency on a version of `time` that was affected by RUSTSEC-2020-0071. `actix-web` still depends on a vulnerable version of `chrono` via `rcgen`, but `rcgen` is only used as a dev dependency therefore this does not affect end users. +- Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Minimum supported Rust version (MSRV) is now 1.54. +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[#2555]: https://github.com/actix/actix-web/pull/2555 +[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + + ## 4.0.0-beta.17 - 2021-12-29 ### Added - `guard::GuardContext` for use with the `Guard` trait. [#2552] diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 0b344b96c..d01e78a61 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -2,7 +2,14 @@ ## Unreleased - 2021-xx-xx ### Changed -- `awc` has upgraded to `cookie` 0.16. This removes `awc`'s dependency on a version of `time` that was affected by RUSTSEC-2020-0071. `awc` still depends on a vulnerable version of `chrono` via `rcgen`, but `rcgen` is only used as a dev dependency therefore this does not affect end users. +- Update `cookie` dependency (re-exported) to `0.16`. [#2555] + +### Security +- `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. + +[#2555]: https://github.com/actix/actix-web/pull/2555 +[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html + ## 3.0.0-beta.16 - 2021-12-29 - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] From a80e93d6db560de06eb5c238c8b16253e20b929d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 10:17:11 +0000 Subject: [PATCH 26/66] prepare actix-web release 4.0.0-beta.18 --- 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 9b2a4747a..28dd6698b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.18 - 2021-12-29 ### Changed - Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/Cargo.toml b/Cargo.toml index 3f91c6f9f..0d2dfda88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.17" +version = "4.0.0-beta.18" authors = ["Nikolay Kim "] 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 aac7818bd..c97748c2f 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![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.17)](https://docs.rs/actix-web/4.0.0-beta.17) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.18)](https://docs.rs/actix-web/4.0.0-beta.18) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.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.17/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.17) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.18)
[![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 adf4408ba..c2acbc761 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.17" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.17", default-features = false } +actix-web = { version = "4.0.0-beta.18", 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.10" -actix-web = "4.0.0-beta.17" +actix-web = "4.0.0-beta.18" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 2883a8f7e..bd6baf743 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.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } actix-http = "3.0.0-beta.17" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9575f55e7..7c9b28944 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -82,7 +82,7 @@ zstd = { version = "0.9", optional = true } actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.17" +actix-web = "4.0.0-beta.18" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 4beddd0b8..715512111 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.17", default-features = false } +actix-web = { version = "4.0.0-beta.18", default-features = false } bytes = "1" derive_more = "0.99.5" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index c523a6566..3720869ff 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -34,7 +34,7 @@ actix-http-test = "3.0.0-beta.10" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.17", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.16", 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 719c563cb..2e96a68ae 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.17" -actix-web = { version = "4.0.0-beta.17", default-features = false } +actix-web = { version = "4.0.0-beta.18", default-features = false } bytes = "1" bytestring = "1" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index b014a47ae..03ff4698f 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.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.17" +actix-web = "4.0.0-beta.18" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index e3a7346b8..46a202b8e 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -99,7 +99,7 @@ actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.17", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.18", features = ["openssl"] } brotli2 = "0.3.2" env_logger = "0.9" From 6df4974234445753e42458df97e940b0949bbb76 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 29 Dec 2021 10:17:28 +0000 Subject: [PATCH 27/66] prepare awc release 3.0.0-beta.17 --- 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 0d2dfda88..7abcba5a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ url = "2.1" [dev-dependencies] actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.16", features = ["openssl"] } +awc = { version = "3.0.0-beta.17", 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 bd6baf743..d973ce151 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-rc.2" -awc = { version = "3.0.0-beta.16", default-features = false } +awc = { version = "3.0.0-beta.17", default-features = false } base64 = "0.13" bytes = "1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 3720869ff..e7ac92e2e 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.18", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.16", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.17", 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 2e96a68ae..52ffca1ba 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.10" -awc = { version = "3.0.0-beta.16", default-features = false } +awc = { version = "3.0.0-beta.17", 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 d01e78a61..346e95af4 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.17 - 2021-12-29 ### Changed - Update `cookie` dependency (re-exported) to `0.16`. [#2555] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 46a202b8e..9c1f56f64 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.16" +version = "3.0.0-beta.17" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/awc/README.md b/awc/README.md index 2f8562181..ace2b2eb5 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.16)](https://docs.rs/awc/3.0.0-beta.16) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.17)](https://docs.rs/awc/3.0.0-beta.17) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.16/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.16) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.17/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.17) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From 231a24ef8d88ad30aeef503fcc00f3d4d5a1973c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 30 Dec 2021 07:11:35 +0000 Subject: [PATCH 28/66] improve application data docs --- src/app.rs | 1 + src/data.rs | 46 ++++++++++++++++++++++++++++++---------------- src/request.rs | 30 +++++++++++++++++++++++++----- src/resource.rs | 1 + src/scope.rs | 1 + 5 files changed, 58 insertions(+), 21 deletions(-) diff --git a/src/app.rs b/src/app.rs index 10868d18d..3fddc055b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -109,6 +109,7 @@ impl App { /// .route("/", web::get().to(handler)) /// }) /// ``` + #[doc(alias = "manage")] pub fn app_data(mut self, ext: U) -> Self { self.extensions.insert(ext); self diff --git a/src/data.rs b/src/data.rs index ef077e87c..986456ac0 100644 --- a/src/data.rs +++ b/src/data.rs @@ -19,23 +19,32 @@ pub(crate) trait DataFactory { pub(crate) type FnDataFactory = Box LocalBoxFuture<'static, Result, ()>>>; -/// Application data. +/// Application data wrapper and extractor. /// -/// Application level data is a piece of arbitrary data attached to the app, scope, or resource. -/// Application data is available to all routes and can be added during the application -/// configuration process via `App::data()`. +/// # Setting Data +/// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped +/// in this `Data` type for those calls, it can be used as an extractor. /// -/// Application data can be accessed by using `Data` extractor where `T` is data type. +/// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared, +/// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`. /// -/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP -/// server constructs an application instance for each thread, thus application data must be -/// constructed multiple times. If you want to share data between different threads, a shareable -/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` -/// or `Sync`. Internally `Data` contains an `Arc`. +/// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data), +/// and [`Resource::app_data`](crate::Resource::app_data). +/// +/// # Extracting `Data` +/// Since the Actix Web router layers application data, the returned object will reference the +/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope` +/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then +/// extracting a `web::>` for that handler will return the `Scope`'s instance. +/// However, using the same router set up and a request that does not get captured by the `Scope`, +/// `web::>` would return the `App`'s instance. /// /// If route data is not set for a handler, using `Data` extractor would cause a `500 Internal /// Server Error` response. /// +/// See also [`HttpRequest::app_data`] +/// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data). +/// /// # Unsized Data /// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first /// constructing an `Arc` and using the `From` implementation to convert it. @@ -79,6 +88,7 @@ pub(crate) type FnDataFactory = /// .route("/index.html", web::get().to(index)) /// .route("/index-alt.html", web::get().to(index_alt)); /// ``` +#[doc(alias = "state")] #[derive(Debug)] pub struct Data(Arc); @@ -90,12 +100,12 @@ impl Data { } impl Data { - /// Get reference to inner app data. + /// Returns reference to inner `T`. pub fn get_ref(&self) -> &T { self.0.as_ref() } - /// Convert to the internal Arc + /// Unwraps to the internal `Arc` pub fn into_inner(self) -> Arc { self.0 } @@ -143,13 +153,17 @@ impl FromRequest for Data { ok(st.clone()) } else { log::debug!( - "Failed to construct App-level Data extractor. \ - Request path: {:?} (type: {})", - req.path(), + "Failed to construct Data extractor type: `{}`. For the Data extractor to work \ + correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \ + Ensure that types align in both the set and retrieve calls. \ + Request path: {}", type_name::(), + req.path() ); + err(ErrorInternalServerError( - "App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()", + "Requested application data is not configured. \ + View/enable debug logs for more details.", )) } } diff --git a/src/request.rs b/src/request.rs index b59369317..2580ed12c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -266,14 +266,34 @@ impl HttpRequest { self.app_state().config() } - /// Get an application data object stored with `App::data` or `App::app_data` - /// methods during application configuration. + /// Retrieves a piece of application state. /// - /// If `App::data` was used to store object, use `Data`: + /// Extracts any object stored with [`App::app_data()`](crate::App::app_data) (or the + /// counterpart methods on [`Scope`](crate::Scope::app_data) and + /// [`Resource`](crate::Resource::app_data)) during application configuration. /// - /// ```ignore - /// let opt_t = req.app_data::>(); + /// Since the Actix Web router layers application data, the returned object will reference the + /// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope` + /// also stores a `u32`, and the delegated request handler falls within that `Scope`, then + /// calling `.app_data::()` on an `HttpRequest` within that handler will return the + /// `Scope`'s instance. However, using the same router set up and a request that does not get + /// captured by the `Scope`, `.app_data::()` would return the `App`'s instance. + /// + /// If the state was stored using the [`Data`] wrapper, then it must also be retrieved using + /// this same type. + /// + /// See also the [`Data`] extractor. + /// + /// # Examples + /// ```no_run + /// # use actix_web::{web, test::TestRequest}; + /// # let req = TestRequest::default().to_http_request(); + /// # type T = u32; + /// let opt_t: Option<&T> = req.app_data::>(); /// ``` + /// + /// [`Data`]: crate::web::Data + #[doc(alias = "state")] pub fn app_data(&self) -> Option<&T> { for container in self.inner.app_data.iter().rev() { if let Some(data) = container.get::() { diff --git a/src/resource.rs b/src/resource.rs index 564c4d3ef..8da0a8a85 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -195,6 +195,7 @@ where /// .route(web::get().to(handler)) /// ); /// ``` + #[doc(alias = "manage")] pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) diff --git a/src/scope.rs b/src/scope.rs index b4618bb6c..fa9807f42 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -154,6 +154,7 @@ where /// .route("/", web::get().to(handler)) /// ); /// ``` + #[doc(alias = "manage")] pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) From b4ff6addfe4856aae65ec6b4d4754d5dce49080b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 30 Dec 2021 07:15:57 +0000 Subject: [PATCH 29/66] use match name if possible in data debug log --- src/data.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/data.rs b/src/data.rs index 986456ac0..ce7b1fee6 100644 --- a/src/data.rs +++ b/src/data.rs @@ -153,16 +153,15 @@ impl FromRequest for Data { ok(st.clone()) } else { log::debug!( - "Failed to construct Data extractor type: `{}`. For the Data extractor to work \ + "Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \ correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \ - Ensure that types align in both the set and retrieve calls. \ - Request path: {}", + Ensure that types align in both the set and retrieve calls.", type_name::(), - req.path() + req.match_name().unwrap_or_else(|| req.path()) ); err(ErrorInternalServerError( - "Requested application data is not configured. \ + "Requested application data is not configured correctly. \ View/enable debug logs for more details.", )) } From 5dcb2502370c2e7a3fa5bc11a338925485dcde4d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 31 Dec 2021 07:53:53 +0000 Subject: [PATCH 30/66] fix doc test --- README.md | 2 +- src/request.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c97748c2f..3072ba1c0 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ![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.18/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.18)
-[![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) +[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) ![downloads](https://img.shields.io/crates/d/actix-web.svg) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/src/request.rs b/src/request.rs index 2580ed12c..cbec70a29 100644 --- a/src/request.rs +++ b/src/request.rs @@ -286,10 +286,10 @@ impl HttpRequest { /// /// # Examples /// ```no_run - /// # use actix_web::{web, test::TestRequest}; + /// # use actix_web::{test::TestRequest, web::Data}; /// # let req = TestRequest::default().to_http_request(); /// # type T = u32; - /// let opt_t: Option<&T> = req.app_data::>(); + /// let opt_t: Option<&Data> = req.app_data::>(); /// ``` /// /// [`Data`]: crate::web::Data From b7089245908f19fe03838ae2ef67929960aaee91 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 31 Dec 2021 08:38:58 +0000 Subject: [PATCH 31/66] only run nightly checks on master ci --- .github/workflows/ci-master.yml | 87 +++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 1 - 2 files changed, 87 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-master.yml b/.github/workflows/ci-master.yml index 548ec21b7..b78617dc5 100644 --- a/.github/workflows/ci-master.yml +++ b/.github/workflows/ci-master.yml @@ -5,6 +5,93 @@ on: branches: [master] jobs: + build_and_test_nightly: + strategy: + fail-fast: false + matrix: + target: + - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } + - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } + - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } + version: + - nightly + + name: ${{ matrix.target.name }} / ${{ matrix.version }} + runs-on: ${{ matrix.target.os }} + + env: + CI: 1 + CARGO_INCREMENTAL: 0 + VCPKGRS_DYNAMIC: 1 + + steps: + - uses: actions/checkout@v2 + + # install OpenSSL on Windows + # TODO: GitHub actions docs state that OpenSSL is + # already installed on these Windows machines somewhere + - name: Set vcpkg root + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Install OpenSSL + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: vcpkg install openssl:x64-windows + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check minimal + uses: actions-rs/cargo@v1 + with: { command: ci-check-min } + + - name: check default + uses: actions-rs/cargo@v1 + with: { command: ci-check-default } + + - name: tests + timeout-minutes: 60 + run: | + 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 + + - name: tests (io-uring) + if: matrix.target.os == 'ubuntu-latest' + timeout-minutes: 60 + run: > + sudo bash -c "ulimit -Sl 512 + && ulimit -Hl 512 + && PATH=$PATH:/usr/share/rust/.cargo/bin + && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo-cache + ci_feature_powerset_check: name: Verify Feature Combinations runs-on: ubuntu-latest diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2689804f0..f41aa972f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,6 @@ jobs: version: - 1.54.0 # MSRV - stable - - nightly name: ${{ matrix.target.name }} / ${{ matrix.version }} runs-on: ${{ matrix.target.os }} From e890307091059c5598d348365ca4e8c41b64731d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 3 Jan 2022 13:17:57 +0000 Subject: [PATCH 32/66] Fix AcceptEncoding header (#2501) --- .github/workflows/bench.yml | 2 - CHANGES.md | 16 + actix-files/src/named.rs | 6 +- actix-http/CHANGES.md | 21 + actix-http/src/encoding/decoder.rs | 12 +- actix-http/src/encoding/encoder.rs | 31 +- actix-http/src/error.rs | 1 + actix-http/src/header/map.rs | 7 + actix-http/src/header/mod.rs | 7 - .../src/header/shared/content_encoding.rs | 51 +- actix-http/src/header/shared/quality.rs | 23 +- actix-http/src/header/shared/quality_item.rs | 14 +- awc/tests/test_client.rs | 14 +- scripts/ci-test | 20 +- src/dev.rs | 126 +++-- src/http/header/accept.rs | 65 +-- src/http/header/accept_charset.rs | 13 +- src/http/header/accept_encoding.rs | 414 ++++++++++++-- src/http/header/accept_language.rs | 64 +-- src/http/header/content_disposition.rs | 1 - src/http/header/encoding.rs | 78 ++- src/middleware/compress.rs | 328 +++-------- src/types/mod.rs | 15 +- src/types/payload.rs | 1 + src/web.rs | 5 +- tests/compression.rs | 313 +++++++++++ tests/fixtures/lorem.txt | 5 + tests/fixtures/lorem.txt.br | Bin 0 -> 905 bytes tests/fixtures/lorem.txt.gz | Bin 0 -> 920 bytes tests/fixtures/lorem.txt.xz | Bin 0 -> 1020 bytes tests/fixtures/lorem.txt.zst | Bin 0 -> 898 bytes tests/test_server.rs | 520 ++++++------------ tests/test_utils.rs | 76 +++ 33 files changed, 1360 insertions(+), 889 deletions(-) create mode 100644 tests/compression.rs create mode 100644 tests/fixtures/lorem.txt create mode 100644 tests/fixtures/lorem.txt.br create mode 100644 tests/fixtures/lorem.txt.gz create mode 100644 tests/fixtures/lorem.txt.xz create mode 100644 tests/fixtures/lorem.txt.zst create mode 100644 tests/test_utils.rs diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 828d62561..a4b54ca7a 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,8 +1,6 @@ name: Benchmark on: - pull_request: - types: [opened, synchronize, reopened] push: branches: - master diff --git a/CHANGES.md b/CHANGES.md index 28dd6698b..423ea9fdc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,22 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- `impl Hash` for `http::header::Encoding`. [#2501] +- `AcceptEncoding::negotiate()`. [#2501] + +### Changed +- `AcceptEncoding::preference` now returns `Option>`. [#2501] +- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] +- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] + +### Fixed +- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] + +### Removed +- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] + +[#2501]: https://github.com/actix/actix-web/pull/2501 ## 4.0.0-beta.18 - 2021-12-29 diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 810988f0c..04e453580 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -420,7 +420,7 @@ impl NamedFile { } if let Some(current_encoding) = self.encoding { - res.encoding(current_encoding); + res.encode_with(current_encoding); } let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); @@ -494,7 +494,7 @@ impl NamedFile { // default compressing if let Some(current_encoding) = self.encoding { - res.encoding(current_encoding); + res.encode_with(current_encoding); } if let Some(lm) = last_modified { @@ -517,7 +517,7 @@ impl NamedFile { length = ranges[0].length; offset = ranges[0].start; - res.encoding(ContentEncoding::Identity); + res.encode_with(ContentEncoding::Identity); res.insert_header(( header::CONTENT_RANGE, format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 6911d9969..cbdccd93c 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,8 +1,29 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +- `impl Eq` for `header::ContentEncoding`. [#2501] +- `impl Copy` for `QualityItem` where `T: Copy`. [#2501] +- `Quality::ZERO` equivalent to `q=0`. [#2501] +- `QualityItem::zero` that uses `Quality::ZERO`. [#2501] +- `ContentEncoding::to_header_value()`. [#2501] + +### Changed +- `Quality::MIN` is now the smallest non-zero value. [#2501] +- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501] +- Rename `ContentEncoding::{Br => Brotli}`. [#2501] - Minimum supported Rust version (MSRV) is now 1.54. +### Fixed +- `ContentEncoding::Identity` can now be parsed from a string. [#2501] +- A `Vary` header is now correctly sent along with compressed content. [#2501] + +### Removed +- `ContentEncoding::Auto` variant. [#2501] +- `ContentEncoding::is_compression()`. [#2501] + +[#2501]: https://github.com/actix/actix-web/pull/2501 + ## 3.0.0-beta.17 - 2021-12-27 ### Changes diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 0f519637a..da4b56c6a 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -47,9 +47,9 @@ where pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { #[cfg(feature = "compress-brotli")] - ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new( - Writer::new(), - )))), + ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new( + BrotliDecoder::new(Writer::new()), + ))), #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), @@ -165,7 +165,7 @@ enum ContentDecoder { #[cfg(feature = "compress-gzip")] Gzip(Box>), #[cfg(feature = "compress-brotli")] - Br(Box>), + Brotli(Box>), // We need explicit 'static lifetime here because ZstdDecoder need lifetime // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` #[cfg(feature = "compress-zstd")] @@ -176,7 +176,7 @@ impl ContentDecoder { fn feed_eof(&mut self) -> io::Result> { match self { #[cfg(feature = "compress-brotli")] - ContentDecoder::Br(ref mut decoder) => match decoder.flush() { + ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() { Ok(()) => { let b = decoder.get_mut().take(); @@ -234,7 +234,7 @@ impl ContentDecoder { fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { #[cfg(feature = "compress-brotli")] - ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { + ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; let b = decoder.get_mut().take(); diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index b565bb2b5..f40e0e579 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -56,11 +56,10 @@ impl Encoder { } pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { - let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) + let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT - || encoding == ContentEncoding::Identity - || encoding == ContentEncoding::Auto); + || encoding == ContentEncoding::Identity); // no need to compress an empty body if matches!(body.size(), BodySize::None) { @@ -72,8 +71,8 @@ impl Encoder { Err(body) => EncoderBody::Stream { body }, }; - if can_encode { - // Modify response body only if encoder is set + if should_encode { + // wrap body only if encoder is feature-enabled if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); @@ -252,10 +251,10 @@ where } fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { - head.headers_mut().insert( - header::CONTENT_ENCODING, - HeaderValue::from_static(encoding.as_str()), - ); + head.headers_mut() + .insert(header::CONTENT_ENCODING, encoding.to_header_value()); + head.headers_mut() + .insert(header::VARY, HeaderValue::from_static("accept-encoding")); head.no_chunking(false); } @@ -268,7 +267,7 @@ enum ContentEncoder { Gzip(GzEncoder), #[cfg(feature = "compress-brotli")] - Br(BrotliEncoder), + Brotli(BrotliEncoder), // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. @@ -292,8 +291,8 @@ impl ContentEncoder { ))), #[cfg(feature = "compress-brotli")] - ContentEncoding::Br => { - Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) + ContentEncoding::Brotli => { + Some(ContentEncoder::Brotli(BrotliEncoder::new(Writer::new(), 3))) } #[cfg(feature = "compress-zstd")] @@ -302,7 +301,7 @@ impl ContentEncoder { Some(ContentEncoder::Zstd(encoder)) } - _ => None, + ContentEncoding::Identity => None, } } @@ -310,7 +309,7 @@ impl ContentEncoder { pub(crate) fn take(&mut self) -> Bytes { match *self { #[cfg(feature = "compress-brotli")] - ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(), #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), @@ -326,7 +325,7 @@ impl ContentEncoder { fn finish(self) -> Result { match self { #[cfg(feature = "compress-brotli")] - ContentEncoder::Br(encoder) => match encoder.finish() { + ContentEncoder::Brotli(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, @@ -354,7 +353,7 @@ impl ContentEncoder { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { #[cfg(feature = "compress-brotli")] - ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { + ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { trace!("Error decoding br encoding: {}", err); diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 3d2a918f4..cdf495c45 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -250,6 +250,7 @@ impl From for Response { /// A set of errors that can occur running blocking tasks in thread pool. #[derive(Debug, Display, Error)] #[display(fmt = "Blocking thread pool is gone")] +// TODO: non-exhaustive pub struct BlockingError; /// A set of errors that can occur during payload parsing. diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 478867ed0..5cf4ba2fa 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap { } } +/// Convert `http::HeaderMap` to our `HeaderMap`. +impl From for HeaderMap { + fn from(mut map: http::HeaderMap) -> HeaderMap { + HeaderMap::from_drain(map.drain()) + } +} + /// Iterator over removed, owned values with the same associated name. /// /// Returned from methods that remove or replace items. See [`HeaderMap::insert`] diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index dd4f06106..4e9140db4 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -57,13 +57,6 @@ pub trait Header: TryIntoHeaderValue { fn parse(msg: &M) -> Result; } -/// Convert `http::HeaderMap` to our `HeaderMap`. -impl From for HeaderMap { - fn from(mut map: http::HeaderMap) -> HeaderMap { - HeaderMap::from_drain(map.drain()) - } -} - /// This encode set is used for HTTP header values and is defined at /// . pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index 68511a8ee..ce011f107 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -20,14 +20,16 @@ pub struct ContentEncodingParseError; /// See [IANA HTTP Content Coding Registry]. /// /// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum ContentEncoding { - /// Automatically select encoding based on encoding negotiation. - Auto, + /// Indicates the no-op identity encoding. + /// + /// I.e., no compression or modification. + Identity, /// A format using the Brotli algorithm. - Br, + Brotli, /// A format using the zlib structure with deflate algorithm. Deflate, @@ -37,27 +39,30 @@ pub enum ContentEncoding { /// Zstd algorithm. Zstd, - - /// Indicates the identity function (i.e. no compression, nor modification). - Identity, } impl ContentEncoding { - /// Is the content compressed? - #[inline] - pub const fn is_compression(self) -> bool { - matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) - } - /// Convert content encoding to string. #[inline] pub const fn as_str(self) -> &'static str { match self { - ContentEncoding::Br => "br", + ContentEncoding::Brotli => "br", ContentEncoding::Gzip => "gzip", ContentEncoding::Deflate => "deflate", ContentEncoding::Zstd => "zstd", - ContentEncoding::Identity | ContentEncoding::Auto => "identity", + ContentEncoding::Identity => "identity", + } + } + + /// Convert content encoding to header value. + #[inline] + pub const fn to_header_value(self) -> HeaderValue { + match self { + ContentEncoding::Brotli => HeaderValue::from_static("br"), + ContentEncoding::Gzip => HeaderValue::from_static("gzip"), + ContentEncoding::Deflate => HeaderValue::from_static("deflate"), + ContentEncoding::Zstd => HeaderValue::from_static("zstd"), + ContentEncoding::Identity => HeaderValue::from_static("identity"), } } } @@ -71,16 +76,18 @@ impl Default for ContentEncoding { impl FromStr for ContentEncoding { type Err = ContentEncodingParseError; - fn from_str(val: &str) -> Result { - let val = val.trim(); + fn from_str(enc: &str) -> Result { + let enc = enc.trim(); - if val.eq_ignore_ascii_case("br") { - Ok(ContentEncoding::Br) - } else if val.eq_ignore_ascii_case("gzip") { + if enc.eq_ignore_ascii_case("br") { + Ok(ContentEncoding::Brotli) + } else if enc.eq_ignore_ascii_case("gzip") { Ok(ContentEncoding::Gzip) - } else if val.eq_ignore_ascii_case("deflate") { + } else if enc.eq_ignore_ascii_case("deflate") { Ok(ContentEncoding::Deflate) - } else if val.eq_ignore_ascii_case("zstd") { + } else if enc.eq_ignore_ascii_case("identity") { + Ok(ContentEncoding::Identity) + } else if enc.eq_ignore_ascii_case("zstd") { Ok(ContentEncoding::Zstd) } else { Err(ContentEncodingParseError) diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs index c2f08edc2..c80dd0a8e 100644 --- a/actix-http/src/header/shared/quality.rs +++ b/actix-http/src/header/shared/quality.rs @@ -27,7 +27,8 @@ const MAX_QUALITY_FLOAT: f32 = 1.0; /// /// assert_eq!(q(0.42).to_string(), "0.42"); /// assert_eq!(q(1.0).to_string(), "1"); -/// assert_eq!(Quality::MIN.to_string(), "0"); +/// assert_eq!(Quality::MIN.to_string(), "0.001"); +/// assert_eq!(Quality::ZERO.to_string(), "0"); /// ``` /// /// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1 @@ -38,8 +39,11 @@ impl Quality { /// The maximum quality value, equivalent to `q=1.0`. pub const MAX: Quality = Quality(MAX_QUALITY_INT); - /// The minimum quality value, equivalent to `q=0.0`. - pub const MIN: Quality = Quality(0); + /// The minimum, non-zero quality value, equivalent to `q=0.001`. + pub const MIN: Quality = Quality(1); + + /// The zero quality value, equivalent to `q=0.0`. + pub const ZERO: Quality = Quality(0); /// Converts a float in the range 0.0–1.0 to a `Quality`. /// @@ -51,7 +55,7 @@ impl Quality { // Check that `value` is within range should be done before calling this method. // Just in case, this debug_assert should catch if we were forgetful. debug_assert!( - (0.0f32..=1.0f32).contains(&value), + (0.0..=MAX_QUALITY_FLOAT).contains(&value), "q value must be between 0.0 and 1.0" ); @@ -154,10 +158,13 @@ impl TryFrom for Quality { /// let q1 = q(1.0); /// assert_eq!(q1, Quality::MAX); /// -/// let q2 = q(0.0); +/// let q2 = q(0.001); /// assert_eq!(q2, Quality::MIN); /// -/// let q3 = q(0.42); +/// let q3 = q(0.0); +/// assert_eq!(q3, Quality::ZERO); +/// +/// let q4 = q(0.42); /// ``` /// /// An out-of-range `f32` quality will panic. @@ -185,6 +192,10 @@ mod tests { #[test] fn display_output() { + assert_eq!(Quality::ZERO.to_string(), "0"); + assert_eq!(Quality::MIN.to_string(), "0.001"); + assert_eq!(Quality::MAX.to_string(), "1"); + assert_eq!(q(0.0).to_string(), "0"); assert_eq!(q(1.0).to_string(), "1"); assert_eq!(q(0.001).to_string(), "0.001"); diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index c9eee7d9d..71a3bdd53 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -31,7 +31,7 @@ use super::Quality; /// let q_item_fallback: QualityItem = "abc;q=0.1".parse().unwrap(); /// assert!(q_item > q_item_fallback); /// ``` -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct QualityItem { /// The wrapped contents of the field. pub item: T, @@ -53,10 +53,15 @@ impl QualityItem { Self::new(item, Quality::MAX) } - /// Constructs a new `QualityItem` from an item, using the minimum q-value. + /// Constructs a new `QualityItem` from an item, using the minimum, non-zero q-value. pub fn min(item: T) -> Self { Self::new(item, Quality::MIN) } + + /// Constructs a new `QualityItem` from an item, using zero q-value of zero. + pub fn zero(item: T) -> Self { + Self::new(item, Quality::ZERO) + } } impl PartialOrd for QualityItem { @@ -73,7 +78,10 @@ impl fmt::Display for QualityItem { // q-factor value is implied for max value Quality::MAX => Ok(()), - Quality::MIN => f.write_str("; q=0"), + // fast path for zero + Quality::ZERO => f.write_str("; q=0"), + + // quality formatting is already using itoa q => write!(f, "; q={}", q), } } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index c453a768d..6eb6800d3 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -473,7 +473,7 @@ async fn test_no_decompress() { .wrap(actix_web::middleware::Compress::default()) .service(web::resource("/").route(web::to(|| { let mut res = HttpResponse::Ok().body(STR); - res.encoding(header::ContentEncoding::Gzip); + res.encode_with(header::ContentEncoding::Gzip); res }))) }); @@ -644,7 +644,9 @@ async fn test_client_brotli_encoding_large_random() { async fn test_client_deflate_encoding() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| { - HttpResponse::Ok().encoding(ContentEncoding::Br).body(body) + HttpResponse::Ok() + .encode_with(ContentEncoding::Brotli) + .body(body) })) }); @@ -667,7 +669,9 @@ async fn test_client_deflate_encoding_large_random() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| { - HttpResponse::Ok().encoding(ContentEncoding::Br).body(body) + HttpResponse::Ok() + .encode_with(ContentEncoding::Brotli) + .body(body) })) }); @@ -685,7 +689,7 @@ async fn test_client_streaming_explicit() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: web::Payload| { HttpResponse::Ok() - .encoding(ContentEncoding::Identity) + .encode_with(ContentEncoding::Identity) .streaming(body) })) }); @@ -710,7 +714,7 @@ async fn test_body_streaming_implicit() { }); HttpResponse::Ok() - .encoding(ContentEncoding::Gzip) + .encode_with(ContentEncoding::Gzip) .streaming(Box::pin(body)) })) }); diff --git a/scripts/ci-test b/scripts/ci-test index 3ab229665..567012d33 100755 --- a/scripts/ci-test +++ b/scripts/ci-test @@ -12,16 +12,16 @@ save_exit_code() { [ "$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 --lib --tests -p=actix-router --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls +save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture +save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture save_exit_code cargo test --workspace --doc diff --git a/src/dev.rs b/src/dev.rs index bb1385bde..b15d15747 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -20,11 +20,9 @@ pub use crate::info::{ConnectionInfo, PeerAddr}; pub use crate::rmap::ResourceMap; pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; -pub use crate::types::form::UrlEncoded; -pub use crate::types::json::JsonBody; -pub use crate::types::readlines::Readlines; +pub use crate::types::{JsonBody, Readlines, UrlEncoded}; -use crate::http::header::ContentEncoding; +use crate::{http::header::ContentEncoding, HttpMessage as _}; use actix_router::Patterns; @@ -47,59 +45,109 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { patterns } -/// Helper trait that allows to set specific encoding for response. +/// Helper trait for managing response encoding. +/// +/// Use `pre_encoded_with` to flag response as already encoded. For example, when serving a Gzip +/// compressed file from disk. pub trait BodyEncoding { /// Get content encoding - fn get_encoding(&self) -> Option; + fn preferred_encoding(&self) -> Option; - /// Set content encoding + /// Set content encoding to use. /// - /// Must be used with [`crate::middleware::Compress`] to take effect. - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; + /// Must be used with [`Compress`] to take effect. + /// + /// [`Compress`]: crate::middleware::Compress + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self; + + // /// Flags that a file already is encoded so that [`Compress`] does not modify it. + // /// + // /// Effectively a shortcut for `compress_with("identity")` + // /// plus `insert_header(ContentEncoding, encoding)`. + // /// + // /// [`Compress`]: crate::middleware::Compress + // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self; } -impl BodyEncoding for actix_http::ResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } +struct CompressWith(ContentEncoding); - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } -} - -struct Enc(ContentEncoding); - -impl BodyEncoding for actix_http::Response { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } -} +// TODO: add or delete this +// struct PreCompressed(ContentEncoding); impl BodyEncoding for crate::HttpResponseBuilder { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) + fn preferred_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) } - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(CompressWith(encoding)); self } + + // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { + // self.extensions_mut().insert(PreCompressed(encoding)); + // self + // } } impl BodyEncoding for crate::HttpResponse { - fn get_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) + fn preferred_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) } - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(CompressWith(encoding)); + self + } + + // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { + // self.extensions_mut().insert(PreCompressed(encoding)); + // self + // } +} + +impl BodyEncoding for ServiceResponse { + fn preferred_encoding(&self) -> Option { + self.request() + .extensions() + .get::() + .map(|enc| enc.0) + } + + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { + self.request() + .extensions_mut() + .insert(CompressWith(encoding)); + self + } + + // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { + // self.request() + // .extensions_mut() + // .insert(PreCompressed(encoding)); + // self + // } +} + +// TODO: remove these impls ? +impl BodyEncoding for actix_http::ResponseBuilder { + fn preferred_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(CompressWith(encoding)); + self + } +} + +impl BodyEncoding for actix_http::Response { + fn preferred_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(CompressWith(encoding)); self } } diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index c61e6ab49..368a05bb2 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -147,6 +147,39 @@ impl Accept { Accept(vec![QualityItem::max(mime::TEXT_HTML)]) } + // TODO: method for getting best content encoding based on q-factors, available from server side + // and if none are acceptable return None + + /// Extracts the most preferable mime type, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first mime type is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained + /// list is empty. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Mime { + use actix_http::header::Quality; + + let mut max_item = None; + let mut max_pref = Quality::ZERO; + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(mime::STAR_STAR) + } + /// Returns a sorted list of mime types from highest to lowest preference, accounting for /// [q-factor weighting] and specificity. /// @@ -196,36 +229,6 @@ impl Accept { types.into_iter().map(|qitem| qitem.item).collect() } - - /// Extracts the most preferable mime type, accounting for [q-factor weighting]. - /// - /// If no q-factors are provided, the first mime type is chosen. Note that items without - /// q-factors are given the maximum preference value. - /// - /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained - /// list is empty. - /// - /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 - pub fn preference(&self) -> Mime { - use actix_http::header::Quality; - - let mut max_item = None; - let mut max_pref = Quality::MIN; - - // uses manual max lookup loop since we want the first occurrence in the case of same - // preference but `Iterator::max_by_key` would give us the last occurrence - - for pref in &self.0 { - // only change if strictly greater - // equal items, even while unsorted, still have higher preference if they appear first - if pref.quality > max_pref { - max_pref = pref.quality; - max_item = Some(pref.item.clone()); - } - } - - max_item.unwrap_or(mime::STAR_STAR) - } } #[cfg(test)] @@ -239,7 +242,7 @@ mod tests { assert!(test.ranked().is_empty()); let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]); - assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); + assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]); let test = Accept(vec![ QualityItem::max(mime::TEXT_HTML), diff --git a/src/http/header/accept_charset.rs b/src/http/header/accept_charset.rs index c8b918c91..c7f7e1a68 100644 --- a/src/http/header/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -1,8 +1,7 @@ -use super::{Charset, QualityItem, ACCEPT_CHARSET}; +use super::{common_header, Charset, QualityItem, ACCEPT_CHARSET}; -crate::http::header::common_header! { - /// `Accept-Charset` header, defined in - /// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3) +common_header! { + /// `Accept-Charset` header, defined in [RFC 7231 §5.3.3]. /// /// The `Accept-Charset` header field can be sent by a user agent to /// indicate what charsets are acceptable in textual response content. @@ -52,10 +51,12 @@ crate::http::header::common_header! { /// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))]) /// ); /// ``` - (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ + /// + /// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3 + (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)* test_parse_and_format { // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/src/http/header/accept_encoding.rs b/src/http/header/accept_encoding.rs index 828a0533c..8c35179b6 100644 --- a/src/http/header/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -1,17 +1,15 @@ -use actix_http::header::QualityItem; +use std::collections::HashSet; -use super::{common_header, Encoding}; +use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem}; use crate::http::header; common_header! { /// `Accept-Encoding` header, defined /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// - /// The `Accept-Encoding` header field can be used by user agents to - /// indicate what response content-codings are - /// acceptable in the response. An `identity` token is used as a synonym - /// for "no encoding" in order to communicate when no encoding is - /// preferred. + /// The `Accept-Encoding` header field can be used by user agents to indicate what response + /// content-codings are acceptable in the response. An `identity` token is used as a synonym + /// for "no encoding" in order to communicate when no encoding is preferred. /// /// # ABNF /// ```plain @@ -29,11 +27,11 @@ common_header! { /// # Examples /// ``` /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem}; + /// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem}; /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)]) + /// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))]) /// ); /// ``` /// @@ -44,40 +42,388 @@ common_header! { /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptEncoding(vec![ - /// QualityItem::max(Encoding::Chunked), - /// QualityItem::max(Encoding::Gzip), - /// QualityItem::max(Encoding::Deflate), + /// "gzip".parse().unwrap(), + /// "br".parse().unwrap(), /// ]) /// ); /// ``` - /// - /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q}; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// AcceptEncoding(vec![ - /// QualityItem::max(Encoding::Chunked), - /// QualityItem::new(Encoding::Gzip, q(0.60)), - /// QualityItem::min(Encoding::EncodingExt("*".to_owned())), - /// ]) - /// ); - /// ``` - (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem)* + (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem>)* test_parse_and_format { - // From the RFC - common_header_test!(test1, vec![b"compress, gzip"]); - common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - common_header_test!(test3, vec![b"*"]); + common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![]))); + common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![]))); + + common_header_test!( + order_of_appearance, + vec![b"br, gzip"], + Some(AcceptEncoding(vec![ + QualityItem::max(Preference::Specific(Encoding::brotli())), + QualityItem::max(Preference::Specific(Encoding::gzip())), + ])) + ); + + common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![ + QualityItem::max(Preference::Any), + ]))); // Note: Removed quality 1 from gzip - common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]); // Note: Removed quality 1 from gzip - common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]); + + common_header_test!( + only_gzip_no_identity, + vec![b"gzip, *; q=0"], + Some(AcceptEncoding(vec![ + QualityItem::max(Preference::Specific(Encoding::gzip())), + QualityItem::zero(Preference::Any), + ])) + ); } } -// TODO: shortcut for EncodingExt(*) = Any +impl AcceptEncoding { + /// Selects the most acceptable encoding according to client preference and supported types. + /// + /// The "identity" encoding is not assumed and should be included in the `supported` iterator + /// if a non-encoded representation can be selected. + /// + /// If `None` is returned, this indicates that none of the supported encodings are acceptable to + /// the client. The caller should generate a 406 Not Acceptable response (unencoded) that + /// includes the server's supported encodings in the body plus a [`Vary`] header. + /// + /// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary + pub fn negotiate<'a>( + &self, + supported: impl Iterator, + ) -> Option { + // 1. If no Accept-Encoding field is in the request, any content-coding is considered + // acceptable by the user agent. + + let supported_set = supported.collect::>(); + + if supported_set.is_empty() { + return None; + } + + if self.0.is_empty() { + // though it is not recommended to encode in this case, return identity encoding + return Some(Encoding::identity()); + } + + // 2. If the representation has no content-coding, then it is acceptable by default unless + // specifically excluded by the Accept-Encoding field stating either "identity;q=0" or + // "*;q=0" without a more specific entry for "identity". + + let acceptable_items = self.ranked_items().collect::>(); + + let identity_acceptable = is_identity_acceptable(&acceptable_items); + let identity_supported = supported_set.contains(&Encoding::identity()); + + if identity_acceptable && identity_supported && supported_set.len() == 1 { + return Some(Encoding::identity()); + } + + // 3. If the representation's content-coding is one of the content-codings listed in the + // Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0. + + // 4. If multiple content-codings are acceptable, then the acceptable content-coding with + // the highest non-zero qvalue is preferred. + + let matched = acceptable_items + .into_iter() + .filter(|q| q.quality > Quality::ZERO) + // search relies on item list being in descending order of quality + .find(|q| { + let enc = &q.item; + matches!(enc, Preference::Specific(enc) if supported_set.contains(enc)) + }) + .map(|q| q.item); + + match matched { + Some(Preference::Specific(enc)) => Some(enc), + + _ if identity_acceptable => Some(Encoding::identity()), + + _ => None, + } + } + + /// Extracts the most preferable encoding, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first encoding is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is + /// returned, it is recommended to use an un-encoded representation. + /// + /// If `None` is returned, it means that the client has signalled that no representations + /// are acceptable. This should never occur for a well behaved user-agent. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Option> { + // empty header indicates no preference + if self.0.is_empty() { + return Some(Preference::Any); + } + + let mut max_item = None; + let mut max_pref = Quality::ZERO; + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + // Return max_item if any items were above 0 quality... + max_item.or_else(|| { + // ...or else check for "*" or "identity". We can elide quality checks since + // entering this block means all items had "q=0". + match self.0.iter().find(|pref| { + matches!( + pref.item, + Preference::Any + | Preference::Specific(Encoding::Known(ContentEncoding::Identity)) + ) + }) { + // "identity" or "*" found so no representation is acceptable + Some(_) => None, + + // implicit "identity" is acceptable + None => Some(Preference::Specific(Encoding::identity())), + } + }) + } + + /// Returns a sorted list of encodings from highest to lowest precedence, accounting + /// for [q-factor weighting]. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn ranked(&self) -> Vec> { + self.ranked_items().map(|q| q.item).collect() + } + + fn ranked_items(&self) -> impl Iterator>> { + if self.0.is_empty() { + return vec![].into_iter(); + } + + let mut types = self.0.clone(); + + // use stable sort so items with equal q-factor retain listed order + types.sort_by(|a, b| { + // sort by q-factor descending + b.quality.cmp(&a.quality) + }); + + types.into_iter() + } +} + +/// Returns true if "identity" is an acceptable encoding. +/// +/// Internal algorithm relies on item list being in descending order of quality. +fn is_identity_acceptable(items: &'_ [QualityItem>]) -> bool { + if items.is_empty() { + return true; + } + + // Loop algorithm depends on items being sorted in descending order of quality. As such, it + // is sufficient to return (q > 0) when reaching either an "identity" or "*" item. + for q in items { + match (q.quality, &q.item) { + // occurrence of "identity;q=n"; return true if quality is non-zero + (q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => { + return q > Quality::ZERO + } + + // occurrence of "*;q=n"; return true if quality is non-zero + (q, Preference::Any) => return q > Quality::ZERO, + + _ => {} + } + } + + // implicit acceptable identity + true +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header::*; + + macro_rules! accept_encoding { + () => { AcceptEncoding(vec![]) }; + ($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) }; + } + + /// Parses an encoding string. + fn enc(enc: &str) -> Preference { + enc.parse().unwrap() + } + + #[test] + fn detect_identity_acceptable() { + macro_rules! accept_encoding_ranked { + () => { accept_encoding!().ranked_items().collect::>() }; + ($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::>() }; + } + + let test = accept_encoding_ranked!(); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip"); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "br"); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "*;q=0.1"); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "identity;q=0.1"); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0"); + assert!(is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1"); + assert!(is_identity_acceptable(&test)); + + let test = accept_encoding_ranked!("gzip", "*;q=0"); + assert!(!is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "identity;q=0"); + assert!(!is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0"); + assert!(!is_identity_acceptable(&test)); + let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0"); + assert!(!is_identity_acceptable(&test)); + } + + #[test] + fn encoding_negotiation() { + // no preference + let test = accept_encoding!(); + assert_eq!(test.negotiate([].iter()), None); + + let test = accept_encoding!(); + assert_eq!( + test.negotiate([Encoding::identity()].iter()), + Some(Encoding::identity()), + ); + + let test = accept_encoding!("identity;q=0"); + assert_eq!(test.negotiate([Encoding::identity()].iter()), None); + + let test = accept_encoding!("*;q=0"); + assert_eq!(test.negotiate([Encoding::identity()].iter()), None); + + let test = accept_encoding!(); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::identity()), + ); + + let test = accept_encoding!("gzip"); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()), + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), + Some(Encoding::identity()), + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()), + ); + + let test = accept_encoding!("gzip", "identity;q=0"); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()), + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), + None + ); + + let test = accept_encoding!("gzip", "*;q=0"); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()), + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), + None + ); + + let test = accept_encoding!("gzip", "deflate", "br"); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()), + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::identity()].iter()), + Some(Encoding::brotli()) + ); + assert_eq!( + test.negotiate([Encoding::deflate(), Encoding::identity()].iter()), + Some(Encoding::deflate()) + ); + assert_eq!( + test.negotiate( + [Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter() + ), + Some(Encoding::gzip()) + ); + assert_eq!( + test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()), + Some(Encoding::gzip()) + ); + assert_eq!( + test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()), + Some(Encoding::gzip()) + ); + } + + #[test] + fn ranking_precedence() { + let test = accept_encoding!(); + assert!(test.ranked().is_empty()); + + let test = accept_encoding!("gzip"); + assert_eq!(test.ranked(), vec![enc("gzip")]); + + let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0"); + assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); + + let test = accept_encoding!("br", "gzip", "*"); + assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); + } + + #[test] + fn preference_selection() { + assert_eq!(accept_encoding!().preference(), Some(Preference::Any)); + + assert_eq!(accept_encoding!("identity;q=0").preference(), None); + assert_eq!(accept_encoding!("*;q=0").preference(), None); + assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None); + assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None); + + let test = accept_encoding!("*;q=0.5"); + assert_eq!(test.preference().unwrap(), enc("*")); + + let test = accept_encoding!("br;q=0"); + assert_eq!(test.preference().unwrap(), enc("identity")); + + let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500"); + assert_eq!(test.preference().unwrap(), enc("gzip")); + + let test = accept_encoding!("br", "gzip", "*"); + assert_eq!(test.preference().unwrap(), enc("br")); + } +} diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs index 011257b87..9943e121f 100644 --- a/src/http/header/accept_language.rs +++ b/src/http/header/accept_language.rs @@ -37,7 +37,7 @@ common_header! { /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// QualityItem::max("en-US".parse().unwrap()) + /// "en-US".parse().unwrap(), /// ]) /// ); /// ``` @@ -49,9 +49,9 @@ common_header! { /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptLanguage(vec![ - /// QualityItem::max("da".parse().unwrap()), - /// QualityItem::new("en-GB".parse().unwrap(), q(0.8)), - /// QualityItem::new("en".parse().unwrap(), q(0.7)), + /// "da".parse().unwrap(), + /// "en-GB;q=0.8".parse().unwrap(), + /// "en;q=0.7".parse().unwrap(), /// ]) /// ); /// ``` @@ -93,6 +93,33 @@ common_header! { } impl AcceptLanguage { + /// Extracts the most preferable language, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first language is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// As per the spec, returns [`Preference::Any`] if contained list is empty. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Preference { + let mut max_item = None; + let mut max_pref = Quality::ZERO; + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(Preference::Any) + } + /// Returns a sorted list of languages from highest to lowest precedence, accounting /// for [q-factor weighting]. /// @@ -112,33 +139,6 @@ impl AcceptLanguage { types.into_iter().map(|qitem| qitem.item).collect() } - - /// Extracts the most preferable language, accounting for [q-factor weighting]. - /// - /// If no q-factors are provided, the first language is chosen. Note that items without - /// q-factors are given the maximum preference value. - /// - /// As per the spec, returns [`Preference::Any`] if contained list is empty. - /// - /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 - pub fn preference(&self) -> Preference { - let mut max_item = None; - let mut max_pref = Quality::MIN; - - // uses manual max lookup loop since we want the first occurrence in the case of same - // preference but `Iterator::max_by_key` would give us the last occurrence - - for pref in &self.0 { - // only change if strictly greater - // equal items, even while unsorted, still have higher preference if they appear first - if pref.quality > max_pref { - max_pref = pref.quality; - max_item = Some(pref.item.clone()); - } - } - - max_item.unwrap_or(Preference::Any) - } } #[cfg(test)] @@ -152,7 +152,7 @@ mod tests { assert!(test.ranked().is_empty()); let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]); - assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); + assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]); let test = AcceptLanguage(vec![ QualityItem::new("fr".parse().unwrap(), q(0.900)), diff --git a/src/http/header/content_disposition.rs b/src/http/header/content_disposition.rs index 26a9d8e76..8b7101aa1 100644 --- a/src/http/header/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -301,7 +301,6 @@ impl DispositionParam { /// change to match local file system conventions if applicable, and do not use directory path /// information that may be present. /// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). -// TODO: think about using private fields and smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type diff --git a/src/http/header/encoding.rs b/src/http/header/encoding.rs index a61edda67..ab11f50be 100644 --- a/src/http/header/encoding.rs +++ b/src/http/header/encoding.rs @@ -1,69 +1,55 @@ use std::{fmt, str}; -pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd, -}; +use actix_http::ContentEncoding; -/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header. -#[derive(Debug, Clone, PartialEq, Eq)] +/// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Encoding { - /// The `chunked` encoding. - Chunked, + /// A supported content encoding. See [`ContentEncoding`] for variants. + Known(ContentEncoding), - /// The `br` encoding. - Brotli, + /// Some other encoding that is less common, can be any string. + Unknown(String), +} - /// The `gzip` encoding. - Gzip, +impl Encoding { + pub const fn identity() -> Self { + Self::Known(ContentEncoding::Identity) + } - /// The `deflate` encoding. - Deflate, + pub const fn brotli() -> Self { + Self::Known(ContentEncoding::Brotli) + } - /// The `compress` encoding. - Compress, + pub const fn deflate() -> Self { + Self::Known(ContentEncoding::Deflate) + } - /// The `identity` encoding. - Identity, + pub const fn gzip() -> Self { + Self::Known(ContentEncoding::Gzip) + } - /// The `trailers` encoding. - Trailers, - - /// The `zstd` encoding. - Zstd, - - /// Some other encoding that is less common, can be any String. - EncodingExt(String), + pub const fn zstd() -> Self { + Self::Known(ContentEncoding::Zstd) + } } impl fmt::Display for Encoding { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match *self { - Chunked => "chunked", - Brotli => "br", - Gzip => "gzip", - Deflate => "deflate", - Compress => "compress", - Identity => "identity", - Trailers => "trailers", - Zstd => "zstd", - EncodingExt(ref s) => s.as_ref(), + f.write_str(match self { + Encoding::Known(enc) => enc.as_str(), + Encoding::Unknown(enc) => enc.as_str(), }) } } impl str::FromStr for Encoding { type Err = crate::error::ParseError; - fn from_str(s: &str) -> Result { - match s { - "chunked" => Ok(Chunked), - "br" => Ok(Brotli), - "deflate" => Ok(Deflate), - "gzip" => Ok(Gzip), - "compress" => Ok(Compress), - "identity" => Ok(Identity), - "trailers" => Ok(Trailers), - "zstd" => Ok(Zstd), - _ => Ok(EncodingExt(s.to_owned())), + + fn from_str(enc: &str) -> Result { + match enc.parse::() { + Ok(enc) => Ok(Self::Known(enc)), + Err(_) => Ok(Self::Unknown(enc.to_owned())), } } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index d3cdf5763..3a0d5630d 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -1,20 +1,13 @@ //! For middleware documentation, see [`Compress`]. use std::{ - cmp, - convert::TryFrom as _, future::Future, marker::PhantomData, pin::Pin, task::{Context, Poll}, }; -use actix_http::{ - body::{EitherBody, MessageBody}, - encoding::Encoder, - header::{ContentEncoding, ACCEPT_ENCODING}, - StatusCode, -}; +use actix_http::encoding::Encoder; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; @@ -22,9 +15,14 @@ use once_cell::sync::Lazy; use pin_project_lite::pin_project; use crate::{ - dev::BodyEncoding, + body::{EitherBody, MessageBody}, + dev::BodyEncoding as _, + http::{ + header::{self, AcceptEncoding, Encoding, HeaderValue}, + StatusCode, + }, service::{ServiceRequest, ServiceResponse}, - Error, HttpResponse, + Error, HttpMessage, HttpResponse, }; /// Middleware for compressing response payloads. @@ -40,21 +38,9 @@ use crate::{ /// .wrap(middleware::Compress::default()) /// .default_service(web::to(|| HttpResponse::NotFound())); /// ``` -#[derive(Debug, Clone)] -pub struct Compress(ContentEncoding); - -impl Compress { - /// Create new `Compress` middleware with the specified encoding. - pub fn new(encoding: ContentEncoding) -> Self { - Compress(encoding) - } -} - -impl Default for Compress { - fn default() -> Self { - Compress::new(ContentEncoding::Auto) - } -} +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct Compress; impl Transform for Compress where @@ -68,44 +54,14 @@ where type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { - ok(CompressMiddleware { - service, - encoding: self.0, - }) + ok(CompressMiddleware { service }) } } pub struct CompressMiddleware { service: S, - encoding: ContentEncoding, } -static SUPPORTED_ALGORITHM_NAMES: Lazy = Lazy::new(|| { - #[allow(unused_mut)] // only unused when no compress features enabled - let mut encoding: Vec<&str> = vec![]; - - #[cfg(feature = "compress-brotli")] - { - encoding.push("br"); - } - - #[cfg(feature = "compress-gzip")] - { - encoding.push("gzip"); - encoding.push("deflate"); - } - - #[cfg(feature = "compress-zstd")] - encoding.push("zstd"); - - assert!( - !encoding.is_empty(), - "encoding can not be empty unless __compress feature has been explicitly enabled by itself" - ); - - encoding.join(", ") -}); - impl Service for CompressMiddleware where S: Service, Error = Error>, @@ -121,39 +77,43 @@ where #[allow(clippy::borrow_interior_mutable_const)] fn call(&self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding - let encoding_result = req - .headers() - .get(&ACCEPT_ENCODING) - .and_then(|val| val.to_str().ok()) - .map(|enc| AcceptEncoding::try_parse(enc, self.encoding)); + let accept_encoding = req.get_header::(); - match encoding_result { - // Missing header => fallback to identity - None => Either::left(CompressResponse { - encoding: ContentEncoding::Identity, - fut: self.service.call(req), - _phantom: PhantomData, - }), + let accept_encoding = match accept_encoding { + // missing header; fallback to identity + None => { + return Either::left(CompressResponse { + encoding: Encoding::identity(), + fut: self.service.call(req), + _phantom: PhantomData, + }) + } - // Valid encoding - Some(Ok(encoding)) => Either::left(CompressResponse { - encoding, - fut: self.service.call(req), - _phantom: PhantomData, - }), + // valid accept-encoding header + Some(accept_encoding) => accept_encoding, + }; - // There is an HTTP header but we cannot match what client as asked for - Some(Err(_)) => { - let res = HttpResponse::with_body( + match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) { + None => { + let mut res = HttpResponse::with_body( StatusCode::NOT_ACCEPTABLE, - SUPPORTED_ALGORITHM_NAMES.clone(), + SUPPORTED_ENCODINGS_STRING.as_str(), ); + res.headers_mut() + .insert(header::VARY, HeaderValue::from_static("Accept-Encoding")); + Either::right(ok(req .into_response(res) .map_into_boxed_body() .map_into_right_body())) } + + Some(encoding) => Either::left(CompressResponse { + fut: self.service.call(req), + encoding, + _phantom: PhantomData, + }), } } } @@ -165,7 +125,7 @@ pin_project! { { #[pin] fut: S::Future, - encoding: ContentEncoding, + encoding: Encoding, _phantom: PhantomData, } } @@ -182,10 +142,15 @@ where match ready!(this.fut.poll(cx)) { Ok(resp) => { - let enc = if let Some(enc) = resp.response().get_encoding() { + let enc = if let Some(enc) = resp.response().preferred_encoding() { enc } else { - *this.encoding + match this.encoding { + Encoding::Known(enc) => *enc, + Encoding::Unknown(enc) => { + unimplemented!("encoding {} should not be here", enc); + } + } }; Poll::Ready(Ok(resp.map_body(move |head, body| { @@ -198,178 +163,57 @@ where } } -struct AcceptEncoding { - encoding: ContentEncoding, - // TODO: use Quality or QualityItem - quality: f64, -} +static SUPPORTED_ENCODINGS_STRING: Lazy = Lazy::new(|| { + #[allow(unused_mut)] // only unused when no compress features enabled + let mut encoding: Vec<&str> = vec![]; -impl Eq for AcceptEncoding {} - -impl Ord for AcceptEncoding { - #[allow(clippy::comparison_chain)] - fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering { - if self.quality > other.quality { - cmp::Ordering::Less - } else if self.quality < other.quality { - cmp::Ordering::Greater - } else { - cmp::Ordering::Equal - } - } -} - -impl PartialOrd for AcceptEncoding { - fn partial_cmp(&self, other: &AcceptEncoding) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for AcceptEncoding { - fn eq(&self, other: &AcceptEncoding) -> bool { - self.encoding == other.encoding && self.quality == other.quality - } -} - -/// Parse q-factor from quality strings. -/// -/// If parse fail, then fallback to default value which is 1. -/// More details available here: -fn parse_quality(parts: &[&str]) -> f64 { - for part in parts { - if part.trim().starts_with("q=") { - return part[2..].parse().unwrap_or(1.0); - } + #[cfg(feature = "compress-brotli")] + { + encoding.push("br"); } - 1.0 -} - -#[derive(Debug, PartialEq, Eq)] -enum AcceptEncodingError { - /// This error occurs when client only support compressed response and server do not have any - /// algorithm that match client accepted algorithms. - CompressionAlgorithmMismatch, -} - -impl AcceptEncoding { - fn new(tag: &str) -> Option { - let parts: Vec<&str> = tag.split(';').collect(); - let encoding = match parts.len() { - 0 => return None, - _ => match ContentEncoding::try_from(parts[0]) { - Err(_) => return None, - Ok(x) => x, - }, - }; - - let quality = parse_quality(&parts[1..]); - if quality <= 0.0 || quality > 1.0 { - return None; - } - - Some(AcceptEncoding { encoding, quality }) + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); } - /// Parse a raw Accept-Encoding header value into an ordered list then return the best match - /// based on middleware configuration. - pub fn try_parse( - raw: &str, - encoding: ContentEncoding, - ) -> Result { - let mut encodings = raw - .replace(' ', "") - .split(',') - .filter_map(AcceptEncoding::new) - .collect::>(); - - encodings.sort(); - - for enc in encodings { - if encoding == ContentEncoding::Auto || encoding == enc.encoding { - return Ok(enc.encoding); - } - } - - // Special case if user cannot accept uncompressed data. - // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding - // TODO: account for whitespace - if raw.contains("*;q=0") || raw.contains("identity;q=0") { - return Err(AcceptEncodingError::CompressionAlgorithmMismatch); - } - - Ok(ContentEncoding::Identity) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - macro_rules! assert_parse_eq { - ($raw:expr, $result:expr) => { - assert_eq!( - AcceptEncoding::try_parse($raw, ContentEncoding::Auto), - Ok($result) - ); - }; + #[cfg(feature = "compress-zstd")] + { + encoding.push("zstd"); } - macro_rules! assert_parse_fail { - ($raw:expr) => { - assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); - }; + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled by itself" + ); + + encoding.join(", ") +}); + +static SUPPORTED_ENCODINGS: Lazy> = Lazy::new(|| { + let mut encodings = vec![Encoding::identity()]; + + #[cfg(feature = "compress-brotli")] + { + encodings.push(Encoding::brotli()); } - #[test] - fn test_parse_encoding() { - // Test simple case - assert_parse_eq!("br", ContentEncoding::Br); - assert_parse_eq!("gzip", ContentEncoding::Gzip); - assert_parse_eq!("deflate", ContentEncoding::Deflate); - assert_parse_eq!("zstd", ContentEncoding::Zstd); - - // Test space, trim, missing values - assert_parse_eq!("br,,,,", ContentEncoding::Br); - assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip); - - // Test float number parsing - assert_parse_eq!("br;q=1 ,", ContentEncoding::Br); - assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br); - - // Test wildcard - assert_parse_eq!("*", ContentEncoding::Identity); - assert_parse_eq!("*;q=1.0", ContentEncoding::Identity); + #[cfg(feature = "compress-gzip")] + { + encodings.push(Encoding::gzip()); + encodings.push(Encoding::deflate()); } - #[test] - fn test_parse_encoding_qfactor_ordering() { - assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip); - assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd); - - assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br); - assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip); + #[cfg(feature = "compress-zstd")] + { + encodings.push(Encoding::zstd()); } - #[test] - fn test_parse_encoding_qfactor_invalid() { - // Out of range - assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity); - assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity); + assert!( + !encodings.is_empty(), + "encodings can not be empty unless __compress feature has been explicitly enabled by itself" + ); - // Disabled - assert_parse_eq!("gzip;q=0", ContentEncoding::Identity); - } - - #[test] - fn test_parse_compression_required() { - // Check we fallback to identity if there is an unsupported compression algorithm - assert_parse_eq!("compress", ContentEncoding::Identity); - - // User do not want any compression - assert_parse_fail!("compress, identity;q=0"); - assert_parse_fail!("compress, identity;q=0.0"); - assert_parse_fail!("compress, *;q=0"); - assert_parse_fail!("compress, *;q=0.0"); - } -} + encodings +}); diff --git a/src/types/mod.rs b/src/types/mod.rs index 461d771eb..bab7c3bc0 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,19 +1,18 @@ //! Common extractors and responders. -// TODO: review visibility mod either; -pub(crate) mod form; +mod form; mod header; -pub(crate) mod json; +mod json; mod path; -pub(crate) mod payload; +mod payload; mod query; -pub(crate) mod readlines; +mod readlines; -pub use self::either::{Either, EitherExtractError}; -pub use self::form::{Form, FormConfig}; +pub use self::either::Either; +pub use self::form::{Form, FormConfig, UrlEncoded}; pub use self::header::Header; -pub use self::json::{Json, JsonConfig}; +pub use self::json::{Json, JsonBody, JsonConfig}; pub use self::path::{Path, PathConfig}; pub use self::payload::{Payload, PayloadConfig}; pub use self::query::{Query, QueryConfig}; diff --git a/src/types/payload.rs b/src/types/payload.rs index 73987def5..d2ab29639 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -248,6 +248,7 @@ impl PayloadConfig { } } } + Ok(()) } diff --git a/src/web.rs b/src/web.rs index e4339352b..4858600af 100644 --- a/src/web.rs +++ b/src/web.rs @@ -2,13 +2,12 @@ use std::future::Future; -use actix_http::Method; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; use crate::{ - error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource, - route::Route, scope::Scope, service::WebService, Responder, + error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource, + Responder, Route, Scope, }; pub use crate::config::ServiceConfig; diff --git a/tests/compression.rs b/tests/compression.rs new file mode 100644 index 000000000..c0bf10e4c --- /dev/null +++ b/tests/compression.rs @@ -0,0 +1,313 @@ +use actix_http::ContentEncoding; +use actix_web::{ + dev::BodyEncoding as _, + http::{header, StatusCode}, + middleware::Compress, + web, App, HttpResponse, +}; +use bytes::Bytes; + +mod test_utils; +use test_utils::{brotli, gzip, zstd}; + +static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt"); +static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz"); +static LOREM_BR: &[u8] = include_bytes!("fixtures/lorem.txt.br"); +static LOREM_ZSTD: &[u8] = include_bytes!("fixtures/lorem.txt.zst"); +static LOREM_XZ: &[u8] = include_bytes!("fixtures/lorem.txt.xz"); + +macro_rules! test_server { + () => { + actix_test::start(|| { + App::new() + .wrap(Compress::default()) + .route("/static", web::to(|| HttpResponse::Ok().body(LOREM))) + .route( + "/static-gzip", + web::to(|| { + HttpResponse::Ok() + // signal to compressor that content should not be altered + .encode_with(ContentEncoding::Identity) + // signal to client that content is encoded + .insert_header(ContentEncoding::Gzip) + .body(LOREM_GZIP) + }), + ) + .route( + "/static-br", + web::to(|| { + HttpResponse::Ok() + // signal to compressor that content should not be altered + .encode_with(ContentEncoding::Identity) + // signal to client that content is encoded + .insert_header(ContentEncoding::Brotli) + .body(LOREM_BR) + }), + ) + .route( + "/static-zstd", + web::to(|| { + HttpResponse::Ok() + // signal to compressor that content should not be altered + .encode_with(ContentEncoding::Identity) + // signal to client that content is encoded + .insert_header(ContentEncoding::Zstd) + .body(LOREM_ZSTD) + }), + ) + .route( + "/static-xz", + web::to(|| { + HttpResponse::Ok() + // signal to compressor that content should not be altered + .encode_with(ContentEncoding::Identity) + // signal to client that content is encoded as 7zip + .insert_header((header::CONTENT_ENCODING, "xz")) + .body(LOREM_XZ) + }), + ) + .route( + "/echo", + web::to(|body: Bytes| HttpResponse::Ok().body(body)), + ) + }) + }; +} + +#[actix_rt::test] +async fn negotiate_encoding_identity() { + let srv = test_server!(); + + let req = srv + .post("/static") + .insert_header((header::ACCEPT_ENCODING, "identity")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + srv.stop().await; +} + +#[actix_rt::test] +async fn negotiate_encoding_gzip() { + let srv = test_server!(); + + let req = srv + .post("/static") + .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + let mut res = srv + .post("/static") + .no_decompress() + .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .send() + .await + .unwrap(); + let bytes = res.body().await.unwrap(); + assert_eq!(gzip::decode(bytes), LOREM); + + srv.stop().await; +} + +#[actix_rt::test] +async fn negotiate_encoding_br() { + let srv = test_server!(); + + let req = srv + .post("/static") + .insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + let mut res = srv + .post("/static") + .no_decompress() + .insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip")) + .send() + .await + .unwrap(); + let bytes = res.body().await.unwrap(); + assert_eq!(brotli::decode(bytes), LOREM); + + srv.stop().await; +} + +#[actix_rt::test] +async fn negotiate_encoding_zstd() { + let srv = test_server!(); + + let req = srv + .post("/static") + .insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "zstd"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + let mut res = srv + .post("/static") + .no_decompress() + .insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br")) + .send() + .await + .unwrap(); + let bytes = res.body().await.unwrap(); + assert_eq!(zstd::decode(bytes), LOREM); + + srv.stop().await; +} + +#[cfg(all( + feature = "compress-brotli", + feature = "compress-gzip", + feature = "compress-zstd", +))] +#[actix_rt::test] +async fn client_encoding_prefers_brotli() { + let srv = test_server!(); + + let req = srv.post("/static").send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + srv.stop().await; +} + +#[actix_rt::test] +async fn gzip_no_decompress() { + let srv = test_server!(); + + let req = srv + .post("/static-gzip") + // don't decompress response body + .no_decompress() + // signal that we want a compressed body + .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM_GZIP)); + + srv.stop().await; +} + +#[actix_rt::test] +async fn manual_custom_coding() { + let srv = test_server!(); + + let req = srv + .post("/static-xz") + // don't decompress response body + .no_decompress() + // signal that we want a compressed body + .insert_header((header::ACCEPT_ENCODING, "xz")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM_XZ)); + + srv.stop().await; +} + +#[actix_rt::test] +async fn deny_identity_coding() { + let srv = test_server!(); + + let req = srv + .post("/static") + // signal that we want a compressed body + .insert_header((header::ACCEPT_ENCODING, "br, identity;q=0")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + srv.stop().await; +} + +#[actix_rt::test] +async fn deny_identity_coding_no_decompress() { + let srv = test_server!(); + + let req = srv + .post("/static-br") + // don't decompress response body + .no_decompress() + // signal that we want a compressed body + .insert_header((header::ACCEPT_ENCODING, "br, identity;q=0")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM_BR)); + + srv.stop().await; +} + +// TODO: fix test +// currently fails because negotiation doesn't consider unknown encoding types +#[ignore] +#[actix_rt::test] +async fn deny_identity_for_manual_coding() { + let srv = test_server!(); + + let req = srv + .post("/static-xz") + // don't decompress response body + .no_decompress() + // signal that we want a compressed body + .insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "xz"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM_XZ)); + + srv.stop().await; +} diff --git a/tests/fixtures/lorem.txt b/tests/fixtures/lorem.txt new file mode 100644 index 000000000..a2abb6432 --- /dev/null +++ b/tests/fixtures/lorem.txt @@ -0,0 +1,5 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin interdum tincidunt lacus, sed tempor lorem consectetur et. Pellentesque et egestas sem, at cursus massa. Nunc feugiat elit sit amet ipsum commodo luctus. Proin auctor dignissim pharetra. Integer iaculis quam a tellus auctor, vitae auctor nisl viverra. Nullam consequat maximus porttitor. Pellentesque tortor enim, molestie at varius non, tempor non nibh. Suspendisse tempus erat lorem, vel faucibus magna blandit vel. Sed pellentesque ligula augue, vitae fermentum eros blandit et. Cras dignissim in massa ut varius. Vestibulum commodo nunc sit amet pellentesque dignissim. + +Donec imperdiet blandit lobortis. Suspendisse fringilla nunc quis venenatis tempor. Nunc tempor sed erat sed convallis. Pellentesque aliquet elit lectus, quis vulputate arcu pharetra sed. Etiam laoreet aliquet arcu cursus vehicula. Maecenas odio odio, elementum faucibus sollicitudin vitae, pellentesque ac purus. Donec venenatis faucibus lorem, et finibus lacus tincidunt vitae. Quisque laoreet metus sapien, vitae euismod mauris lobortis malesuada. Integer sit amet elementum turpis. Maecenas ex mauris, dapibus eu placerat vitae, rutrum convallis enim. Nulla vitae orci ultricies, sagittis turpis et, lacinia dui. Praesent egestas urna turpis, sit amet feugiat mauris tristique eu. Quisque id tempor libero. Donec ullamcorper dapibus lorem, vel consequat risus congue a. + +Nullam dignissim ut lectus vitae tempor. Pellentesque ut odio fringilla, volutpat mi et, vulputate tellus. Fusce eget diam non odio tincidunt viverra eu vel augue. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam sed eleifend purus, vitae aliquam orci. Cras fringilla justo eget tempus bibendum. Phasellus venenatis, odio nec pulvinar commodo, quam neque lacinia turpis, ut rutrum tortor massa eu nulla. Vivamus tincidunt ut lectus a gravida. Donec varius mi quis enim interdum ultrices. Sed aliquam consectetur nisi vitae viverra. Praesent nec ligula egestas, porta lectus sed, consectetur augue. diff --git a/tests/fixtures/lorem.txt.br b/tests/fixtures/lorem.txt.br new file mode 100644 index 0000000000000000000000000000000000000000..e12d7510ee184cbd234d803327ee4f17285eb5be GIT binary patch literal 905 zcmV;419tquKtKQ>rfUAzr>O?L;iCEFef5mQ_{~D193IB{^CrMb%JNqgxdcY!GuiB4Q*LyOU-gf=gDZa z13H*He)yu{FUh~(Ya(fly(}@LRcr|DP|4_WjB%ofX${h=V=S!raXd?*2|YAY0-fSXL{(T_o2SrYjaJguL#0Wb-#9r9^!2279s?dde4r3#dO|l zAR!C397=Hx7Vnw;c6+l-8#qsO%e29-FxGu}GCGic7{M6FkRfW%I!(a!rY6Z~9@wI9 z2oa@W{93j0GWsel0$CSFK#%7!*mXl1je=fu zgqr9{2?nPbFE{x`+m?SY8Pq(J&PZ%Kpw+ZCn%e^J>I)FDgiUSvsa~QNs`HAnlOZE* z*#T?+e!IDeQ(C^rh?)bxWA0WfDP_{wvEf>y@+>05J~$GqwXNLouA*rtdJ>((>@^Z8 z;)s|W!^63YS;Rt@wUuz*6{mADoWV#a_^1;z6BVhnI^*?G>WWr(4vJ6)L|-|nsCDg7 zKGZtT&3{>46`8SNNzM%B{;SAU68!O%JMU_}=8hjP5{7P4$!_&BVh7F|7fsx@vfCt3 zHPwakCy=CG$xQgT`rwEb>_WGTyksiP!FXD*BR&fT%2ms+PC5-!+)bvTXX@Ue0nr3H fZVdLv<|E@cj{%nl-80*ql$VUo3+=PJV4S`IH*m~F literal 0 HcmV?d00001 diff --git a/tests/fixtures/lorem.txt.gz b/tests/fixtures/lorem.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..d3a0664a320f93c575f0b77417152154140cefc5 GIT binary patch literal 920 zcmV;J184jniwFn>|IlFq18i?{Wo<5Wcys_=RZEWSHVoX)DR=;*FOX$EK!7Aj@PO>u zHrvBMmOT12-tkk^&)sh`)3zn5SXC_j5uzNVXDSD6!G%akGw~sLp*8pf zM^yYBXYUe8f!ArGufhj0Fgu51+s1{Snps8})Npw7lo%@{=eYNU%^6U?>w&%KIcLR& zADYA)t0G>};Sn7ARk2o|92NU~@XO!~zj56C^G4rFI>m2*FZCl9O2nqhDgcV32k_~= zlC^uLyJM`+_6gge!}*HB>0TV8rM<}L?;-KP0?;Tiq*=gZ`Z+?Ai$8E$6`?X<-{@a! z<5H;vU67TO%`^Ff8AA1%sxvmUffwSXRL{9nclH#H~KXz)OQRSuz3`z z3%hln@~Q9^tnw$z3Ydw)rlI~>aGZ3`r+g^@-m1B5pt&luymQAZoh2IQjk&A)S)aooMYewv;trisdpSpj@3$Ct~xM%+&ZohSEb@|1Y2TZ!@K5cf_fxbdZ8)^ zVA_gW8n7gwnkiEv2BocDF6<*+(BJ;xDQ zOS=HPw3%q$83qB>-IYDVTLyw5Z}7p6KKXc?vP(jyJ@Qj+So+kqxQ9kHWz62!MLPIu u$vv8Xy*%L&?cB_}X!_gg_k{0OU+R6t0}Y7&;byP;e*6!TUtO9B2mk=cqQn#c literal 0 HcmV?d00001 diff --git a/tests/fixtures/lorem.txt.xz b/tests/fixtures/lorem.txt.xz new file mode 100644 index 0000000000000000000000000000000000000000..bb45fe7535a6be83dfd723f7cef89a30bf91a8fe GIT binary patch literal 1020 zcmVvH<%0_2e@^`p>hS)*d z1vSlsrF;^l1>2ClFBH>pa+auyeS}tVjcOv8tDZf>RqxED!`*h!Yk|yv>94Zxt%Y>l zx!)*UoMfrNhCmDJ5z-pE^?*Khxa-Rkh5F9&sE&}>fzuKP9f!n_^287?_V%J<`&(N` z%EaV)8FkZ3m^+%RdW2gqzqwI}Q-@s)e7;FLTr8-qXl)82u5Hyax3CUJMg#$f!?{6e zGBqGhAn>3a8r!A2pCP9*NKi{NZ<;hPB&2^lHx8=1J!PW_M*Kw&UoK6hjjm#={;IW1 zr~sJjK%Y_b^=_4(;5Q+kFY@-ALsfu$YNagy9ARpbW;`4-`UsFa@D#G!9mcK&*wutK z3(km&0Jk^-G~}Be%LNxwy6^K`PpRgUE_x+3D6HC*EJDO;B7(0)U)%=bHQZYEY+6;I zB820Pp)Gz4ddfA|<_FHC0EGP$@bE~f)cpQg3{!iop(5NgPt{jUZEVbSg=rNEK>H}D zIp)Mv^j~cD=QMWp7piqRNER2Bp zzI+&u&w$(0wMCe36A*NKf{(uiZKzT{EclGTf3oq|ZGa*e<}tpst}3p&gT%#->^?DE zY~BJL`vq0E>H7W#OrO8WGSj6f+3!~V&YqHqviV3a-(<1gz28HGOyfzUQtT%k?T!noG7SCE zTqagZa=8zm#8ZuT_|B+W7qkPXZ`)VqUHNQAqNuXZq6U zjz!qrdmNv9e3Ys{EG(b|wF%cc*de0X=h*ESnMga0K7|S26hB_NHz>u?T5o(>T|4oC>mUaFFC`5PY5P z_Q{~AG~^+!`Xv0uZzrP@}x z^QzK5oQB4H^|Oz4>rnhswCs$vpjX0(ljl5ZvbFb4-n9oiNN1WPP9RSwLoT>X@CbSC zYIKgaCpfoE=H=AYtEu0ptuzi<&! literal 0 HcmV?d00001 diff --git a/tests/fixtures/lorem.txt.zst b/tests/fixtures/lorem.txt.zst new file mode 100644 index 0000000000000000000000000000000000000000..13bea6d7a26e41c8c2bc848dc4d7ec0e764eed2b GIT binary patch literal 898 zcmV-|1AY7`wJ-f-2?wPc07kQU9biq;2GAngv5Dk9#kP_V3OgqJdn{wnx>gdr1Uzp5 za{zGwhv^wOQK*LHfEYdVcib40maSYZdz?%{C>5u!*md62)c`18UO%Dl-oP2XmDbE&q9GrXhy4N2yJ0|Y`M-JKZuR#&DBx3 z?ohmJEqpAop@rsL%e*;5av7X(Q2cCMI_DlMv1{5%$OifuCdH&z#e_H)J))4c7NX8U zwB(6T{;c$gjofogT}HJK&YvxHqXjRR(4sDuI*8YhGBzSCkKn4QO*@60$1P-&P=4*~ zNU5Rkf)n?J)o>9P#KzsMzWkh&cFab)NzOVeRtQei*>VR<)C^M{qq-6}u?5SfrHh<3 za<$m`sr)&WTq?#guXT2oIKL2e&W}4T@`_23j@nHCWjuci;iEE73J({m)Kl{s77jsvW4i|gl#EjF~xgg8yjcjwU&F}+$dsvE17+yW<#=$@pz zW!ob+k7x*{lF$_YLR|BxMjh^1T78Y{cSFs2)gP5M(q4{w)&YR5sNs=ONX^UyNmEeB zbPx~$Aw)T+2OvtBvu)A=;|kqt*(622pA>V)D3A4`#+|OGKDq$i0EBx>XFM<+AkHCQ zSQhEV{!7ayBqNf8M8{>!6p+w=Z0H4=knDrU7AUsv-Wrtv^HmIHe6+fBg8^QWEpC*q z>mdf7%Ui3~|M=8+V!@|p5L%I!3-nqR!t4s<#78)j&8*R0LQb0`F-H$v5r12S8pCKJ zb=NYN=ieAP(;iKfU7^K{%L;VVG>SVVsDN}iH+>r4GfZ<58IAnmXiOxHfkcqEzBbtG znUus>V3JezR^va-&N4fpk!3iLiBGHAUbhFiH}$g*-f`DfVIj#KccO`v) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = GzDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod deflate { + use super::*; + use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = ZlibDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod brotli { + use super::*; + use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = BrotliEncoder::new(Vec::new(), 3); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = BrotliDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod zstd { + use super::*; + use ::zstd::stream::{read::Decoder, write::Encoder}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = Encoder::new(Vec::new(), 3).unwrap(); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = Decoder::new(bytes.as_ref()).unwrap(); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} From 25fe1bbaa5b3cc2c8e4629fb0866036cf4c778d4 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 3 Jan 2022 14:05:08 +0000 Subject: [PATCH 33/66] add double compress layer test --- Cargo.toml | 1 + actix-http/src/encoding/encoder.rs | 12 +++--- src/middleware/compress.rs | 60 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7abcba5a6..0e966cf46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -109,6 +109,7 @@ actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.17", features = ["openssl"] } brotli2 = "0.3.2" +const-str = "0.3" criterion = { version = "0.3", features = ["html_reports"] } env_logger = "0.9" flate2 = "1.0.13" diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index f40e0e579..6f6fc09d8 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -56,16 +56,16 @@ impl Encoder { } pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { - let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING) - || head.status == StatusCode::SWITCHING_PROTOCOLS - || head.status == StatusCode::NO_CONTENT - || encoding == ContentEncoding::Identity); - // no need to compress an empty body if matches!(body.size(), BodySize::None) { return Self::none(); } + let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING) + || head.status == StatusCode::SWITCHING_PROTOCOLS + || head.status == StatusCode::NO_CONTENT + || encoding == ContentEncoding::Identity); + let body = match body.try_into_bytes() { Ok(body) => EncoderBody::Full { body }, Err(body) => EncoderBody::Stream { body }, @@ -301,7 +301,7 @@ impl ContentEncoder { Some(ContentEncoder::Zstd(encoder)) } - ContentEncoding::Identity => None, + _ => None, } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 3a0d5630d..056381847 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -217,3 +217,63 @@ static SUPPORTED_ENCODINGS: Lazy> = Lazy::new(|| { encodings }); + +// move cfg(feature) to prevents_double_compressing if more tests are added +#[cfg(feature = "compress-gzip")] +#[cfg(test)] +mod tests { + use super::*; + use crate::{middleware::DefaultHeaders, test, web, App}; + + pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec { + use std::io::Read as _; + let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } + + #[actix_rt::test] + async fn prevents_double_compressing() { + const D: &str = "hello world "; + const DATA: &str = const_str::repeat!(D, 100); + + let app = test::init_service({ + App::new() + .wrap(Compress::default()) + .route( + "/single", + web::get().to(move || HttpResponse::Ok().body(DATA)), + ) + .service( + web::resource("/double") + .wrap(Compress::default()) + .wrap(DefaultHeaders::new().add(("x-double", "true"))) + .route(web::get().to(move || HttpResponse::Ok().body(DATA))), + ) + }) + .await; + + let req = test::TestRequest::default() + .uri("/single") + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get("x-double"), None); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); + let bytes = test::read_body(res).await; + assert_eq!(gzip_decode(bytes), DATA.as_bytes()); + + let req = test::TestRequest::default() + .uri("/double") + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get("x-double").unwrap(), "true"); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); + let bytes = test::read_body(res).await; + assert_eq!(gzip_decode(bytes), DATA.as_bytes()); + } +} From 68cd853aa22aa8f9ffa204e15db2828ae6ca5049 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 3 Jan 2022 14:59:01 +0000 Subject: [PATCH 34/66] improve docs for Compress --- Cargo.toml | 11 ++++---- actix-http/src/encoding/encoder.rs | 5 ++-- src/dev.rs | 31 ---------------------- src/middleware/compress.rs | 42 +++++++++++++++++++++++++++--- 4 files changed, 47 insertions(+), 42 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0e966cf46..3118f3b03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,15 +28,15 @@ path = "src/lib.rs" resolver = "2" members = [ ".", - "awc", - "actix-http", "actix-files", + "actix-http-test", + "actix-http", "actix-multipart", + "actix-router", + "actix-test", "actix-web-actors", "actix-web-codegen", - "actix-http-test", - "actix-test", - "actix-router", + "awc", ] [features] @@ -105,6 +105,7 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] +actix-files = "0.6.0-beta.12" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.17", features = ["openssl"] } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 6f6fc09d8..2848ad697 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -73,7 +73,7 @@ impl Encoder { if should_encode { // wrap body only if encoder is feature-enabled - if let Some(enc) = ContentEncoder::encoder(encoding) { + if let Some(enc) = ContentEncoder::select(encoding) { update_head(encoding, head); return Encoder { @@ -168,6 +168,7 @@ where cx: &mut Context<'_>, ) -> Poll>> { let mut this = self.project(); + loop { if *this.eof { return Poll::Ready(None); @@ -276,7 +277,7 @@ enum ContentEncoder { } impl ContentEncoder { - fn encoder(encoding: ContentEncoding) -> Option { + fn select(encoding: ContentEncoding) -> Option { match encoding { #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( diff --git a/src/dev.rs b/src/dev.rs index b15d15747..333d1acf8 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -46,9 +46,6 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { } /// Helper trait for managing response encoding. -/// -/// Use `pre_encoded_with` to flag response as already encoded. For example, when serving a Gzip -/// compressed file from disk. pub trait BodyEncoding { /// Get content encoding fn preferred_encoding(&self) -> Option; @@ -59,21 +56,10 @@ pub trait BodyEncoding { /// /// [`Compress`]: crate::middleware::Compress fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self; - - // /// Flags that a file already is encoded so that [`Compress`] does not modify it. - // /// - // /// Effectively a shortcut for `compress_with("identity")` - // /// plus `insert_header(ContentEncoding, encoding)`. - // /// - // /// [`Compress`]: crate::middleware::Compress - // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self; } struct CompressWith(ContentEncoding); -// TODO: add or delete this -// struct PreCompressed(ContentEncoding); - impl BodyEncoding for crate::HttpResponseBuilder { fn preferred_encoding(&self) -> Option { self.extensions().get::().map(|enc| enc.0) @@ -83,11 +69,6 @@ impl BodyEncoding for crate::HttpResponseBuilder { self.extensions_mut().insert(CompressWith(encoding)); self } - - // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { - // self.extensions_mut().insert(PreCompressed(encoding)); - // self - // } } impl BodyEncoding for crate::HttpResponse { @@ -99,11 +80,6 @@ impl BodyEncoding for crate::HttpResponse { self.extensions_mut().insert(CompressWith(encoding)); self } - - // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { - // self.extensions_mut().insert(PreCompressed(encoding)); - // self - // } } impl BodyEncoding for ServiceResponse { @@ -120,13 +96,6 @@ impl BodyEncoding for ServiceResponse { .insert(CompressWith(encoding)); self } - - // fn pre_encoded_with(&mut self, encoding: ContentEncoding) -> &mut Self { - // self.request() - // .extensions_mut() - // .insert(PreCompressed(encoding)); - // self - // } } // TODO: remove these impls ? diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 056381847..058f3d43e 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -27,17 +27,51 @@ use crate::{ /// Middleware for compressing response payloads. /// -/// Use `BodyEncoding` trait for overriding response compression. To disable compression set -/// encoding to `ContentEncoding::Identity`. +/// # Encoding Negotiation +/// `Compress` will read the `Accept-Encoding` header to negotiate which compression codec to use. +/// Payloads are not compressed if the header is not sent. The `compress-*` [feature flags] are also +/// considered in this selection process. +/// +/// # Pre-compressed Payload +/// If you are serving some data is already using a compressed representation (e.g., a gzip +/// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate +/// `Content-Encoding` header. In addition to preventing double compressing the payload, this header +/// is required by the spec when using compressed representations and will inform the client that +/// the content should be uncompressed. +/// +/// However, it is not advised to unconditionally serve encoded representations of content because +/// the client may not support it. The [`AcceptEncoding`] typed header has some utilities to help +/// perform manual encoding negotiation, if required. When negotiating content encoding, it is also +/// required by the spec to send a `Vary: Accept-Encoding` header. +/// +/// A (naïve) example serving an pre-compressed Gzip file is included below. /// /// # Examples +/// To enable automatic payload compression just include `Compress` as a top-level middleware: /// ``` -/// use actix_web::{web, middleware, App, HttpResponse}; +/// use actix_web::{middleware, web, App, HttpResponse}; /// /// let app = App::new() /// .wrap(middleware::Compress::default()) -/// .default_service(web::to(|| HttpResponse::NotFound())); +/// .default_service(web::to(|| HttpResponse::Ok().body("hello world"))); /// ``` +/// +/// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware: +/// ```no_run +/// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder}; +/// +/// async fn index_handler() -> actix_web::Result { +/// Ok(actix_files::NamedFile::open("./assets/index.html.gz")? +/// .customize() +/// .insert_header(header::ContentEncoding::Gzip)) +/// } +/// +/// let app = App::new() +/// .wrap(middleware::Compress::default()) +/// .default_service(web::to(index_handler)); +/// ``` +/// +/// [feature flags]: ../index.html#crate-features #[derive(Debug, Clone, Default)] #[non_exhaustive] pub struct Compress; From 19a46e3925fb1b6f443a5923972870cf4fe9dd9a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 3 Jan 2022 15:35:47 +0000 Subject: [PATCH 35/66] fix doc test --- src/middleware/compress.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 058f3d43e..bdd193693 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -61,7 +61,7 @@ use crate::{ /// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder}; /// /// async fn index_handler() -> actix_web::Result { -/// Ok(actix_files::NamedFile::open("./assets/index.html.gz")? +/// Ok(actix_files::NamedFile::open_async("./assets/index.html.gz").await? /// .customize() /// .insert_header(header::ContentEncoding::Gzip)) /// } From 0bc4ae9158d51e06e4845bc2e0ba987a5e3aae2e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Mon, 3 Jan 2022 18:46:04 +0000 Subject: [PATCH 36/66] remove `BodyEncoding` trait (#2565) --- .github/workflows/clippy-fmt.yml | 11 +- CHANGES.md | 2 + actix-files/src/lib.rs | 3 +- actix-files/src/named.rs | 67 +++--- actix-http/CHANGES.md | 2 + actix-http/src/header/map.rs | 2 +- actix-http/src/header/mod.rs | 4 +- .../src/header/shared/content_encoding.rs | 1 + awc/Cargo.toml | 2 + awc/tests/test_client.rs | 151 ++++-------- tests/test_utils.rs => awc/tests/utils.rs | 0 src/dev.rs | 78 ------- src/http/header/entity.rs | 84 ++++--- src/http/header/etag.rs | 20 +- src/http/header/if_match.rs | 9 +- src/http/header/if_none_match.rs | 4 +- src/middleware/compress.rs | 13 +- tests/compression.rs | 14 +- tests/test_server.rs | 215 ++++++++---------- tests/utils.rs | 76 +++++++ 20 files changed, 355 insertions(+), 403 deletions(-) rename tests/test_utils.rs => awc/tests/utils.rs (100%) create mode 100644 tests/utils.rs diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index 957256d32..9fcb0a561 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -14,6 +14,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + profile: minimal components: rustfmt - name: Check with rustfmt uses: actions-rs/cargo@v1 @@ -30,10 +31,18 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + profile: minimal components: clippy override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + - name: Check with Clippy uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --all-features --tests + args: --workspace --tests --examples --all-features diff --git a/CHANGES.md b/CHANGES.md index 423ea9fdc..9e5acce5d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -15,8 +15,10 @@ ### Removed - `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] +- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] [#2501]: https://github.com/actix/actix-web/pull/2501 +[#2565]: https://github.com/actix/actix-web/pull/2565 ## 4.0.0-beta.18 - 2021-12-29 diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 6408e02da..8ed7d44e0 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -597,7 +597,8 @@ mod tests { .to_request(); let res = test::call_service(&srv, request).await; assert_eq!(res.status(), StatusCode::OK); - assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + assert!(res.headers().contains_key(header::CONTENT_ENCODING)); + assert!(!test::read_body(res).await.is_empty()); } #[actix_rt::test] diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 04e453580..019730dc6 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -9,14 +9,11 @@ use std::{ use actix_service::{Service, ServiceFactory}; use actix_web::{ body::{self, BoxBody, SizedStream}, - dev::{ - AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, - ServiceResponse, - }, + dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, http::{ header::{ self, Charset, ContentDisposition, ContentEncoding, DispositionParam, - DispositionType, ExtendedValue, + DispositionType, ExtendedValue, HeaderValue, }, StatusCode, }, @@ -224,7 +221,6 @@ impl NamedFile { }) } - #[cfg(not(feature = "experimental-io-uring"))] /// Attempts to open a file in read-only mode. /// /// # Examples @@ -232,6 +228,7 @@ impl NamedFile { /// use actix_files::NamedFile; /// let file = NamedFile::open("foo.txt"); /// ``` + #[cfg(not(feature = "experimental-io-uring"))] pub fn open>(path: P) -> io::Result { let file = File::open(&path)?; Self::from_file(file, path) @@ -295,23 +292,21 @@ impl NamedFile { self } - /// Set the MIME Content-Type for serving this file. By default - /// the Content-Type is inferred from the filename extension. + /// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred + /// from the filename extension. #[inline] pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self { self.content_type = mime_type; self } - /// Set the Content-Disposition for serving this file. This allows - /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. + /// Set the Content-Disposition for serving this file. This allows changing the + /// `inline/attachment` disposition as well as the filename sent to the peer. /// /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and - /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, - /// and the filename is taken from the path provided in the `open` method - /// after converting it to UTF-8 using. - /// [`std::ffi::OsStr::to_string_lossy`] + /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the + /// filename is taken from the path provided in the `open` method after converting it to UTF-8 + /// (using `to_string_lossy`). #[inline] pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { self.content_disposition = cd; @@ -337,7 +332,7 @@ impl NamedFile { self } - /// Specifies whether to use ETag or not. + /// Specifies whether to return `ETag` header in response. /// /// Default is true. #[inline] @@ -346,7 +341,7 @@ impl NamedFile { self } - /// Specifies whether to use Last-Modified or not. + /// Specifies whether to return `Last-Modified` header in response. /// /// Default is true. #[inline] @@ -364,7 +359,7 @@ impl NamedFile { self } - /// Creates a etag in a format is similar to Apache's. + /// Creates an `ETag` in a format is similar to Apache's. pub(crate) fn etag(&self) -> Option { self.modified.as_ref().map(|mtime| { let ino = { @@ -386,7 +381,7 @@ impl NamedFile { .duration_since(UNIX_EPOCH) .expect("modification time must be after epoch"); - header::EntityTag::strong(format!( + header::EntityTag::new_strong(format!( "{:x}:{:x}:{:x}:{:x}", ino, self.md.len(), @@ -405,12 +400,13 @@ impl NamedFile { if self.status_code != StatusCode::OK { let mut res = HttpResponse::build(self.status_code); - if self.flags.contains(Flags::PREFER_UTF8) { - let ct = equiv_utf8_text(self.content_type.clone()); - res.insert_header((header::CONTENT_TYPE, ct.to_string())); + let ct = if self.flags.contains(Flags::PREFER_UTF8) { + equiv_utf8_text(self.content_type.clone()) } else { - res.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); - } + self.content_type + }; + + res.insert_header((header::CONTENT_TYPE, ct.to_string())); if self.flags.contains(Flags::CONTENT_DISPOSITION) { res.insert_header(( @@ -420,7 +416,7 @@ impl NamedFile { } if let Some(current_encoding) = self.encoding { - res.encode_with(current_encoding); + res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); } let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); @@ -478,12 +474,13 @@ impl NamedFile { let mut res = HttpResponse::build(self.status_code); - if self.flags.contains(Flags::PREFER_UTF8) { - let ct = equiv_utf8_text(self.content_type.clone()); - res.insert_header((header::CONTENT_TYPE, ct.to_string())); + let ct = if self.flags.contains(Flags::PREFER_UTF8) { + equiv_utf8_text(self.content_type.clone()) } else { - res.insert_header((header::CONTENT_TYPE, self.content_type.to_string())); - } + self.content_type + }; + + res.insert_header((header::CONTENT_TYPE, ct.to_string())); if self.flags.contains(Flags::CONTENT_DISPOSITION) { res.insert_header(( @@ -492,9 +489,8 @@ impl NamedFile { )); } - // default compressing if let Some(current_encoding) = self.encoding { - res.encode_with(current_encoding); + res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str())); } if let Some(lm) = last_modified { @@ -517,7 +513,12 @@ impl NamedFile { length = ranges[0].length; offset = ranges[0].start; - res.encode_with(ContentEncoding::Identity); + // don't allow compression middleware to modify partial content + res.insert_header(( + header::CONTENT_ENCODING, + HeaderValue::from_static("identity"), + )); + res.insert_header(( header::CONTENT_RANGE, format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index cbdccd93c..621b42450 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -13,6 +13,7 @@ - `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501] - Rename `ContentEncoding::{Br => Brotli}`. [#2501] - Minimum supported Rust version (MSRV) is now 1.54. +- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565] ### Fixed - `ContentEncoding::Identity` can now be parsed from a string. [#2501] @@ -23,6 +24,7 @@ - `ContentEncoding::is_compression()`. [#2501] [#2501]: https://github.com/actix/actix-web/pull/2501 +[#2565]: https://github.com/actix/actix-web/pull/2565 ## 3.0.0-beta.17 - 2021-12-27 diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 5cf4ba2fa..33fb262c4 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -6,7 +6,7 @@ use ahash::AHashMap; use http::header::{HeaderName, HeaderValue}; use smallvec::{smallvec, SmallVec}; -use crate::header::AsHeaderName; +use super::AsHeaderName; /// A multi-map of HTTP headers. /// diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index 4e9140db4..5f352fc12 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -50,10 +50,10 @@ pub use self::utils::{ /// An interface for types that already represent a valid header. pub trait Header: TryIntoHeaderValue { - /// Returns the name of the header field + /// Returns the name of the header field. fn name() -> HeaderName; - /// Parse a header + /// Parse the header from a HTTP message. fn parse(msg: &M) -> Result; } diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index ce011f107..bd25de704 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -68,6 +68,7 @@ impl ContentEncoding { } impl Default for ContentEncoding { + #[inline] fn default() -> Self { Self::Identity } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 9c1f56f64..0650b5508 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -102,12 +102,14 @@ actix-utils = "3.0.0" actix-web = { version = "4.0.0-beta.18", features = ["openssl"] } brotli2 = "0.3.2" +const-str = "0.3" env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } static_assertions = "1.1" rcgen = "0.8" rustls-pemfile = "0.2" +zstd = "0.9" [[example]] name = "client" diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 6eb6800d3..c1378157b 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,5 +1,6 @@ use std::{ collections::HashMap, + convert::Infallible, io::{Read, Write}, net::{IpAddr, Ipv4Addr}, sync::{ @@ -15,43 +16,16 @@ use cookie::Cookie; use futures_util::stream; use rand::Rng; -#[cfg(feature = "compress-brotli")] -use brotli2::write::BrotliEncoder; - -#[cfg(feature = "compress-gzip")] -use flate2::{read::GzDecoder, write::GzEncoder, Compression}; - -use actix_http::{ContentEncoding, HttpService, StatusCode}; +use actix_http::{HttpService, StatusCode}; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; -use actix_web::{ - dev::{AppConfig, BodyEncoding}, - http::header, - web, App, Error, HttpRequest, HttpResponse, -}; +use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse}; use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; -const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World"; +mod utils; + +const S: &str = "Hello World "; +const STR: &str = const_str::repeat!(S, 100); #[actix_rt::test] async fn test_simple() { @@ -471,15 +445,12 @@ async fn test_no_decompress() { let srv = actix_test::start(|| { App::new() .wrap(actix_web::middleware::Compress::default()) - .service(web::resource("/").route(web::to(|| { - let mut res = HttpResponse::Ok().body(STR); - res.encode_with(header::ContentEncoding::Gzip); - res - }))) + .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); let mut res = awc::Client::new() .get(srv.url("/")) + .insert_header((header::ACCEPT_ENCODING, "gzip")) .no_decompress() .send() .await @@ -488,15 +459,12 @@ async fn test_no_decompress() { // read response let bytes = res.body().await.unwrap(); - - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + assert_eq!(utils::gzip::decode(bytes), STR.as_bytes()); // POST let mut res = awc::Client::new() .post(srv.url("/")) + .insert_header((header::ACCEPT_ENCODING, "gzip")) .no_decompress() .send() .await @@ -504,10 +472,7 @@ async fn test_no_decompress() { assert!(res.status().is_success()); let bytes = res.body().await.unwrap(); - let mut e = GzDecoder::new(&bytes[..]); - let mut dec = Vec::new(); - e.read_to_end(&mut dec).unwrap(); - assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + assert_eq!(utils::gzip::decode(bytes), STR.as_bytes()); } #[cfg(feature = "compress-gzip")] @@ -515,13 +480,9 @@ async fn test_no_decompress() { async fn test_client_gzip_encoding() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| { - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.as_ref()).unwrap(); - let data = e.finish().unwrap(); - HttpResponse::Ok() - .insert_header(("content-encoding", "gzip")) - .body(data) + .insert_header(header::ContentEncoding::Gzip) + .body(utils::gzip::encode(STR)) }))) }); @@ -531,7 +492,7 @@ async fn test_client_gzip_encoding() { // read response let bytes = response.body().await.unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + assert_eq!(bytes, STR); } #[cfg(feature = "compress-gzip")] @@ -539,13 +500,9 @@ async fn test_client_gzip_encoding() { async fn test_client_gzip_encoding_large() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| { - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(STR.repeat(10).as_ref()).unwrap(); - let data = e.finish().unwrap(); - HttpResponse::Ok() - .insert_header(("content-encoding", "gzip")) - .body(data) + .insert_header(header::ContentEncoding::Gzip) + .body(utils::gzip::encode(STR.repeat(10))) }))) }); @@ -555,7 +512,7 @@ async fn test_client_gzip_encoding_large() { // read response let bytes = response.body().await.unwrap(); - assert_eq!(bytes, Bytes::from(STR.repeat(10))); + assert_eq!(bytes, STR.repeat(10)); } #[cfg(feature = "compress-gzip")] @@ -569,12 +526,9 @@ async fn test_client_gzip_encoding_large_random() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { - let mut e = GzEncoder::new(Vec::new(), Compression::default()); - e.write_all(&data).unwrap(); - let data = e.finish().unwrap(); HttpResponse::Ok() - .insert_header(("content-encoding", "gzip")) - .body(data) + .insert_header(header::ContentEncoding::Gzip) + .body(utils::gzip::encode(data)) }))) }); @@ -584,7 +538,7 @@ async fn test_client_gzip_encoding_large_random() { // read response let bytes = response.body().await.unwrap(); - assert_eq!(bytes, Bytes::from(data)); + assert_eq!(bytes, data); } #[cfg(feature = "compress-brotli")] @@ -592,12 +546,9 @@ async fn test_client_gzip_encoding_large_random() { async fn test_client_brotli_encoding() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(&data).unwrap(); - let data = e.finish().unwrap(); HttpResponse::Ok() .insert_header(("content-encoding", "br")) - .body(data) + .body(utils::brotli::encode(data)) }))) }); @@ -621,12 +572,9 @@ async fn test_client_brotli_encoding_large_random() { let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { - let mut e = BrotliEncoder::new(Vec::new(), 5); - e.write_all(&data).unwrap(); - let data = e.finish().unwrap(); HttpResponse::Ok() - .insert_header(("content-encoding", "br")) - .body(data) + .insert_header(header::ContentEncoding::Brotli) + .body(utils::brotli::encode(&data)) }))) }); @@ -636,27 +584,25 @@ async fn test_client_brotli_encoding_large_random() { // read response let bytes = response.body().await.unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); + assert_eq!(bytes, data); } #[actix_rt::test] async fn test_client_deflate_encoding() { let srv = actix_test::start(|| { - App::new().default_service(web::to(|body: Bytes| { - HttpResponse::Ok() - .encode_with(ContentEncoding::Brotli) - .body(body) - })) + App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body))) }); - let req = srv.post("/").send_body(STR); + let req = srv + .post("/") + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .send_body(STR); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + assert_eq!(bytes, STR); } #[actix_rt::test] @@ -668,14 +614,13 @@ async fn test_client_deflate_encoding_large_random() { .collect::(); let srv = actix_test::start(|| { - App::new().default_service(web::to(|body: Bytes| { - HttpResponse::Ok() - .encode_with(ContentEncoding::Brotli) - .body(body) - })) + App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body))) }); - let req = srv.post("/").send_body(data.clone()); + let req = srv + .post("/") + .insert_header((header::ACCEPT_ENCODING, "br")) + .send_body(data.clone()); let mut res = req.await.unwrap(); let bytes = res.body().await.unwrap(); @@ -688,15 +633,16 @@ async fn test_client_deflate_encoding_large_random() { async fn test_client_streaming_explicit() { let srv = actix_test::start(|| { App::new().default_service(web::to(|body: web::Payload| { - HttpResponse::Ok() - .encode_with(ContentEncoding::Identity) - .streaming(body) + HttpResponse::Ok().streaming(body) })) }); let body = stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) }); - let req = srv.post("/").send_stream(Box::pin(body)); + let req = srv + .post("/") + .insert_header((header::ACCEPT_ENCODING, "identity")) + .send_stream(Box::pin(body)); let mut res = req.await.unwrap(); assert!(res.status().is_success()); @@ -709,17 +655,16 @@ async fn test_client_streaming_explicit() { async fn test_body_streaming_implicit() { let srv = actix_test::start(|| { App::new().default_service(web::to(|| { - let body = stream::once(async { - Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) - }); - - HttpResponse::Ok() - .encode_with(ContentEncoding::Gzip) - .streaming(Box::pin(body)) + let body = + stream::once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_bytes())) }); + HttpResponse::Ok().streaming(body) })) }); - let req = srv.get("/").send(); + let req = srv + .get("/") + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .send(); let mut res = req.await.unwrap(); assert!(res.status().is_success()); diff --git a/tests/test_utils.rs b/awc/tests/utils.rs similarity index 100% rename from tests/test_utils.rs rename to awc/tests/utils.rs diff --git a/src/dev.rs b/src/dev.rs index 333d1acf8..def545ec7 100644 --- a/src/dev.rs +++ b/src/dev.rs @@ -22,8 +22,6 @@ pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, We pub use crate::types::{JsonBody, Readlines, UrlEncoded}; -use crate::{http::header::ContentEncoding, HttpMessage as _}; - use actix_router::Patterns; pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { @@ -44,79 +42,3 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { patterns } - -/// Helper trait for managing response encoding. -pub trait BodyEncoding { - /// Get content encoding - fn preferred_encoding(&self) -> Option; - - /// Set content encoding to use. - /// - /// Must be used with [`Compress`] to take effect. - /// - /// [`Compress`]: crate::middleware::Compress - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self; -} - -struct CompressWith(ContentEncoding); - -impl BodyEncoding for crate::HttpResponseBuilder { - fn preferred_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(CompressWith(encoding)); - self - } -} - -impl BodyEncoding for crate::HttpResponse { - fn preferred_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(CompressWith(encoding)); - self - } -} - -impl BodyEncoding for ServiceResponse { - fn preferred_encoding(&self) -> Option { - self.request() - .extensions() - .get::() - .map(|enc| enc.0) - } - - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { - self.request() - .extensions_mut() - .insert(CompressWith(encoding)); - self - } -} - -// TODO: remove these impls ? -impl BodyEncoding for actix_http::ResponseBuilder { - fn preferred_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(CompressWith(encoding)); - self - } -} - -impl BodyEncoding for actix_http::Response { - fn preferred_encoding(&self) -> Option { - self.extensions().get::().map(|enc| enc.0) - } - - fn encode_with(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(CompressWith(encoding)); - self - } -} diff --git a/src/http/header/entity.rs b/src/http/header/entity.rs index 76fe39f23..0eaa12b5d 100644 --- a/src/http/header/entity.rs +++ b/src/http/header/entity.rs @@ -17,8 +17,7 @@ fn check_slice_validity(slice: &str) -> bool { slice.bytes().all(entity_validate_char) } -/// An entity tag, defined -/// in [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) +/// An entity tag, defined in [RFC 7232 §2.3]. /// /// An entity tag consists of a string enclosed by two literal double quotes. /// Preceding the first double quote is an optional weakness indicator, @@ -48,16 +47,20 @@ fn check_slice_validity(slice: &str) -> bool { /// | `W/"1"` | `W/"2"` | no match | no match | /// | `W/"1"` | `"1"` | no match | match | /// | `"1"` | `"1"` | match | match | -#[derive(Clone, Debug, Eq, PartialEq)] +/// +/// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EntityTag { /// Weakness indicator for the tag pub weak: bool, + /// The opaque string in between the DQUOTEs tag: String, } impl EntityTag { - /// Constructs a new EntityTag. + /// Constructs a new `EntityTag`. + /// /// # Panics /// If the tag contains invalid characters. pub fn new(weak: bool, tag: String) -> EntityTag { @@ -66,51 +69,64 @@ impl EntityTag { } /// Constructs a new weak EntityTag. + /// /// # Panics /// If the tag contains invalid characters. - pub fn weak(tag: String) -> EntityTag { + pub fn new_weak(tag: String) -> EntityTag { EntityTag::new(true, tag) } + #[deprecated(since = "3.0.0", note = "Renamed to `new_weak`.")] + pub fn weak(tag: String) -> EntityTag { + Self::new_weak(tag) + } + /// Constructs a new strong EntityTag. + /// /// # Panics /// If the tag contains invalid characters. - pub fn strong(tag: String) -> EntityTag { + pub fn new_strong(tag: String) -> EntityTag { EntityTag::new(false, tag) } - /// Get the tag. + #[deprecated(since = "3.0.0", note = "Renamed to `new_strong`.")] + pub fn strong(tag: String) -> EntityTag { + Self::new_strong(tag) + } + + /// Returns tag. pub fn tag(&self) -> &str { self.tag.as_ref() } - /// Set the tag. + /// Sets tag. + /// /// # Panics /// If the tag contains invalid characters. - pub fn set_tag(&mut self, tag: String) { + pub fn set_tag(&mut self, tag: impl Into) { + let tag = tag.into(); assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag); self.tag = tag } - /// For strong comparison two entity-tags are equivalent if both are not - /// weak and their opaque-tags match character-by-character. + /// For strong comparison two entity-tags are equivalent if both are not weak and their + /// opaque-tags match character-by-character. pub fn strong_eq(&self, other: &EntityTag) -> bool { !self.weak && !other.weak && self.tag == other.tag } - /// For weak comparison two entity-tags are equivalent if their - /// opaque-tags match character-by-character, regardless of either or - /// both being tagged as "weak". + /// For weak comparison two entity-tags are equivalent if their opaque-tags match + /// character-by-character, regardless of either or both being tagged as "weak". pub fn weak_eq(&self, other: &EntityTag) -> bool { self.tag == other.tag } - /// The inverse of `EntityTag.strong_eq()`. + /// Returns the inverse of `strong_eq()`. pub fn strong_ne(&self, other: &EntityTag) -> bool { !self.strong_eq(other) } - /// The inverse of `EntityTag.weak_eq()`. + /// Returns inverse of `weak_eq()`. pub fn weak_ne(&self, other: &EntityTag) -> bool { !self.weak_eq(other) } @@ -178,23 +194,23 @@ mod tests { // Expected success assert_eq!( "\"foobar\"".parse::().unwrap(), - EntityTag::strong("foobar".to_owned()) + EntityTag::new_strong("foobar".to_owned()) ); assert_eq!( "\"\"".parse::().unwrap(), - EntityTag::strong("".to_owned()) + EntityTag::new_strong("".to_owned()) ); assert_eq!( "W/\"weaktag\"".parse::().unwrap(), - EntityTag::weak("weaktag".to_owned()) + EntityTag::new_weak("weaktag".to_owned()) ); assert_eq!( "W/\"\x65\x62\"".parse::().unwrap(), - EntityTag::weak("\x65\x62".to_owned()) + EntityTag::new_weak("\x65\x62".to_owned()) ); assert_eq!( "W/\"\"".parse::().unwrap(), - EntityTag::weak("".to_owned()) + EntityTag::new_weak("".to_owned()) ); } @@ -214,19 +230,19 @@ mod tests { #[test] fn test_etag_fmt() { assert_eq!( - format!("{}", EntityTag::strong("foobar".to_owned())), + format!("{}", EntityTag::new_strong("foobar".to_owned())), "\"foobar\"" ); - assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\""); + assert_eq!(format!("{}", EntityTag::new_strong("".to_owned())), "\"\""); assert_eq!( - format!("{}", EntityTag::weak("weak-etag".to_owned())), + format!("{}", EntityTag::new_weak("weak-etag".to_owned())), "W/\"weak-etag\"" ); assert_eq!( - format!("{}", EntityTag::weak("\u{0065}".to_owned())), + format!("{}", EntityTag::new_weak("\u{0065}".to_owned())), "W/\"\x65\"" ); - assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\""); + assert_eq!(format!("{}", EntityTag::new_weak("".to_owned())), "W/\"\""); } #[test] @@ -237,29 +253,29 @@ mod tests { // | `W/"1"` | `W/"2"` | no match | no match | // | `W/"1"` | `"1"` | no match | match | // | `"1"` | `"1"` | match | match | - let mut etag1 = EntityTag::weak("1".to_owned()); - let mut etag2 = EntityTag::weak("1".to_owned()); + let mut etag1 = EntityTag::new_weak("1".to_owned()); + let mut etag2 = EntityTag::new_weak("1".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(!etag1.weak_ne(&etag2)); - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::weak("2".to_owned()); + etag1 = EntityTag::new_weak("1".to_owned()); + etag2 = EntityTag::new_weak("2".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(!etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(etag1.weak_ne(&etag2)); - etag1 = EntityTag::weak("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); + etag1 = EntityTag::new_weak("1".to_owned()); + etag2 = EntityTag::new_strong("1".to_owned()); assert!(!etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(etag1.strong_ne(&etag2)); assert!(!etag1.weak_ne(&etag2)); - etag1 = EntityTag::strong("1".to_owned()); - etag2 = EntityTag::strong("1".to_owned()); + etag1 = EntityTag::new_strong("1".to_owned()); + etag2 = EntityTag::new_strong("1".to_owned()); assert!(etag1.strong_eq(&etag2)); assert!(etag1.weak_eq(&etag2)); assert!(!etag1.strong_ne(&etag2)); diff --git a/src/http/header/etag.rs b/src/http/header/etag.rs index 4724c917e..78f5447b3 100644 --- a/src/http/header/etag.rs +++ b/src/http/header/etag.rs @@ -31,7 +31,7 @@ crate::http::header::common_header! { /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// ETag(EntityTag::new(false, "xyzzy".to_owned())) + /// ETag(EntityTag::new_strong("xyzzy".to_owned())) /// ); /// ``` /// @@ -41,7 +41,7 @@ crate::http::header::common_header! { /// /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( - /// ETag(EntityTag::new(true, "xyzzy".to_owned())) + /// ETag(EntityTag::new_weak("xyzzy".to_owned())) /// ); /// ``` (ETag, ETAG) => [EntityTag] @@ -50,29 +50,29 @@ crate::http::header::common_header! { // From the RFC crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""], - Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); + Some(ETag(EntityTag::new_strong("xyzzy".to_owned())))); crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""], - Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); + Some(ETag(EntityTag::new_weak("xyzzy".to_owned())))); crate::http::header::common_header_test!(test3, vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); + Some(ETag(EntityTag::new_strong("".to_owned())))); // Own tests crate::http::header::common_header_test!(test4, vec![b"\"foobar\""], - Some(ETag(EntityTag::new(false, "foobar".to_owned())))); + Some(ETag(EntityTag::new_strong("foobar".to_owned())))); crate::http::header::common_header_test!(test5, vec![b"\"\""], - Some(ETag(EntityTag::new(false, "".to_owned())))); + Some(ETag(EntityTag::new_strong("".to_owned())))); crate::http::header::common_header_test!(test6, vec![b"W/\"weak-etag\""], - Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); + Some(ETag(EntityTag::new_weak("weak-etag".to_owned())))); crate::http::header::common_header_test!(test7, vec![b"W/\"\x65\x62\""], - Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); + Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned())))); crate::http::header::common_header_test!(test8, vec![b"W/\"\""], - Some(ETag(EntityTag::new(true, "".to_owned())))); + Some(ETag(EntityTag::new_weak("".to_owned())))); crate::http::header::common_header_test!(test9, vec![b"no-dquotes"], None::); diff --git a/src/http/header/if_match.rs b/src/http/header/if_match.rs index a565b9125..e299d30fe 100644 --- a/src/http/header/if_match.rs +++ b/src/http/header/if_match.rs @@ -54,14 +54,15 @@ common_header! { test1, vec![b"\"xyzzy\""], Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned())]))); + vec![EntityTag::new_strong("xyzzy".to_owned())]))); + crate::http::header::common_header_test!( test2, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( - vec![EntityTag::new(false, "xyzzy".to_owned()), - EntityTag::new(false, "r2d2xxxx".to_owned()), - EntityTag::new(false, "c3piozzzz".to_owned())]))); + vec![EntityTag::new_strong("xyzzy".to_owned()), + EntityTag::new_strong("r2d2xxxx".to_owned()), + EntityTag::new_strong("c3piozzzz".to_owned())]))); crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); } } diff --git a/src/http/header/if_none_match.rs b/src/http/header/if_none_match.rs index fb1895fc8..863be70cf 100644 --- a/src/http/header/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -82,8 +82,8 @@ mod tests { if_none_match = Header::parse(&req); let mut entities: Vec = Vec::new(); - let foobar_etag = EntityTag::new(false, "foobar".to_owned()); - let weak_etag = EntityTag::new(true, "weak-etag".to_owned()); + let foobar_etag = EntityTag::new_strong("foobar".to_owned()); + let weak_etag = EntityTag::new_weak("weak-etag".to_owned()); entities.push(foobar_etag); entities.push(weak_etag); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities))); diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index bdd193693..16af4c2cd 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -16,7 +16,6 @@ use pin_project_lite::pin_project; use crate::{ body::{EitherBody, MessageBody}, - dev::BodyEncoding as _, http::{ header::{self, AcceptEncoding, Encoding, HeaderValue}, StatusCode, @@ -176,14 +175,10 @@ where match ready!(this.fut.poll(cx)) { Ok(resp) => { - let enc = if let Some(enc) = resp.response().preferred_encoding() { - enc - } else { - match this.encoding { - Encoding::Known(enc) => *enc, - Encoding::Unknown(enc) => { - unimplemented!("encoding {} should not be here", enc); - } + let enc = match this.encoding { + Encoding::Known(enc) => *enc, + Encoding::Unknown(enc) => { + unimplemented!("encoding {} should not be here", enc); } }; diff --git a/tests/compression.rs b/tests/compression.rs index c0bf10e4c..88c462f60 100644 --- a/tests/compression.rs +++ b/tests/compression.rs @@ -1,14 +1,12 @@ use actix_http::ContentEncoding; use actix_web::{ - dev::BodyEncoding as _, http::{header, StatusCode}, middleware::Compress, web, App, HttpResponse, }; use bytes::Bytes; -mod test_utils; -use test_utils::{brotli, gzip, zstd}; +mod utils; static LOREM: &[u8] = include_bytes!("fixtures/lorem.txt"); static LOREM_GZIP: &[u8] = include_bytes!("fixtures/lorem.txt.gz"); @@ -27,7 +25,6 @@ macro_rules! test_server { web::to(|| { HttpResponse::Ok() // signal to compressor that content should not be altered - .encode_with(ContentEncoding::Identity) // signal to client that content is encoded .insert_header(ContentEncoding::Gzip) .body(LOREM_GZIP) @@ -38,7 +35,6 @@ macro_rules! test_server { web::to(|| { HttpResponse::Ok() // signal to compressor that content should not be altered - .encode_with(ContentEncoding::Identity) // signal to client that content is encoded .insert_header(ContentEncoding::Brotli) .body(LOREM_BR) @@ -49,7 +45,6 @@ macro_rules! test_server { web::to(|| { HttpResponse::Ok() // signal to compressor that content should not be altered - .encode_with(ContentEncoding::Identity) // signal to client that content is encoded .insert_header(ContentEncoding::Zstd) .body(LOREM_ZSTD) @@ -60,7 +55,6 @@ macro_rules! test_server { web::to(|| { HttpResponse::Ok() // signal to compressor that content should not be altered - .encode_with(ContentEncoding::Identity) // signal to client that content is encoded as 7zip .insert_header((header::CONTENT_ENCODING, "xz")) .body(LOREM_XZ) @@ -117,7 +111,7 @@ async fn negotiate_encoding_gzip() { .await .unwrap(); let bytes = res.body().await.unwrap(); - assert_eq!(gzip::decode(bytes), LOREM); + assert_eq!(utils::gzip::decode(bytes), LOREM); srv.stop().await; } @@ -146,7 +140,7 @@ async fn negotiate_encoding_br() { .await .unwrap(); let bytes = res.body().await.unwrap(); - assert_eq!(brotli::decode(bytes), LOREM); + assert_eq!(utils::brotli::decode(bytes), LOREM); srv.stop().await; } @@ -175,7 +169,7 @@ async fn negotiate_encoding_zstd() { .await .unwrap(); let bytes = res.body().await.unwrap(); - assert_eq!(zstd::decode(bytes), LOREM); + assert_eq!(utils::zstd::decode(bytes), LOREM); srv.stop().await; } diff --git a/tests/test_server.rs b/tests/test_server.rs index e8d514e64..987e51a65 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -12,11 +12,7 @@ use std::{ use actix_web::{ cookie::{Cookie, CookieBuilder}, - dev::BodyEncoding, - http::{ - header::{self, ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, TRANSFER_ENCODING}, - StatusCode, - }, + http::{header, StatusCode}, middleware::{Compress, NormalizePath, TrailingSlash}, web, App, Error, HttpResponse, }; @@ -31,30 +27,10 @@ use openssl::{ x509::X509, }; -mod test_utils; -use test_utils::{brotli, deflate, gzip, zstd}; +mod utils; -const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World \ - Hello World Hello World Hello World Hello World Hello World"; +const S: &str = "Hello World "; +const STR: &str = const_str::repeat!(S, 100); #[cfg(feature = "openssl")] fn openssl_config() -> SslAcceptor { @@ -129,51 +105,52 @@ async fn test_body() { srv.stop().await; } -#[actix_rt::test] -async fn test_body_encoding_override() { - let srv = actix_test::start_with(actix_test::config().h1(), || { - App::new() - .wrap(Compress::default()) - .service(web::resource("/").route(web::to(|| { - HttpResponse::Ok() - .encode_with(ContentEncoding::Deflate) - .body(STR) - }))) - .service(web::resource("/raw").route(web::to(|| { - let mut res = HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); - res.encode_with(ContentEncoding::Deflate); - res.map_into_boxed_body() - }))) - }); +// enforcing an encoding per-response is removed +// #[actix_rt::test] +// async fn test_body_encoding_override() { +// let srv = actix_test::start_with(actix_test::config().h1(), || { +// App::new() +// .wrap(Compress::default()) +// .service(web::resource("/").route(web::to(|| { +// HttpResponse::Ok() +// .encode_with(ContentEncoding::Deflate) +// .body(STR) +// }))) +// .service(web::resource("/raw").route(web::to(|| { +// let mut res = HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); +// res.encode_with(ContentEncoding::Deflate); +// res.map_into_boxed_body() +// }))) +// }); - // Builder - let mut res = srv - .get("/") - .no_decompress() - .append_header((ACCEPT_ENCODING, "deflate")) - .send() - .await - .unwrap(); - assert_eq!(res.status(), StatusCode::OK); +// // Builder +// let mut res = srv +// .get("/") +// .no_decompress() +// .append_header((ACCEPT_ENCODING, "deflate")) +// .send() +// .await +// .unwrap(); +// assert_eq!(res.status(), StatusCode::OK); - let bytes = res.body().await.unwrap(); - assert_eq!(deflate::decode(bytes), STR.as_bytes()); +// let bytes = res.body().await.unwrap(); +// assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); - // Raw Response - let mut res = srv - .request(actix_web::http::Method::GET, srv.url("/raw")) - .no_decompress() - .append_header((ACCEPT_ENCODING, "deflate")) - .send() - .await - .unwrap(); - assert_eq!(res.status(), StatusCode::OK); +// // Raw Response +// let mut res = srv +// .request(actix_web::http::Method::GET, srv.url("/raw")) +// .no_decompress() +// .append_header((ACCEPT_ENCODING, "deflate")) +// .send() +// .await +// .unwrap(); +// assert_eq!(res.status(), StatusCode::OK); - let bytes = res.body().await.unwrap(); - assert_eq!(deflate::decode(bytes), STR.as_bytes()); +// let bytes = res.body().await.unwrap(); +// assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); - srv.stop().await; -} +// srv.stop().await; +// } #[actix_rt::test] async fn body_gzip_large() { @@ -191,14 +168,14 @@ async fn body_gzip_large() { let mut res = srv .get("/") .no_decompress() - .append_header((ACCEPT_ENCODING, "gzip")) + .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(gzip::decode(bytes), data.as_bytes()); + assert_eq!(utils::gzip::decode(bytes), data.as_bytes()); srv.stop().await; } @@ -222,14 +199,14 @@ async fn test_body_gzip_large_random() { let mut res = srv .get("/") .no_decompress() - .append_header((ACCEPT_ENCODING, "gzip")) + .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(gzip::decode(bytes), data.as_bytes()); + assert_eq!(utils::gzip::decode(bytes), data.as_bytes()); srv.stop().await; } @@ -248,15 +225,18 @@ async fn test_body_chunked_implicit() { let mut res = srv .get("/") .no_decompress() - .append_header((ACCEPT_ENCODING, "gzip")) + .append_header((header::ACCEPT_ENCODING, "gzip")) .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.headers().get(TRANSFER_ENCODING).unwrap(), "chunked"); + assert_eq!( + res.headers().get(header::TRANSFER_ENCODING).unwrap(), + "chunked" + ); let bytes = res.body().await.unwrap(); - assert_eq!(gzip::decode(bytes), STR.as_bytes()); + assert_eq!(utils::gzip::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -274,7 +254,7 @@ async fn test_body_br_streaming() { let mut res = srv .get("/") - .append_header((ACCEPT_ENCODING, "br")) + .append_header((header::ACCEPT_ENCODING, "br")) .no_decompress() .send() .await @@ -282,7 +262,7 @@ async fn test_body_br_streaming() { assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(brotli::decode(bytes), STR.as_bytes()); + assert_eq!(utils::brotli::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -319,7 +299,7 @@ async fn test_no_chunking() { let mut res = srv.get("/").send().await.unwrap(); assert_eq!(res.status(), StatusCode::OK); - assert!(!res.headers().contains_key(TRANSFER_ENCODING)); + assert!(!res.headers().contains_key(header::TRANSFER_ENCODING)); let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); @@ -337,7 +317,7 @@ async fn test_body_deflate() { let mut res = srv .get("/") - .append_header((ACCEPT_ENCODING, "deflate")) + .append_header((header::ACCEPT_ENCODING, "deflate")) .no_decompress() .send() .await @@ -345,7 +325,7 @@ async fn test_body_deflate() { assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(deflate::decode(bytes), STR.as_bytes()); + assert_eq!(utils::deflate::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -360,7 +340,7 @@ async fn test_body_brotli() { let mut res = srv .get("/") - .append_header((ACCEPT_ENCODING, "br")) + .append_header((header::ACCEPT_ENCODING, "br")) .no_decompress() .send() .await @@ -368,7 +348,7 @@ async fn test_body_brotli() { assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(brotli::decode(bytes), STR.as_bytes()); + assert_eq!(utils::brotli::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -383,7 +363,7 @@ async fn test_body_zstd() { let mut res = srv .get("/") - .append_header((ACCEPT_ENCODING, "zstd")) + .append_header((header::ACCEPT_ENCODING, "zstd")) .no_decompress() .send() .await @@ -391,7 +371,7 @@ async fn test_body_zstd() { assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(zstd::decode(bytes), STR.as_bytes()); + assert_eq!(utils::zstd::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -409,7 +389,7 @@ async fn test_body_zstd_streaming() { let mut res = srv .get("/") - .append_header((ACCEPT_ENCODING, "zstd")) + .append_header((header::ACCEPT_ENCODING, "zstd")) .no_decompress() .send() .await @@ -417,7 +397,7 @@ async fn test_body_zstd_streaming() { assert_eq!(res.status(), StatusCode::OK); let bytes = res.body().await.unwrap(); - assert_eq!(zstd::decode(bytes), STR.as_bytes()); + assert_eq!(utils::zstd::decode(bytes), STR.as_bytes()); srv.stop().await; } @@ -432,8 +412,8 @@ async fn test_zstd_encoding() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "zstd")) - .send_body(zstd::encode(STR)); + .append_header((header::CONTENT_ENCODING, "zstd")) + .send_body(utils::zstd::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -463,8 +443,8 @@ async fn test_zstd_encoding_large() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "zstd")) - .send_body(zstd::encode(&data)); + .append_header((header::CONTENT_ENCODING, "zstd")) + .send_body(utils::zstd::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -484,8 +464,8 @@ async fn test_encoding() { let request = srv .post("/") - .insert_header((CONTENT_ENCODING, "gzip")) - .send_body(gzip::encode(STR)); + .insert_header((header::CONTENT_ENCODING, "gzip")) + .send_body(utils::gzip::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -505,8 +485,8 @@ async fn test_gzip_encoding() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "gzip")) - .send_body(gzip::encode(STR)); + .append_header((header::CONTENT_ENCODING, "gzip")) + .send_body(utils::gzip::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -527,8 +507,8 @@ async fn test_gzip_encoding_large() { let req = srv .post("/") - .append_header((CONTENT_ENCODING, "gzip")) - .send_body(gzip::encode(&data)); + .append_header((header::CONTENT_ENCODING, "gzip")) + .send_body(utils::gzip::encode(&data)); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -554,8 +534,8 @@ async fn test_reading_gzip_encoding_large_random() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "gzip")) - .send_body(gzip::encode(&data)); + .append_header((header::CONTENT_ENCODING, "gzip")) + .send_body(utils::gzip::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -575,8 +555,8 @@ async fn test_reading_deflate_encoding() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "deflate")) - .send_body(deflate::encode(STR)); + .append_header((header::CONTENT_ENCODING, "deflate")) + .send_body(utils::deflate::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -597,8 +577,8 @@ async fn test_reading_deflate_encoding_large() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "deflate")) - .send_body(deflate::encode(&data)); + .append_header((header::CONTENT_ENCODING, "deflate")) + .send_body(utils::deflate::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -624,8 +604,8 @@ async fn test_reading_deflate_encoding_large_random() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "deflate")) - .send_body(deflate::encode(&data)); + .append_header((header::CONTENT_ENCODING, "deflate")) + .send_body(utils::deflate::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -646,8 +626,8 @@ async fn test_brotli_encoding() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "br")) - .send_body(brotli::encode(STR)); + .append_header((header::CONTENT_ENCODING, "br")) + .send_body(utils::brotli::encode(STR)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -677,8 +657,8 @@ async fn test_brotli_encoding_large() { let request = srv .post("/") - .append_header((CONTENT_ENCODING, "br")) - .send_body(brotli::encode(&data)); + .append_header((header::CONTENT_ENCODING, "br")) + .send_body(utils::brotli::encode(&data)); let mut res = request.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -697,8 +677,9 @@ async fn test_brotli_encoding_large_openssl() { let srv = actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { + // echo decompressed request body back in response HttpResponse::Ok() - .encode_with(ContentEncoding::Identity) + .insert_header(header::ContentEncoding::Identity) .body(bytes) }))) }); @@ -706,7 +687,7 @@ async fn test_brotli_encoding_large_openssl() { let mut res = srv .post("/") .append_header((header::CONTENT_ENCODING, "br")) - .send_body(brotli::encode(&data)) + .send_body(utils::brotli::encode(&data)) .await .unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -758,16 +739,20 @@ mod plus_rustls { let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { + // echo decompressed request body back in response HttpResponse::Ok() - .encode_with(ContentEncoding::Identity) + .insert_header(header::ContentEncoding::Identity) .body(bytes) }))) }); let req = srv .post("/") - .insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate")) - .send_stream(TestBody::new(Bytes::from(deflate::encode(&data)), 1024)); + .insert_header((header::CONTENT_ENCODING, "deflate")) + .send_stream(TestBody::new( + Bytes::from(utils::deflate::encode(&data)), + 1024, + )); let mut res = req.await.unwrap(); assert_eq!(res.status(), StatusCode::OK); @@ -931,14 +916,14 @@ async fn test_accept_encoding_no_match() { let mut res = srv .get("/") - .insert_header((ACCEPT_ENCODING, "xz, identity;q=0")) + .insert_header((header::ACCEPT_ENCODING, "xz, identity;q=0")) .no_decompress() .send() .await .unwrap(); assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE); - assert_eq!(res.headers().get(CONTENT_ENCODING), None); + assert_eq!(res.headers().get(header::CONTENT_ENCODING), None); let bytes = res.body().await.unwrap(); // body should contain the supported encodings diff --git a/tests/utils.rs b/tests/utils.rs new file mode 100644 index 000000000..9a3743d8b --- /dev/null +++ b/tests/utils.rs @@ -0,0 +1,76 @@ +// compiling some tests will trigger unused function warnings even though other tests use them +#![allow(dead_code)] + +use std::io::{Read as _, Write as _}; + +pub mod gzip { + use super::*; + use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = GzDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod deflate { + use super::*; + use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = ZlibDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod brotli { + use super::*; + use ::brotli2::{read::BrotliDecoder, write::BrotliEncoder}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = BrotliEncoder::new(Vec::new(), 3); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = BrotliDecoder::new(bytes.as_ref()); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} + +pub mod zstd { + use super::*; + use ::zstd::stream::{read::Decoder, write::Encoder}; + + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { + let mut encoder = Encoder::new(Vec::new(), 3).unwrap(); + encoder.write_all(bytes.as_ref()).unwrap(); + encoder.finish().unwrap() + } + + pub fn decode(bytes: impl AsRef<[u8]>) -> Vec { + let mut decoder = Decoder::new(bytes.as_ref()).unwrap(); + let mut buf = Vec::new(); + decoder.read_to_end(&mut buf).unwrap(); + buf + } +} From c7639bc3be5b1779d573dc1d2c0d0396b4968554 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 03:48:12 +0000 Subject: [PATCH 37/66] document quoter --- actix-files/src/service.rs | 8 +-- actix-router/src/url.rs | 141 +++++++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 49 deletions(-) diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 057dbe5a3..152e1855e 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -114,7 +114,7 @@ impl Service for FilesService { Box::pin(async move { if !is_method_valid { return Ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() + HttpResponse::MethodNotAllowed() .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .body("Request did not meet this resource's requirements."), )); @@ -123,7 +123,7 @@ impl Service for FilesService { let real_path = match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) { Ok(item) => item, - Err(e) => return Ok(req.error_response(e)), + Err(err) => return Ok(req.error_response(err)), }; if let Some(filter) = &this.path_filter { @@ -131,9 +131,7 @@ impl Service for FilesService { if let Some(ref default) = this.default { return default.call(req).await; } else { - return Ok( - req.into_response(actix_web::HttpResponse::NotFound().finish()) - ); + return Ok(req.into_response(HttpResponse::NotFound().finish())); } } } diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index 10193dde8..fee8eaaf3 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -26,16 +26,6 @@ const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz const QS: &[u8] = b"+&=;b"; -#[inline] -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 -} - -#[inline] -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 1 << (ch & 7) -} - thread_local! { static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); } @@ -96,7 +86,10 @@ impl ResourcePath for Url { /// A quoter pub struct Quoter { + /// Simple bit-map of safe values in the 0-127 ASCII range. safe_table: [u8; 16], + + /// Simple bit-map of protected values in the 0-127 ASCII range. protected_table: [u8; 16], } @@ -108,28 +101,32 @@ impl Quoter { }; // prepare safe table - for i in 0..128 { - if ALLOWED.contains(&i) { - set_bit(&mut quoter.safe_table, i); + for ch in 0..128 { + if ALLOWED.contains(&ch) { + set_bit(&mut quoter.safe_table, ch); } - if QS.contains(&i) { - set_bit(&mut quoter.safe_table, i); + + if QS.contains(&ch) { + set_bit(&mut quoter.safe_table, ch); } } - for ch in safe { - set_bit(&mut quoter.safe_table, *ch) + for &ch in safe { + set_bit(&mut quoter.safe_table, ch) } // prepare protected table - for ch in protected { - set_bit(&mut quoter.safe_table, *ch); - set_bit(&mut quoter.protected_table, *ch); + for &ch in protected { + set_bit(&mut quoter.safe_table, ch); + set_bit(&mut quoter.protected_table, ch); } quoter } + /// Re-quotes... ? + /// + /// Returns `None` when no modification to the original string was required. pub fn requote(&self, val: &[u8]) -> Option { let mut has_pct = 0; let mut pct = [b'%', 0, 0]; @@ -137,17 +134,19 @@ impl Quoter { let mut cloned: Option> = None; let len = val.len(); + while idx < len { let ch = val[idx]; if has_pct != 0 { pct[has_pct] = val[idx]; has_pct += 1; + if has_pct == 3 { has_pct = 0; let buf = cloned.as_mut().unwrap(); - if let Some(ch) = restore_ch(pct[1], pct[2]) { + if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) { if ch < 128 { if bit_at(&self.protected_table, ch) { buf.extend_from_slice(&pct); @@ -161,6 +160,7 @@ impl Quoter { continue; } } + buf.push(ch); } else { buf.extend_from_slice(&pct[..]); @@ -168,6 +168,7 @@ impl Quoter { } } else if ch == b'%' { has_pct = 1; + if cloned.is_none() { let mut c = Vec::with_capacity(len); c.extend_from_slice(&val[..idx]); @@ -176,6 +177,7 @@ impl Quoter { } else if let Some(ref mut cloned) = cloned { cloned.push(ch) } + idx += 1; } @@ -183,22 +185,52 @@ impl Quoter { } } -#[inline] -fn from_hex(v: u8) -> Option { - if (b'0'..=b'9').contains(&v) { - Some(v - 0x30) // ord('0') == 0x30 - } else if (b'A'..=b'F').contains(&v) { - Some(v - 0x41 + 10) // ord('A') == 0x41 - } else if (b'a'..=b'f').contains(&v) { - Some(v - 0x61 + 10) // ord('a') == 0x61 - } else { - None +/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer +/// representation from `0x0`–`0xF`. +/// +/// - `0x30 ('0') => 0x0` +/// - `0x39 ('9') => 0x9` +/// - `0x41 ('a') => 0xA` +/// - `0x61 ('A') => 0xA` +/// - `0x46 ('f') => 0xF` +/// - `0x66 ('F') => 0xF` +fn from_ascii_hex(v: u8) -> Option { + match v { + b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30 + b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41 + b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61 + _ => None, } } -#[inline] -fn restore_ch(d1: u8, d2: u8) -> Option { - from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2)) +/// Decode a ASCII hex-encoded pair to an integer. +/// +/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value. +/// +/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')` +/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')` +/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')` +fn hex_pair_to_char(d1: u8, d2: u8) -> Option { + let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?); + + // left shift high nibble by 4 bits + Some(d_high << 4 | d_low) +} + +/// Sets bit in given bit-map to 1=true. +/// +/// # Panics +/// Panics if `ch` index is out of bounds. +fn set_bit(array: &mut [u8], ch: u8) { + array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) +} + +/// Returns true if bit to true in given bit-map. +/// +/// # Panics +/// Panics if `ch` index is out of bounds. +fn bit_at(array: &[u8], ch: u8) -> bool { + array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 } #[cfg(test)] @@ -229,6 +261,16 @@ mod tests { let path = match_url(re, "/user/2345/test"); assert_eq!(path.get("id").unwrap(), "2345"); + } + + #[test] + fn protected_chars() { + let re = "/user/{id}/test"; + + let encoded = percent_encode(PROTECTED); + let path = match_url(re, format!("/user/{}/test", encoded)); + // characters in captured segment remain unencoded + assert_eq!(path.get("id").unwrap(), &encoded); // "%25" should never be decoded into '%' to guarantee the output is a valid // percent-encoded format @@ -239,13 +281,6 @@ mod tests { assert_eq!(path.get("id").unwrap(), "qwe%25rty"); } - #[test] - 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 non_protected_ascii() { let non_protected_ascii = ('\u{0}'..='\u{7F}') @@ -281,9 +316,9 @@ mod tests { for i in 0..256 { let c = i as u8; if hex.contains(&c) { - assert!(from_hex(c).is_some()) + assert!(from_ascii_hex(c).is_some()) } else { - assert!(from_hex(c).is_none()) + assert!(from_ascii_hex(c).is_none()) } } @@ -291,7 +326,25 @@ mod tests { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, ]; for i in 0..hex.len() { - assert_eq!(from_hex(hex[i]).unwrap(), expected[i]); + assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]); } } + + #[test] + fn custom_quoter() { + let q = Quoter::new(b"", b"+"); + assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c"); + assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc"); + + let q = Quoter::new(b"%+", b"/"); + assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); + assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); + assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); + } + + #[test] + fn quoter_no_modification() { + let q = Quoter::new(b"", b""); + assert_eq!(q.requote(b"/abc/../efg"), None); + } } From 93754f307f4e3ffc1e895ccb87d8ad61368d178c Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 04:08:46 +0000 Subject: [PATCH 38/66] try path config from Data as well --- actix-files/src/directory.rs | 13 +++++++++++-- actix-router/src/url.rs | 1 + src/types/path.rs | 3 +++ 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 80e0c98d0..26225ea5c 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -40,14 +40,23 @@ impl Directory { pub(crate) type DirectoryRenderer = dyn Fn(&Directory, &HttpRequest) -> Result; -// show file url as relative to static path +/// Returns percent encoded file URL path. macro_rules! encode_file_url { ($path:ident) => { utf8_percent_encode(&$path, CONTROLS) }; } -// " -- " & -- & ' -- ' < -- < > -- > / -- / +/// Returns HTML entity encoded formatter. +/// +/// ```plain +/// " => " +/// & => & +/// ' => ' +/// < => < +/// > => > +/// / => / +/// ``` macro_rules! encode_file_name { ($entry:ident) => { escape_html_entity(&$entry.file_name().to_string_lossy(), Html) diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index fee8eaaf3..156c1e1c6 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -340,6 +340,7 @@ mod tests { assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); + assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb"); } #[test] diff --git a/src/types/path.rs b/src/types/path.rs index 4b60d27c0..4a694b763 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -9,6 +9,7 @@ use serde::de; use crate::{ dev::Payload, error::{Error, ErrorNotFound, PathError}, + web::Data, FromRequest, HttpRequest, }; @@ -102,6 +103,7 @@ where fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req .app_data::() + .or_else(|| req.app_data::>().map(Data::get_ref)) .and_then(|c| c.err_handler.clone()); ready( @@ -113,6 +115,7 @@ where Request path: {:?}", req.path() ); + if let Some(error_handler) = error_handler { let e = PathError::Deserialize(err); (error_handler)(e, req) From 374dc9bfc92176635c1b54ff112cefdb06e64646 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Tue, 4 Jan 2022 15:54:11 +0300 Subject: [PATCH 39/66] files: percent-decode url path (#2398) Co-authored-by: Rob Ede --- actix-files/CHANGES.md | 4 ++++ actix-files/Cargo.toml | 1 + actix-files/src/error.rs | 7 +++++++ actix-files/src/files.rs | 1 + actix-files/src/lib.rs | 32 ++++++++++++++++++++++++++++++++ actix-files/src/path_buf.rs | 24 +++++++++++++++++++++++- 6 files changed, 68 insertions(+), 1 deletion(-) diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index c626fd3fb..501181d92 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,8 +1,12 @@ # Changes ## Unreleased - 2021-xx-xx +- `Files`: request URL paths with `%2F` are now rejected. [#2398] +- `Files`: Fixed a regression where `%25` in the URL path is not decoded to `%` in the file path. [#2398] - Minimum supported Rust version (MSRV) is now 1.54. +[#2398]: https://github.com/actix/actix-web/pull/2398 + ## 0.6.0-beta.12 - 2021-12-29 - No significant changes since `0.6.0-beta.11`. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index c2acbc761..745e3afee 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -45,3 +45,4 @@ tokio-uring = { version = "0.1", optional = true } actix-rt = "2.2" actix-test = "0.1.0-beta.10" actix-web = "4.0.0-beta.18" +tempfile = "3.2" diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index f8e32eef7..d28889e73 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -23,16 +23,23 @@ impl ResponseError for FilesError { #[allow(clippy::enum_variant_names)] #[derive(Display, Debug, PartialEq)] +#[non_exhaustive] pub enum UriSegmentError { /// The segment started with the wrapped invalid character. #[display(fmt = "The segment started with the wrapped invalid character")] BadStart(char), + /// The segment contained the wrapped invalid character. #[display(fmt = "The segment contained the wrapped invalid character")] BadChar(char), + /// The segment ended with the wrapped invalid character. #[display(fmt = "The segment ended with the wrapped invalid character")] BadEnd(char), + + /// The path is not a valid UTF-8 string after doing percent decoding. + #[display(fmt = "The path is not a valif UTF-8 string after percent-decoding")] + NotValidUtf8, } /// Return `BadRequest` for `UriSegmentError` diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index d1dd6739d..adfb93232 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -28,6 +28,7 @@ use crate::{ /// /// `Files` service must be registered with `App::service()` method. /// +/// # Examples /// ``` /// use actix_web::App; /// use actix_files::Files; diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 8ed7d44e0..a11aa32c7 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -803,6 +803,38 @@ mod tests { let req = TestRequest::get().uri("/test/%43argo.toml").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); + + // `%2F` == `/` + let req = TestRequest::get().uri("/test%2Ftest.binary").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_percent_encoding_2() { + let tmpdir = tempfile::tempdir().unwrap(); + let filename = match cfg!(unix) { + true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test", + false => "ض#[]{}()@!$&'`+,;= %20.test", + }; + let filename_encoded = filename + .as_bytes() + .iter() + .map(|c| format!("%{:02X}", c)) + .collect::(); + std::fs::File::create(tmpdir.path().join(filename)).unwrap(); + + let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await; + + let req = TestRequest::get() + .uri(&format!("/{}", filename_encoded)) + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 0e0d4f51d..03b2cd766 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -1,5 +1,5 @@ use std::{ - path::{Path, PathBuf}, + path::{Component, Path, PathBuf}, str::FromStr, }; @@ -26,8 +26,23 @@ impl PathBufWrap { pub fn parse_path(path: &str, hidden_files: bool) -> Result { let mut buf = PathBuf::new(); + // equivalent to `path.split('/').count()` + let mut segment_count = path.matches('/').count() + 1; + + // we can decode the whole path here (instead of per-segment decoding) + // because we will reject `%2F` in paths using `segement_count`. + let path = percent_encoding::percent_decode_str(path) + .decode_utf8() + .map_err(|_| UriSegmentError::NotValidUtf8)?; + + // disallow decoding `%2F` into `/` + if segment_count != path.matches('/').count() + 1 { + return Err(UriSegmentError::BadChar('/')); + } + for segment in path.split('/') { if segment == ".." { + segment_count -= 1; buf.pop(); } else if !hidden_files && segment.starts_with('.') { return Err(UriSegmentError::BadStart('.')); @@ -40,6 +55,7 @@ impl PathBufWrap { } else if segment.ends_with('<') { return Err(UriSegmentError::BadEnd('<')); } else if segment.is_empty() { + segment_count -= 1; continue; } else if cfg!(windows) && segment.contains('\\') { return Err(UriSegmentError::BadChar('\\')); @@ -48,6 +64,12 @@ impl PathBufWrap { } } + // make sure we agree with stdlib parser + for (i, component) in buf.components().enumerate() { + assert!(matches!(component, Component::Normal(_))); + assert!(i < segment_count); + } + Ok(PathBufWrap(buf)) } } From 577597a80a775c6cab8d3b905aff5dc70fdd4b2e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 12:09:48 +0000 Subject: [PATCH 40/66] rename on-connect example --- Cargo.toml | 2 +- examples/{on_connect.rs => on-connect.rs} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename examples/{on_connect.rs => on-connect.rs} (100%) diff --git a/Cargo.toml b/Cargo.toml index 3118f3b03..5c4abe46f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,7 +165,7 @@ name = "uds" required-features = ["compress-gzip"] [[example]] -name = "on_connect" +name = "on-connect" required-features = [] [[bench]] diff --git a/examples/on_connect.rs b/examples/on-connect.rs similarity index 100% rename from examples/on_connect.rs rename to examples/on-connect.rs From 85c9b1a263366d279d544cddf407d00989372269 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 12:58:40 +0000 Subject: [PATCH 41/66] move quoter --- actix-files/CHANGES.md | 4 +- actix-files/src/error.rs | 2 +- actix-router/src/lib.rs | 4 +- actix-router/src/quoter.rs | 219 +++++++++++++++++++++++++++++++++++++ actix-router/src/url.rs | 219 +------------------------------------ src/request.rs | 2 +- src/types/path.rs | 27 +++-- tests/weird_poll.rs | 30 +++++ 8 files changed, 273 insertions(+), 234 deletions(-) create mode 100644 actix-router/src/quoter.rs create mode 100644 tests/weird_poll.rs diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 501181d92..93a122941 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,8 +1,8 @@ # Changes ## Unreleased - 2021-xx-xx -- `Files`: request URL paths with `%2F` are now rejected. [#2398] -- `Files`: Fixed a regression where `%25` in the URL path is not decoded to `%` in the file path. [#2398] +- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] +- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - Minimum supported Rust version (MSRV) is now 1.54. [#2398]: https://github.com/actix/actix-web/pull/2398 diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index d28889e73..6682529f8 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -38,7 +38,7 @@ pub enum UriSegmentError { BadEnd(char), /// The path is not a valid UTF-8 string after doing percent decoding. - #[display(fmt = "The path is not a valif UTF-8 string after percent-decoding")] + #[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")] NotValidUtf8, } diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index 03f464626..22f294b9d 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -8,6 +8,7 @@ mod de; mod path; mod pattern; +mod quoter; mod resource; mod resource_path; mod router; @@ -18,9 +19,10 @@ mod url; pub use self::de::PathDeserializer; pub use self::path::Path; pub use self::pattern::{IntoPatterns, Patterns}; +pub use self::quoter::Quoter; 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}; +pub use self::url::Url; diff --git a/actix-router/src/quoter.rs b/actix-router/src/quoter.rs new file mode 100644 index 000000000..26ecc92cd --- /dev/null +++ b/actix-router/src/quoter.rs @@ -0,0 +1,219 @@ +#[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"; + +/// A quoter +pub struct Quoter { + /// Simple bit-map of safe values in the 0-127 ASCII range. + safe_table: [u8; 16], + + /// Simple bit-map of protected values in the 0-127 ASCII range. + protected_table: [u8; 16], +} + +impl Quoter { + pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { + let mut quoter = Quoter { + safe_table: [0; 16], + protected_table: [0; 16], + }; + + // prepare safe table + for ch in 0..128 { + if ALLOWED.contains(&ch) { + set_bit(&mut quoter.safe_table, ch); + } + + if QS.contains(&ch) { + set_bit(&mut quoter.safe_table, ch); + } + } + + for &ch in safe { + set_bit(&mut quoter.safe_table, ch) + } + + // prepare protected table + for &ch in protected { + set_bit(&mut quoter.safe_table, ch); + set_bit(&mut quoter.protected_table, ch); + } + + quoter + } + + /// Re-quotes... ? + /// + /// Returns `None` when no modification to the original string was required. + pub fn requote(&self, val: &[u8]) -> Option { + let mut has_pct = 0; + let mut pct = [b'%', 0, 0]; + let mut idx = 0; + let mut cloned: Option> = None; + + let len = val.len(); + + while idx < len { + let ch = val[idx]; + + if has_pct != 0 { + pct[has_pct] = val[idx]; + has_pct += 1; + + if has_pct == 3 { + has_pct = 0; + let buf = cloned.as_mut().unwrap(); + + if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) { + if ch < 128 { + if bit_at(&self.protected_table, ch) { + buf.extend_from_slice(&pct); + idx += 1; + continue; + } + + if bit_at(&self.safe_table, ch) { + buf.push(ch); + idx += 1; + continue; + } + } + + buf.push(ch); + } else { + buf.extend_from_slice(&pct[..]); + } + } + } else if ch == b'%' { + has_pct = 1; + + if cloned.is_none() { + let mut c = Vec::with_capacity(len); + c.extend_from_slice(&val[..idx]); + cloned = Some(c); + } + } else if let Some(ref mut cloned) = cloned { + cloned.push(ch) + } + + idx += 1; + } + + cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) + } +} + +/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer +/// representation from `0x0`–`0xF`. +/// +/// - `0x30 ('0') => 0x0` +/// - `0x39 ('9') => 0x9` +/// - `0x41 ('a') => 0xA` +/// - `0x61 ('A') => 0xA` +/// - `0x46 ('f') => 0xF` +/// - `0x66 ('F') => 0xF` +fn from_ascii_hex(v: u8) -> Option { + match v { + b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30 + b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41 + b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61 + _ => None, + } +} + +/// Decode a ASCII hex-encoded pair to an integer. +/// +/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value. +/// +/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')` +/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')` +/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')` +fn hex_pair_to_char(d1: u8, d2: u8) -> Option { + let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?); + + // left shift high nibble by 4 bits + Some(d_high << 4 | d_low) +} + +/// Sets bit in given bit-map to 1=true. +/// +/// # Panics +/// Panics if `ch` index is out of bounds. +fn set_bit(array: &mut [u8], ch: u8) { + array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) +} + +/// Returns true if bit to true in given bit-map. +/// +/// # Panics +/// Panics if `ch` index is out of bounds. +fn bit_at(array: &[u8], ch: u8) -> bool { + array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_encoding() { + let hex = b"0123456789abcdefABCDEF"; + + for i in 0..256 { + let c = i as u8; + if hex.contains(&c) { + assert!(from_ascii_hex(c).is_some()) + } else { + assert!(from_ascii_hex(c).is_none()) + } + } + + let expected = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, + ]; + for i in 0..hex.len() { + assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]); + } + } + + #[test] + fn custom_quoter() { + let q = Quoter::new(b"", b"+"); + assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c"); + assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc"); + + let q = Quoter::new(b"%+", b"/"); + assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); + assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); + assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); + assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb"); + } + + #[test] + fn quoter_no_modification() { + let q = Quoter::new(b"", b""); + assert_eq!(q.requote(b"/abc/../efg"), None); + } +} diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index 156c1e1c6..c5a3508aa 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -1,30 +1,6 @@ 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"; +use crate::Quoter; thread_local! { static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); @@ -44,18 +20,20 @@ impl Url { } #[inline] - pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { + pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { Url { path: quoter.requote(uri.path().as_bytes()), uri, } } + /// Returns URI. #[inline] pub fn uri(&self) -> &http::Uri { &self.uri } + /// Returns path. #[inline] pub fn path(&self) -> &str { match self.path { @@ -84,155 +62,6 @@ impl ResourcePath for Url { } } -/// A quoter -pub struct Quoter { - /// Simple bit-map of safe values in the 0-127 ASCII range. - safe_table: [u8; 16], - - /// Simple bit-map of protected values in the 0-127 ASCII range. - protected_table: [u8; 16], -} - -impl Quoter { - pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { - let mut quoter = Quoter { - safe_table: [0; 16], - protected_table: [0; 16], - }; - - // prepare safe table - for ch in 0..128 { - if ALLOWED.contains(&ch) { - set_bit(&mut quoter.safe_table, ch); - } - - if QS.contains(&ch) { - set_bit(&mut quoter.safe_table, ch); - } - } - - for &ch in safe { - set_bit(&mut quoter.safe_table, ch) - } - - // prepare protected table - for &ch in protected { - set_bit(&mut quoter.safe_table, ch); - set_bit(&mut quoter.protected_table, ch); - } - - quoter - } - - /// Re-quotes... ? - /// - /// Returns `None` when no modification to the original string was required. - pub fn requote(&self, val: &[u8]) -> Option { - let mut has_pct = 0; - let mut pct = [b'%', 0, 0]; - let mut idx = 0; - let mut cloned: Option> = None; - - let len = val.len(); - - while idx < len { - let ch = val[idx]; - - if has_pct != 0 { - pct[has_pct] = val[idx]; - has_pct += 1; - - if has_pct == 3 { - has_pct = 0; - let buf = cloned.as_mut().unwrap(); - - if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) { - if ch < 128 { - if bit_at(&self.protected_table, ch) { - buf.extend_from_slice(&pct); - idx += 1; - continue; - } - - if bit_at(&self.safe_table, ch) { - buf.push(ch); - idx += 1; - continue; - } - } - - buf.push(ch); - } else { - buf.extend_from_slice(&pct[..]); - } - } - } else if ch == b'%' { - has_pct = 1; - - if cloned.is_none() { - let mut c = Vec::with_capacity(len); - c.extend_from_slice(&val[..idx]); - cloned = Some(c); - } - } else if let Some(ref mut cloned) = cloned { - cloned.push(ch) - } - - idx += 1; - } - - cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) - } -} - -/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer -/// representation from `0x0`–`0xF`. -/// -/// - `0x30 ('0') => 0x0` -/// - `0x39 ('9') => 0x9` -/// - `0x41 ('a') => 0xA` -/// - `0x61 ('A') => 0xA` -/// - `0x46 ('f') => 0xF` -/// - `0x66 ('F') => 0xF` -fn from_ascii_hex(v: u8) -> Option { - match v { - b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30 - b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41 - b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61 - _ => None, - } -} - -/// Decode a ASCII hex-encoded pair to an integer. -/// -/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value. -/// -/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')` -/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')` -/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')` -fn hex_pair_to_char(d1: u8, d2: u8) -> Option { - let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?); - - // left shift high nibble by 4 bits - Some(d_high << 4 | d_low) -} - -/// Sets bit in given bit-map to 1=true. -/// -/// # Panics -/// Panics if `ch` index is out of bounds. -fn set_bit(array: &mut [u8], ch: u8) { - array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111) -} - -/// Returns true if bit to true in given bit-map. -/// -/// # Panics -/// Panics if `ch` index is out of bounds. -fn bit_at(array: &[u8], ch: u8) -> bool { - array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0 -} - #[cfg(test)] mod tests { use http::Uri; @@ -308,44 +137,4 @@ mod tests { // We should always get a valid utf8 string assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); } - - #[test] - fn hex_encoding() { - let hex = b"0123456789abcdefABCDEF"; - - for i in 0..256 { - let c = i as u8; - if hex.contains(&c) { - assert!(from_ascii_hex(c).is_some()) - } else { - assert!(from_ascii_hex(c).is_none()) - } - } - - let expected = [ - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, - ]; - for i in 0..hex.len() { - assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]); - } - } - - #[test] - fn custom_quoter() { - let q = Quoter::new(b"", b"+"); - assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c"); - assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc"); - - let q = Quoter::new(b"%+", b"/"); - assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c"); - assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb"); - assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb"); - assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb"); - } - - #[test] - fn quoter_no_modification() { - let q = Quoter::new(b"", b""); - assert_eq!(q.requote(b"/abc/../efg"), None); - } } diff --git a/src/request.rs b/src/request.rs index cbec70a29..e876c3b4d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -122,7 +122,7 @@ impl HttpRequest { /// Returns a reference to the URL parameters container. /// - /// A url parameter is specified in the form `{identifier}`, where the identifier can be used + /// A URL parameter is specified in the form `{identifier}`, where the identifier can be used /// later in a request handler to access the matched value for that parameter. /// /// # Percent Encoding and URL Parameters diff --git a/src/types/path.rs b/src/types/path.rs index 4a694b763..5d52e0e1e 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -138,6 +138,7 @@ where /// enum Folder { /// #[serde(rename = "inbox")] /// Inbox, +/// /// #[serde(rename = "outbox")] /// Outbox, /// } @@ -147,19 +148,17 @@ where /// format!("Selected folder: {:?}!", folder) /// } /// -/// fn main() { -/// let app = App::new().service( -/// web::resource("/messages/{folder}") -/// .app_data(PathConfig::default().error_handler(|err, req| { -/// error::InternalError::from_response( -/// err, -/// HttpResponse::Conflict().into(), -/// ) -/// .into() -/// })) -/// .route(web::post().to(index)), -/// ); -/// } +/// let app = App::new().service( +/// web::resource("/messages/{folder}") +/// .app_data(PathConfig::default().error_handler(|err, req| { +/// error::InternalError::from_response( +/// err, +/// HttpResponse::Conflict().into(), +/// ) +/// .into() +/// })) +/// .route(web::post().to(index)), +/// ); /// ``` #[derive(Clone, Default)] pub struct PathConfig { @@ -167,7 +166,7 @@ pub struct PathConfig { } impl PathConfig { - /// Set custom error handler + /// Set custom error handler. pub fn error_handler(mut self, f: F) -> Self where F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, diff --git a/tests/weird_poll.rs b/tests/weird_poll.rs new file mode 100644 index 000000000..5844ea2c2 --- /dev/null +++ b/tests/weird_poll.rs @@ -0,0 +1,30 @@ +//! Regression test for https://github.com/actix/actix-web/issues/1321 + +// use actix_http::body::{BodyStream, MessageBody}; +// use bytes::Bytes; +// use futures_channel::oneshot; +// use futures_util::{ +// stream::once, +// task::{noop_waker, Context}, +// }; + +// #[test] +// fn weird_poll() { +// let (sender, receiver) = oneshot::channel(); +// let mut body_stream = Ok(BodyStream::new(once(async { +// let x = Box::new(0); +// let y = &x; +// receiver.await.unwrap(); +// let _z = **y; +// Ok::<_, ()>(Bytes::new()) +// }))); + +// let waker = noop_waker(); +// let mut cx = Context::from_waker(&waker); + +// let _ = body_stream.as_mut().unwrap().poll_next(&mut cx); +// sender.send(()).unwrap(); +// let _ = std::mem::replace(&mut body_stream, Err([0; 32])) +// .unwrap() +// .poll_next(&mut cx); +// } From 86df295ee29587a0800c4b59f948b5d0fe046744 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:19:29 +0000 Subject: [PATCH 42/66] fully percent decode path segments when capturing (#2566) --- actix-router/CHANGES.md | 3 + actix-router/src/de.rs | 124 ++++++++++++++++++++++++++-------------- src/types/path.rs | 12 ++++ 3 files changed, 97 insertions(+), 42 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index c85d10e2a..7b8615570 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,8 +1,11 @@ # Changes ## Unreleased - 2021-xx-xx +- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Minimum supported Rust version (MSRV) is now 1.54. +[#2566]: https://github.com/actix/actix-net/pull/2566 + ## 0.5.0-beta.3 - 2021-12-17 - Minimum supported Rust version (MSRV) is now 1.52. diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index 775c48b8a..ec7b1066a 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -2,7 +2,11 @@ use serde::de::{self, Deserializer, Error as DeError, Visitor}; use serde::forward_to_deserialize_any; use crate::path::{Path, PathIter}; -use crate::ResourcePath; +use crate::{Quoter, ResourcePath}; + +thread_local! { + static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b""); +} macro_rules! unsupported_type { ($trait_fn:ident, $name:expr) => { @@ -10,16 +14,13 @@ macro_rules! unsupported_type { where V: Visitor<'de>, { - Err(de::value::Error::custom(concat!( - "unsupported type: ", - $name - ))) + Err(de::Error::custom(concat!("unsupported type: ", $name))) } }; } macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + ($trait_fn:ident, $visit_fn:ident, $tp:expr) => { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de>, @@ -33,18 +34,39 @@ macro_rules! parse_single_value { .as_str(), )) } else { - let v = self.path[0].parse().map_err(|_| { - de::value::Error::custom(format!( - "can not parse {:?} to a {}", - &self.path[0], $tp - )) + let decoded = FULL_QUOTER + .with(|q| q.requote(self.path[0].as_bytes())) + .unwrap_or_else(|| self.path[0].to_owned()); + + let v = decoded.parse().map_err(|_| { + de::Error::custom(format!("can not parse {:?} to a {}", &self.path[0], $tp)) })?; + visitor.$visit_fn(v) } } }; } +macro_rules! parse_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let decoded = FULL_QUOTER + .with(|q| q.requote(self.value.as_bytes())) + .unwrap_or_else(|| self.value.to_owned()); + + let v = decoded.parse().map_err(|_| { + de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) + })?; + + visitor.$visit_fn(v) + } + }; +} + pub struct PathDeserializer<'de, T: ResourcePath> { path: &'de Path, } @@ -172,23 +194,6 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> } } - fn deserialize_str(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - if self.path.segment_count() != 1 { - Err(de::value::Error::custom( - format!( - "wrong number of parameters: {} expected 1", - self.path.segment_count() - ) - .as_str(), - )) - } else { - visitor.visit_str(&self.path[0]) - } - } - fn deserialize_seq(self, visitor: V) -> Result where V: Visitor<'de>, @@ -215,6 +220,7 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> parse_single_value!(deserialize_u64, visit_u64, "u64"); parse_single_value!(deserialize_f32, visit_f32, "f32"); parse_single_value!(deserialize_f64, visit_f64, "f64"); + parse_single_value!(deserialize_str, visit_string, "String"); parse_single_value!(deserialize_string, visit_string, "String"); parse_single_value!(deserialize_byte_buf, visit_string, "String"); parse_single_value!(deserialize_char, visit_char, "char"); @@ -279,20 +285,6 @@ impl<'de> Deserializer<'de> for Key<'de> { } } -macro_rules! parse_value { - ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { - fn $trait_fn(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - let v = self.value.parse().map_err(|_| { - de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) - })?; - visitor.$visit_fn(v) - } - }; -} - struct Value<'de> { value: &'de str, } @@ -497,6 +489,7 @@ mod tests { use super::*; use crate::path::Path; use crate::router::Router; + use crate::ResourceDef; #[derive(Deserialize)] struct MyStruct { @@ -657,6 +650,53 @@ mod tests { assert!(format!("{:?}", s).contains("can not parse")); } + #[test] + fn deserialize_path_decode_string() { + let rdef = ResourceDef::new("/{key}"); + + let mut path = Path::new("/%25"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: String = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, "%"); + + let mut path = Path::new("/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: String = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment, "/") + } + + #[test] + fn deserialize_path_decode_seq() { + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/%25/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(segment.0, "%"); + assert_eq!(segment.1, "/"); + } + + #[test] + fn deserialize_path_decode_map() { + #[derive(Deserialize)] + struct Vals { + key: String, + value: String, + } + + let rdef = ResourceDef::new("/{key}/{value}"); + + let mut path = Path::new("/%25/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let vals: Vals = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(vals.key, "%"); + assert_eq!(vals.value, "/"); + } + // #[test] // fn test_extract_path_decode() { // let mut router = Router::<()>::default(); diff --git a/src/types/path.rs b/src/types/path.rs index 5d52e0e1e..c3efc22c0 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -285,6 +285,18 @@ mod tests { assert_eq!(res[1], "32".to_owned()); } + #[actix_rt::test] + async fn paths_decoded() { + let resource = ResourceDef::new("/{key}/{value}"); + let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%251").to_srv_request(); + resource.capture_match_info(req.match_info_mut()); + + let (req, mut pl) = req.into_parts(); + let path_items = Path::::from_request(&req, &mut pl).await.unwrap(); + assert_eq!(path_items.key, "na+me"); + assert_eq!(path_items.value, "us/er%1"); + } + #[actix_rt::test] async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") From 05336269f98045703b27a0fbc18fe84f06456125 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:33:44 +0000 Subject: [PATCH 43/66] prepare actix-router release 0.5.0-beta.4 --- Cargo.toml | 2 +- actix-router/CHANGES.md | 3 +++ actix-router/Cargo.toml | 2 +- actix-web-codegen/Cargo.toml | 4 ++-- docs/graphs/web-only.dot | 1 + src/types/json.rs | 4 ++-- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5c4abe46f..cfb7cc0ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-http = "3.0.0-beta.17" -actix-router = "0.5.0-beta.3" +actix-router = "0.5.0-beta.4" actix-web-codegen = "0.5.0-beta.6" ahash = "0.7" diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 7b8615570..66b5379fc 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-beta.4 - 2022-01-04 - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index c63448bc7..801613568 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-beta.3" +version = "0.5.0-beta.4" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 03ff4698f..cd463cef9 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -15,10 +15,10 @@ edition = "2018" proc-macro = true [dependencies] +actix-router = "0.5.0-beta.4" +proc-macro2 = "1" quote = "1" syn = { version = "1", features = ["full", "parsing"] } -proc-macro2 = "1" -actix-router = "0.5.0-beta.3" [dev-dependencies] actix-macros = "0.2.3" diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index ee74c292b..b27dd0943 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -15,6 +15,7 @@ digraph { "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } "awc" -> { "actix-http" } + "actix-web-codegen" -> { "actix-router" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-multipart" -> { "actix-web" } "actix-files" -> { "actix-web" } diff --git a/src/types/json.rs b/src/types/json.rs index be6078b2b..8fdbfafa4 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -11,7 +11,7 @@ use std::{ }; use bytes::BytesMut; -use futures_core::{ready, stream::Stream as _}; +use futures_core::{ready, Stream as _}; use serde::{de::DeserializeOwned, Serialize}; use actix_http::Payload; @@ -515,7 +515,7 @@ mod tests { .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; - let resp = HttpResponse::from_error(s.err().unwrap()); + let resp = HttpResponse::from_error(s.unwrap_err()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let body = body::to_bytes(resp.into_body()).await.unwrap(); From 5abd1c2c2c49379e98dc801124523944038c25e4 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:34:16 +0000 Subject: [PATCH 44/66] prepare actix-web-codegen release 0.5.0-rc.1 --- Cargo.toml | 2 +- actix-web-codegen/CHANGES.md | 3 +++ actix-web-codegen/Cargo.toml | 2 +- actix-web-codegen/README.md | 4 ++-- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cfb7cc0ac..4d91b6a8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,7 +79,7 @@ actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-http = "3.0.0-beta.17" actix-router = "0.5.0-beta.4" -actix-web-codegen = "0.5.0-beta.6" +actix-web-codegen = "0.5.0-rc.1" ahash = "0.7" bytes = "1" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 5f8c0f259..c044ff74d 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-rc.1 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index cd463cef9..f0f29cc74 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.6" +version = "0.5.0-rc.1" description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index abb638cee..1fd97184c 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.6)](https://docs.rs/actix-web-codegen/0.5.0-beta.6) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-rc.1)](https://docs.rs/actix-web-codegen/0.5.0-rc.1) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From b338eb84736d766d5d62bf38e702877dc0f83e1b Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:34:52 +0000 Subject: [PATCH 45/66] prepare actix-http release 3.0.0-beta.18 --- 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 4d91b6a8a..464929968 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", default-features = false, optional = true } -actix-http = "3.0.0-beta.17" +actix-http = "3.0.0-beta.18" actix-router = "0.5.0-beta.4" actix-web-codegen = "0.5.0-rc.1" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 745e3afee..f5a469703 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.17" +actix-http = "3.0.0-beta.18" actix-service = "2" actix-utils = "3" actix-web = { version = "4.0.0-beta.18", default-features = false } diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index d973ce151..29d3e323b 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -52,4 +52,4 @@ tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } -actix-http = "3.0.0-beta.17" +actix-http = "3.0.0-beta.18" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 621b42450..935e35561 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.18 - 2022-01-04 ### Added - `impl Eq` for `header::ContentEncoding`. [#2501] - `impl Copy` for `QualityItem` where `T: Copy`. [#2501] diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 7c9b28944..e18614f1f 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.17" +version = "3.0.0-beta.18" authors = ["Nikolay Kim "] 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 084753ac9..9883cc3f0 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.17)](https://docs.rs/actix-http/3.0.0-beta.17) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.18)](https://docs.rs/actix-http/3.0.0-beta.18) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.17/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.17) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.18/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.18) [![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 715512111..5a8c3708e 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.17" +actix-http = "3.0.0-beta.18" futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } tokio = { version = "1.8.4", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index e7ac92e2e..b32fcee1f 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.17" +actix-http = "3.0.0-beta.18" actix-http-test = "3.0.0-beta.10" actix-rt = "2.1" actix-service = "2.0.0" diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 52ffca1ba..0252c1de0 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.17" +actix-http = "3.0.0-beta.18" actix-web = { version = "4.0.0-beta.18", default-features = false } bytes = "1" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 0650b5508..d6b6f56c1 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.17" +actix-http = "3.0.0-beta.18" actix-rt = { version = "2.1", default-features = false } actix-tls = { version = "3.0.0", 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.17", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.18", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } From 8621ae12f8b5dbd14f3ac00056b364e065c817e3 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:35:08 +0000 Subject: [PATCH 46/66] prepare actix-web release 4.0.0-beta.19 --- 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 9e5acce5d..e39eaef61 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.19 - 2022-01-04 ### Added - `impl Hash` for `http::header::Encoding`. [#2501] - `AcceptEncoding::negotiate()`. [#2501] diff --git a/Cargo.toml b/Cargo.toml index 464929968..05b4379ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.18" +version = "4.0.0-beta.19" authors = ["Nikolay Kim "] 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 3072ba1c0..b6ee79e3e 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![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.18)](https://docs.rs/actix-web/4.0.0-beta.18) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.19)](https://docs.rs/actix-web/4.0.0-beta.19) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.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.18/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.18) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.19/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.19)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index f5a469703..8f8cdbce5 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.18" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.18", default-features = false } +actix-web = { version = "4.0.0-beta.19", default-features = false } askama_escape = "0.10" bitflags = "1" @@ -44,5 +44,5 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.10" -actix-web = "4.0.0-beta.18" +actix-web = "4.0.0-beta.19" tempfile = "3.2" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 29d3e323b..6312b0573 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.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] } actix-http = "3.0.0-beta.18" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index e18614f1f..f458eacf5 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -82,7 +82,7 @@ zstd = { version = "0.9", optional = true } actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.18" +actix-web = "4.0.0-beta.19" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 5a8c3708e..b05dd82dc 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.18", default-features = false } +actix-web = { version = "4.0.0-beta.19", default-features = false } bytes = "1" derive_more = "0.99.5" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index b32fcee1f..573da3e47 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -34,7 +34,7 @@ actix-http-test = "3.0.0-beta.10" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.18", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.17", 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 0252c1de0..e71684a3b 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.18" -actix-web = { version = "4.0.0-beta.18", default-features = false } +actix-web = { version = "4.0.0-beta.19", default-features = false } bytes = "1" bytestring = "1" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index f0f29cc74..b49fe099f 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.10" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.18" +actix-web = "4.0.0-beta.19" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index d6b6f56c1..e203988df 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -99,7 +99,7 @@ actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.18", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.19", features = ["openssl"] } brotli2 = "0.3.2" const-str = "0.3" From f659098d219927d3f33264dbfca92d584abc2af9 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:35:21 +0000 Subject: [PATCH 47/66] prepare awc release 3.0.0-beta.18 --- Cargo.toml | 2 +- actix-http-test/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- actix-web-actors/Cargo.toml | 2 +- awc/CHANGES.md | 4 ++++ awc/Cargo.toml | 2 +- awc/README.md | 4 ++-- 7 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05b4379ab..2ac6e5f06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,7 @@ url = "2.1" [dev-dependencies] actix-files = "0.6.0-beta.12" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } -awc = { version = "3.0.0-beta.17", features = ["openssl"] } +awc = { version = "3.0.0-beta.18", features = ["openssl"] } brotli2 = "0.3.2" const-str = "0.3" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 6312b0573..1a7520539 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -35,7 +35,7 @@ actix-tls = "3.0.0" actix-utils = "3.0.0" actix-rt = "2.2" actix-server = "2.0.0-rc.2" -awc = { version = "3.0.0-beta.17", default-features = false } +awc = { version = "3.0.0-beta.18", default-features = false } base64 = "0.13" bytes = "1" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 573da3e47..c3a4bb730 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.19", default-features = false, features = ["cookies"] } -awc = { version = "3.0.0-beta.17", default-features = false, features = ["cookies"] } +awc = { version = "3.0.0-beta.18", 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 e71684a3b..3dd731f43 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -28,7 +28,7 @@ tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.10" -awc = { version = "3.0.0-beta.17", default-features = false } +awc = { version = "3.0.0-beta.18", 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 346e95af4..3db649586 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.18 - 2022-01-04 +- No significant changes since `3.0.0-beta.17`. + + ## 3.0.0-beta.17 - 2021-12-29 ### Changed - Update `cookie` dependency (re-exported) to `0.16`. [#2555] diff --git a/awc/Cargo.toml b/awc/Cargo.toml index e203988df..9ac7be0d4 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.17" +version = "3.0.0-beta.18" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/awc/README.md b/awc/README.md index ace2b2eb5..6a68ac05a 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.17)](https://docs.rs/awc/3.0.0-beta.17) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.18)](https://docs.rs/awc/3.0.0-beta.18) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.17/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.17) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.18/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.18) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources From bcc8d5c4413297ed7783483b59bf58fc34af9db6 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:36:56 +0000 Subject: [PATCH 48/66] prepare actix-multipart release 0.4.0-beta.12 --- Cargo.toml | 2 +- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 2 +- actix-files/README.md | 4 ++-- actix-multipart/CHANGES.md | 3 +++ actix-multipart/Cargo.toml | 2 +- actix-multipart/README.md | 4 ++-- awc/CHANGES.md | 2 +- 8 files changed, 14 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2ac6e5f06..28c822573 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-files = "0.6.0-beta.12" +actix-files = "0.6.0-beta.13" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.18", features = ["openssl"] } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 93a122941..e8a07d884 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.13 - 2022-01-04 - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 8f8cdbce5..a83dd399d 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.12" +version = "0.6.0-beta.13" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/actix-files/README.md b/actix-files/README.md index 41dd714d3..be878d958 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.12)](https://docs.rs/actix-files/0.6.0-beta.12) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.13)](https://docs.rs/actix-files/0.6.0-beta.13) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.12/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.12) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.13/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.13) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 65fe51d44..92feade3b 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.4.0-beta.12 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index b05dd82dc..4f41caf44 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.11" +version = "0.4.0-beta.12" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-multipart/README.md b/actix-multipart/README.md index a773f5d52..91cd8a6e9 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,11 +3,11 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.11)](https://docs.rs/actix-multipart/0.4.0-beta.11) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.12)](https://docs.rs/actix-multipart/0.4.0-beta.12) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.11/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.11) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.12/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.12) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 3db649586..f2c81ef25 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -4,7 +4,7 @@ ## 3.0.0-beta.18 - 2022-01-04 -- No significant changes since `3.0.0-beta.17`. +- Minimum supported Rust version (MSRV) is now 1.54. ## 3.0.0-beta.17 - 2021-12-29 From 742ad56d30b965a9b00040597e72cc874c3b336e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:37:14 +0000 Subject: [PATCH 49/66] prepare actix-web-actors release 4.0.0-beta.10 --- actix-web-actors/CHANGES.md | 3 +++ actix-web-actors/Cargo.toml | 2 +- actix-web-actors/README.md | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 5c8091fbb..74ab3c785 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.10 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 3dd731f43..f9abf3a0f 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.9" +version = "4.0.0-beta.10" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index 0bd007e6a..60e6a9bd9 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web-actors/4.0.0-beta.9) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.10)](https://docs.rs/actix-web-actors/4.0.0-beta.10) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) From 8c975bcc1f0631e4986146b370bb1f6221a9926d Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:37:33 +0000 Subject: [PATCH 50/66] prepare actix-http-test release 3.0.0-beta.11 --- actix-http-test/CHANGES.md | 3 +++ actix-http-test/Cargo.toml | 2 +- actix-http-test/README.md | 4 ++-- actix-http/Cargo.toml | 2 +- actix-test/Cargo.toml | 2 +- awc/Cargo.toml | 2 +- 6 files changed, 9 insertions(+), 6 deletions(-) diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index e83e95bd3..b62281798 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.11 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 1a7520539..db92f1983 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.10" +version = "3.0.0-beta.11" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] diff --git a/actix-http-test/README.md b/actix-http-test/README.md index e2cdc0ba2..10c04b368 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,11 +3,11 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http-test/3.0.0-beta.10) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.11)](https://docs.rs/actix-http-test/3.0.0-beta.11) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
-[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.10) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.11/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.11) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index f458eacf5..b0aa199b9 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -79,7 +79,7 @@ flate2 = { version = "1.0.13", optional = true } zstd = { version = "0.9", optional = true } [dev-dependencies] -actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } actix-web = "4.0.0-beta.19" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index c3a4bb730..39fb43b3e 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -30,7 +30,7 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.4.1" actix-http = "3.0.0-beta.18" -actix-http-test = "3.0.0-beta.10" +actix-http-test = "3.0.0-beta.11" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 9ac7be0d4..339391e09 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -94,7 +94,7 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-http = { version = "3.0.0-beta.18", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } From 8bbf2b505295b9729b9894e5612deff6520ddf9a Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Tue, 4 Jan 2022 15:37:48 +0000 Subject: [PATCH 51/66] prepare actix-test release 0.1.0-beta.11 --- 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 28c822573..130307827 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ url = "2.1" [dev-dependencies] actix-files = "0.6.0-beta.13" -actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.18", features = ["openssl"] } brotli2 = "0.3.2" diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index a83dd399d..fe780f5ab 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -43,6 +43,6 @@ tokio-uring = { version = "0.1", optional = true } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.10" +actix-test = "0.1.0-beta.11" actix-web = "4.0.0-beta.19" tempfile = "3.2" diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 9838c6f5f..32ab2344f 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.11 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 39fb43b3e..89f28a5da 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.10" +version = "0.1.0-beta.11" authors = [ "Nikolay Kim ", "Rob Ede ", diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index f9abf3a0f..25b6ea538 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -27,7 +27,7 @@ tokio = { version = "1.8.4", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.10" +actix-test = "0.1.0-beta.11" awc = { version = "3.0.0-beta.18", default-features = false } env_logger = "0.9" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index b49fe099f..9b1887012 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -23,7 +23,7 @@ syn = { version = "1", features = ["full", "parsing"] } [dev-dependencies] actix-macros = "0.2.3" actix-rt = "2.2" -actix-test = "0.1.0-beta.10" +actix-test = "0.1.0-beta.11" actix-utils = "3.0.0" actix-web = "4.0.0-beta.19" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 339391e09..036c74da9 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.18", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } actix-server = "2.0.0-rc.2" -actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" actix-web = { version = "4.0.0-beta.19", features = ["openssl"] } From 0f7292c69a5a229c6d06536682001f7f4ea34ff7 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 5 Jan 2022 04:24:40 +0000 Subject: [PATCH 52/66] remove readme msrv link --- README.md | 4 ++-- scripts/bump | 11 ++++++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index b6ee79e3e..5c5a55743 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,12 @@ [![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.19)](https://docs.rs/actix-web/4.0.0-beta.19) -[![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) +![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.19/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.19)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) ![downloads](https://img.shields.io/crates/d/actix-web.svg) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) diff --git a/scripts/bump b/scripts/bump index 1cd190e03..c43b92dc8 100755 --- a/scripts/bump +++ b/scripts/bump @@ -17,9 +17,18 @@ if [ "$(uname)" = "Darwin" ]; then fi CARGO_MANIFEST=$DIR/Cargo.toml -CHANGELOG_FILE=$DIR/CHANGES.md README_FILE=$DIR/README.md +# determine changelog file name +if [ -f "$DIR/CHANGES.md" ]; then + CHANGELOG_FILE=$DIR/CHANGES.md +elif [ -f "$DIR/CHANGELOG.md" ]; then + CHANGELOG_FILE=$DIR/CHANGELOG.md +else + echo "No changelog file found" + exit 1 +fi + # 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")" From 49cfabeaf51a795ee49fc058bd05528225ad5101 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 5 Jan 2022 07:34:13 +0300 Subject: [PATCH 53/66] simplify Resource trait (#2568) Co-authored-by: Rob Ede --- actix-router/CHANGES.md | 4 ++++ actix-router/src/path.rs | 36 ++++++++++++++++++++++++++++--- actix-router/src/resource.rs | 5 ++--- actix-router/src/resource_path.rs | 7 ++++-- actix-router/src/router.rs | 22 ++++++++----------- src/service.rs | 6 ++++-- 6 files changed, 57 insertions(+), 23 deletions(-) diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 66b5379fc..3034ed794 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568] +- `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] + +[#2568]: https://github.com/actix/actix-web/pull/2568 ## 0.5.0-beta.4 - 2022-01-04 diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index 9af7b0b8b..fc7bb16ac 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use std::ops::Index; +use std::ops::{DerefMut, Index}; use firestorm::profile_method; use serde::de; @@ -213,8 +213,38 @@ impl Index for Path { } } -impl Resource for Path { - fn resource_path(&mut self) -> &mut Self { +impl Resource for Path { + type Path = T; + + fn resource_path(&mut self) -> &mut Path { self } } + +impl Resource for T +where + T: DerefMut>, + P: ResourcePath, +{ + type Path = P; + + fn resource_path(&mut self) -> &mut Path { + &mut *self + } +} + +#[cfg(test)] +mod tests { + use std::cell::RefCell; + + use super::*; + + #[test] + fn deref_impls() { + let mut foo = Path::new("/foo"); + let _ = (&mut foo).resource_path(); + + let foo = RefCell::new(foo); + let _ = foo.borrow_mut().resource_path(); + } +} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index f1eb9caf5..d39a6b923 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -678,15 +678,14 @@ impl ResourceDef { /// assert!(!try_match(&resource, &mut path)); /// assert_eq!(path.unprocessed(), "/user/admin/stars"); /// ``` - pub fn capture_match_info_fn( + pub fn capture_match_info_fn( &self, resource: &mut R, check_fn: F, user_data: U, ) -> bool where - R: Resource, - T: ResourcePath, + R: Resource, F: FnOnce(&R, U) -> bool, { profile_method!(capture_match_info_fn); diff --git a/actix-router/src/resource_path.rs b/actix-router/src/resource_path.rs index 91a7f2f55..00eb0927c 100644 --- a/actix-router/src/resource_path.rs +++ b/actix-router/src/resource_path.rs @@ -2,8 +2,11 @@ use crate::Path; // TODO: this trait is necessary, document it // see impl Resource for ServiceRequest -pub trait Resource { - fn resource_path(&mut self) -> &mut Path; +pub trait Resource { + /// Type of resource's path returned in `resource_path`. + type Path: ResourcePath; + + fn resource_path(&mut self) -> &mut Path; } pub trait ResourcePath { diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index fad1a440b..47940708e 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -1,6 +1,6 @@ use firestorm::profile_method; -use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; +use crate::{IntoPatterns, Resource, ResourceDef}; #[derive(Debug, Copy, Clone, PartialEq)] pub struct ResourceId(pub u16); @@ -26,10 +26,9 @@ impl Router { } } - pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> + pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> where - R: Resource

, - P: ResourcePath, + R: Resource, { profile_method!(recognize); @@ -42,10 +41,9 @@ impl Router { None } - pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> + pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> where - R: Resource

, - P: ResourcePath, + R: Resource, { profile_method!(recognize_mut); @@ -58,11 +56,10 @@ impl Router { None } - pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> + pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> where F: Fn(&R, &Option) -> bool, - R: Resource

, - P: ResourcePath, + R: Resource, { profile_method!(recognize_checked); @@ -75,15 +72,14 @@ impl Router { None } - pub fn recognize_mut_fn( + pub fn recognize_mut_fn( &mut self, resource: &mut R, check: F, ) -> Option<(&mut T, ResourceId)> where F: Fn(&R, &Option) -> bool, - R: Resource

, - P: ResourcePath, + R: Resource, { profile_method!(recognize_mut_checked); diff --git a/src/service.rs b/src/service.rs index 975556197..f15cbfc9f 100644 --- a/src/service.rs +++ b/src/service.rs @@ -307,9 +307,11 @@ impl ServiceRequest { } } -impl Resource for ServiceRequest { +impl Resource for ServiceRequest { + type Path = Url; + #[inline] - fn resource_path(&mut self) -> &mut Path { + fn resource_path(&mut self) -> &mut Path { self.match_info_mut() } } From 2462b6dd5d2162f8bbe26c3cbccb351e6497d31e Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 5 Jan 2022 07:42:52 +0300 Subject: [PATCH 54/66] generalize impl Responder for HttpResponse (#2567) Co-authored-by: Rob Ede --- CHANGES.md | 4 ++++ Cargo.toml | 1 + src/response/builder.rs | 11 ++++++++++- src/response/customize_responder.rs | 8 ++------ src/response/responder.rs | 22 +--------------------- src/response/response.rs | 21 ++++++++++++++++++++- 6 files changed, 38 insertions(+), 29 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e39eaef61..922e0c7c8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +- `HttpResponse` can now be used as a `Responder` with any body type. + +[#2501]: https://github.com/actix/actix-web/pull/2501 ## 4.0.0-beta.19 - 2022-01-04 diff --git a/Cargo.toml b/Cargo.toml index 130307827..a2b8fb885 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,6 +118,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"] rand = "0.8" rcgen = "0.8" rustls-pemfile = "0.2" +static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.9" } tls-rustls = { package = "rustls", version = "0.20.0" } zstd = "0.9" diff --git a/src/response/builder.rs b/src/response/builder.rs index 93d8ab567..bdb0aaa12 100644 --- a/src/response/builder.rs +++ b/src/response/builder.rs @@ -23,7 +23,7 @@ use cookie::{Cookie, CookieJar}; use crate::{ error::{Error, JsonPayloadError}, - BoxError, HttpResponse, + BoxError, HttpRequest, HttpResponse, Responder, }; /// An HTTP response builder. @@ -424,6 +424,15 @@ impl Future for HttpResponseBuilder { } } +impl Responder for HttpResponseBuilder { + type Body = BoxBody; + + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + self.finish() + } +} + #[cfg(test)] mod tests { use actix_http::body; diff --git a/src/response/customize_responder.rs b/src/response/customize_responder.rs index 11f6b2916..8cb146dda 100644 --- a/src/response/customize_responder.rs +++ b/src/response/customize_responder.rs @@ -1,12 +1,9 @@ use actix_http::{ - body::{EitherBody, MessageBody}, - error::HttpError, - header::HeaderMap, - header::TryIntoHeaderPair, + body::EitherBody, error::HttpError, header::HeaderMap, header::TryIntoHeaderPair, StatusCode, }; -use crate::{BoxError, HttpRequest, HttpResponse, Responder}; +use crate::{HttpRequest, HttpResponse, Responder}; /// Allows overriding status code and headers for a [`Responder`]. /// @@ -143,7 +140,6 @@ impl CustomizeResponder { impl Responder for CustomizeResponder where T: Responder, - ::Error: Into, { type Body = EitherBody; diff --git a/src/response/responder.rs b/src/response/responder.rs index 319b824f1..d1b9e49e0 100644 --- a/src/response/responder.rs +++ b/src/response/responder.rs @@ -7,7 +7,7 @@ use actix_http::{ }; use bytes::{Bytes, BytesMut}; -use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; +use crate::{Error, HttpRequest, HttpResponse}; use super::CustomizeResponder; @@ -57,15 +57,6 @@ pub trait Responder { } } -impl Responder for HttpResponse { - type Body = BoxBody; - - #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - self - } -} - impl Responder for actix_http::Response { type Body = BoxBody; @@ -75,15 +66,6 @@ impl Responder for actix_http::Response { } } -impl Responder for HttpResponseBuilder { - type Body = BoxBody; - - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { - self.finish() - } -} - impl Responder for actix_http::ResponseBuilder { type Body = BoxBody; @@ -96,7 +78,6 @@ impl Responder for actix_http::ResponseBuilder { impl Responder for Option where T: Responder, - ::Error: Into, { type Body = EitherBody; @@ -111,7 +92,6 @@ where impl Responder for Result where T: Responder, - ::Error: Into, E: Into, { type Body = EitherBody; diff --git a/src/response/response.rs b/src/response/response.rs index 6fa2082e7..f24a75b19 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -22,7 +22,7 @@ use { cookie::Cookie, }; -use crate::{error::Error, HttpResponseBuilder}; +use crate::{error::Error, HttpRequest, HttpResponseBuilder, Responder}; /// An outgoing response. pub struct HttpResponse { @@ -311,6 +311,18 @@ impl Future for HttpResponse { } } +impl Responder for HttpResponse +where + B: MessageBody + 'static, +{ + type Body = B; + + #[inline] + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + self + } +} + #[cfg(feature = "cookies")] pub struct CookieIter<'a> { iter: std::slice::Iter<'a, HeaderValue>, @@ -333,9 +345,16 @@ impl<'a> Iterator for CookieIter<'a> { #[cfg(test)] mod tests { + use static_assertions::assert_impl_all; + use super::*; use crate::http::header::{HeaderValue, COOKIE}; + assert_impl_all!(HttpResponse: Responder); + assert_impl_all!(HttpResponse: Responder); + assert_impl_all!(HttpResponse<&'static str>: Responder); + assert_impl_all!(HttpResponse: Responder); + #[test] fn test_debug() { let resp = HttpResponse::Ok() From fe0bbfb3da03a29ee7b715334531f4b770496e2d Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 5 Jan 2022 13:48:20 +0300 Subject: [PATCH 55/66] optimize PathDeserializer (#2570) --- actix-router/src/de.rs | 122 +++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 40 deletions(-) diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index ec7b1066a..27aa49ef2 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -1,3 +1,5 @@ +use std::borrow::Cow; + use serde::de::{self, Deserializer, Error as DeError, Visitor}; use serde::forward_to_deserialize_any; @@ -20,7 +22,7 @@ macro_rules! unsupported_type { } macro_rules! parse_single_value { - ($trait_fn:ident, $visit_fn:ident, $tp:expr) => { + ($trait_fn:ident) => { fn $trait_fn(self, visitor: V) -> Result where V: Visitor<'de>, @@ -34,15 +36,10 @@ macro_rules! parse_single_value { .as_str(), )) } else { - let decoded = FULL_QUOTER - .with(|q| q.requote(self.path[0].as_bytes())) - .unwrap_or_else(|| self.path[0].to_owned()); - - let v = decoded.parse().map_err(|_| { - de::Error::custom(format!("can not parse {:?} to a {}", &self.path[0], $tp)) - })?; - - visitor.$visit_fn(v) + Value { + value: &self.path[0], + } + .$trait_fn(visitor) } } }; @@ -56,7 +53,8 @@ macro_rules! parse_value { { let decoded = FULL_QUOTER .with(|q| q.requote(self.value.as_bytes())) - .unwrap_or_else(|| self.value.to_owned()); + .map(Cow::Owned) + .unwrap_or(Cow::Borrowed(self.value)); let v = decoded.parse().map_err(|_| { de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) @@ -204,26 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> } unsupported_type!(deserialize_any, "'any'"); - unsupported_type!(deserialize_bytes, "bytes"); unsupported_type!(deserialize_option, "Option"); unsupported_type!(deserialize_identifier, "identifier"); unsupported_type!(deserialize_ignored_any, "ignored_any"); - parse_single_value!(deserialize_bool, visit_bool, "bool"); - parse_single_value!(deserialize_i8, visit_i8, "i8"); - parse_single_value!(deserialize_i16, visit_i16, "i16"); - parse_single_value!(deserialize_i32, visit_i32, "i32"); - parse_single_value!(deserialize_i64, visit_i64, "i64"); - parse_single_value!(deserialize_u8, visit_u8, "u8"); - parse_single_value!(deserialize_u16, visit_u16, "u16"); - parse_single_value!(deserialize_u32, visit_u32, "u32"); - parse_single_value!(deserialize_u64, visit_u64, "u64"); - parse_single_value!(deserialize_f32, visit_f32, "f32"); - parse_single_value!(deserialize_f64, visit_f64, "f64"); - parse_single_value!(deserialize_str, visit_string, "String"); - parse_single_value!(deserialize_string, visit_string, "String"); - parse_single_value!(deserialize_byte_buf, visit_string, "String"); - parse_single_value!(deserialize_char, visit_char, "char"); + parse_single_value!(deserialize_bool); + parse_single_value!(deserialize_i8); + parse_single_value!(deserialize_i16); + parse_single_value!(deserialize_i32); + parse_single_value!(deserialize_i64); + parse_single_value!(deserialize_u8); + parse_single_value!(deserialize_u16); + parse_single_value!(deserialize_u32); + parse_single_value!(deserialize_u64); + parse_single_value!(deserialize_f32); + parse_single_value!(deserialize_f64); + parse_single_value!(deserialize_str); + parse_single_value!(deserialize_string); + parse_single_value!(deserialize_bytes); + parse_single_value!(deserialize_byte_buf); + parse_single_value!(deserialize_char); } struct ParamsDeserializer<'de, T: ResourcePath> { @@ -303,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> { parse_value!(deserialize_u64, visit_u64, "u64"); parse_value!(deserialize_f32, visit_f32, "f32"); parse_value!(deserialize_f64, visit_f64, "f64"); - parse_value!(deserialize_string, visit_string, "String"); - parse_value!(deserialize_byte_buf, visit_string, "String"); parse_value!(deserialize_char, visit_char, "char"); fn deserialize_ignored_any(self, visitor: V) -> Result @@ -332,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> { visitor.visit_unit() } - fn deserialize_bytes(self, visitor: V) -> Result - where - V: Visitor<'de>, - { - visitor.visit_borrowed_bytes(self.value.as_bytes()) - } - fn deserialize_str(self, visitor: V) -> Result where V: Visitor<'de>, { - visitor.visit_borrowed_str(self.value) + match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) { + Some(s) => visitor.visit_string(s), + None => visitor.visit_borrowed_str(self.value), + } + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) { + Some(s) => visitor.visit_byte_buf(s.into()), + None => visitor.visit_borrowed_bytes(self.value.as_bytes()), + } + } + + fn deserialize_byte_buf(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_bytes(visitor) + } + + fn deserialize_string(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + self.deserialize_str(visitor) } fn deserialize_option(self, visitor: V) -> Result @@ -671,12 +687,12 @@ mod tests { fn deserialize_path_decode_seq() { let rdef = ResourceDef::new("/{key}/{value}"); - let mut path = Path::new("/%25/%2F"); + let mut path = Path::new("/%30%25/%30%2F"); rdef.capture_match_info(&mut path); let de = PathDeserializer::new(&path); let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap(); - assert_eq!(segment.0, "%"); - assert_eq!(segment.1, "/"); + assert_eq!(segment.0, "0%"); + assert_eq!(segment.1, "0/"); } #[test] @@ -697,6 +713,32 @@ mod tests { assert_eq!(vals.value, "/"); } + #[test] + fn deserialize_borrowed() { + #[derive(Debug, Deserialize)] + struct Params<'a> { + val: &'a str, + } + + let rdef = ResourceDef::new("/{val}"); + + let mut path = Path::new("/X"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(params.val, "X"); + let de = PathDeserializer::new(&path); + let params: &str = serde::Deserialize::deserialize(de).unwrap(); + assert_eq!(params, "X"); + + let mut path = Path::new("/%2F"); + rdef.capture_match_info(&mut path); + let de = PathDeserializer::new(&path); + assert!( as serde::Deserialize>::deserialize(de).is_err()); + let de = PathDeserializer::new(&path); + assert!(<&str as serde::Deserialize>::deserialize(de).is_err()); + } + // #[test] // fn test_extract_path_decode() { // let mut router = Router::<()>::default(); From 4ebf16890d2e3e45dd9f8352d5ddd712eafa6c47 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 5 Jan 2022 11:47:14 +0000 Subject: [PATCH 56/66] add `GuardContext::header` (#2569) --- CHANGES.md | 10 +++++++--- src/guard.rs | 22 +++++++++++++++++++++- src/http/header/accept.rs | 4 ++-- 3 files changed, 30 insertions(+), 6 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 922e0c7c8..ec09de2ac 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,14 @@ # Changes ## Unreleased - 2021-xx-xx -### Changed -- `HttpResponse` can now be used as a `Responder` with any body type. +### Added +- `GuardContext::header` [#2569] -[#2501]: https://github.com/actix/actix-web/pull/2501 +### Changed +- `HttpResponse` can now be used as a `Responder` with any body type. [#2567] + +[#2567]: https://github.com/actix/actix-web/pull/2567 +[#2569]: https://github.com/actix/actix-web/pull/2569 ## 4.0.0-beta.19 - 2022-01-04 diff --git a/src/guard.rs b/src/guard.rs index 7a015d2da..f4200a382 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -54,7 +54,7 @@ use std::{ use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; -use crate::service::ServiceRequest; +use crate::{http::header::Header, service::ServiceRequest}; /// Provides access to request parts that are useful during routing. #[derive(Debug)] @@ -80,6 +80,26 @@ impl<'a> GuardContext<'a> { pub fn req_data_mut(&self) -> RefMut<'a, Extensions> { self.req.req_data_mut() } + + /// Extracts a typed header from the request. + /// + /// Returns `None` if parsing `H` fails. + /// + /// # Examples + /// ``` + /// use actix_web::{guard::fn_guard, http::header}; + /// + /// let image_accept_guard = fn_guard(|ctx| { + /// match ctx.header::() { + /// Some(hdr) => hdr.preference() == "image/*", + /// None => false, + /// } + /// }); + /// ``` + #[inline] + pub fn header(&self) -> Option { + H::parse(self.req).ok() + } } /// Interface for routing guards. diff --git a/src/http/header/accept.rs b/src/http/header/accept.rs index 368a05bb2..744c9b6e8 100644 --- a/src/http/header/accept.rs +++ b/src/http/header/accept.rs @@ -2,10 +2,10 @@ use std::cmp::Ordering; use mime::Mime; -use super::QualityItem; +use super::{common_header, QualityItem}; use crate::http::header; -crate::http::header::common_header! { +common_header! { /// `Accept` header, defined /// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) /// From 2d11ab5977ad1394db01c76a79f94c17628a84a7 Mon Sep 17 00:00:00 2001 From: Michael <5672750+mibac138@users.noreply.github.com> Date: Wed, 5 Jan 2022 13:31:39 +0100 Subject: [PATCH 57/66] Add `ServiceConfig::configure` (#1988) Co-authored-by: Rob Ede --- CHANGES.md | 2 ++ src/config.rs | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index ec09de2ac..c379c1545 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,10 +3,12 @@ ## Unreleased - 2021-xx-xx ### Added - `GuardContext::header` [#2569] +- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] ### Changed - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] +[#1988]: https://github.com/actix/actix-web/pull/1988 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 diff --git a/src/config.rs b/src/config.rs index d68374387..2482ee6c4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -215,6 +215,17 @@ impl ServiceConfig { self } + /// Run external configuration as part of the application building process + /// + /// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting. + pub fn configure(&mut self, f: F) -> &mut Self + where + F: FnOnce(&mut ServiceConfig), + { + f(self); + self + } + /// Configure route for a specific path. /// /// Counterpart to [`App::route()`](crate::App::route). @@ -264,7 +275,7 @@ mod tests { use super::*; use crate::http::{Method, StatusCode}; - use crate::test::{call_service, init_service, read_body, TestRequest}; + use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest}; use crate::{web, App, HttpRequest, HttpResponse}; // allow deprecated `ServiceConfig::data` @@ -363,4 +374,22 @@ mod tests { let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + + #[actix_rt::test] + async fn nested_service_configure() { + fn cfg_root(cfg: &mut ServiceConfig) { + cfg.configure(cfg_sub); + } + + fn cfg_sub(cfg: &mut ServiceConfig) { + cfg.route("/", web::get().to(|| async { "hello world" })); + } + + let srv = init_service(App::new().configure(cfg_root)).await; + + let req = TestRequest::with_uri("/").to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert_body_eq!(res, b"hello world"); + } } From 4431c8da654f141f564c9715e4d2962d48e0ed69 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 5 Jan 2022 12:18:01 +0000 Subject: [PATCH 58/66] fix bench --- actix-http/benches/quality-value.rs | 17 +++++++++------ src/config.rs | 32 ----------------------------- 2 files changed, 11 insertions(+), 38 deletions(-) diff --git a/actix-http/benches/quality-value.rs b/actix-http/benches/quality-value.rs index 31b67f999..33ba9c4c8 100644 --- a/actix-http/benches/quality-value.rs +++ b/actix-http/benches/quality-value.rs @@ -42,32 +42,37 @@ mod _new { if x < 10 { f.write_str("00")?; // 0 is handled so it's not possible to have a trailing 0, we can just return - itoa::fmt(f, x) + itoa_fmt(f, x) } else if x < 100 { f.write_str("0")?; if x % 10 == 0 { // trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } else { // x is in range 101–999 if x % 100 == 0 { // two trailing 0s, divide by 100 and write - itoa::fmt(f, x / 100) + itoa_fmt(f, x / 100) } else if x % 10 == 0 { // one trailing 0, divide by 10 and write - itoa::fmt(f, x / 10) + itoa_fmt(f, x / 10) } else { - itoa::fmt(f, x) + itoa_fmt(f, x) } } } } } } + + pub fn itoa_fmt(mut wr: W, value: V) -> fmt::Result { + let mut buf = itoa::Buffer::new(); + wr.write_str(buf.format(value)) + } } mod _naive { diff --git a/src/config.rs b/src/config.rs index 2482ee6c4..77fba18ed 100644 --- a/src/config.rs +++ b/src/config.rs @@ -299,38 +299,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } - // #[actix_rt::test] - // async fn test_data_factory() { - // let cfg = |cfg: &mut ServiceConfig| { - // cfg.data_factory(|| { - // sleep(std::time::Duration::from_millis(50)).then(|_| { - // println!("READY"); - // Ok::<_, ()>(10usize) - // }) - // }); - // }; - - // let srv = - // init_service(App::new().configure(cfg).service( - // web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - // )); - // let req = TestRequest::default().to_request(); - // let resp = srv.call(req).await.unwrap(); - // assert_eq!(resp.status(), StatusCode::OK); - - // let cfg2 = |cfg: &mut ServiceConfig| { - // cfg.data_factory(|| Ok::<_, ()>(10u32)); - // }; - // let srv = init_service( - // App::new() - // .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())) - // .configure(cfg2), - // ); - // let req = TestRequest::default().to_request(); - // let resp = srv.call(req).await.unwrap(); - // assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - // } - #[actix_rt::test] async fn test_external_resource() { let srv = init_service( From c3ce33df0564f6838dcd9cfdcd9a4681145c9322 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 5 Jan 2022 18:02:28 +0300 Subject: [PATCH 59/66] unify generics across App, Scope and Resource (#2572) Co-authored-by: Rob Ede --- src/app.rs | 150 +++++++++++++++++++-------------------- src/middleware/compat.rs | 25 +++++-- src/middleware/mod.rs | 2 +- src/resource.rs | 125 ++++++++++++-------------------- src/scope.rs | 116 ++++++++++++------------------ 5 files changed, 186 insertions(+), 232 deletions(-) diff --git a/src/app.rs b/src/app.rs index 3fddc055b..da33ebc4b 100644 --- a/src/app.rs +++ b/src/app.rs @@ -51,7 +51,10 @@ impl App { } } -impl App { +impl App +where + T: ServiceFactory, +{ /// Set application (root level) data. /// /// Application data stored with `App::app_data()` method is available through the @@ -317,65 +320,63 @@ impl App { self } - /// 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 *Application*. + /// Registers an app-wide middleware. /// - /// Use middleware when you need to read or modify *every* request or - /// response in some way. + /// Registers middleware, in the form of a middleware compo nen t (type), that runs during + /// inbound and/or outbound processing in the request life-cycle (request -> response), + /// modifying request/response as necessary, across all requests managed by the `App`. /// - /// Notice that the keyword for registering middleware is `wrap`. As you - /// register middleware using `wrap` in the App builder, imagine wrapping - /// layers around an inner App. The first middleware layer exposed to a - /// Request is the outermost layer-- the *last* registered in - /// the builder chain. Consequently, the *first* middleware registered - /// in the builder chain is the *last* to execute during request processing. + /// Use middleware when you need to read or modify *every* request or response in some way. /// + /// Middleware can be applied similarly to individual `Scope`s and `Resource`s. + /// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`]. + /// + /// # Middleware Order + /// Notice that the keyword for registering middleware is `wrap`. As you register middleware + /// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first + /// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in + /// the builder chain). Consequently, the *first* middleware registered in the builder chain is + /// the *last* to start executing during request processing. + /// + /// Ordering is less obvious when wrapped services also have middleware applied. In this case, + /// middlewares are run in reverse order for `App` _and then_ in reverse order for the + /// wrapped service. + /// + /// # Examples /// ``` - /// use actix_service::Service; /// use actix_web::{middleware, web, App}; - /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; /// /// async fn index() -> &'static str { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap(middleware::Logger::default()) - /// .route("/index.html", web::get().to(index)); - /// } + /// let app = App::new() + /// .wrap(middleware::Logger::default()) + /// .route("/index.html", web::get().to(index)); /// ``` - pub fn wrap( + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap( self, mw: M, ) -> App< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, > where - T: ServiceFactory< - ServiceRequest, - Response = ServiceResponse, - Error = Error, - Config = (), - InitError = (), - >, - B: MessageBody, M: Transform< - T::Service, - ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - B1: MessageBody, + T::Service, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + 'static, + B: MessageBody, { App { endpoint: apply(mw, self.endpoint), @@ -388,61 +389,57 @@ impl App { } } - /// Registers middleware, in the form of a closure, that runs during inbound - /// and/or outbound processing in the request life-cycle (request -> response), - /// modifying request/response as necessary, across all requests managed by - /// the *Application*. + /// Registers an app-wide function middleware. + /// + /// `mw` is a closure that runs during inbound and/or outbound processing in the request + /// life-cycle (request -> response), modifying request/response as necessary, across all + /// requests handled by the `App`. /// /// Use middleware when you need to read or modify *every* request or response in some way. /// + /// Middleware can also be applied to individual `Scope`s and `Resource`s. + /// + /// See [`App::wrap`] for details on how middlewares compose with each other. + /// + /// # Examples /// ``` - /// use actix_service::Service; - /// use actix_web::{web, App}; + /// use actix_web::{dev::Service as _, middleware, web, App}; /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; /// /// async fn index() -> &'static str { /// "Welcome!" /// } /// - /// fn main() { - /// let app = App::new() - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index)); - /// } + /// let app = App::new() + /// .wrap_fn(|req, srv| { + /// let fut = srv.call(req); + /// async { + /// let mut res = fut.await?; + /// res.headers_mut() + /// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain")); + /// Ok(res) + /// } + /// }) + /// .route("/index.html", web::get().to(index)); /// ``` - pub fn wrap_fn( + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap_fn( self, mw: F, ) -> App< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, > where - T: ServiceFactory< - ServiceRequest, - Response = ServiceResponse, - Error = Error, - Config = (), - InitError = (), - >, + F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, + R: Future, Error>>, B: MessageBody, - F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future, Error>>, - B1: MessageBody, { App { endpoint: apply_fn_factory(self.endpoint, mw), @@ -458,15 +455,14 @@ impl App { impl IntoServiceFactory, Request> for App where - B: MessageBody, T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - T::Future: 'static, + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = Error, + InitError = (), + > + 'static, + B: MessageBody, { fn into_factory(self) -> AppInit { AppInit { diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 18c9ff6a7..ee8b8a498 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -150,11 +150,13 @@ mod tests { use actix_service::IntoService; - use crate::dev::ServiceRequest; - use crate::http::StatusCode; - use crate::middleware::{self, Condition, Logger}; - use crate::test::{call_service, init_service, TestRequest}; - use crate::{web, App, HttpResponse}; + use crate::{ + dev::ServiceRequest, + http::StatusCode, + middleware::{self, Condition, Logger}, + test::{self, call_service, init_service, TestRequest}, + web, App, HttpResponse, + }; #[actix_rt::test] #[cfg(all(feature = "cookies", feature = "__compress"))] @@ -219,4 +221,17 @@ mod tests { let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + + #[actix_rt::test] + async fn compat_noop_is_noop() { + let srv = test::ok_service(); + + let mw = Compat::noop() + .new_transform(srv.into_service()) + .await + .unwrap(); + + let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; + assert_eq!(resp.status(), StatusCode::OK); + } } diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index a781052a6..0a61ad6cb 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -1,4 +1,4 @@ -//! Commonly used middleware. +//! A collection of common middleware. mod compat; mod condition; diff --git a/src/resource.rs b/src/resource.rs index 8da0a8a85..193757eaa 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,6 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, rc::Rc}; -use actix_http::{body::BoxBody, Extensions}; +use actix_http::Extensions; use actix_router::{IntoPatterns, Patterns}; use actix_service::{ apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, @@ -42,7 +42,7 @@ use crate::{ /// /// If no matching route could be found, *405* response code get returned. Default behavior could be /// overridden with `default_resource()` method. -pub struct Resource { +pub struct Resource { endpoint: T, rdef: Patterns, name: Option, @@ -51,7 +51,6 @@ pub struct Resource { guards: Vec>, default: BoxedHttpServiceFactory, factory_ref: Rc>>, - _phantom: PhantomData, } impl Resource { @@ -69,21 +68,13 @@ impl Resource { default: boxed::factory(fn_service(|req: ServiceRequest| async { Ok(req.into_response(HttpResponse::MethodNotAllowed())) })), - _phantom: PhantomData, } } } -impl Resource +impl Resource where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - B: MessageBody, + T: ServiceFactory, { /// Set resource name. /// @@ -241,35 +232,35 @@ where self } - /// Register a resource middleware. + /// Registers a resource middleware. /// - /// This is similar to `App's` middlewares, but middleware get invoked on resource level. - /// Resource level middlewares are not allowed to change response - /// type (i.e modify response's body). + /// `mw` is a middleware component (type), that can modify the request and response across all + /// routes managed by this `Resource`. /// - /// **Note**: middlewares get called in opposite order of middlewares registration. - pub fn wrap( + /// See [`App::wrap`](crate::App::wrap) for more details. + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap( self, mw: M, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, - B1, > where M: Transform< - T::Service, - ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - B1: MessageBody, + T::Service, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + 'static, + B: MessageBody, { Resource { endpoint: apply(mw, self.endpoint), @@ -280,61 +271,34 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, - _phantom: PhantomData, } } - /// Register a resource middleware function. + /// Registers a resource function middleware. /// - /// This function accepts instance of `ServiceRequest` type and - /// mutable reference to the next middleware in chain. + /// `mw` is a closure that runs during inbound and/or outbound processing in the request + /// life-cycle (request -> response), modifying request/response as necessary, across all + /// requests handled by the `Resource`. /// - /// This is similar to `App's` middlewares, but middleware get invoked on resource level. - /// Resource level middlewares are not allowed to change response - /// type (i.e modify response's body). - /// - /// ``` - /// use actix_service::Service; - /// use actix_web::{web, App}; - /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; - /// - /// async fn index() -> &'static str { - /// "Welcome!" - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/index.html") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route(web::get().to(index))); - /// } - /// ``` - pub fn wrap_fn( + /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details. + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap_fn( self, mw: F, ) -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, - B1, > where - F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future, Error>>, - B1: MessageBody, + F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, + R: Future, Error>>, + B: MessageBody, { Resource { endpoint: apply_fn_factory(self.endpoint, mw), @@ -345,7 +309,6 @@ where default: self.default, app_data: self.app_data, factory_ref: self.factory_ref, - _phantom: PhantomData, } } @@ -373,7 +336,7 @@ where } } -impl HttpServiceFactory for Resource +impl HttpServiceFactory for Resource where T: ServiceFactory< ServiceRequest, @@ -517,7 +480,7 @@ mod tests { header::{self, HeaderValue}, Method, StatusCode, }, - middleware::{Compat, DefaultHeaders}, + middleware::DefaultHeaders, service::{ServiceRequest, ServiceResponse}, test::{call_service, init_service, TestRequest}, web, App, Error, HttpMessage, HttpResponse, @@ -525,31 +488,35 @@ mod tests { #[test] fn can_be_returned_from_fn() { - fn my_resource() -> Resource { - web::resource("/test").route(web::get().to(|| async { "hello" })) + fn my_resource_1() -> Resource { + web::resource("/test1").route(web::get().to(|| async { "hello" })) } - fn my_compat_resource() -> Resource< + fn my_resource_2() -> Resource< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, > { - web::resource("/test-compat") + web::resource("/test2") .wrap_fn(|req, srv| { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) - .wrap(Compat::noop()) .route(web::get().to(|| async { "hello" })) } + fn my_resource_3() -> impl HttpServiceFactory { + web::resource("/test2").route(web::get().to(|| async { "hello" })) + } + App::new() - .service(my_resource()) - .service(my_compat_resource()); + .service(my_resource_1()) + .service(my_resource_2()) + .service(my_resource_3()); } #[actix_rt::test] diff --git a/src/scope.rs b/src/scope.rs index fa9807f42..c05ce054d 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,9 +1,6 @@ -use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, mem, rc::Rc}; +use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; -use actix_http::{ - body::{BoxBody, MessageBody}, - Extensions, -}; +use actix_http::{body::MessageBody, Extensions}; use actix_router::{ResourceDef, Router}; use actix_service::{ apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, @@ -57,7 +54,7 @@ type Guards = Vec>; /// /// [pat]: crate::dev::ResourceDef#prefix-resources /// [dynamic segments]: crate::dev::ResourceDef#dynamic-segments -pub struct Scope { +pub struct Scope { endpoint: T, rdef: String, app_data: Option, @@ -66,7 +63,6 @@ pub struct Scope { default: Option>, external: Vec, factory_ref: Rc>>, - _phantom: PhantomData, } impl Scope { @@ -83,21 +79,13 @@ impl Scope { default: None, external: Vec::new(), factory_ref, - _phantom: Default::default(), } } } -impl Scope +impl Scope where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, - B: 'static, + T: ServiceFactory, { /// Add match guard to a scope. /// @@ -296,32 +284,35 @@ where self } - /// Registers middleware, in the form of a middleware component (type), that runs during inbound - /// processing in the request life-cycle (request -> response), modifying request as necessary, - /// across all requests managed by the *Scope*. + /// Registers a scope-wide middleware. /// - /// Use middleware when you need to read or modify *every* request in some way. - pub fn wrap( + /// `mw` is a middleware component (type), that can modify the request and response across all + /// sub-resources managed by this `Scope`. + /// + /// See [`App::wrap`](crate::App::wrap) for more details. + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap( self, mw: M, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, - B1, > where M: Transform< - T::Service, - ServiceRequest, - Response = ServiceResponse, - Error = Error, - InitError = (), - >, + T::Service, + ServiceRequest, + Response = ServiceResponse, + Error = Error, + InitError = (), + > + 'static, + B: MessageBody, { Scope { endpoint: apply(mw, self.endpoint), @@ -332,54 +323,34 @@ where default: self.default, external: self.external, factory_ref: self.factory_ref, - _phantom: PhantomData, } } - /// Registers middleware, in the form of a closure, that runs during inbound processing in the - /// request life-cycle (request -> response), modifying request as necessary, across all - /// requests managed by the *Scope*. + /// Registers a scope-wide function middleware. /// - /// # Examples - /// ``` - /// use actix_service::Service; - /// use actix_web::{web, App}; - /// use actix_web::http::header::{CONTENT_TYPE, HeaderValue}; + /// `mw` is a closure that runs during inbound and/or outbound processing in the request + /// life-cycle (request -> response), modifying request/response as necessary, across all + /// requests handled by the `Scope`. /// - /// async fn index() -> &'static str { - /// "Welcome!" - /// } - /// - /// let app = App::new().service( - /// web::scope("/app") - /// .wrap_fn(|req, srv| { - /// let fut = srv.call(req); - /// async { - /// let mut res = fut.await?; - /// res.headers_mut().insert( - /// CONTENT_TYPE, HeaderValue::from_static("text/plain"), - /// ); - /// Ok(res) - /// } - /// }) - /// .route("/index.html", web::get().to(index))); - /// ``` - pub fn wrap_fn( + /// See [`App::wrap_fn`](crate::App::wrap_fn) for examples and more details. + #[doc(alias = "middleware")] + #[doc(alias = "use")] // nodejs terminology + pub fn wrap_fn( self, mw: F, ) -> Scope< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, - B1, > where - F: Fn(ServiceRequest, &T::Service) -> R + Clone, - R: Future, Error>>, + F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static, + R: Future, Error>>, + B: MessageBody, { Scope { endpoint: apply_fn_factory(self.endpoint, mw), @@ -390,12 +361,11 @@ where default: self.default, external: self.external, factory_ref: self.factory_ref, - _phantom: PhantomData, } } } -impl HttpServiceFactory for Scope +impl HttpServiceFactory for Scope where T: ServiceFactory< ServiceRequest, @@ -596,7 +566,7 @@ mod tests { header::{self, HeaderValue}, Method, StatusCode, }, - middleware::{Compat, DefaultHeaders}, + middleware::DefaultHeaders, service::{ServiceRequest, ServiceResponse}, test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, @@ -604,16 +574,16 @@ mod tests { #[test] fn can_be_returned_from_fn() { - fn my_scope() -> Scope { + fn my_scope_1() -> Scope { web::scope("/test") .service(web::resource("").route(web::get().to(|| async { "hello" }))) } - fn my_compat_scope() -> Scope< + fn my_scope_2() -> Scope< impl ServiceFactory< ServiceRequest, Config = (), - Response = ServiceResponse, + Response = ServiceResponse, Error = Error, InitError = (), >, @@ -623,11 +593,17 @@ mod tests { let fut = srv.call(req); async { Ok(fut.await?.map_into_right_body::<()>()) } }) - .wrap(Compat::noop()) .service(web::resource("").route(web::get().to(|| async { "hello" }))) } - App::new().service(my_scope()).service(my_compat_scope()); + fn my_scope_3() -> impl HttpServiceFactory { + my_scope_2() + } + + App::new() + .service(my_scope_1()) + .service(my_scope_2()) + .service(my_scope_3()); } #[actix_rt::test] From 6c97d448b7efec443945dfc7c2363fd7b2b86962 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Wed, 12 Jan 2022 17:53:36 +0000 Subject: [PATCH 60/66] update tokio-uring to 0.2 (#2583) --- Cargo.toml | 4 +-- actix-files/CHANGES.md | 3 ++ actix-files/Cargo.toml | 2 +- actix-files/src/chunked.rs | 64 ++--------------------------------- actix-files/src/lib.rs | 4 +-- actix-files/src/named.rs | 41 +++++----------------- actix-files/src/service.rs | 4 +-- actix-files/tests/encoding.rs | 8 ++--- 8 files changed, 25 insertions(+), 105 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a2b8fb885..42de9df15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,8 +71,8 @@ experimental-io-uring = ["actix-server/io-uring"] [dependencies] actix-codec = "0.4.1" actix-macros = "0.2.3" -actix-rt = "2.3" -actix-server = "2.0.0-rc.2" +actix-rt = "2.6" +actix-server = "2.0.0-rc.4" actix-service = "2.0.0" actix-utils = "3.0.0" actix-tls = { version = "3.0.0", default-features = false, optional = true } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index e8a07d884..c0504fedc 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] + +[#2583]: https://github.com/actix/actix-web/pull/2583 ## 0.6.0-beta.13 - 2022-01-04 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index fe780f5ab..54a4bd47d 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -39,7 +39,7 @@ mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" -tokio-uring = { version = "0.1", optional = true } +tokio-uring = { version = "0.2", optional = true, features = ["bytes"] } [dev-dependencies] actix-rt = "2.2" diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index 68221ccc3..3ee2ee072 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes}; use futures_core::{ready, Stream}; use pin_project_lite::pin_project; +#[cfg(feature = "experimental-io-uring")] +use bytes::BytesMut; + use super::named::File; pin_project! { @@ -214,64 +217,3 @@ where } } } - -#[cfg(feature = "experimental-io-uring")] -use bytes_mut::BytesMut; - -// TODO: remove new type and use bytes::BytesMut directly -#[doc(hidden)] -#[cfg(feature = "experimental-io-uring")] -mod bytes_mut { - use std::ops::{Deref, DerefMut}; - - use tokio_uring::buf::{IoBuf, IoBufMut}; - - #[derive(Debug)] - pub struct BytesMut(bytes::BytesMut); - - impl BytesMut { - pub(super) fn new() -> Self { - Self(bytes::BytesMut::new()) - } - } - - impl Deref for BytesMut { - type Target = bytes::BytesMut; - - fn deref(&self) -> &Self::Target { - &self.0 - } - } - - impl DerefMut for BytesMut { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } - } - - unsafe impl IoBuf for BytesMut { - fn stable_ptr(&self) -> *const u8 { - self.0.as_ptr() - } - - fn bytes_init(&self) -> usize { - self.0.len() - } - - fn bytes_total(&self) -> usize { - self.0.capacity() - } - } - - unsafe impl IoBufMut for BytesMut { - fn stable_mut_ptr(&mut self) -> *mut u8 { - self.0.as_mut_ptr() - } - - unsafe fn set_init(&mut self, init_len: usize) { - if self.len() < init_len { - self.0.set_len(init_len); - } - } - } -} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index a11aa32c7..af404721c 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -67,8 +67,8 @@ mod tests { time::{Duration, SystemTime}, }; - use actix_service::ServiceFactory; use actix_web::{ + dev::ServiceFactory, guard, http::{ header::{self, ContentDisposition, DispositionParam, DispositionType}, @@ -303,7 +303,7 @@ mod tests { let resp = file.respond_to(&req).await.unwrap(); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), - "application/javascript" + "application/javascript; charset=utf-8" ); assert_eq!( resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 019730dc6..14495e660 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -1,15 +1,16 @@ use std::{ - fmt, fs::Metadata, io, path::{Path, PathBuf}, time::{SystemTime, UNIX_EPOCH}, }; -use actix_service::{Service, ServiceFactory}; use actix_web::{ body::{self, BoxBody, SizedStream}, - dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, + dev::{ + self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, + ServiceRequest, ServiceResponse, + }, http::{ header::{ self, Charset, ContentDisposition, ContentEncoding, DispositionParam, @@ -37,7 +38,7 @@ bitflags! { impl Default for Flags { fn default() -> Self { - Flags::from_bits_truncate(0b0000_0111) + Flags::from_bits_truncate(0b0000_1111) } } @@ -65,12 +66,12 @@ impl Default for Flags { /// NamedFile::open_async("./static/index.html").await /// } /// ``` -#[derive(Deref, DerefMut)] +#[derive(Debug, Deref, DerefMut)] pub struct NamedFile { - path: PathBuf, #[deref] #[deref_mut] file: File, + path: PathBuf, modified: Option, pub(crate) md: Metadata, pub(crate) flags: Flags, @@ -80,32 +81,6 @@ pub struct NamedFile { pub(crate) encoding: Option, } -impl fmt::Debug for NamedFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("NamedFile") - .field("path", &self.path) - .field( - "file", - #[cfg(feature = "experimental-io-uring")] - { - &"tokio_uring::File" - }, - #[cfg(not(feature = "experimental-io-uring"))] - { - &self.file - }, - ) - .field("modified", &self.modified) - .field("md", &self.md) - .field("flags", &self.flags) - .field("status_code", &self.status_code) - .field("content_type", &self.content_type) - .field("content_disposition", &self.content_disposition) - .field("encoding", &self.encoding) - .finish() - } -} - #[cfg(not(feature = "experimental-io-uring"))] pub(crate) use std::fs::File; #[cfg(feature = "experimental-io-uring")] @@ -627,7 +602,7 @@ impl Service for NamedFileService { type Error = Error; type Future = LocalBoxFuture<'static, Result>; - actix_service::always_ready!(); + dev::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, _) = req.into_parts(); diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 152e1855e..4e8b72311 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -2,7 +2,7 @@ use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; use actix_web::{ body::BoxBody, - dev::{Service, ServiceRequest, ServiceResponse}, + dev::{self, Service, ServiceRequest, ServiceResponse}, error::Error, guard::Guard, http::{header, Method}, @@ -98,7 +98,7 @@ impl Service for FilesService { type Error = Error; type Future = LocalBoxFuture<'static, Result>; - actix_service::always_ready!(); + dev::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 652a7c12b..080292af5 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -19,12 +19,12 @@ async fn test_utf8_file_contents() { assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_TYPE), - Some(&HeaderValue::from_static("text/plain")), + Some(&HeaderValue::from_static("text/plain; charset=utf-8")), ); - // prefer UTF-8 encoding + // disable UTF-8 attribute let srv = - test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true))) + test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))) .await; let req = TestRequest::with_uri("/utf8.txt").to_request(); @@ -33,6 +33,6 @@ async fn test_utf8_file_contents() { assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers().get(header::CONTENT_TYPE), - Some(&HeaderValue::from_static("text/plain; charset=utf-8")), + Some(&HeaderValue::from_static("text/plain")), ); } From 2a12b41456f40b28c1efe0ec6947e8f50ba22006 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 12 Jan 2022 21:31:48 +0300 Subject: [PATCH 61/66] fix support for 12 extractors (#2582) Co-authored-by: Rob Ede --- CHANGES.md | 2 ++ src/extract.rs | 21 +++++++++++---------- src/handler.rs | 49 +++++++++++++++++++++++++++++++++++-------------- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index c379c1545..805030dfb 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,10 +7,12 @@ ### Changed - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] +- Maximim number of extractors has changed from 10 to 12. [#2582] [#1988]: https://github.com/actix/actix-web/pull/1988 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 +[#2582]: https://github.com/actix/actix-web/pull/2582 ## 4.0.0-beta.19 - 2022-01-04 diff --git a/src/extract.rs b/src/extract.rs index f74a0a54e..de1cdde0c 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -290,16 +290,6 @@ impl FromRequest for Method { } } -#[doc(hidden)] -impl FromRequest for () { - type Error = Infallible; - type Future = Ready>; - - fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { - ok(()) - } -} - #[doc(hidden)] #[allow(non_snake_case)] mod tuple_from_req { @@ -388,6 +378,15 @@ mod tuple_from_req { } } + impl FromRequest for () { + type Error = Infallible; + type Future = Ready>; + + fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(()) + } + } + tuple_from_req! { TupleFromRequest1; A } tuple_from_req! { TupleFromRequest2; A, B } tuple_from_req! { TupleFromRequest3; A, B, C } @@ -398,6 +397,8 @@ mod tuple_from_req { tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H } tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I } tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } + tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K } + tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L } } #[cfg(test)] diff --git a/src/handler.rs b/src/handler.rs index d458e22e1..7eb70ed25 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -12,17 +12,14 @@ use crate::{ /// # What Is A Request Handler /// A request handler has three requirements: /// 1. It is an async function (or a function/closure that returns an appropriate future); -/// 1. The function accepts zero or more parameters that implement [`FromRequest`]; +/// 1. The function parameters (up to 12) implement [`FromRequest`]; /// 1. The async function (or future) resolves to a type that can be converted into an /// [`HttpResponse`] (i.e., it implements the [`Responder`] trait). /// /// # Compiler Errors /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not -/// fulfill one or more of the above requirements. -/// -/// Unfortunately we cannot provide a better compile error message (while keeping the trait's -/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added -/// to Rust. +/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on +/// implementing [`FromRequest`] and [`Responder`], respectively. /// /// # How Do Handlers Receive Variable Numbers Of Arguments /// Rest assured there is no macro magic here; it's just traits. @@ -62,13 +59,15 @@ use crate::{ /// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the /// bounds of the handler call after argument extraction: /// ```ignore -/// impl Handler<(Arg1, Arg2), R> for Func +/// impl Handler<(Arg1, Arg2)> for Func /// where -/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static, -/// R: Future, -/// R::Output: Responder, +/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static, +/// Fut: Future, /// { -/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R { +/// type Output = Fut::Output; +/// type Future = Fut; +/// +/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future { /// (self)(arg1, arg2) /// } /// } @@ -76,7 +75,6 @@ use crate::{ /// /// [arity]: https://en.wikipedia.org/wiki/Arity /// [`from_request`]: FromRequest::from_request -/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628 pub trait Handler: Clone + 'static { type Output; type Future: Future; @@ -121,8 +119,9 @@ where /// ``` macro_rules! factory_tuple ({ $($param:ident)* } => { impl Handler<($($param,)*)> for Func - where Func: Fn($($param),*) -> Fut + Clone + 'static, - Fut: Future, + where + Func: Fn($($param),*) -> Fut + Clone + 'static, + Fut: Future, { type Output = Fut::Output; type Future = Fut; @@ -148,3 +147,25 @@ factory_tuple! { A B C D E F G H I } factory_tuple! { A B C D E F G H I J } factory_tuple! { A B C D E F G H I J K } factory_tuple! { A B C D E F G H I J K L } + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_impl_handler(_: impl Handler) {} + + #[test] + fn arg_number() { + async fn handler_min() {} + + #[rustfmt::skip] + #[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)] + async fn handler_max( + _01: (), _02: (), _03: (), _04: (), _05: (), _06: (), + _07: (), _08: (), _09: (), _10: (), _11: (), _12: (), + ) {} + + assert_impl_handler(handler_min); + assert_impl_handler(handler_max); + } +} From d90c1a23317547701a284f522d63edd4f269a842 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Wed, 12 Jan 2022 21:59:22 +0300 Subject: [PATCH 62/66] convert error in `Result` extractor (#2581) Co-authored-by: Rob Ede --- CHANGES.md | 3 +++ src/extract.rs | 41 +++++++++++++++++++++++++---------------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 805030dfb..a82d0eddc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -7,11 +7,14 @@ ### Changed - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] +- `Result` extractor wrapper can now convert error types. [#2581] +- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] - Maximim number of extractors has changed from 10 to 12. [#2582] [#1988]: https://github.com/actix/actix-web/pull/1988 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 +[#2581]: https://github.com/actix/actix-web/pull/2581 [#2582]: https://github.com/actix/actix-web/pull/2582 diff --git a/src/extract.rs b/src/extract.rs index de1cdde0c..f16c29ca5 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -3,6 +3,7 @@ use std::{ convert::Infallible, future::Future, + marker::PhantomData, pin::Pin, task::{Context, Poll}, }; @@ -124,12 +125,11 @@ pub trait FromRequest: Sized { /// ); /// } /// ``` -impl FromRequest for Option +impl FromRequest for Option where T: FromRequest, - T::Future: 'static, { - type Error = Error; + type Error = Infallible; type Future = FromRequestOptFuture; #[inline] @@ -152,7 +152,7 @@ where Fut: Future>, E: Into, { - type Output = Result, Error>; + type Output = Result, Infallible>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -211,40 +211,42 @@ where /// ); /// } /// ``` -impl FromRequest for Result +impl FromRequest for Result where - T: FromRequest + 'static, - T::Error: 'static, - T::Future: 'static, + T: FromRequest, + T::Error: Into, { - type Error = Error; - type Future = FromRequestResFuture; + type Error = Infallible; + type Future = FromRequestResFuture; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { FromRequestResFuture { fut: T::from_request(req, payload), + _phantom: PhantomData, } } } pin_project! { - pub struct FromRequestResFuture { + pub struct FromRequestResFuture { #[pin] fut: Fut, + _phantom: PhantomData, } } -impl Future for FromRequestResFuture +impl Future for FromRequestResFuture where - Fut: Future>, + Fut: Future>, + Ei: Into, { - type Output = Result, Error>; + type Output = Result, Infallible>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); let res = ready!(this.fut.poll(cx)); - Poll::Ready(Ok(res)) + Poll::Ready(Ok(res.map_err(Into::into))) } } @@ -481,7 +483,14 @@ mod tests { .set_payload(Bytes::from_static(b"bye=world")) .to_http_parts(); - let r = Result::, Error>::from_request(&req, &mut pl) + struct MyError; + impl From for MyError { + fn from(_: Error) -> Self { + Self + } + } + + let r = Result::, MyError>::from_request(&req, &mut pl) .await .unwrap(); assert!(r.is_err()); From 32742d07155957d9a6a4eed9eae43adede614a16 Mon Sep 17 00:00:00 2001 From: Ali MJ Al-Nasrawy Date: Fri, 14 Jan 2022 22:45:32 +0300 Subject: [PATCH 63/66] support opaque app in test helpers (#2584) --- CHANGES.md | 2 ++ src/test/test_utils.rs | 47 +++++++++++++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index a82d0eddc..8ab756100 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,12 +10,14 @@ - `Result` extractor wrapper can now convert error types. [#2581] - Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] - Maximim number of extractors has changed from 10 to 12. [#2582] +- Removed bound `::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584] [#1988]: https://github.com/actix/actix-web/pull/1988 [#2567]: https://github.com/actix/actix-web/pull/2567 [#2569]: https://github.com/actix/actix-web/pull/2569 [#2581]: https://github.com/actix/actix-web/pull/2581 [#2582]: https://github.com/actix/actix-web/pull/2582 +[#2584]: https://github.com/actix/actix-web/pull/2584 ## 4.0.0-beta.19 - 2022-01-04 diff --git a/src/test/test_utils.rs b/src/test/test_utils.rs index 02d4c9bf3..8207ce270 100644 --- a/src/test/test_utils.rs +++ b/src/test/test_utils.rs @@ -1,4 +1,4 @@ -use std::fmt; +use std::error::Error as StdError; use actix_http::Request; use actix_service::IntoServiceFactory; @@ -135,7 +135,6 @@ pub async fn call_and_read_body(app: &S, req: Request) -> Bytes where S: Service, Error = Error>, B: MessageBody, - B::Error: fmt::Debug, { let res = call_service(app, req).await; read_body(res).await @@ -147,7 +146,6 @@ pub async fn read_response(app: &S, req: Request) -> Bytes where S: Service, Error = Error>, B: MessageBody, - B::Error: fmt::Debug, { let res = call_service(app, req).await; read_body(res).await @@ -186,11 +184,11 @@ where pub async fn read_body(res: ServiceResponse) -> Bytes where B: MessageBody, - B::Error: fmt::Debug, { let body = res.into_body(); body::to_bytes(body) .await + .map_err(Into::>::into) .expect("error reading test response body") } @@ -240,7 +238,6 @@ where pub async fn read_body_json(res: ServiceResponse) -> T where B: MessageBody, - B::Error: fmt::Debug, T: DeserializeOwned, { let body = read_body(res).await; @@ -300,7 +297,6 @@ pub async fn call_and_read_body_json(app: &S, req: Request) -> T where S: Service, Error = Error>, B: MessageBody, - B::Error: fmt::Debug, T: DeserializeOwned, { let res = call_service(app, req).await; @@ -313,7 +309,6 @@ pub async fn read_response_json(app: &S, req: Request) -> T where S: Service, Error = Error>, B: MessageBody, - B::Error: fmt::Debug, T: DeserializeOwned, { call_and_read_body_json(app, req).await @@ -325,7 +320,10 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::{http::header, test::TestRequest, web, App, HttpMessage, HttpResponse}; + use crate::{ + dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage, + HttpResponse, + }; #[actix_rt::test] async fn test_request_methods() { @@ -471,4 +469,37 @@ mod tests { assert_eq!(&result.id, "12345"); assert_eq!(&result.name, "User name"); } + + #[actix_rt::test] + #[allow(dead_code)] + async fn return_opaque_types() { + fn test_app() -> App< + impl ServiceFactory< + ServiceRequest, + Config = (), + Response = ServiceResponse, + Error = crate::Error, + InitError = (), + >, + > { + App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + )) + } + + async fn test_service( + ) -> impl Service, Error = crate::Error> + { + init_service(test_app()).await + } + + async fn compile_test(mut req: Vec) { + let svc = test_service().await; + call_service(&svc, req.pop().unwrap()).await; + call_and_read_body(&svc, req.pop().unwrap()).await; + read_body(call_service(&svc, req.pop().unwrap()).await).await; + let _: String = call_and_read_body_json(&svc, req.pop().unwrap()).await; + let _: String = read_body_json(call_service(&svc, req.pop().unwrap()).await).await; + } + } } From edbb9b047eab4f2897a5488d506ba1f4ef7957a7 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 14 Jan 2022 19:59:36 +0000 Subject: [PATCH 64/66] prepare actix-router release 0.5.0-rc.1 --- Cargo.toml | 2 +- actix-router/CHANGES.md | 3 +++ actix-router/Cargo.toml | 2 +- src/resource.rs | 2 +- 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 42de9df15..1323b4ccf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,7 +78,7 @@ actix-utils = "3.0.0" actix-tls = { version = "3.0.0", default-features = false, optional = true } actix-http = "3.0.0-beta.18" -actix-router = "0.5.0-beta.4" +actix-router = "0.5.0-rc.1" actix-web-codegen = "0.5.0-rc.1" ahash = "0.7" diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 3034ed794..f268ffa9c 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.5.0-rc.1 - 2022-01-14 - `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568] - `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index 801613568..56a755ef4 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.0-beta.4" +version = "0.5.0-rc.1" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", diff --git a/src/resource.rs b/src/resource.rs index 193757eaa..dd7d4b0d5 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -510,7 +510,7 @@ mod tests { } fn my_resource_3() -> impl HttpServiceFactory { - web::resource("/test2").route(web::get().to(|| async { "hello" })) + web::resource("/test3").route(web::get().to(|| async { "hello" })) } App::new() From 8faca783fa3084ce0feeca7256abd6508ef52646 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 14 Jan 2022 20:00:26 +0000 Subject: [PATCH 65/66] prepare actix-web release 4.0.0-beta.20 --- 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 8ab756100..b0fa81296 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 4.0.0-beta.20 - 2022-01-14 ### Added - `GuardContext::header` [#2569] - `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] diff --git a/Cargo.toml b/Cargo.toml index 1323b4ccf..b3b35790d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.19" +version = "4.0.0-beta.20" authors = ["Nikolay Kim "] 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 5c5a55743..0085c1d6d 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![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.19)](https://docs.rs/actix-web/4.0.0-beta.19) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.20)](https://docs.rs/actix-web/4.0.0-beta.20) ![MSRV](https://img.shields.io/badge/rustc-1.54+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.19/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.19) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.20/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.20)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 54a4bd47d..a783a2cb1 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.18" actix-service = "2" actix-utils = "3" -actix-web = { version = "4.0.0-beta.19", default-features = false } +actix-web = { version = "4.0.0-beta.20", default-features = false } askama_escape = "0.10" bitflags = "1" @@ -44,5 +44,5 @@ tokio-uring = { version = "0.2", optional = true, features = ["bytes"] } [dev-dependencies] actix-rt = "2.2" actix-test = "0.1.0-beta.11" -actix-web = "4.0.0-beta.19" +actix-web = "4.0.0-beta.20" tempfile = "3.2" diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index db92f1983..b8521dd0c 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.8.4", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] } actix-http = "3.0.0-beta.18" diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index b0aa199b9..519230aab 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -82,7 +82,7 @@ zstd = { version = "0.9", optional = true } actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] } actix-server = "2.0.0-rc.2" actix-tls = { version = "3.0.0", features = ["openssl"] } -actix-web = "4.0.0-beta.19" +actix-web = "4.0.0-beta.20" async-stream = "0.3" criterion = { version = "0.3", features = ["html_reports"] } diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 4f41caf44..03a2bdfe2 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.19", default-features = false } +actix-web = { version = "4.0.0-beta.20", default-features = false } bytes = "1" derive_more = "0.99.5" diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 89f28a5da..9bd41ed0c 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -34,7 +34,7 @@ actix-http-test = "3.0.0-beta.11" actix-rt = "2.1" actix-service = "2.0.0" actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.19", default-features = false, features = ["cookies"] } +actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.18", 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 25b6ea538..169665ddf 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.18" -actix-web = { version = "4.0.0-beta.19", default-features = false } +actix-web = { version = "4.0.0-beta.20", default-features = false } bytes = "1" bytestring = "1" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 9b1887012..51ccac27c 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.11" actix-utils = "3.0.0" -actix-web = "4.0.0-beta.19" +actix-web = "4.0.0-beta.20" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 036c74da9..8accf4d0d 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -99,7 +99,7 @@ actix-server = "2.0.0-rc.2" actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] } actix-utils = "3.0.0" -actix-web = { version = "4.0.0-beta.19", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.20", features = ["openssl"] } brotli2 = "0.3.2" const-str = "0.3" From 455d5c460d18743b5550de138ea81f27cef7d934 Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Fri, 14 Jan 2022 20:01:11 +0000 Subject: [PATCH 66/66] prepare actix-files release 0.6.0-beta.14 --- CHANGES.md | 2 +- Cargo.toml | 2 +- actix-files/CHANGES.md | 3 +++ actix-files/Cargo.toml | 2 +- actix-files/README.md | 4 ++-- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b0fa81296..fa4feab6e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -12,7 +12,7 @@ - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] - `Result` extractor wrapper can now convert error types. [#2581] - Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] -- Maximim number of extractors has changed from 10 to 12. [#2582] +- Maximum number of handler extractors has increased to 12. [#2582] - Removed bound `::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584] [#1988]: https://github.com/actix/actix-web/pull/1988 diff --git a/Cargo.toml b/Cargo.toml index b3b35790d..945d72ef8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -105,7 +105,7 @@ time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" [dev-dependencies] -actix-files = "0.6.0-beta.13" +actix-files = "0.6.0-beta.14" actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.18", features = ["openssl"] } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index c0504fedc..f37e27518 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.14 - 2022-01-14 - The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] [#2583]: https://github.com/actix/actix-web/pull/2583 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index a783a2cb1..304cfa9da 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.13" +version = "0.6.0-beta.14" authors = [ "Nikolay Kim ", "fakeshadow <24548779@qq.com>", diff --git a/actix-files/README.md b/actix-files/README.md index be878d958..77dd1677e 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,11 +3,11 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.13)](https://docs.rs/actix-files/0.6.0-beta.13) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.14)](https://docs.rs/actix-files/0.6.0-beta.14) [![Version](https://img.shields.io/badge/rustc-1.54+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.13/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.13) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.14/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.14) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)