diff --git a/.travis.yml b/.travis.yml index c3698625..00f64d24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,6 +8,7 @@ cache: matrix: include: + - rust: 1.31.0 - rust: stable - rust: beta - rust: nightly-2019-03-02 diff --git a/Cargo.toml b/Cargo.toml index 3abe3129..b5d0e876 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,3 +113,7 @@ flate2 = "1.0.2" lto = true opt-level = 3 codegen-units = 1 + +[patch.crates-io] +actix = { git = "https://github.com/actix/actix.git" } +actix-http = { path = "actix-http" } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 54b4f961..d5a47653 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -566,7 +566,10 @@ mod tests { use bytes::BytesMut; use super::*; - use actix_web::http::{header, header::DispositionType, Method, StatusCode}; + use actix_web::http::header::{ + self, ContentDisposition, DispositionParam, DispositionType, + }; + use actix_web::http::{Method, StatusCode}; use actix_web::test::{self, TestRequest}; use actix_web::App; @@ -683,7 +686,6 @@ mod tests { #[test] fn test_named_file_image_attachment() { - use header::{ContentDisposition, DispositionParam, DispositionType}; let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 5659597c..c5c02865 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,12 @@ # Changes +## [0.1.0-alpha.3] - 2019-04-xx + +### Fixed + +* Rust 1.31.0 compatibility + + ## [0.1.0-alpha.2] - 2019-03-29 ### Added diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs index 5545624a..0f5f4548 100644 --- a/actix-http/src/cookie/mod.rs +++ b/actix-http/src/cookie/mod.rs @@ -59,7 +59,7 @@ mod parse; #[macro_use] mod secure; #[cfg(feature = "secure-cookies")] -pub use secure::*; +pub use self::secure::*; use std::borrow::Cow; use std::fmt; @@ -68,11 +68,11 @@ use std::str::FromStr; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use time::{Duration, Tm}; -pub use builder::CookieBuilder; -pub use draft::*; -pub use jar::{CookieJar, Delta, Iter}; -use parse::parse_cookie; -pub use parse::ParseError; +pub use self::builder::CookieBuilder; +pub use self::draft::*; +pub use self::jar::{CookieJar, Delta, Iter}; +use self::parse::parse_cookie; +pub use self::parse::ParseError; #[derive(Debug, Clone)] enum CookieStr { diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e0e83214..c3359318 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,5 +1,22 @@ # Changes + +## [0.1.0-alpha.3] - 2019-04-xx + +### Added + +* Added `Deref` for `ClientRequest`. + +* Export `MessageBody` type + +* `ClientResponse::json()` - Loads and parse `application/json` encoded body + + +### Changed + +* `ClientResponse::body()` does not consume response object. + + ## [0.1.0-alpha.2] - 2019-03-29 ### Added diff --git a/awc/Cargo.toml b/awc/Cargo.toml index c2cc9e7f..fdaf0a55 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "0.1.0-alpha.2" +version = "0.1.0-alpha.3" authors = ["Nikolay Kim "] description = "Actix http client." readme = "README.md" @@ -44,6 +44,7 @@ bytes = "0.4" derive_more = "0.14" futures = "0.1.25" log =" 0.4" +mime = "0.3" percent-encoding = "1.0" rand = "0.6" serde = "1.0" @@ -62,6 +63,5 @@ actix-server = { version = "0.4.1", features=["ssl"] } brotli2 = { version="0.3.2" } flate2 = { version="1.0.2" } env_logger = "0.6" -mime = "0.3" rand = "0.6" tokio-tcp = "0.1" \ No newline at end of file diff --git a/awc/src/error.rs b/awc/src/error.rs index 8f51fd7d..bbfd9b97 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -4,6 +4,9 @@ pub use actix_http::error::PayloadError; pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::ProtocolError as WsProtocolError; +use actix_http::{Response, ResponseError}; +use serde_json::error::Error as JsonError; + use actix_http::http::{header::HeaderValue, Error as HttpError, StatusCode}; use derive_more::{Display, From}; @@ -47,3 +50,27 @@ impl From for WsClientError { WsClientError::SendRequest(err.into()) } } + +/// A set of errors that can occur during parsing json payloads +#[derive(Debug, Display, From)] +pub enum JsonPayloadError { + /// Payload size is bigger than allowed. (default: 32kB) + #[display(fmt = "Json payload size is bigger than allowed.")] + Overflow, + /// Content type error + #[display(fmt = "Content type error")] + ContentType, + /// Deserialize error + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(JsonError), + /// Payload error + #[display(fmt = "Error that occur during reading payload: {}", _0)] + Payload(PayloadError), +} + +/// Return `InternlaServerError` for `JsonPayloadError` +impl ResponseError for JsonPayloadError { + fn error_response(&self) -> Response { + Response::new(StatusCode::INTERNAL_SERVER_ERROR) + } +} diff --git a/awc/src/lib.rs b/awc/src/lib.rs index ff1fb3fe..8d0ac6a5 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -39,7 +39,7 @@ pub mod ws; pub use self::builder::ClientBuilder; pub use self::request::ClientRequest; -pub use self::response::ClientResponse; +pub use self::response::{ClientResponse, JsonBody, MessageBody}; use self::connect::{Connect, ConnectorWrapper}; diff --git a/awc/src/request.rs b/awc/src/request.rs index a462479e..f732657d 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -554,6 +554,20 @@ impl ClientRequest { } } +impl std::ops::Deref for ClientRequest { + type Target = RequestHead; + + fn deref(&self) -> &RequestHead { + &self.head + } +} + +impl std::ops::DerefMut for ClientRequest { + fn deref_mut(&mut self) -> &mut RequestHead { + &mut self.head + } +} + impl fmt::Debug for ClientRequest { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln!( diff --git a/awc/src/response.rs b/awc/src/response.rs index 038a9a33..b9173520 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -4,13 +4,14 @@ use std::fmt; use bytes::{Bytes, BytesMut}; use futures::{Future, Poll, Stream}; -use actix_http::error::PayloadError; +use actix_http::cookie::Cookie; +use actix_http::error::{CookieParseError, PayloadError}; use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; +use serde::de::DeserializeOwned; -use actix_http::cookie::Cookie; -use actix_http::error::CookieParseError; +use crate::error::JsonPayloadError; /// Client Response pub struct ClientResponse { @@ -104,10 +105,21 @@ impl ClientResponse where S: Stream + 'static, { - /// Load http response's body. - pub fn body(self) -> MessageBody { + /// Loads http response's body. + pub fn body(&mut self) -> MessageBody { MessageBody::new(self) } + + /// Loads and parse `application/json` encoded body. + /// Return `JsonBody` future. It resolves to a `T` value. + /// + /// Returns error: + /// + /// * content type is not `application/json` + /// * content length is greater than 256k + pub fn json(&mut self) -> JsonBody { + JsonBody::new(self) + } } impl Stream for ClientResponse @@ -137,7 +149,7 @@ impl fmt::Debug for ClientResponse { pub struct MessageBody { limit: usize, length: Option, - stream: Option>, + stream: Option>, err: Option, fut: Option>>, } @@ -147,7 +159,7 @@ where S: Stream + 'static, { /// Create `MessageBody` for request. - pub fn new(res: ClientResponse) -> MessageBody { + pub fn new(res: &mut ClientResponse) -> MessageBody { let mut len = None; if let Some(l) = res.headers().get(CONTENT_LENGTH) { if let Ok(s) = l.to_str() { @@ -164,7 +176,7 @@ where MessageBody { limit: 262_144, length: len, - stream: Some(res), + stream: Some(res.take_payload()), fut: None, err: None, } @@ -230,28 +242,132 @@ where } } +/// Response's payload json parser, it resolves to a deserialized `T` value. +/// +/// Returns error: +/// +/// * content type is not `application/json` +/// * content length is greater than 64k +pub struct JsonBody { + limit: usize, + length: Option, + stream: Payload, + err: Option, + fut: Option>>, +} + +impl JsonBody +where + S: Stream + 'static, + U: DeserializeOwned, +{ + /// Create `JsonBody` for request. + pub fn new(req: &mut ClientResponse) -> Self { + // check content-type + let json = if let Ok(Some(mime)) = req.mime_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + if !json { + return JsonBody { + limit: 65536, + length: None, + stream: Payload::None, + fut: None, + err: Some(JsonPayloadError::ContentType), + }; + } + + let mut len = None; + if let Some(l) = req.headers().get(CONTENT_LENGTH) { + if let Ok(s) = l.to_str() { + if let Ok(l) = s.parse::() { + len = Some(l) + } + } + } + + JsonBody { + limit: 65536, + length: len, + stream: req.take_payload(), + fut: None, + err: None, + } + } + + /// Change max size of payload. By default max size is 64Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } +} + +impl Future for JsonBody +where + T: Stream + 'static, + U: DeserializeOwned + 'static, +{ + type Item = U; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + if let Some(ref mut fut) = self.fut { + return fut.poll(); + } + + if let Some(err) = self.err.take() { + return Err(err); + } + + let limit = self.limit; + if let Some(len) = self.length.take() { + if len > limit { + return Err(JsonPayloadError::Overflow); + } + } + + let fut = std::mem::replace(&mut self.stream, Payload::None) + .from_err() + .fold(BytesMut::with_capacity(8192), move |mut body, chunk| { + if (body.len() + chunk.len()) > limit { + Err(JsonPayloadError::Overflow) + } else { + body.extend_from_slice(&chunk); + Ok(body) + } + }) + .and_then(|body| Ok(serde_json::from_slice::(&body)?)); + self.fut = Some(Box::new(fut)); + self.poll() + } +} + #[cfg(test)] mod tests { use super::*; use futures::Async; + use serde::{Deserialize, Serialize}; - use crate::{http::header, test::TestResponse}; + use crate::{http::header, test::block_on, test::TestResponse}; #[test] fn test_body() { - let req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); match req.body().poll().err().unwrap() { PayloadError::UnknownLength => (), _ => unreachable!("error"), } - let req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = + TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); match req.body().poll().err().unwrap() { PayloadError::Overflow => (), _ => unreachable!("error"), } - let req = TestResponse::default() + let mut req = TestResponse::default() .set_payload(Bytes::from_static(b"test")) .finish(); match req.body().poll().ok().unwrap() { @@ -259,7 +375,7 @@ mod tests { _ => unreachable!("error"), } - let req = TestResponse::default() + let mut req = TestResponse::default() .set_payload(Bytes::from_static(b"11111111111111")) .finish(); match req.body().limit(5).poll().err().unwrap() { @@ -267,4 +383,73 @@ mod tests { _ => unreachable!("error"), } } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, + } + + fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { + match err { + JsonPayloadError::Overflow => match other { + JsonPayloadError::Overflow => true, + _ => false, + }, + JsonPayloadError::ContentType => match other { + JsonPayloadError::ContentType => true, + _ => false, + }, + _ => false, + } + } + + #[test] + fn test_json_body() { + let mut req = TestResponse::default().finish(); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/text"), + ) + .finish(); + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000"), + ) + .finish(); + + let json = block_on(JsonBody::<_, MyObject>::new(&mut req).limit(100)); + assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + + let mut req = TestResponse::default() + .header( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ) + .header( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + ) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .finish(); + + let json = block_on(JsonBody::<_, MyObject>::new(&mut req)); + assert_eq!( + json.ok().unwrap(), + MyObject { + name: "test".to_owned() + } + ); + } } diff --git a/awc/src/test.rs b/awc/src/test.rs index 5e595d15..1c772905 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -6,6 +6,8 @@ use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; +#[cfg(test)] +use futures::Future; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use crate::ClientResponse; @@ -18,7 +20,7 @@ thread_local! { } #[cfg(test)] -pub fn run_on(f: F) -> R +pub(crate) fn run_on(f: F) -> R where F: Fn() -> R, { @@ -29,6 +31,14 @@ where .unwrap() } +#[cfg(test)] +pub(crate) fn block_on(f: F) -> Result +where + F: Future, +{ + RT.with(move |rt| rt.borrow_mut().block_on(f)) +} + /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, diff --git a/src/error.rs b/src/error.rs index 02e17241..78dc2fb6 100644 --- a/src/error.rs +++ b/src/error.rs @@ -79,7 +79,7 @@ pub enum JsonPayloadError { Payload(PayloadError), } -/// Return `BadRequest` for `UrlencodedError` +/// Return `BadRequest` for `JsonPayloadError` impl ResponseError for JsonPayloadError { fn error_response(&self) -> HttpResponse { match *self { diff --git a/src/lib.rs b/src/lib.rs index cb29fa5b..ca496883 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -62,7 +62,7 @@ //! * SSL support with OpenSSL or `native-tls` //! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) //! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Supported Rust version: 1.32 or later +//! * Supported Rust version: 1.31 or later //! //! ## Package feature //!