diff --git a/actix-http/src/client/error.rs b/actix-http/src/client/error.rs index fc4b5b72b..40aef2cce 100644 --- a/actix-http/src/client/error.rs +++ b/actix-http/src/client/error.rs @@ -128,3 +128,23 @@ impl ResponseError for SendRequestError { .into() } } + +/// A set of errors that can occur during freezing a request +#[derive(Debug, Display, From)] +pub enum FreezeRequestError { + /// Invalid URL + #[display(fmt = "Invalid URL: {}", _0)] + Url(InvalidUrl), + /// Http error + #[display(fmt = "{}", _0)] + Http(HttpError), +} + +impl From for SendRequestError { + fn from(e: FreezeRequestError) -> Self { + match e { + FreezeRequestError::Url(e) => e.into(), + FreezeRequestError::Http(e) => e.into(), + } + } +} \ No newline at end of file diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index 1d10117cd..04427ce42 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -10,7 +10,7 @@ mod pool; pub use self::connection::Connection; pub use self::connector::Connector; -pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; +pub use self::error::{ConnectError, InvalidUrl, SendRequestError, FreezeRequestError}; pub use self::pool::Protocol; #[derive(Clone)] diff --git a/awc/src/error.rs b/awc/src/error.rs index f78355c67..4eb929007 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,5 +1,5 @@ //! Http client errors -pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; +pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError, FreezeRequestError}; pub use actix_http::error::PayloadError; pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::ProtocolError as WsProtocolError; diff --git a/awc/src/request.rs b/awc/src/request.rs index c6dc9c256..b28912e1a 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -22,7 +22,7 @@ use actix_http::http::{ }; use actix_http::{Error, Payload, RequestHead}; -use crate::error::{InvalidUrl, PayloadError, SendRequestError}; +use crate::error::{InvalidUrl, PayloadError, SendRequestError, FreezeRequestError}; use crate::response::ClientResponse; use crate::ClientConfig; @@ -373,34 +373,24 @@ impl ClientRequest { } } - /// Complete request construction and send body. - pub fn send_body( - mut self, - body: B, - ) -> impl Future< - Item = ClientResponse>, - Error = SendRequestError, - > - where - B: Into, - { - if let Some(e) = self.err.take() { - return Either::A(err(e.into())); + pub fn freeze(mut self) -> Result { + if let Some(e) = self.err { + return Err(e.into()); } // validate uri let uri = &self.head.uri; if uri.host().is_none() { - return Either::A(err(InvalidUrl::MissingHost.into())); + return Err(InvalidUrl::MissingHost.into()); } else if uri.scheme_part().is_none() { - return Either::A(err(InvalidUrl::MissingScheme.into())); + return Err(InvalidUrl::MissingScheme.into()); } else if let Some(scheme) = uri.scheme_part() { match scheme.as_str() { "http" | "ws" | "https" | "wss" => (), - _ => return Either::A(err(InvalidUrl::UnknownScheme.into())), + _ => return Err(InvalidUrl::UnknownScheme.into()), } } else { - return Either::A(err(InvalidUrl::UnknownScheme.into())); + return Err(InvalidUrl::UnknownScheme.into()); } // set cookies @@ -421,9 +411,9 @@ impl ClientRequest { // enable br only for https #[cfg(any( - feature = "brotli", - feature = "flate2-zlib", - feature = "flate2-rust" + feature = "brotli", + feature = "flate2-zlib", + feature = "flate2-rust" ))] { if slf.response_decompress { @@ -438,44 +428,42 @@ impl ClientRequest { slf = slf.set_header_if_none(header::ACCEPT_ENCODING, HTTPS_ENCODING) } else { #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] - { - slf = slf - .set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") - } + { + slf = slf + .set_header_if_none(header::ACCEPT_ENCODING, "gzip, deflate") + } }; } } - let head = slf.head; - let config = slf.config.as_ref(); - let response_decompress = slf.response_decompress; + let request = FrozenClientRequest { + head: Rc::new(slf.head), + addr: slf.addr, + response_decompress: slf.response_decompress, + timeout: slf.timeout, + config: slf.config, + }; - let fut = config - .connector - .borrow_mut() - .send_request(Rc::new(head), None, body.into(), slf.addr) - .map(move |res| { - res.map_body(|head, payload| { - if response_decompress { - Payload::Stream(Decoder::from_headers(payload, &head.headers)) - } else { - Payload::Stream(Decoder::new(payload, ContentEncoding::Identity)) - } - }) - }); + Ok(request) + } - // set request timeout - if let Some(timeout) = slf.timeout.or_else(|| config.timeout.clone()) { - Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| { - if let Some(e) = e.into_inner() { - e - } else { - SendRequestError::Timeout - } - }))) - } else { - Either::B(Either::B(fut)) - } + /// Complete request construction and send body. + pub fn send_body( + self, + body: B, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > + where + B: Into, + { + let frozen_request = match self.freeze() { + Ok(r) => r, + Err(e) => return Either::A(err(e.into())), + }; + + Either::B(frozen_request.send_body(None, body)) } /// Set a JSON body and generate `ClientRequest` @@ -486,15 +474,15 @@ impl ClientRequest { Item = ClientResponse>, Error = SendRequestError, > { - let body = match serde_json::to_string(value) { - Ok(body) => body, - Err(e) => return Either::A(err(Error::from(e).into())), - }; - // set content-type let slf = self.set_header_if_none(header::CONTENT_TYPE, "application/json"); - Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + let frozen_request = match slf.freeze() { + Ok(r) => r, + Err(e) => return Either::A(err(e.into())), + }; + + Either::B(frozen_request.send_json(None, value)) } /// Set a urlencoded body and generate `ClientRequest` @@ -507,18 +495,18 @@ impl ClientRequest { Item = ClientResponse>, Error = SendRequestError, > { - let body = match serde_urlencoded::to_string(value) { - Ok(body) => body, - Err(e) => return Either::A(err(Error::from(e).into())), - }; - // set content-type let slf = self.set_header_if_none( header::CONTENT_TYPE, "application/x-www-form-urlencoded", ); - Either::B(slf.send_body(Body::Bytes(Bytes::from(body)))) + let frozen_request = match slf.freeze() { + Ok(r) => r, + Err(e) => return Either::A(err(e.into())), + }; + + Either::B(frozen_request.send_form(None, value)) } /// Set an streaming body and generate `ClientRequest`. @@ -533,7 +521,12 @@ impl ClientRequest { S: Stream + 'static, E: Into + 'static, { - self.send_body(Body::from_message(BodyStream::new(stream))) + let frozen_request = match self.freeze() { + Ok(r) => r, + Err(e) => return Either::A(err(e.into())), + }; + + Either::B(frozen_request.send_stream(None, stream)) } /// Set an empty body and generate `ClientRequest`. @@ -543,7 +536,12 @@ impl ClientRequest { Item = ClientResponse>, Error = SendRequestError, > { - self.send_body(Body::Empty) + let frozen_request = match self.freeze() { + Ok(r) => r, + Err(e) => return Either::A(err(e.into())), + }; + + Either::B(frozen_request.send(None)) } } @@ -562,6 +560,154 @@ impl fmt::Debug for ClientRequest { } } +pub struct FrozenClientRequest { + head: Rc, + addr: Option, + response_decompress: bool, + timeout: Option, + config: Rc, +} + +impl FrozenClientRequest { + /// Send body with optional extra headers. + /// Extra headers will override corresponding existing headers in a frozen request. + pub fn send_body( + &self, + extra_headers: Option, + body: B, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > + where + B: Into, + { + let config = self.config.as_ref(); + let response_decompress = self.response_decompress; + + let fut = config + .connector + .borrow_mut() + .send_request(self.head.clone(), extra_headers, body.into(), self.addr) + .map(move |res| { + res.map_body(|head, payload| { + if response_decompress { + Payload::Stream(Decoder::from_headers(payload, &head.headers)) + } else { + Payload::Stream(Decoder::new(payload, ContentEncoding::Identity)) + } + }) + }); + + // set request timeout + if let Some(timeout) = self.timeout.or_else(|| config.timeout.clone()) { + Either::A(Timeout::new(fut, timeout).map_err(|e| { + if let Some(e) = e.into_inner() { + e + } else { + SendRequestError::Timeout + } + })) + } else { + Either::B(fut) + } + } + + /// Send a JSON body with optional extra headers. + /// Extra headers will override corresponding existing headers in a frozen request. + pub fn send_json( + &self, + mut extra_headers: Option, + value: &T, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { + let body = match serde_json::to_string(value) { + Ok(body) => body, + Err(e) => return Either::A(err(Error::from(e).into())), + }; + + // set content-type + if let Err(e) = self.set_extra_header_if_none(&mut extra_headers, header::CONTENT_TYPE, "application/json") { + return Either::A(err(e.into())); + } + + Either::B(self.send_body(extra_headers, Body::Bytes(Bytes::from(body)))) + } + + /// Send a urlencoded body with optional extra headers. + /// Extra headers will override corresponding existing headers in a frozen request. + pub fn send_form( + &self, + mut extra_headers: Option, + value: &T, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { + let body = match serde_urlencoded::to_string(value) { + Ok(body) => body, + Err(e) => return Either::A(err(Error::from(e).into())), + }; + + // set content-type + if let Err(e) = self.set_extra_header_if_none(&mut extra_headers, header::CONTENT_TYPE, "application/x-www-form-urlencoded") { + return Either::A(err(e.into())); + } + + Either::B(self.send_body(extra_headers, Body::Bytes(Bytes::from(body)))) + } + + /// Send a streaming body with optional extra headers. + /// Extra headers will override corresponding existing headers in a frozen request. + pub fn send_stream( + &self, + extra_headers: Option, + stream: S, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > + where + S: Stream + 'static, + E: Into + 'static, + { + self.send_body(extra_headers, Body::from_message(BodyStream::new(stream))) + } + + /// Send an empty body with optional extra headers. + /// Extra headers will override corresponding existing headers in a frozen request. + pub fn send( + &self, + extra_headers: Option, + ) -> impl Future< + Item = ClientResponse>, + Error = SendRequestError, + > { + self.send_body(extra_headers, Body::Empty) + } + + fn set_extra_header_if_none(&self, extra_headers: &mut Option, key: HeaderName, value: V) -> Result<(), HttpError> + where + V: IntoHeaderValue, + { + if !self.head.headers.contains_key(&key) + && !extra_headers.iter().any(|h| h.contains_key(&key)) { + + match value.try_into(){ + Ok(v) => { + let h = extra_headers.get_or_insert(HeaderMap::new()); + h.insert(key, v) + }, + Err(e) => return Err(e.into()), + }; + } + + Ok(()) + } +} + #[cfg(test)] mod tests { use std::time::SystemTime;