From 913b69893f762c87d5948e78dae6264a1863c73e Mon Sep 17 00:00:00 2001 From: Rob Ede Date: Thu, 8 Apr 2021 20:22:38 +0100 Subject: [PATCH] introduce response and builder types to web --- actix-files/src/error.rs | 6 +- actix-http/examples/ws.rs | 8 +- actix-http/src/error.rs | 14 +- actix-http/src/h1/dispatcher.rs | 4 +- actix-http/src/message.rs | 4 +- actix-http/src/request.rs | 1 - actix-http/src/response.rs | 72 +-- actix-http/src/ws/mod.rs | 46 +- actix-http/tests/test_ws.rs | 2 +- actix-multipart/src/error.rs | 3 +- actix-test/src/lib.rs | 8 +- actix-web-actors/src/ws.rs | 2 +- actix-web-codegen/tests/test_macro.rs | 18 +- awc/tests/test_ws.rs | 2 +- src/error.rs | 26 +- src/handler.rs | 27 +- src/lib.rs | 26 +- src/responder.rs | 20 +- src/response.rs | 797 +++++++++++++++++++++----- src/service.rs | 40 +- src/test.rs | 4 +- src/types/json.rs | 4 +- src/types/path.rs | 11 +- src/types/query.rs | 4 +- 24 files changed, 842 insertions(+), 307 deletions(-) diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 9b30cbaa2..e5f2d4779 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/examples/ws.rs b/actix-http/examples/ws.rs index 4e03aa8ab..2374c2d96 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())?; + let 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.set_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 bd3c73a4c..e315501bc 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -16,7 +16,7 @@ use serde_urlencoded::ser::Error as FormError; use crate::body::Body; use crate::helpers::Writer; -use crate::response::{Response, ResponseBuilder}; +use crate::response::Response; /// A specialized [`std::result::Result`] /// for actix web operations @@ -135,12 +135,12 @@ impl From for Error { } } -/// Convert ResponseBuilder to a Error -impl From for Error { - fn from(mut res: ResponseBuilder) -> Error { - InternalError::from_response("", res.finish()).into() - } -} +// /// Convert ResponseBuilder to a Error +// impl From for Error { +// fn from(mut res: ResponseBuilder) -> Error { +// InternalError::from_response("", res.finish()).into() +// } +// } #[derive(Debug, Display)] #[display(fmt = "UnknownError")] diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index bf0365693..4fc200b43 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -340,13 +340,15 @@ where StateProj::ServiceCall(fut) => match fut.poll(cx) { // service call resolved. send response. Poll::Ready(Ok(res)) => { + eprintln!("dispatcher ok!"); let (res, body) = res.into().replace_body(()); self.as_mut().send_response(res, body)?; } // send service call error as response Poll::Ready(Err(err)) => { - let res: Response = err.into().into(); + eprintln!("dispatcher err"); + 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/message.rs b/actix-http/src/message.rs index 6438ccba0..c89f5311a 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 2632fc2ba..9209de108 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -9,7 +9,6 @@ use std::{ use cookie::{Cookie, ParseError as CookieParseError}; use http::{header, Method, Uri, Version}; - use crate::{ extensions::Extensions, header::HeaderMap, diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index d13e52689..5767e2247 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -39,18 +39,6 @@ pub struct Response { } impl Response { - /// Create HTTP response builder with specific status. - #[inline] - pub fn build(status: StatusCode) -> ResponseBuilder { - ResponseBuilder::new(status) - } - - /// Create HTTP response builder - #[inline] - pub fn build_from>(source: T) -> ResponseBuilder { - source.into() - } - /// Constructs a response #[inline] pub fn new(status: StatusCode) -> Response { @@ -139,54 +127,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 { @@ -732,7 +672,7 @@ impl From> for ResponseBuilder { head: Some(res.head), err: None, #[cfg(feature = "cookies")] - cookies: None + cookies: None, } } } @@ -876,7 +816,7 @@ mod tests { #[test] fn test_upgrade() { - let resp = Response::build(StatusCode::OK) + let resp = ResponseBuilder::new(StatusCode::OK) .upgrade("websocket") .finish(); assert!(resp.upgrade()); @@ -888,13 +828,13 @@ mod tests { #[test] fn test_force_close() { - let resp = Response::build(StatusCode::OK).force_close().finish(); + let resp = ResponseBuilder::new(StatusCode::OK).force_close().finish(); assert!(!resp.keep_alive()) } #[test] fn test_content_type() { - let resp = Response::build(StatusCode::OK) + let resp = ResponseBuilder::new(StatusCode::OK) .content_type("text/plain") .body(Body::Empty); assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") @@ -915,7 +855,7 @@ mod tests { #[test] fn test_json_ct() { - let resp = Response::build(StatusCode::OK) + let resp = ResponseBuilder::new(StatusCode::OK) .insert_header((CONTENT_TYPE, "text/json")) .json(&vec!["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); @@ -927,7 +867,7 @@ mod tests { fn test_serde_json_in_body() { use serde_json::json; let resp = - Response::build(StatusCode::OK).body(json!({"test-key":"test-value"})); + ResponseBuilder::new(StatusCode::OK).body(json!({"test-key":"test-value"})); assert_eq!(resp.body().get_ref(), br#"{"test-key":"test-value"}"#); } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index cec73db96..01d8655b6 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -11,8 +11,8 @@ use http::{header, Method, StatusCode}; use crate::{ error::ResponseError, header::HeaderValue, - message::RequestHead, - response::{Response, ResponseBuilder}, + message::{ConnectionType, RequestHead}, + response::Response, }; mod codec; @@ -131,7 +131,7 @@ impl ResponseError for HandshakeError { } /// Verify WebSocket handshake request and create handshake response. -pub fn handshake(req: &RequestHead) -> Result { +pub fn handshake(req: &RequestHead) -> Result { verify_handshake(req)?; Ok(handshake_response(req)) } @@ -187,21 +187,39 @@ pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { /// Create WebSocket handshake response. /// /// This function returns handshake `Response`, ready to send to peer. -pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { +pub fn handshake_response(req: &RequestHead) -> Response { let key = { let key = req.headers().get(header::SEC_WEBSOCKET_KEY).unwrap(); proto::hash_key(key.as_ref()) }; - Response::build(StatusCode::SWITCHING_PROTOCOLS) - .upgrade("websocket") - .insert_header((header::TRANSFER_ENCODING, "chunked")) - .insert_header(( - header::SEC_WEBSOCKET_ACCEPT, - // key is known to be header value safe ascii - HeaderValue::from_bytes(&key).unwrap(), - )) - .take() + let mut res = Response::new(StatusCode::SWITCHING_PROTOCOLS); + + res.head_mut().set_connection_type(ConnectionType::Upgrade); + let headers = res.headers_mut(); + + headers.insert(header::UPGRADE, HeaderValue::from_static("websocket")); + + headers.insert( + header::TRANSFER_ENCODING, + HeaderValue::from_static("chunked"), + ); + headers.insert( + header::SEC_WEBSOCKET_ACCEPT, + // key is known to be header value safe ascii + HeaderValue::from_bytes(&key).unwrap(), + ); + + res + + // Response::build(StatusCode::SWITCHING_PROTOCOLS) + // .upgrade("websocket") + // .insert_header((header::TRANSFER_ENCODING, "chunked")) + // .insert_header(( + // header::SEC_WEBSOCKET_ACCEPT, + // // key is known to be header value safe ascii + // HeaderValue::from_bytes(&key).unwrap(), + // )) } #[cfg(test)] @@ -316,7 +334,7 @@ mod tests { .finish(); assert_eq!( StatusCode::SWITCHING_PROTOCOLS, - handshake_response(req.head()).finish().status() + handshake_response(req.head()).status() ); } diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 9a2e57711..041c7b467 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -52,7 +52,7 @@ where fn call(&self, (req, mut framed): (Request, Framed)) -> Self::Future { let fut = async move { - let res = ws::handshake(req.head()).unwrap().message_body(()); + let res = ws::handshake(req.head()).unwrap().set_body(()); framed .send((res, body::BodySize::None).into()) diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index cdbb5d395..5f91c60df 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/src/lib.rs b/actix-test/src/lib.rs index bd86c27ad..8fab33289 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 c849ebdf1..f729871c1 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::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 7b95edba2..b983e6b1d 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/tests/test_ws.rs b/awc/tests/test_ws.rs index 3f19ac4e8..f07b04836 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -25,7 +25,7 @@ async fn test_simple() { HttpService::build() .upgrade(|(req, mut framed): (Request, Framed<_, _>)| { async move { - let res = ws::handshake_response(req.head()).finish(); + let res = ws::handshake_response(req.head()); // send handshake response framed .send(h1::Message::Item((res.drop_body(), BodySize::None))) diff --git a/src/error.rs b/src/error.rs index 0865257d3..25cdc9feb 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 e005a96a6..822dcafdd 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 04735e228..54db969df 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -96,10 +96,10 @@ pub mod test; pub(crate) mod types; pub mod web; +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_http::Response as HttpResponse; pub use actix_web_codegen::*; #[cfg(feature = "cookies")] pub use cookie; @@ -109,7 +109,7 @@ pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::Responder; -pub use crate::response::HttpResponseBuilder; +pub use crate::response::{HttpResponse, HttpResponseBuilder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; @@ -191,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/responder.rs b/src/responder.rs index b75c95083..7b5803f1c 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -7,7 +7,7 @@ use actix_http::{ }; 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 +66,18 @@ impl Responder for HttpResponse { } } +impl Responder for actix_http::Response { + #[inline] + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from(self) + } +} + 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(), + None => HttpResponseBuilder::new(StatusCode::NOT_FOUND).finish(), } } } @@ -88,13 +95,20 @@ where } } -impl Responder for ResponseBuilder { +impl Responder for HttpResponseBuilder { #[inline] fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { self.finish() } } +impl Responder for ResponseBuilder { + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from(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 index 1795c841d..267064d60 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,12 +1,16 @@ use std::{ cell::{Ref, RefMut}, convert::TryInto, + fmt, + future::Future, + pin::Pin, + task::{Context, Poll}, }; use actix_http::{ - body::{Body, BodyStream}, + body::{Body, BodyStream, MessageBody, ResponseBody}, http::{ - header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, + header::{self, HeaderMap, HeaderName, IntoHeaderPair, IntoHeaderValue}, ConnectionType, Error as HttpError, StatusCode, }, Extensions, Response, ResponseHead, @@ -15,49 +19,301 @@ 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; -// pub struct HttpResponse(dev::BaseHttpResponse); +/// An HTTP Response +pub struct HttpResponse { + res: Response, + error: Option, +} -// impl HttpResponse { -// /// Create HTTP response builder with specific status. -// #[inline] -// pub fn build(status: http::StatusCode) -> HttpResponseBuilder { -// HttpResponseBuilder(dev::BaseHttpResponse::build(status)) -// } +impl HttpResponse { + /// Create HTTP response builder with specific status. + #[inline] + pub fn build(status: StatusCode) -> HttpResponseBuilder { + HttpResponseBuilder::new(status) + } -// /// Constructs a response -// #[inline] -// pub fn new(status: http::StatusCode) -> HttpResponse { -// HttpResponse(dev::BaseHttpResponse::new(status)) -// } + /// Create HTTP response builder + #[inline] + pub fn build_from>(source: T) -> HttpResponseBuilder { + source.into() + } -// /// Constructs an error response -// #[inline] -// pub fn from_error(error: Error) -> HttpResponse { -// HttpResponse(dev::BaseHttpResponse::from_error(error)) -// } -// } + /// Create a response. + #[inline] + pub fn new(status: StatusCode) -> Self { + Self { + res: Response::new(status), + error: None, + } + } -// impl ops::Deref for HttpResponse { -// type Target = dev::BaseHttpResponse; + /// Create an error response. + #[inline] + pub fn from_error(error: Error) -> Self { + let res = error.as_response_error().error_response(); -// fn deref(&self) -> &Self::Target { -// &self.0 -// } -// } + Self { + res, + error: Some(error), + } + } -// impl ops::DerefMut for HttpResponse { -// fn deref_mut(&mut self) -> &mut Self::Target { -// &mut self.0 -// } -// } + /// Convert response to response with body + pub fn into_body(self) -> HttpResponse { + HttpResponse { + res: self.res.into_body(), + error: self.error, + } + } +} -// impl From> for dev::BaseHttpResponse { -// fn from(res: HttpResponse) -> Self { -// res.0 -// } -// } +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(crate) fn replace_body(self, body: B2) -> (HttpResponse, ResponseBody) { + // let (res, old_body) = self.res.replace_body(body); + + // ( + // HttpResponse { + // res, + // error: self.error, + // }, + // old_body, + // ) + // } + + /// 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. /// @@ -65,6 +321,8 @@ use crate::error::Error; pub struct HttpResponseBuilder { head: Option, err: Option, + #[cfg(feature = "cookies")] + cookies: Option, } impl HttpResponseBuilder { @@ -74,6 +332,8 @@ impl HttpResponseBuilder { Self { head: Some(ResponseHead::new(status)), err: None, + #[cfg(feature = "cookies")] + cookies: None, } } @@ -254,6 +514,63 @@ impl HttpResponseBuilder { 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 + } + /// Responses extensions #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { @@ -272,24 +589,35 @@ impl HttpResponseBuilder { /// /// `ResponseBuilder` can not be used after this call. #[inline] - pub fn body>(&mut self, body: B) -> Response { + 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) -> Response { - if let Some(e) = self.err.take() { - return Response::from(Error::from(e)).into_body(); + 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 = Response::with_body(StatusCode::OK, body); + 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 } @@ -297,7 +625,7 @@ impl HttpResponseBuilder { /// /// `ResponseBuilder` can not be used after this call. #[inline] - pub fn streaming(&mut self, stream: S) -> Response + pub fn streaming(&mut self, stream: S) -> HttpResponse where S: Stream> + Unpin + 'static, E: Into + 'static, @@ -308,7 +636,7 @@ impl HttpResponseBuilder { /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: impl Serialize) -> Response { + pub fn json(&mut self, value: impl Serialize) -> HttpResponse { match serde_json::to_string(&value) { Ok(body) => { let contains = if let Some(parts) = self.parts() { @@ -323,7 +651,7 @@ impl HttpResponseBuilder { self.body(Body::from(body)) } - Err(e) => Error::from(e).into(), + Err(e) => HttpResponse::from_error(Error::from(e)), } } @@ -331,7 +659,7 @@ impl HttpResponseBuilder { /// /// `ResponseBuilder` can not be used after this call. #[inline] - pub fn finish(&mut self) -> Response { + pub fn finish(&mut self) -> HttpResponse { self.body(Body::Empty) } @@ -340,6 +668,8 @@ impl HttpResponseBuilder { Self { head: self.head.take(), err: self.err.take(), + #[cfg(feature = "cookies")] + cookies: self.cookies.take(), } } @@ -353,104 +683,301 @@ impl HttpResponseBuilder { } } -// mod http_codes { -// //! Status code based HTTP response builders. +impl From for HttpResponse { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish() + } +} -// use actix_http::http::StatusCode; +impl From for Response { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish().into() + } +} -// use super::{HttpResponse, HttpResponseBuilder}; +impl Future for HttpResponseBuilder { + type Output = Result; -// macro_rules! static_resp { -// ($name:ident, $status:expr) => { -// #[allow(non_snake_case, missing_docs)] -// pub fn $name() -> HttpResponseBuilder { -// HttpResponseBuilder::new($status) -// } -// }; -// } + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { + eprintln!("httpresponse future error"); + Poll::Ready(Ok(self.finish())) + } +} -// impl HttpResponse { -// static_resp!(Continue, StatusCode::CONTINUE); -// static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS); -// static_resp!(Processing, StatusCode::PROCESSING); +#[cfg(feature = "cookies")] +pub struct CookieIter<'a> { + iter: header::GetAll<'a>, +} -// static_resp!(Ok, StatusCode::OK); -// static_resp!(Created, StatusCode::CREATED); -// static_resp!(Accepted, StatusCode::ACCEPTED); -// static_resp!( -// NonAuthoritativeInformation, -// StatusCode::NON_AUTHORITATIVE_INFORMATION -// ); +#[cfg(feature = "cookies")] +impl<'a> Iterator for CookieIter<'a> { + type Item = Cookie<'a>; -// 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); + #[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 + } +} -// 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); +mod http_codes { + //! Status code based HTTP response builders. -// 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 -// ); + use actix_http::http::StatusCode; -// 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); -// } + use super::{HttpResponse, HttpResponseBuilder}; -// #[cfg(test)] -// mod tests { -// use crate::dev::Body; -// use crate::http::StatusCode; -// use crate::HttpRespone; + macro_rules! static_resp { + ($name:ident, $status:expr) => { + #[allow(non_snake_case, missing_docs)] + pub fn $name() -> HttpResponseBuilder { + HttpResponseBuilder::new($status) + } + }; + } -// #[test] -// fn test_build() { -// let resp = HttpResponse::Ok().body(Body::Empty); -// assert_eq!(resp.status(), StatusCode::OK); -// } -// } -// } + 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 c2ecc0033..9765343c1 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 f5e19258f..b6fd8d2c9 100644 --- a/src/test.rs +++ b/src/test.rs @@ -28,7 +28,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. @@ -42,7 +42,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() } diff --git a/src/types/json.rs b/src/types/json.rs index 068dfeb2c..b069de0b8 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -233,7 +233,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() @@ -494,7 +494,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 90ee5296b..3bcff7b60 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -155,7 +155,7 @@ where /// .app_data(PathConfig::default().error_handler(|err, req| { /// error::InternalError::from_response( /// err, -/// HttpResponse::Conflict().finish(), +/// HttpResponse::Conflict().into(), /// ) /// .into() /// })) @@ -298,15 +298,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 b6c025ef3..2bc313d4a 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -172,7 +172,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();