diff --git a/.cargo/config.toml b/.cargo/config.toml index 40fe3e57..0bab205c 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,7 @@ [alias] chk = "hack check --workspace --all-features --tests --examples" lint = "hack --clean-per-run clippy --workspace --tests --examples" +ci-min = "hack check --workspace --no-default-features" +ci-min-test = "hack check --workspace --no-default-features --tests --examples" +ci-default = "hack check --workspace" +ci-full = "check --workspace --bins --examples --tests" diff --git a/CHANGES.md b/CHANGES.md index a55005c7..64e0891e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,7 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* `HttpResponse` and `HttpResponseBuilder` structs. [#2065] +[#2065]: https://github.com/actix/actix-web/pull/2065 ## 4.0.0-beta.5 - 2021-04-02 ### Added diff --git a/Cargo.toml b/Cargo.toml index 52db6c33..b8b18a84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,11 +16,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "secure-cookies"] - -[badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } +features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] [lib] name = "actix_web" @@ -38,6 +34,8 @@ members = [ "actix-http-test", "actix-test", ] +# enable when MSRV is 1.51+ +# resolver = "2" [features] default = ["compress", "cookies"] @@ -46,10 +44,10 @@ default = ["compress", "cookies"] compress = ["actix-http/compress"] # support for cookies -cookies = ["actix-http/cookies"] +cookies = ["cookie"] # secure cookies feature -secure-cookies = ["actix-http/secure-cookies"] +secure-cookies = ["cookie/secure"] # openssl openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] @@ -57,22 +55,6 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] -[[example]] -name = "basic" -required-features = ["compress"] - -[[example]] -name = "uds" -required-features = ["compress"] - -[[test]] -name = "test_server" -required-features = ["compress", "cookies"] - -[[example]] -name = "on_connect" -required-features = [] - [dependencies] actix-codec = "0.4.0-beta.1" actix-macros = "0.2.0" @@ -88,11 +70,13 @@ actix-http = "3.0.0-beta.5" ahash = "0.7" bytes = "1" +cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" either = "1.5.3" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } +itoa = "0.4" language-tags = "0.2" once_cell = "1.5" log = "0.4" @@ -137,6 +121,22 @@ actix-web-actors = { path = "actix-web-actors" } actix-web-codegen = { path = "actix-web-codegen" } awc = { path = "awc" } +[[test]] +name = "test_server" +required-features = ["compress", "cookies"] + +[[example]] +name = "basic" +required-features = ["compress"] + +[[example]] +name = "uds" +required-features = ["compress"] + +[[example]] +name = "on_connect" +required-features = [] + [[bench]] name = "server" harness = false diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 9b30cbaa..e5f2d477 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -1,4 +1,4 @@ -use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use actix_web::{http::StatusCode, ResponseError}; use derive_more::Display; /// Errors which can occur when serving static files. @@ -16,8 +16,8 @@ pub enum FilesError { /// Return `NotFound` for `FilesError` impl ResponseError for FilesError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND } } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 9765be3a..5e425867 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,13 @@ # Changes ## Unreleased - 2021-xx-xx +### Removed +* `cookies` feature flag. [#2065] +* Top-level `cookies` mod (re-export). [#2065] +* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] +* `impl ResponseError for CookieParseError`. [#2065] + +[#2065]: https://github.com/actix/actix-web/pull/2065 ## 3.0.0-beta.5 - 2021-04-02 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index e77d1139..573376b0 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -16,7 +16,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] +features = ["openssl", "rustls", "compress"] [lib] name = "actix_http" @@ -34,12 +34,6 @@ rustls = ["actix-tls/rustls"] # enable compression support compress = ["flate2", "brotli2"] -# support for cookies -cookies = ["cookie"] - -# support for secure cookies -secure-cookies = ["cookies", "cookie/secure"] - # trust-dns as client dns resolver trust-dns = ["trust-dns-resolver"] @@ -55,7 +49,6 @@ base64 = "0.13" bitflags = "1.2" bytes = "1" bytestring = "1" -cookie = { version = "0.14.1", features = ["percent-encode"], optional = true } derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } @@ -95,7 +88,7 @@ actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } criterion = "0.3" env_logger = "0.8" rcgen = "0.8" -serde_derive = "1.0" +serde = { version = "1.0", features = ["derive"] } tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index 4e03aa8a..af66f7d7 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -11,7 +11,7 @@ use std::{ }; use actix_codec::Encoder; -use actix_http::{error::Error, ws, HttpService, Request, Response}; +use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response}; use actix_rt::time::{interval, Interval}; use actix_server::Server; use bytes::{Bytes, BytesMut}; @@ -34,14 +34,14 @@ async fn main() -> io::Result<()> { .await } -async fn handler(req: Request) -> Result { +async fn handler(req: Request) -> Result>, Error> { log::info!("handshaking"); let mut res = ws::handshake(req.head())?; // handshake will always fail under HTTP/2 log::info!("responding"); - Ok(res.streaming(Heartbeat::new(ws::Codec::new()))) + Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))) } struct Heartbeat { diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 0178be80..03693ff2 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -14,12 +14,7 @@ use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; use serde_urlencoded::ser::Error as FormError; -use crate::body::Body; -use crate::helpers::Writer; -use crate::response::{Response, ResponseBuilder}; - -#[cfg(feature = "cookies")] -pub use crate::cookie::ParseError as CookieParseError; +use crate::{body::Body, helpers::Writer, Response, ResponseBuilder}; /// A specialized [`std::result::Result`] /// for actix web operations @@ -378,14 +373,6 @@ impl ResponseError for PayloadError { } } -/// Return `BadRequest` for `cookie::ParseError` -#[cfg(feature = "cookies")] -impl ResponseError for crate::cookie::ParseError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - #[derive(Debug, Display, From)] /// A set of errors that can occur during dispatching HTTP requests pub enum DispatchError { @@ -959,13 +946,6 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - #[cfg(feature = "cookies")] - #[test] - fn test_cookie_parse() { - let resp: Response = CookieParseError::EmptyName.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - #[test] fn test_as_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index bf036569..bba79217 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -346,7 +346,7 @@ where // send service call error as response Poll::Ready(Err(err)) => { - let res: Response = err.into().into(); + let res = Response::from_error(err.into()); let (res, body) = res.replace_body(()); self.as_mut().send_response(res, body.into_body())?; } diff --git a/actix-http/src/http_message.rs b/actix-http/src/http_message.rs index b1f04e50..0f4e347e 100644 --- a/actix-http/src/http_message.rs +++ b/actix-http/src/http_message.rs @@ -9,11 +9,6 @@ use crate::error::{ContentTypeError, ParseError}; use crate::extensions::Extensions; use crate::header::{Header, HeaderMap}; use crate::payload::Payload; -#[cfg(feature = "cookies")] -use crate::{cookie::Cookie, error::CookieParseError}; - -#[cfg(feature = "cookies")] -struct Cookies(Vec>); /// Trait that implements general purpose operations on HTTP messages. pub trait HttpMessage: Sized { @@ -104,41 +99,6 @@ pub trait HttpMessage: Sized { Ok(false) } } - - /// Load request cookies. - #[cfg(feature = "cookies")] - fn cookies(&self) -> Result>>, CookieParseError> { - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(header::COOKIE) { - let s = - str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); - } - } - } - self.extensions_mut().insert(Cookies(cookies)); - } - - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - #[cfg(feature = "cookies")] - 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<'a, T> HttpMessage for &'a mut T diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 574d4ef6..5dd23249 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -6,13 +6,11 @@ //! | `openssl` | TLS support via [OpenSSL]. | //! | `rustls` | TLS support via [rustls]. | //! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | -//! | `cookies` | Support for cookies backed by the [cookie] crate. | //! | `secure-cookies` | Adds for secure cookies. Enables `cookies` feature. | //! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! //! [OpenSSL]: https://crates.io/crates/openssl //! [rustls]: https://crates.io/crates/rustls -//! [cookie]: https://crates.io/crates/cookie //! [trust-dns]: https://crates.io/crates/trust-dns #![deny(rust_2018_idioms, nonstandard_style)] @@ -55,9 +53,6 @@ pub mod h2; pub mod test; pub mod ws; -#[cfg(feature = "cookies")] -pub use cookie; - pub use self::builder::HttpServiceBuilder; pub use self::config::{KeepAlive, ServiceConfig}; pub use self::error::{Error, ResponseError, Result}; @@ -78,8 +73,6 @@ pub mod http { pub use http::{uri, Error, Uri}; pub use http::{Method, StatusCode, Version}; - #[cfg(feature = "cookies")] - pub use crate::cookie::{Cookie, CookieBuilder}; pub use crate::header::HeaderMap; /// A collection of HTTP headers and helpers. diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 6438ccba..c89f5311 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -345,8 +345,8 @@ impl ResponseHead { } pub struct Message { - // Rc here should not be cloned by anyone. - // It's used to reuse allocation of T and no shared ownership is allowed. + /// Rc here should not be cloned by anyone. + /// It's used to reuse allocation of T and no shared ownership is allowed. head: Rc, } diff --git a/actix-http/src/request.rs b/actix-http/src/request.rs index 197ec11c..09c6dd29 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -2,16 +2,18 @@ use std::{ cell::{Ref, RefMut}, - fmt, net, + fmt, net, str, }; use http::{header, Method, Uri, Version}; -use crate::extensions::Extensions; -use crate::header::HeaderMap; -use crate::message::{Message, RequestHead}; -use crate::payload::{Payload, PayloadStream}; -use crate::HttpMessage; +use crate::{ + extensions::Extensions, + header::HeaderMap, + message::{Message, RequestHead}, + payload::{Payload, PayloadStream}, + HttpMessage, +}; /// Request pub struct Request

{ diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index b27f477c..a96a13bd 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -14,17 +14,16 @@ use bytes::{Bytes, BytesMut}; use futures_core::Stream; use serde::Serialize; -use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; -use crate::error::Error; -use crate::extensions::Extensions; -use crate::header::{IntoHeaderPair, IntoHeaderValue}; -use crate::http::header::{self, HeaderName}; -use crate::http::{Error as HttpError, HeaderMap, StatusCode}; -use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead}; -#[cfg(feature = "cookies")] use crate::{ - cookie::{Cookie, CookieJar}, - http::header::HeaderValue, + body::{Body, BodyStream, MessageBody, ResponseBody}, + error::Error, + extensions::Extensions, + header::{IntoHeaderPair, IntoHeaderValue}, + http::{ + header::{self, HeaderName}, + Error as HttpError, HeaderMap, StatusCode, + }, + message::{BoxedResponseHead, ConnectionType, ResponseHead}, }; /// An HTTP Response @@ -135,54 +134,6 @@ impl Response { &mut self.head.headers } - /// Get an iterator for the cookies set by this response - #[cfg(feature = "cookies")] - #[inline] - pub fn cookies(&self) -> CookieIter<'_> { - CookieIter { - iter: self.head.headers.get_all(header::SET_COOKIE), - } - } - - /// Add a cookie to this response - #[cfg(feature = "cookies")] - #[inline] - pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { - let h = &mut self.head.headers; - HeaderValue::from_str(&cookie.to_string()) - .map(|c| { - h.append(header::SET_COOKIE, c); - }) - .map_err(|e| e.into()) - } - - /// Remove all cookies with the given name from this response. Returns - /// the number of cookies removed. - #[cfg(feature = "cookies")] - #[inline] - pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.head.headers; - let vals: Vec = h - .get_all(header::SET_COOKIE) - .map(|v| v.to_owned()) - .collect(); - h.remove(header::SET_COOKIE); - - let mut count: usize = 0; - for v in vals { - if let Ok(s) = v.to_str() { - if let Ok(c) = Cookie::parse_encoded(s) { - if c.name() == name { - count += 1; - continue; - } - } - } - h.append(header::SET_COOKIE, v); - } - count - } - /// Connection upgrade status #[inline] pub fn upgrade(&self) -> bool { @@ -304,34 +255,12 @@ impl Future for Response { } } -#[cfg(feature = "cookies")] -pub struct CookieIter<'a> { - iter: header::GetAll<'a>, -} - -#[cfg(feature = "cookies")] -impl<'a> Iterator for CookieIter<'a> { - type Item = Cookie<'a>; - - #[inline] - fn next(&mut self) -> Option> { - for v in self.iter.by_ref() { - if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { - return Some(c); - } - } - None - } -} - /// An HTTP response builder. /// /// This type can be used to construct an instance of `Response` through a builder-like pattern. pub struct ResponseBuilder { head: Option, err: Option, - #[cfg(feature = "cookies")] - cookies: Option, } impl ResponseBuilder { @@ -341,8 +270,6 @@ impl ResponseBuilder { ResponseBuilder { head: Some(BoxedResponseHead::new(status)), err: None, - #[cfg(feature = "cookies")] - cookies: None, } } @@ -409,7 +336,10 @@ impl ResponseBuilder { } /// Replaced with [`Self::insert_header()`]. - #[deprecated = "Replaced with `insert_header((key, value))`."] + #[deprecated( + since = "4.0.0", + note = "Replaced with `insert_header((key, value))`. Will be removed in v5." + )] pub fn set_header(&mut self, key: K, value: V) -> &mut Self where K: TryInto, @@ -430,7 +360,10 @@ impl ResponseBuilder { } /// Replaced with [`Self::append_header()`]. - #[deprecated = "Replaced with `append_header((key, value))`."] + #[deprecated( + since = "4.0.0", + note = "Replaced with `append_header((key, value))`. Will be removed in v5." + )] pub fn header(&mut self, key: K, value: V) -> &mut Self where K: TryInto, @@ -523,63 +456,6 @@ impl ResponseBuilder { self } - /// Set a cookie - /// - /// ``` - /// use actix_http::{http, Request, Response}; - /// - /// fn index(req: Request) -> Response { - /// Response::Ok() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// } - /// ``` - #[cfg(feature = "cookies")] - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Remove cookie - /// - /// ``` - /// use actix_http::{http, Request, Response, HttpMessage}; - /// - /// fn index(req: Request) -> Response { - /// let mut builder = Response::Ok(); - /// - /// if let Some(ref cookie) = req.cookie("name") { - /// builder.del_cookie(cookie); - /// } - /// - /// builder.finish() - /// } - /// ``` - #[cfg(feature = "cookies")] - pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { - if self.cookies.is_none() { - self.cookies = Some(CookieJar::new()) - } - let jar = self.cookies.as_mut().unwrap(); - let cookie = cookie.clone().into_owned(); - jar.add_original(cookie.clone()); - jar.remove(cookie); - self - } - /// This method calls provided closure with builder reference if value is `true`. #[doc(hidden)] #[deprecated = "Use an if statement."] @@ -636,19 +512,7 @@ impl ResponseBuilder { return Response::from(Error::from(e)).into_body(); } - // allow unused mut when cookies feature is disabled - #[allow(unused_mut)] - let mut response = self.head.take().expect("cannot reuse response builder"); - - #[cfg(feature = "cookies")] - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.headers.append(header::SET_COOKIE, val), - Err(e) => return Response::from(Error::from(e)).into_body(), - }; - } - } + let response = self.head.take().expect("cannot reuse response builder"); Response { head: response, @@ -704,8 +568,6 @@ impl ResponseBuilder { ResponseBuilder { head: self.head.take(), err: self.err.take(), - #[cfg(feature = "cookies")] - cookies: self.cookies.take(), } } } @@ -724,29 +586,9 @@ fn parts<'a>( /// Convert `Response` to a `ResponseBuilder`. Body get dropped. impl From> for ResponseBuilder { fn from(res: Response) -> ResponseBuilder { - #[cfg(feature = "cookies")] - let jar = { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - - for c in res.cookies() { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - jar - }; - ResponseBuilder { head: Some(res.head), err: None, - #[cfg(feature = "cookies")] - cookies: jar, } } } @@ -764,33 +606,9 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder { msg.no_chunking(!head.chunked()); - #[cfg(feature = "cookies")] - let jar = { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - - let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE), - }; - - for c in cookies { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - jar - }; - ResponseBuilder { head: Some(msg), err: None, - #[cfg(feature = "cookies")] - cookies: jar, } } } @@ -893,8 +711,6 @@ mod tests { use super::*; use crate::body::Body; use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; - #[cfg(feature = "cookies")] - use crate::{http::header::SET_COOKIE, HttpMessage}; #[test] fn test_debug() { @@ -906,68 +722,6 @@ mod tests { assert!(dbg.contains("Response")); } - #[cfg(feature = "cookies")] - #[test] - fn test_response_cookies() { - let req = crate::test::TestRequest::default() - .append_header((COOKIE, "cookie1=value1")) - .append_header((COOKIE, "cookie2=value2")) - .finish(); - let cookies = req.cookies().unwrap(); - - let resp = Response::Ok() - .cookie( - crate::http::Cookie::build("name", "value") - .domain("www.rust-lang.org") - .path("/test") - .http_only(true) - .max_age(time::Duration::days(1)) - .finish(), - ) - .del_cookie(&cookies[0]) - .finish(); - - let mut val = resp - .headers() - .get_all(SET_COOKIE) - .map(|v| v.to_str().unwrap().to_owned()) - .collect::>(); - val.sort(); - - // the .del_cookie call - assert!(val[0].starts_with("cookie1=; Max-Age=0;")); - - // the .cookie call - assert_eq!( - val[1], - "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - ); - } - - #[cfg(feature = "cookies")] - #[test] - fn test_update_response_cookies() { - let mut r = Response::Ok() - .cookie(crate::http::Cookie::new("original", "val100")) - .finish(); - - r.add_cookie(&crate::http::Cookie::new("cookie2", "val200")) - .unwrap(); - r.add_cookie(&crate::http::Cookie::new("cookie2", "val250")) - .unwrap(); - r.add_cookie(&crate::http::Cookie::new("cookie3", "val300")) - .unwrap(); - - assert_eq!(r.cookies().count(), 4); - r.del_cookie("cookie2"); - - let mut iter = r.cookies(); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("original", "val100")); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("cookie3", "val300")); - } - #[test] fn test_basic_builder() { let resp = Response::Ok().insert_header(("X-TEST", "value")).finish(); @@ -1026,6 +780,7 @@ mod tests { #[test] fn test_serde_json_in_body() { use serde_json::json; + let resp = Response::build(StatusCode::OK).body(json!({"test-key":"test-value"})); assert_eq!(resp.body().get_ref(), br#"{"test-key":"test-value"}"#); @@ -1101,21 +856,22 @@ mod tests { assert_eq!(resp.body().get_ref(), b"test"); } - #[cfg(feature = "cookies")] #[test] fn test_into_builder() { let mut resp: Response = "test".into(); assert_eq!(resp.status(), StatusCode::OK); - resp.add_cookie(&crate::http::Cookie::new("cookie1", "val100")) - .unwrap(); + resp.headers_mut().insert( + HeaderName::from_static("cookie"), + HeaderValue::from_static("cookie1=val100"), + ); let mut builder: ResponseBuilder = resp.into(); let resp = builder.status(StatusCode::BAD_REQUEST).finish(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let cookie = resp.cookies().next().unwrap(); - assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100")); + let cookie = resp.headers().get_all("Cookie").next().unwrap(); + assert_eq!(cookie.to_str().unwrap(), "cookie1=val100"); } #[test] diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index ad3dc74b..ec781743 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -13,11 +13,6 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use bytes::{Bytes, BytesMut}; use http::{Method, Uri, Version}; -#[cfg(feature = "cookies")] -use crate::{ - cookie::{Cookie, CookieJar}, - header::{self, HeaderValue}, -}; use crate::{ header::{HeaderMap, IntoHeaderPair}, payload::Payload, @@ -54,8 +49,6 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, - #[cfg(feature = "cookies")] - cookies: CookieJar, payload: Option, } @@ -66,8 +59,6 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - #[cfg(feature = "cookies")] - cookies: CookieJar::new(), payload: None, })) } @@ -134,13 +125,6 @@ impl TestRequest { self } - /// Set cookie for this request. - #[cfg(feature = "cookies")] - pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { - parts(&mut self.0).cookies.add(cookie.into_owned()); - self - } - /// Set request payload. pub fn set_payload>(&mut self, data: B) -> &mut Self { let mut payload = crate::h1::Payload::empty(); @@ -169,22 +153,6 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - #[cfg(feature = "cookies")] - { - let cookie: String = inner - .cookies - .delta() - // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) - .collect::>() - .join("; "); - - if !cookie.is_empty() { - head.headers - .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); - } - } - req } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index cec73db9..1a82ad83 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -9,10 +9,8 @@ use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; use crate::{ - error::ResponseError, - header::HeaderValue, - message::RequestHead, - response::{Response, ResponseBuilder}, + error::ResponseError, header::HeaderValue, message::RequestHead, response::Response, + ResponseBuilder, }; mod codec; diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index cdbb5d39..5f91c60d 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -45,11 +45,10 @@ impl ResponseError for MultipartError { #[cfg(test)] mod tests { use super::*; - use actix_web::HttpResponse; #[test] fn test_multipart_error() { - let resp: HttpResponse = MultipartError::Boundary.error_response(); + let resp = MultipartError::Boundary.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index aa82fd04..db7a50c5 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -20,7 +20,7 @@ openssl = ["tls-openssl", "actix-http/openssl"] [dependencies] actix-codec = "0.4.0-beta.1" -actix-http = { version = "3.0.0-beta.5", features = ["cookies"] } +actix-http = "3.0.0-beta.5" actix-http-test = { version = "3.0.0-beta.4", features = [] } actix-service = "2.0.0-beta.4" actix-utils = "3.0.0-beta.2" diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index bd86c27a..8fab3328 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -37,12 +37,12 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed}; pub use actix_http::test::TestBuffer; use actix_http::{ http::{HeaderMap, Method}, - ws, HttpService, Request, + ws, HttpService, Request, Response, }; use actix_service::{map_config, IntoServiceFactory, ServiceFactory}; use actix_web::{ dev::{AppConfig, MessageBody, Server, Service}, - rt, web, Error, HttpResponse, + rt, web, Error, }; use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use futures_core::Stream; @@ -83,7 +83,7 @@ where S: ServiceFactory + 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, - S::Response: Into> + 'static, + S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, { @@ -122,7 +122,7 @@ where S: ServiceFactory + 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, - S::Response: Into> + 'static, + S::Response: Into> + 'static, >::Future: 'static, B: MessageBody + 'static, { diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 77d7041f..8c575206 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -22,9 +22,9 @@ use actix_http::{ http::HeaderValue, ws::{hash_key, Codec}, }; -use actix_web::dev::HttpResponseBuilder; use actix_web::error::{Error, PayloadError}; use actix_web::http::{header, Method, StatusCode}; +use actix_web::HttpResponseBuilder; use actix_web::{HttpRequest, HttpResponse}; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 7b95edba..b983e6b1 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,9 +1,10 @@ use std::future::Future; use std::task::{Context, Poll}; -use actix_utils::future; +use actix_utils::future::{ok, Ready}; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::http::header::{HeaderName, HeaderValue}; +use actix_web::http::StatusCode; use actix_web::{http, web::Path, App, Error, HttpResponse, Responder}; use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; use futures_core::future::LocalBoxFuture; @@ -56,12 +57,12 @@ async fn trace_test() -> impl Responder { #[get("/test")] fn auto_async() -> impl Future> { - future::ok(HttpResponse::Ok().finish()) + ok(HttpResponse::Ok().finish()) } #[get("/test")] fn auto_sync() -> impl Future> { - future::ok(HttpResponse::Ok().finish()) + ok(HttpResponse::Ok().finish()) } #[put("/test/{param}")] @@ -103,10 +104,10 @@ where type Error = Error; type Transform = ChangeStatusCodeMiddleware; type InitError = (); - type Future = future::Ready>; + type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { - future::ok(ChangeStatusCodeMiddleware { service }) + ok(ChangeStatusCodeMiddleware { service }) } } @@ -144,6 +145,7 @@ where #[get("/test/wrap", wrap = "ChangeStatusCode")] async fn get_wrap(_: Path) -> impl Responder { + // panic!("actually never gets called because path failed to extract"); HttpResponse::Ok() } @@ -257,6 +259,10 @@ async fn test_wrap() { let srv = actix_test::start(|| App::new().service(get_wrap)); let request = srv.request(http::Method::GET, srv.url("/test/wrap")); - let response = request.send().await.unwrap(); + let mut response = request.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); assert!(response.headers().contains_key("custom-header")); + let body = response.body().await.unwrap(); + let body = String::from_utf8(body.to_vec()).unwrap(); + assert!(body.contains("wrong number of parameters")); } diff --git a/awc/Cargo.toml b/awc/Cargo.toml index aebf73ef..27d8bdfb 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -41,7 +41,7 @@ rustls = ["tls-rustls", "actix-http/rustls"] compress = ["actix-http/compress"] # cookie parsing and cookie jar -cookies = ["actix-http/cookies"] +cookies = ["cookie"] # trust-dns as dns resolver trust-dns = ["actix-http/trust-dns"] @@ -54,6 +54,7 @@ actix-rt = { version = "2.1", default-features = false } base64 = "0.13" bytes = "1" +cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } itoa = "0.4" diff --git a/awc/src/lib.rs b/awc/src/lib.rs index f1aecbd3..562d6ee7 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -93,12 +93,11 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] -use std::convert::TryFrom; -use std::rc::Rc; -use std::time::Duration; +use std::{convert::TryFrom, rc::Rc, time::Duration}; #[cfg(feature = "cookies")] -pub use actix_http::cookie; +pub use cookie; + pub use actix_http::{client::Connector, http}; use actix_http::{ diff --git a/awc/src/request.rs b/awc/src/request.rs index 6ecb64f8..f5cb08f1 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -8,14 +8,14 @@ use futures_core::Stream; use serde::Serialize; use actix_http::body::Body; -#[cfg(feature = "cookies")] -use actix_http::cookie::{Cookie, CookieJar}; use actix_http::http::header::{self, IntoHeaderPair}; use actix_http::http::{ uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, }; use actix_http::{Error, RequestHead}; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::error::{FreezeRequestError, InvalidUrl}; use crate::frozen::FrozenClientRequest; use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest}; @@ -271,7 +271,7 @@ impl ClientRequest { /// async fn main() { /// let resp = awc::Client::new().get("https://www.rust-lang.org") /// .cookie( - /// awc::http::Cookie::build("name", "value") + /// awc::cookie::Cookie::build("name", "value") /// .domain("www.rust-lang.org") /// .path("/") /// .secure(true) @@ -494,7 +494,7 @@ impl ClientRequest { let cookie: String = jar .delta() // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .map(|c| c.stripped().encoded().to_string()) .collect::>() .join("; "); diff --git a/awc/src/response.rs b/awc/src/response.rs index 27ba83af..a966edd0 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -20,8 +20,7 @@ use futures_core::{ready, Stream}; use serde::de::DeserializeOwned; #[cfg(feature = "cookies")] -use actix_http::{cookie::Cookie, error::CookieParseError}; - +use crate::cookie::{Cookie, ParseError as CookieParseError}; use crate::error::JsonPayloadError; /// Client Response @@ -80,24 +79,6 @@ impl HttpMessage for ClientResponse { fn extensions_mut(&self) -> RefMut<'_, Extensions> { self.head.extensions_mut() } - - /// Load request cookies. - #[cfg(feature = "cookies")] - 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 - })) - } } impl ClientResponse { @@ -180,6 +161,37 @@ impl ClientResponse { 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 diff --git a/awc/src/test.rs b/awc/src/test.rs index 8e95396b..1abe7881 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,14 +1,11 @@ //! Test helpers for actix http client to use during testing. use actix_http::http::header::IntoHeaderPair; use actix_http::http::{StatusCode, Version}; -#[cfg(feature = "cookies")] -use actix_http::{ - cookie::{Cookie, CookieJar}, - http::header::{self, HeaderValue}, -}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::ClientResponse; /// Test `ClientResponse` builder @@ -92,6 +89,8 @@ impl TestResponse { #[cfg(feature = "cookies")] for cookie in self.cookies.delta() { + use actix_http::http::header::{self, HeaderValue}; + head.headers.insert( header::SET_COOKIE, HeaderValue::from_str(&cookie.encoded().to_string()).unwrap(), diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 8458d3e3..34b71f05 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -31,8 +31,6 @@ use std::net::SocketAddr; use std::{fmt, str}; use actix_codec::Framed; -#[cfg(feature = "cookies")] -use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; use actix_service::Service; @@ -40,6 +38,8 @@ use actix_service::Service; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use crate::connect::{BoxedSocket, ConnectRequest}; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; use crate::http::header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; use crate::http::{ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version}; @@ -280,7 +280,7 @@ impl WebsocketsRequest { let cookie: String = jar .delta() // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .map(|c| c.stripped().encoded().to_string()) .collect::>() .join("; "); diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index a393a641..f1d29f0b 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -8,6 +8,7 @@ use std::time::Duration; use actix_utils::future::ok; use brotli2::write::BrotliEncoder; use bytes::Bytes; +use cookie::Cookie; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; @@ -22,9 +23,9 @@ use actix_http_test::test_server; use actix_service::{map_config, pipeline_factory}; use actix_web::{ dev::{AppConfig, BodyEncoding}, - http::{header, Cookie}, + http::header, middleware::Compress, - web, App, Error, HttpMessage, HttpRequest, HttpResponse, + web, App, Error, HttpRequest, HttpResponse, }; use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; diff --git a/src/error.rs b/src/error.rs index 0865257d..25cdc9fe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use derive_more::{Display, Error, From}; use serde_json::error::Error as JsonError; use url::ParseError as UrlParseError; -use crate::{http::StatusCode, HttpResponse}; +use crate::http::StatusCode; /// Errors which can occur when attempting to generate resource uri. #[derive(Debug, PartialEq, Display, From)] @@ -90,12 +90,11 @@ pub enum JsonPayloadError { impl std::error::Error for JsonPayloadError {} -/// Return `BadRequest` for `JsonPayloadError` impl ResponseError for JsonPayloadError { - fn error_response(&self) -> HttpResponse { + fn status_code(&self) -> StatusCode { match *self { - JsonPayloadError::Overflow => HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE), - _ => HttpResponse::new(StatusCode::BAD_REQUEST), + JsonPayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, + _ => StatusCode::BAD_REQUEST, } } } @@ -168,26 +167,25 @@ mod tests { #[test] fn test_urlencoded_error() { - let resp: HttpResponse = - UrlencodedError::Overflow { size: 0, limit: 0 }.error_response(); + let resp = UrlencodedError::Overflow { size: 0, limit: 0 }.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = UrlencodedError::UnknownLength.error_response(); + let resp = UrlencodedError::UnknownLength.error_response(); assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); - let resp: HttpResponse = UrlencodedError::ContentType.error_response(); + let resp = UrlencodedError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_json_payload_error() { - let resp: HttpResponse = JsonPayloadError::Overflow.error_response(); + let resp = JsonPayloadError::Overflow.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = JsonPayloadError::ContentType.error_response(); + let resp = JsonPayloadError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_query_payload_error() { - let resp: HttpResponse = QueryPayloadError::Deserialize( + let resp = QueryPayloadError::Deserialize( serde_urlencoded::from_str::("bad query").unwrap_err(), ) .error_response(); @@ -196,9 +194,9 @@ mod tests { #[test] fn test_readlines_error() { - let resp: HttpResponse = ReadlinesError::LimitOverflow.error_response(); + let resp = ReadlinesError::LimitOverflow.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); + let resp = ReadlinesError::EncodingError.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/src/handler.rs b/src/handler.rs index e005a96a..822dcafd 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -3,20 +3,23 @@ use std::marker::PhantomData; use std::pin::Pin; use std::task::{Context, Poll}; -use actix_http::{Error, Response}; +use actix_http::Error; use actix_service::{Service, ServiceFactory}; use actix_utils::future::{ready, Ready}; use futures_core::ready; use pin_project::pin_project; -use crate::extract::FromRequest; -use crate::request::HttpRequest; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{ + extract::FromRequest, + request::HttpRequest, + responder::Responder, + response::HttpResponse, + service::{ServiceRequest, ServiceResponse}, +}; -/// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (ie, [`impl FromRequest`](crate::FromRequest)) and returns a type that can be converted into -/// an [`HttpResponse`](crate::HttpResponse) (ie, [`impl Responder`](crate::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`](crate::FromRequest)) and returns a type +/// that can be converted into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). /// /// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not /// a valid handler. See [Request Handlers](https://actix.rs/docs/handlers/) for more information. @@ -102,9 +105,7 @@ where type Error = Error; type Future = HandlerServiceFuture; - fn poll_ready(&self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_service::always_ready!(); fn call(&self, req: ServiceRequest) -> Self::Future { let (req, mut payload) = req.into_parts(); @@ -147,9 +148,9 @@ where let state = HandlerServiceFuture::Handle(fut, req.take()); self.as_mut().set(state); } - Err(e) => { - let res: Response = e.into().into(); + Err(err) => { let req = req.take().unwrap(); + let res = HttpResponse::from_error(err.into()); return Poll::Ready(Ok(ServiceResponse::new(req, res))); } }; diff --git a/src/lib.rs b/src/lib.rs index 1d5d5b83..54db969d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -86,6 +86,7 @@ mod request; mod request_data; mod resource; mod responder; +mod response; mod rmap; mod route; mod scope; @@ -95,19 +96,20 @@ pub mod test; pub(crate) mod types; pub mod web; -#[cfg(feature = "cookies")] -pub use actix_http::cookie; -pub use actix_http::Response as HttpResponse; +pub use actix_http::Response as BaseHttpResponse; pub use actix_http::{body, Error, HttpMessage, ResponseError, Result}; #[doc(inline)] pub use actix_rt as rt; pub use actix_web_codegen::*; +#[cfg(feature = "cookies")] +pub use cookie; pub use crate::app::App; pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::Responder; +pub use crate::response::{HttpResponse, HttpResponseBuilder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; @@ -139,7 +141,7 @@ pub mod dev { pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody, SizedStream}; #[cfg(feature = "compress")] pub use actix_http::encoding::Decoder as Decompress; - pub use actix_http::ResponseBuilder as HttpResponseBuilder; + pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::Server; @@ -189,4 +191,26 @@ pub mod dev { self } } + + impl BodyEncoding for crate::HttpResponseBuilder { + 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 + } + } + + impl BodyEncoding for crate::HttpResponse { + 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 + } + } } diff --git a/src/request.rs b/src/request.rs index f3cbc07b..e3da991d 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,19 +1,27 @@ -use std::cell::{Ref, RefCell, RefMut}; -use std::rc::Rc; -use std::{fmt, net}; +use std::{ + cell::{Ref, RefCell, RefMut}, + fmt, net, + rc::Rc, + str, +}; -use actix_http::http::{HeaderMap, Method, Uri, Version}; -use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; +use actix_http::{ + http::{HeaderMap, Method, Uri, Version}, + Error, Extensions, HttpMessage, Message, Payload, RequestHead, +}; use actix_router::{Path, Url}; use actix_utils::future::{ok, Ready}; +#[cfg(feature = "cookies")] +use cookie::{Cookie, ParseError as CookieParseError}; use smallvec::SmallVec; -use crate::app_service::AppInitServiceState; -use crate::config::AppConfig; -use crate::error::UrlGenerationError; -use crate::extract::FromRequest; -use crate::info::ConnectionInfo; -use crate::rmap::ResourceMap; +use crate::{ + app_service::AppInitServiceState, config::AppConfig, error::UrlGenerationError, + extract::FromRequest, info::ConnectionInfo, rmap::ResourceMap, +}; + +#[cfg(feature = "cookies")] +struct Cookies(Vec>); #[derive(Clone)] /// An HTTP Request @@ -260,6 +268,42 @@ impl HttpRequest { fn app_state(&self) -> &AppInitServiceState { &*self.inner.app_state } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + use actix_http::http::header::COOKIE; + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(COOKIE) { + let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + for cookie_str in s.split(';').map(|s| s.trim()) { + if !cookie_str.is_empty() { + cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); + } + } + } + self.extensions_mut().insert(Cookies(cookies)); + } + + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().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 HttpMessage for HttpRequest { diff --git a/src/responder.rs b/src/responder.rs index b75c9508..66c93d25 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -3,11 +3,10 @@ use std::fmt; use actix_http::{ error::InternalError, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, - ResponseBuilder, }; use bytes::{Bytes, BytesMut}; -use crate::{Error, HttpRequest, HttpResponse}; +use crate::{Error, HttpRequest, HttpResponse, HttpResponseBuilder}; /// Trait implemented by types that can be converted to an HTTP response. /// @@ -66,11 +65,32 @@ impl Responder for HttpResponse { } } +impl Responder for actix_http::Response { + #[inline] + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from(self) + } +} + +impl Responder for HttpResponseBuilder { + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + self.finish() + } +} + +impl Responder for actix_http::ResponseBuilder { + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from(self.finish()) + } +} + impl Responder for Option { fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Some(t) => t.respond_to(req), - None => HttpResponse::build(StatusCode::NOT_FOUND).finish(), + Some(val) => val.respond_to(req), + None => HttpResponse::new(StatusCode::NOT_FOUND), } } } @@ -88,13 +108,6 @@ where } } -impl Responder for ResponseBuilder { - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { - self.finish() - } -} - impl Responder for (T, StatusCode) { fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); diff --git a/src/response.rs b/src/response.rs new file mode 100644 index 00000000..1d017f08 --- /dev/null +++ b/src/response.rs @@ -0,0 +1,968 @@ +use std::{ + cell::{Ref, RefMut}, + convert::TryInto, + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{ + body::{Body, BodyStream, MessageBody, ResponseBody}, + http::{ + header::{self, HeaderMap, HeaderName, IntoHeaderPair, IntoHeaderValue}, + ConnectionType, Error as HttpError, StatusCode, + }, + Extensions, Response, ResponseHead, +}; +use bytes::Bytes; +use futures_core::Stream; +use serde::Serialize; + +#[cfg(feature = "cookies")] +use actix_http::http::header::HeaderValue; +#[cfg(feature = "cookies")] +use cookie::{Cookie, CookieJar}; + +use crate::error::Error; + +/// An HTTP Response +pub struct HttpResponse { + res: Response, + error: Option, +} + +impl HttpResponse { + /// Create HTTP response builder with specific status. + #[inline] + pub fn build(status: StatusCode) -> HttpResponseBuilder { + HttpResponseBuilder::new(status) + } + + /// Create HTTP response builder + #[inline] + pub fn build_from>(source: T) -> HttpResponseBuilder { + source.into() + } + + /// Create a response. + #[inline] + pub fn new(status: StatusCode) -> Self { + Self { + res: Response::new(status), + error: None, + } + } + + /// Create an error response. + #[inline] + pub fn from_error(error: Error) -> Self { + let res = error.as_response_error().error_response(); + + Self { + res, + error: Some(error), + } + } + + /// Convert response to response with body + pub fn into_body(self) -> HttpResponse { + HttpResponse { + res: self.res.into_body(), + error: self.error, + } + } +} + +impl HttpResponse { + /// Constructs a response with body + #[inline] + pub fn with_body(status: StatusCode, body: B) -> Self { + Self { + res: Response::with_body(status, body), + error: None, + } + } + + /// Returns a reference to response head. + #[inline] + pub fn head(&self) -> &ResponseHead { + self.res.head() + } + + /// Returns a mutable reference to response head. + #[inline] + pub fn head_mut(&mut self) -> &mut ResponseHead { + self.res.head_mut() + } + + /// The source `error` for this response + #[inline] + pub fn error(&self) -> Option<&Error> { + self.error.as_ref() + } + + /// Get the response status code + #[inline] + pub fn status(&self) -> StatusCode { + self.res.status() + } + + /// Set the `StatusCode` for this response + #[inline] + pub fn status_mut(&mut self) -> &mut StatusCode { + self.res.status_mut() + } + + /// Get the headers from the response + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.res.headers() + } + + /// Get a mutable reference to the headers + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.res.headers_mut() + } + + /// Get an iterator for the cookies set by this response. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> CookieIter<'_> { + CookieIter { + iter: self.headers().get_all(header::SET_COOKIE), + } + } + + /// Add a cookie to this response + #[cfg(feature = "cookies")] + pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { + HeaderValue::from_str(&cookie.to_string()) + .map(|c| { + self.headers_mut().append(header::SET_COOKIE, c); + }) + .map_err(|e| e.into()) + } + + /// Remove all cookies with the given name from this response. Returns + /// the number of cookies removed. + #[cfg(feature = "cookies")] + pub fn del_cookie(&mut self, name: &str) -> usize { + let headers = self.headers_mut(); + + let vals: Vec = headers + .get_all(header::SET_COOKIE) + .map(|v| v.to_owned()) + .collect(); + + headers.remove(header::SET_COOKIE); + + let mut count: usize = 0; + for v in vals { + if let Ok(s) = v.to_str() { + if let Ok(c) = Cookie::parse_encoded(s) { + if c.name() == name { + count += 1; + continue; + } + } + } + + // put set-cookie header head back if it does not validate + headers.append(header::SET_COOKIE, v); + } + + count + } + + /// Connection upgrade status + #[inline] + pub fn upgrade(&self) -> bool { + self.res.upgrade() + } + + /// Keep-alive status for this connection + pub fn keep_alive(&self) -> bool { + self.res.keep_alive() + } + + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + self.res.extensions() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { + self.res.extensions_mut() + } + + /// Get body of this response + #[inline] + pub fn body(&self) -> &ResponseBody { + self.res.body() + } + + /// Set a body + pub fn set_body(self, body: B2) -> HttpResponse { + HttpResponse { + res: self.res.set_body(body), + error: None, + // error: self.error, ?? + } + } + + /// Split response and body + pub fn into_parts(self) -> (HttpResponse<()>, ResponseBody) { + let (head, body) = self.res.into_parts(); + + ( + HttpResponse { + res: head, + error: None, + }, + body, + ) + } + + /// Drop request's body + pub fn drop_body(self) -> HttpResponse<()> { + HttpResponse { + res: self.res.drop_body(), + error: None, + } + } + + /// Set a body and return previous body value + pub fn map_body(self, f: F) -> HttpResponse + where + F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + { + HttpResponse { + res: self.res.map_body(f), + error: self.error, + } + } + + /// Extract response body + pub fn take_body(&mut self) -> ResponseBody { + self.res.take_body() + } +} + +impl fmt::Debug for HttpResponse { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HttpResponse") + .field("error", &self.error) + .field("res", &self.res) + .finish() + } +} + +impl From> for HttpResponse { + fn from(res: Response) -> Self { + HttpResponse { res, error: None } + } +} + +impl From for HttpResponse { + fn from(err: Error) -> Self { + HttpResponse::from_error(err) + } +} + +impl From> for Response { + fn from(res: HttpResponse) -> Self { + // this impl will always be called as part of dispatcher + + // TODO: expose cause somewhere? + // if let Some(err) = res.error { + // eprintln!("impl From> for Response let Some(err)"); + // return Response::from_error(err).into_body(); + // } + + res.res + } +} + +impl Future for HttpResponse { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if let Some(err) = self.error.take() { + eprintln!("httpresponse future error"); + return Poll::Ready(Ok(Response::from_error(err).into_body())); + } + + let res = &mut self.res; + actix_rt::pin!(res); + + res.poll(cx) + } +} + +/// An HTTP response builder. +/// +/// This type can be used to construct an instance of `Response` through a builder-like pattern. +pub struct HttpResponseBuilder { + head: Option, + err: Option, + #[cfg(feature = "cookies")] + cookies: Option, +} + +impl HttpResponseBuilder { + #[inline] + /// Create response builder + pub fn new(status: StatusCode) -> Self { + Self { + head: Some(ResponseHead::new(status)), + err: None, + #[cfg(feature = "cookies")] + cookies: None, + } + } + + /// Set HTTP status code of this response. + #[inline] + pub fn status(&mut self, status: StatusCode) -> &mut Self { + if let Some(parts) = self.inner() { + parts.status = status; + } + self + } + + /// Insert a header, replacing any that were set with an equivalent field name. + /// + /// ``` + /// use actix_web::{HttpResponse, http::header}; + /// + /// HttpResponse::Ok() + /// .insert_header(header::ContentType(mime::APPLICATION_JSON)) + /// .insert_header(("X-TEST", "value")) + /// .finish(); + /// ``` + pub fn insert_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Append a header, keeping any that were set with an equivalent field name. + /// + /// ``` + /// use actix_web::{HttpResponse, http::header}; + /// + /// HttpResponse::Ok() + /// .append_header(header::ContentType(mime::APPLICATION_JSON)) + /// .append_header(("X-TEST", "value1")) + /// .append_header(("X-TEST", "value2")) + /// .finish(); + /// ``` + pub fn append_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => parts.headers.append(key, value), + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Replaced with [`Self::insert_header()`]. + #[deprecated = "Replaced with `insert_header((key, value))`."] + pub fn set_header(&mut self, key: K, value: V) -> &mut Self + where + K: TryInto, + K::Error: Into, + V: IntoHeaderValue, + { + if self.err.is_some() { + return self; + } + + match (key.try_into(), value.try_into_value()) { + (Ok(name), Ok(value)) => return self.insert_header((name, value)), + (Err(err), _) => self.err = Some(err.into()), + (_, Err(err)) => self.err = Some(err.into()), + } + + self + } + + /// Replaced with [`Self::append_header()`]. + #[deprecated = "Replaced with `append_header((key, value))`."] + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + K: TryInto, + K::Error: Into, + V: IntoHeaderValue, + { + if self.err.is_some() { + return self; + } + + match (key.try_into(), value.try_into_value()) { + (Ok(name), Ok(value)) => return self.append_header((name, value)), + (Err(err), _) => self.err = Some(err.into()), + (_, Err(err)) => self.err = Some(err.into()), + } + + self + } + + /// Set the custom reason for the response. + #[inline] + pub fn reason(&mut self, reason: &'static str) -> &mut Self { + if let Some(parts) = self.inner() { + parts.reason = Some(reason); + } + self + } + + /// Set connection type to KeepAlive + #[inline] + pub fn keep_alive(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::KeepAlive); + } + self + } + + /// Set connection type to Upgrade + #[inline] + pub fn upgrade(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Upgrade); + } + + if let Ok(value) = value.try_into_value() { + self.insert_header((header::UPGRADE, value)); + } + + self + } + + /// Force close connection, even if it is marked as keep-alive + #[inline] + pub fn force_close(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Close); + } + self + } + + /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. + #[inline] + pub fn no_chunking(&mut self, len: u64) -> &mut Self { + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))); + + if let Some(parts) = self.inner() { + parts.no_chunking(true); + } + self + } + + /// Set response content type. + #[inline] + pub fn content_type(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + match value.try_into_value() { + Ok(value) => { + parts.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Set a cookie. + /// + /// ``` + /// use actix_web::{HttpResponse, cookie::Cookie}; + /// + /// HttpResponse::Ok() + /// .cookie( + /// Cookie::build("name", "value") + /// .domain("www.rust-lang.org") + /// .path("/") + /// .secure(true) + /// .http_only(true) + /// .finish(), + /// ) + /// .finish(); + /// ``` + #[cfg(feature = "cookies")] + pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { + if self.cookies.is_none() { + let mut jar = CookieJar::new(); + jar.add(cookie.into_owned()); + self.cookies = Some(jar) + } else { + self.cookies.as_mut().unwrap().add(cookie.into_owned()); + } + self + } + + /// Remove cookie. + /// + /// A `Set-Cookie` header is added that will delete a cookie with the same name from the client. + /// + /// ``` + /// use actix_web::{HttpRequest, HttpResponse, Responder}; + /// + /// async fn handler(req: HttpRequest) -> impl Responder { + /// let mut builder = HttpResponse::Ok(); + /// + /// if let Some(ref cookie) = req.cookie("name") { + /// builder.del_cookie(cookie); + /// } + /// + /// builder.finish() + /// } + /// ``` + #[cfg(feature = "cookies")] + pub fn del_cookie(&mut self, cookie: &Cookie<'_>) -> &mut Self { + if self.cookies.is_none() { + self.cookies = Some(CookieJar::new()) + } + let jar = self.cookies.as_mut().unwrap(); + let cookie = cookie.clone().into_owned(); + jar.add_original(cookie.clone()); + jar.remove(cookie); + self + } + + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions_mut() + } + + /// Set a body and generate `Response`. + /// + /// `ResponseBuilder` can not be used after this call. + #[inline] + pub fn body>(&mut self, body: B) -> HttpResponse { + self.message_body(body.into()) + } + + /// Set a body and generate `Response`. + /// + /// `ResponseBuilder` can not be used after this call. + pub fn message_body(&mut self, body: B) -> HttpResponse { + if let Some(err) = self.err.take() { + return HttpResponse::from_error(Error::from(err)).into_body(); + } + + // allow unused mut when cookies feature is disabled + #[allow(unused_mut)] + let mut head = self.head.take().expect("cannot reuse response builder"); + + let mut res = HttpResponse::with_body(StatusCode::OK, body); + *res.head_mut() = head; + + #[cfg(feature = "cookies")] + if let Some(ref jar) = self.cookies { + for cookie in jar.delta() { + match HeaderValue::from_str(&cookie.to_string()) { + Ok(val) => res.headers_mut().append(header::SET_COOKIE, val), + Err(err) => return HttpResponse::from_error(Error::from(err)).into_body(), + }; + } + } + + res + } + + /// Set a streaming body and generate `Response`. + /// + /// `ResponseBuilder` can not be used after this call. + #[inline] + pub fn streaming(&mut self, stream: S) -> HttpResponse + where + S: Stream> + Unpin + 'static, + E: Into + 'static, + { + self.body(Body::from_message(BodyStream::new(stream))) + } + + /// Set a json body and generate `Response` + /// + /// `ResponseBuilder` can not be used after this call. + pub fn json(&mut self, value: impl Serialize) -> HttpResponse { + match serde_json::to_string(&value) { + Ok(body) => { + let contains = if let Some(parts) = self.inner() { + parts.headers.contains_key(header::CONTENT_TYPE) + } else { + true + }; + + if !contains { + self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); + } + + self.body(Body::from(body)) + } + Err(e) => HttpResponse::from_error(Error::from(e)), + } + } + + /// Set an empty body and generate `Response` + /// + /// `ResponseBuilder` can not be used after this call. + #[inline] + pub fn finish(&mut self) -> HttpResponse { + self.body(Body::Empty) + } + + /// This method construct new `ResponseBuilder` + pub fn take(&mut self) -> Self { + Self { + head: self.head.take(), + err: self.err.take(), + #[cfg(feature = "cookies")] + cookies: self.cookies.take(), + } + } + + #[inline] + fn inner(&mut self) -> Option<&mut ResponseHead> { + if self.err.is_some() { + return None; + } + + self.head.as_mut() + } +} + +impl From for HttpResponse { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish() + } +} + +impl From for Response { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish().into() + } +} + +impl Future for HttpResponseBuilder { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { + eprintln!("httpresponse future error"); + Poll::Ready(Ok(self.finish())) + } +} + +#[cfg(feature = "cookies")] +pub struct CookieIter<'a> { + iter: header::GetAll<'a>, +} + +#[cfg(feature = "cookies")] +impl<'a> Iterator for CookieIter<'a> { + type Item = Cookie<'a>; + + #[inline] + fn next(&mut self) -> Option> { + for v in self.iter.by_ref() { + if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { + return Some(c); + } + } + None + } +} + +mod http_codes { + //! Status code based HTTP response builders. + + use actix_http::http::StatusCode; + + use super::{HttpResponse, HttpResponseBuilder}; + + macro_rules! static_resp { + ($name:ident, $status:expr) => { + #[allow(non_snake_case, missing_docs)] + pub fn $name() -> HttpResponseBuilder { + HttpResponseBuilder::new($status) + } + }; + } + + impl HttpResponse { + static_resp!(Continue, StatusCode::CONTINUE); + static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS); + static_resp!(Processing, StatusCode::PROCESSING); + + static_resp!(Ok, StatusCode::OK); + static_resp!(Created, StatusCode::CREATED); + static_resp!(Accepted, StatusCode::ACCEPTED); + static_resp!( + NonAuthoritativeInformation, + StatusCode::NON_AUTHORITATIVE_INFORMATION + ); + + static_resp!(NoContent, StatusCode::NO_CONTENT); + static_resp!(ResetContent, StatusCode::RESET_CONTENT); + static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT); + static_resp!(MultiStatus, StatusCode::MULTI_STATUS); + static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED); + + static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); + static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); + static_resp!(Found, StatusCode::FOUND); + static_resp!(SeeOther, StatusCode::SEE_OTHER); + static_resp!(NotModified, StatusCode::NOT_MODIFIED); + static_resp!(UseProxy, StatusCode::USE_PROXY); + static_resp!(TemporaryRedirect, StatusCode::TEMPORARY_REDIRECT); + static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); + + static_resp!(BadRequest, StatusCode::BAD_REQUEST); + static_resp!(NotFound, StatusCode::NOT_FOUND); + static_resp!(Unauthorized, StatusCode::UNAUTHORIZED); + static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); + static_resp!(Forbidden, StatusCode::FORBIDDEN); + static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); + static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); + static_resp!( + ProxyAuthenticationRequired, + StatusCode::PROXY_AUTHENTICATION_REQUIRED + ); + static_resp!(RequestTimeout, StatusCode::REQUEST_TIMEOUT); + static_resp!(Conflict, StatusCode::CONFLICT); + static_resp!(Gone, StatusCode::GONE); + static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED); + static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); + static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); + static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); + static_resp!(UriTooLong, StatusCode::URI_TOO_LONG); + static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); + static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); + static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); + static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); + static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); + static_resp!( + RequestHeaderFieldsTooLarge, + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + ); + static_resp!( + UnavailableForLegalReasons, + StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS + ); + + static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); + static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED); + static_resp!(BadGateway, StatusCode::BAD_GATEWAY); + static_resp!(ServiceUnavailable, StatusCode::SERVICE_UNAVAILABLE); + static_resp!(GatewayTimeout, StatusCode::GATEWAY_TIMEOUT); + static_resp!(VersionNotSupported, StatusCode::HTTP_VERSION_NOT_SUPPORTED); + static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); + static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); + static_resp!(LoopDetected, StatusCode::LOOP_DETECTED); + } + + #[cfg(test)] + mod tests { + use crate::dev::Body; + use crate::http::StatusCode; + use crate::HttpResponse; + + #[test] + fn test_build() { + let resp = HttpResponse::Ok().body(Body::Empty); + assert_eq!(resp.status(), StatusCode::OK); + } + } +} + +#[cfg(test)] +mod tests { + use bytes::{Bytes, BytesMut}; + use serde_json::json; + + use super::{HttpResponse as Response, HttpResponseBuilder as ResponseBuilder}; + use crate::dev::{Body, MessageBody, ResponseBody}; + use crate::http::header::{self, HeaderValue, CONTENT_TYPE, COOKIE}; + use crate::http::StatusCode; + + #[test] + fn test_debug() { + let resp = Response::Ok() + .append_header((COOKIE, HeaderValue::from_static("cookie1=value1; "))) + .append_header((COOKIE, HeaderValue::from_static("cookie2=value2; "))) + .finish(); + let dbg = format!("{:?}", resp); + assert!(dbg.contains("Response")); + } + + #[test] + fn test_basic_builder() { + let resp = Response::Ok().insert_header(("X-TEST", "value")).finish(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_upgrade() { + let resp = ResponseBuilder::new(StatusCode::OK) + .upgrade("websocket") + .finish(); + assert!(resp.upgrade()); + assert_eq!( + resp.headers().get(header::UPGRADE).unwrap(), + HeaderValue::from_static("websocket") + ); + } + + #[test] + fn test_force_close() { + let resp = ResponseBuilder::new(StatusCode::OK).force_close().finish(); + assert!(!resp.keep_alive()) + } + + #[test] + fn test_content_type() { + let resp = ResponseBuilder::new(StatusCode::OK) + .content_type("text/plain") + .body(Body::Empty); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") + } + + pub async fn read_body(mut body: ResponseBody) -> Bytes + where + B: MessageBody + Unpin, + { + use futures_util::StreamExt as _; + + let mut bytes = BytesMut::new(); + while let Some(item) = body.next().await { + bytes.extend_from_slice(&item.unwrap()); + } + bytes.freeze() + } + + #[actix_rt::test] + async fn test_json() { + let mut resp = Response::Ok().json(vec!["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("application/json")); + assert_eq!( + read_body(resp.take_body()).await.as_ref(), + br#"["v1","v2","v3"]"# + ); + + let mut resp = Response::Ok().json(&["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("application/json")); + assert_eq!( + read_body(resp.take_body()).await.as_ref(), + br#"["v1","v2","v3"]"# + ); + + // content type override + let mut resp = Response::Ok() + .insert_header((CONTENT_TYPE, "text/json")) + .json(&vec!["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("text/json")); + assert_eq!( + read_body(resp.take_body()).await.as_ref(), + br#"["v1","v2","v3"]"# + ); + } + + #[actix_rt::test] + async fn test_serde_json_in_body() { + use serde_json::json; + let mut resp = Response::Ok().body(json!({"test-key":"test-value"})); + assert_eq!( + read_body(resp.take_body()).await.as_ref(), + br#"{"test-key":"test-value"}"# + ); + } + + #[test] + fn response_builder_header_insert_kv() { + let mut res = Response::Ok(); + res.insert_header(("Content-Type", "application/octet-stream")); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_insert_typed() { + let mut res = Response::Ok(); + res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_append_kv() { + let mut res = Response::Ok(); + res.append_header(("Content-Type", "application/octet-stream")); + res.append_header(("Content-Type", "application/json")); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } + + #[test] + fn response_builder_header_append_typed() { + let mut res = Response::Ok(); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } +} diff --git a/src/service.rs b/src/service.rs index c2ecc003..9765343c 100644 --- a/src/service.rs +++ b/src/service.rs @@ -10,12 +10,15 @@ use actix_http::{ use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; use actix_service::{IntoServiceFactory, ServiceFactory}; -use crate::config::{AppConfig, AppService}; use crate::dev::insert_slash; use crate::guard::Guard; use crate::info::ConnectionInfo; use crate::request::HttpRequest; use crate::rmap::ResourceMap; +use crate::{ + config::{AppConfig, AppService}, + HttpResponse, +}; pub trait HttpServiceFactory { fn register(self, config: &mut AppService); @@ -99,13 +102,14 @@ impl ServiceRequest { /// Create service response #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { - ServiceResponse::new(self.req, res.into()) + let res = HttpResponse::from(res.into()); + ServiceResponse::new(self.req, res) } /// Create service response for error #[inline] pub fn error_response>(self, err: E) -> ServiceResponse { - let res: Response = err.into().into(); + let res = HttpResponse::from_error(err.into()); ServiceResponse::new(self.req, res.into_body()) } @@ -315,23 +319,19 @@ impl fmt::Debug for ServiceRequest { pub struct ServiceResponse { request: HttpRequest, - response: Response, + response: HttpResponse, } impl ServiceResponse { /// Create service response instance - pub fn new(request: HttpRequest, response: Response) -> Self { + pub fn new(request: HttpRequest, response: HttpResponse) -> Self { ServiceResponse { request, response } } /// Create service response from the error pub fn from_err>(err: E, request: HttpRequest) -> Self { - let e: Error = err.into(); - let res: Response = e.into(); - ServiceResponse { - request, - response: res.into_body(), - } + let response = HttpResponse::from_error(err.into()).into_body(); + ServiceResponse { request, response } } /// Create service response for error @@ -342,7 +342,7 @@ impl ServiceResponse { /// Create service response #[inline] - pub fn into_response(self, response: Response) -> ServiceResponse { + pub fn into_response(self, response: HttpResponse) -> ServiceResponse { ServiceResponse::new(self.request, response) } @@ -354,13 +354,13 @@ impl ServiceResponse { /// Get reference to response #[inline] - pub fn response(&self) -> &Response { + pub fn response(&self) -> &HttpResponse { &self.response } /// Get mutable reference to response #[inline] - pub fn response_mut(&mut self) -> &mut Response { + pub fn response_mut(&mut self) -> &mut HttpResponse { &mut self.response } @@ -376,8 +376,8 @@ impl ServiceResponse { self.response.headers() } - #[inline] /// Returns mutable response's headers. + #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } @@ -391,7 +391,7 @@ impl ServiceResponse { match f(&mut self) { Ok(_) => self, Err(err) => { - let res: Response = err.into().into(); + let res = HttpResponse::from_error(err.into()); ServiceResponse::new(self.request, res.into_body()) } } @@ -418,9 +418,15 @@ impl ServiceResponse { } } +impl From> for HttpResponse { + fn from(res: ServiceResponse) -> HttpResponse { + res.response + } +} + impl From> for Response { fn from(res: ServiceResponse) -> Response { - res.response + res.response.into() } } diff --git a/src/test.rs b/src/test.rs index 37fb96e0..c2e456e5 100644 --- a/src/test.rs +++ b/src/test.rs @@ -2,8 +2,6 @@ use std::{net::SocketAddr, rc::Rc}; -#[cfg(feature = "cookies")] -use actix_http::cookie::Cookie; pub use actix_http::test::TestBuffer; use actix_http::{ http::{header::IntoHeaderPair, Method, StatusCode, Uri, Version}, @@ -17,6 +15,8 @@ use futures_core::Stream; use futures_util::StreamExt as _; use serde::{de::DeserializeOwned, Serialize}; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::{ app_service::AppInitServiceState, config::AppConfig, @@ -26,7 +26,7 @@ use crate::{ rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, web::{Bytes, BytesMut}, - Error, HttpRequest, HttpResponse, + Error, HttpRequest, HttpResponse, HttpResponseBuilder, }; /// Create service that always responds with `HttpResponse::Ok()` and no body. @@ -40,7 +40,7 @@ pub fn default_service( status_code: StatusCode, ) -> impl Service, Error = Error> { (move |req: ServiceRequest| { - ok(req.into_response(HttpResponse::build(status_code).finish())) + ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) .into_service() } @@ -359,6 +359,8 @@ pub struct TestRequest { path: Path, peer_addr: Option, app_data: Extensions, + #[cfg(feature = "cookies")] + cookies: CookieJar, } impl Default for TestRequest { @@ -370,6 +372,8 @@ impl Default for TestRequest { path: Path::new(Url::new(Uri::default())), peer_addr: None, app_data: Extensions::new(), + #[cfg(feature = "cookies")] + cookies: CookieJar::new(), } } } @@ -445,7 +449,7 @@ impl TestRequest { /// Set cookie for this request. #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { - self.req.cookie(cookie); + self.cookies.add(cookie.into_owned()); self } @@ -507,16 +511,42 @@ impl TestRequest { self } + fn finish(&mut self) -> Request { + // mut used when cookie feature is enabled + #[allow(unused_mut)] + let mut req = self.req.finish(); + + #[cfg(feature = "cookies")] + { + use actix_http::http::header::{HeaderValue, COOKIE}; + + let cookie: String = self + .cookies + .delta() + // ensure only name=value is written to cookie header + .map(|c| c.stripped().encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + req.headers_mut() + .insert(COOKIE, HeaderValue::from_str(&cookie).unwrap()); + } + } + + req + } + /// Complete request creation and generate `Request` instance pub fn to_request(mut self) -> Request { - let mut req = self.req.finish(); + let mut req = self.finish(); req.head_mut().peer_addr = self.peer_addr; req } /// Complete request creation and generate `ServiceRequest` instance pub fn to_srv_request(mut self) -> ServiceRequest { - let (mut head, payload) = self.req.finish().into_parts(); + let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); @@ -535,7 +565,7 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { - let (mut head, _) = self.req.finish().into_parts(); + let (mut head, _) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); @@ -546,7 +576,7 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` and `Payload` instances pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { - let (mut head, payload) = self.req.finish().into_parts(); + let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); diff --git a/src/types/json.rs b/src/types/json.rs index 97439d8f..265beb56 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -225,7 +225,7 @@ where /// .content_type(|mime| mime == mime::TEXT_PLAIN) /// // use custom error handler /// .error_handler(|err, req| { -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() +/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() /// }); /// /// App::new() @@ -486,7 +486,7 @@ mod tests { }; let resp = HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); - InternalError::from_response(err, resp).into() + InternalError::from_response(err, resp.into()).into() })) .to_http_parts(); diff --git a/src/types/path.rs b/src/types/path.rs index 33ea7062..69a75e9c 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -149,7 +149,7 @@ where /// .app_data(PathConfig::default().error_handler(|err, req| { /// error::InternalError::from_response( /// err, -/// HttpResponse::Conflict().finish(), +/// HttpResponse::Conflict().into(), /// ) /// .into() /// })) @@ -292,15 +292,18 @@ mod tests { async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") .app_data(PathConfig::default().error_handler(|err, _| { - error::InternalError::from_response(err, HttpResponse::Conflict().finish()) - .into() + error::InternalError::from_response( + err, + HttpResponse::Conflict().finish().into(), + ) + .into() })) .to_http_parts(); let s = Path::<(usize,)>::from_request(&req, &mut pl) .await .unwrap_err(); - let res: HttpResponse = s.into(); + let res = HttpResponse::from_error(s.into()); assert_eq!(res.status(), http::StatusCode::CONFLICT); } diff --git a/src/types/query.rs b/src/types/query.rs index 4807335b..978d00b5 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -166,7 +166,7 @@ where /// let query_cfg = web::QueryConfig::default() /// // use custom error handler /// .error_handler(|err, req| { -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() +/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() /// }); /// /// App::new() @@ -267,7 +267,7 @@ mod tests { let req = TestRequest::with_uri("/name/user1/") .app_data(QueryConfig::default().error_handler(|e, _| { let resp = HttpResponse::UnprocessableEntity().finish(); - InternalError::from_response(e, resp).into() + InternalError::from_response(e, resp.into()).into() })) .to_srv_request(); diff --git a/tests/test_server.rs b/tests/test_server.rs index d114b022..2760cc7f 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -15,6 +15,7 @@ use actix_http::http::header::{ }; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; +use cookie::{Cookie, CookieBuilder}; use flate2::{ read::GzDecoder, write::{GzEncoder, ZlibDecoder, ZlibEncoder}, @@ -826,18 +827,18 @@ mod plus_rustls { #[actix_rt::test] async fn test_server_cookies() { - use actix_web::{http, HttpMessage}; + use actix_web::http; let srv = actix_test::start(|| { App::new().default_service(web::to(|| { HttpResponse::Ok() .cookie( - http::CookieBuilder::new("first", "first_value") + CookieBuilder::new("first", "first_value") .http_only(true) .finish(), ) - .cookie(http::Cookie::new("second", "first_value")) - .cookie(http::Cookie::new("second", "second_value")) + .cookie(Cookie::new("second", "first_value")) + .cookie(Cookie::new("second", "second_value")) .finish() })) }); @@ -846,10 +847,10 @@ async fn test_server_cookies() { let res = req.send().await.unwrap(); assert!(res.status().is_success()); - let first_cookie = http::CookieBuilder::new("first", "first_value") + let first_cookie = CookieBuilder::new("first", "first_value") .http_only(true) .finish(); - let second_cookie = http::Cookie::new("second", "second_value"); + let second_cookie = Cookie::new("second", "second_value"); let cookies = res.cookies().expect("To have cookies"); assert_eq!(cookies.len(), 2);