diff --git a/examples/json/src/main.rs b/examples/json/src/main.rs index f47c195ca..fae29053d 100644 --- a/examples/json/src/main.rs +++ b/examples/json/src/main.rs @@ -6,7 +6,7 @@ extern crate serde_json; #[macro_use] extern crate serde_derive; use actix_web::*; -use futures::{Future, Stream}; +use futures::Future; #[derive(Debug, Serialize, Deserialize)] struct MyObj { @@ -14,28 +14,13 @@ struct MyObj { number: i32, } -fn index(mut req: HttpRequest) -> Result>> { - // check content-type - if req.content_type() != "application/json" { - return Err(error::ErrorBadRequest("wrong content-type").into()) - } - - Ok(Box::new( - // `concat2` will asynchronously read each chunk of the request body and - // return a single, concatenated, chunk - req.payload_mut().readany().concat2() - // `Future::from_err` acts like `?` in that it coerces the error type from - // the future into the final error type - .from_err() - // `Future::and_then` can be used to merge an asynchronous workflow with a - // synchronous workflow - .and_then(|body| { // <- body is loaded, now we can deserialize json - let obj = serde_json::from_slice::(&body).map_err(error::ErrorBadRequest)?; - - println!("model: {:?}", obj); - Ok(httpcodes::HTTPOk.build().json(obj)?) // <- send response - }) - )) +fn index(mut req: HttpRequest) -> Box> { + req.json().from_err() + .and_then(|val: MyObj| { + println!("model: {:?}", val); + Ok(httpcodes::HTTPOk.build().json(val)?) // <- send response + }) + .responder() } fn main() { diff --git a/guide/src/qs_7.md b/guide/src/qs_7.md index 2d4368c84..3e321ff4d 100644 --- a/guide/src/qs_7.md +++ b/guide/src/qs_7.md @@ -59,21 +59,41 @@ fn index(req: HttpRequest) -> HttpResponse { ## JSON Request -Unfortunately, because of async nature of actix web framework, json requests deserialization -is not very ergonomic process. First you need to load whole body into a -temporal storage and only then you can deserialize it. +There are two options of json body deserialization. -Here is simple example. We will deserialize *MyObj* struct. +First option is to use *HttpResponse::json()* method. This method returns +[*JsonBody*](../actix_web/dev/struct.JsonBody.html) object which resolves into +deserialized value. -```rust,ignore -#[derive(Debug, Deserialize)] +```rust +# extern crate actix; +# extern crate actix_web; +# extern crate futures; +# extern crate env_logger; +# extern crate serde_json; +# #[macro_use] extern crate serde_derive; +# use actix_web::*; +# use futures::Future; +#[derive(Debug, Serialize, Deserialize)] struct MyObj { name: String, number: i32, } + +fn index(mut req: HttpRequest) -> Box> { + req.json().from_err() + .and_then(|val: MyObj| { + println!("model: {:?}", val); + Ok(httpcodes::HTTPOk.build().json(val)?) // <- send response + }) + .responder() +} +# fn main() {} ``` -We need to load request body first and then deserialize json into object. +Or you can manually load payload into memory and ther deserialize it. +Here is simple example. We will deserialize *MyObj* struct. We need to load request +body first and then deserialize json into object. ```rust,ignore fn index(mut req: HttpRequest) -> Future { @@ -92,7 +112,7 @@ fn index(mut req: HttpRequest) -> Future { } ``` -Full example is available in +Example is available in [examples directory](https://github.com/actix/actix-web/tree/master/examples/json/). diff --git a/src/error.rs b/src/error.rs index 275485603..ea9f06526 100644 --- a/src/error.rs +++ b/src/error.rs @@ -10,6 +10,7 @@ use std::error::Error as StdError; use cookie; use httparse; use failure::Fail; +use futures::Canceled; use http2::Error as Http2Error; use http::{header, StatusCode, Error as HttpError}; use http::uri::InvalidUriBytes; @@ -110,6 +111,9 @@ impl ResponseError for io::Error { /// `InternalServerError` for `InvalidHeaderValue` impl ResponseError for header::InvalidHeaderValue {} +/// `InternalServerError` for `futures::Canceled` +impl ResponseError for Canceled {} + /// Internal error #[derive(Fail, Debug)] #[fail(display="Unexpected task frame")] @@ -393,6 +397,43 @@ impl From for UrlencodedError { } } +/// A set of errors that can occur during parsing json payloads +#[derive(Fail, Debug)] +pub enum JsonPayloadError { + /// Payload size is bigger than 256k + #[fail(display="Payload size is bigger than 256k")] + Overflow, + /// Content type error + #[fail(display="Content type error")] + ContentType, + /// Deserialize error + #[fail(display="Json deserialize error")] + Deserialize(JsonError), + /// Payload error + #[fail(display="Error that occur during reading payload")] + Payload(PayloadError), +} + +/// Return `BadRequest` for `UrlencodedError` +impl ResponseError for JsonPayloadError { + + fn error_response(&self) -> HttpResponse { + HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty) + } +} + +impl From for JsonPayloadError { + fn from(err: PayloadError) -> JsonPayloadError { + JsonPayloadError::Payload(err) + } +} + +impl From for JsonPayloadError { + fn from(err: JsonError) -> JsonPayloadError { + JsonPayloadError::Deserialize(err) + } +} + /// Errors which can occur when attempting to interpret a segment string as a /// valid path segment. #[derive(Fail, Debug, PartialEq)] diff --git a/src/handler.rs b/src/handler.rs index 934345da6..6ad426d53 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -36,6 +36,21 @@ pub trait Responder { fn respond_to(self, req: HttpRequest) -> Result; } +/// Convinience trait that convert `Future` object into `Boxed` future +pub trait AsyncResponder: Sized { + fn responder(self) -> Box>; +} + +impl AsyncResponder for F + where F: Future + 'static, + I: Responder + 'static, + E: Into + 'static, +{ + fn responder(self) -> Box> { + Box::new(self) + } +} + /// Handler for Fn() impl Handler for F where F: Fn(HttpRequest) -> R + 'static, diff --git a/src/httprequest.rs b/src/httprequest.rs index e48cfcd85..3308d8513 100644 --- a/src/httprequest.rs +++ b/src/httprequest.rs @@ -5,9 +5,10 @@ use std::rc::Rc; use std::net::SocketAddr; use std::collections::HashMap; use bytes::BytesMut; -use futures::{Async, Future, Stream, Poll}; use cookie::Cookie; +use futures::{Async, Future, Stream, Poll}; use http_range::HttpRange; +use serde::de::DeserializeOwned; use url::{Url, form_urlencoded}; use http::{header, Uri, Method, Version, HeaderMap, Extensions}; @@ -15,6 +16,7 @@ use info::ConnectionInfo; use param::Params; use router::Router; use payload::Payload; +use json::JsonBody; use multipart::Multipart; use helpers::SharedHttpMessage; use error::{ParseError, UrlGenerationError, CookieParseError, HttpRangeError, UrlencodedError}; @@ -468,6 +470,40 @@ impl HttpRequest { pub fn urlencoded(&mut self) -> UrlEncoded { UrlEncoded::from_request(self) } + + /// 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 + /// + /// ```rust + /// # extern crate actix_web; + /// # extern crate futures; + /// # #[macro_use] extern crate serde_derive; + /// use actix_web::*; + /// use futures::future::{Future, ok}; + /// + /// #[derive(Deserialize, Debug)] + /// struct MyObj { + /// name: String, + /// } + /// + /// fn index(mut req: HttpRequest) -> Box> { + /// req.json() // <- get JsonBody future + /// .from_err() + /// .and_then(|val: MyObj| { // <- deserialized value + /// println!("==== BODY ==== {:?}", val); + /// Ok(httpcodes::HTTPOk.response()) + /// }).responder() + /// } + /// # fn main() {} + /// ``` + pub fn json(&mut self) -> JsonBody { + JsonBody::from_request(self) + } } impl Default for HttpRequest<()> { diff --git a/src/json.rs b/src/json.rs index fd9d2d9fc..93cff4135 100644 --- a/src/json.rs +++ b/src/json.rs @@ -1,7 +1,12 @@ +use bytes::BytesMut; +use futures::{Poll, Future, Stream}; +use http::header::CONTENT_LENGTH; + use serde_json; use serde::Serialize; +use serde::de::DeserializeOwned; -use error::Error; +use error::{Error, JsonPayloadError}; use handler::Responder; use httprequest::HttpRequest; use httpresponse::HttpResponse; @@ -42,22 +47,168 @@ impl Responder for Json { } } +/// Request payload json parser that resolves to a deserialized `T` value. +/// +/// Returns error: +/// +/// * content type is not `application/json` +/// * content length is greater than 256k +/// +/// ```rust +/// # extern crate actix_web; +/// # extern crate futures; +/// # #[macro_use] extern crate serde_derive; +/// use actix_web::*; +/// use futures::future::Future; +/// +/// #[derive(Deserialize, Debug)] +/// struct MyObj { +/// name: String, +/// } +/// +/// fn index(mut req: HttpRequest) -> Box> { +/// req.json() // <- get JsonBody future +/// .from_err() +/// .and_then(|val: MyObj| { // <- deserialized value +/// println!("==== BODY ==== {:?}", val); +/// Ok(httpcodes::HTTPOk.response()) +/// }).responder() +/// } +/// # fn main() {} +/// ``` +pub struct JsonBody{ + limit: usize, + ct: &'static str, + req: Option>, + fut: Option>>, +} + +impl JsonBody { + + pub fn from_request(req: &mut HttpRequest) -> Self { + JsonBody{ + limit: 262_144, + req: Some(req.clone()), + fut: None, + ct: "application/json", + } + } + + /// Change max size of payload. By default max size is 256Kb + pub fn limit(mut self, limit: usize) -> Self { + self.limit = limit; + self + } + + /// Set allowed content type. + /// + /// By default *application/json* content type is used. Set content type + /// to empty string if you want to disable content type check. + pub fn content_type(mut self, ct: &'static str) -> Self { + self.ct = ct; + self + } +} + +impl Future for JsonBody { + type Item = T; + type Error = JsonPayloadError; + + fn poll(&mut self) -> Poll { + if let Some(mut req) = self.req.take() { + if let Some(len) = req.headers().get(CONTENT_LENGTH) { + if let Ok(s) = len.to_str() { + if let Ok(len) = s.parse::() { + if len > self.limit { + return Err(JsonPayloadError::Overflow); + } + } else { + return Err(JsonPayloadError::Overflow); + } + } + } + // check content-type + if !self.ct.is_empty() && req.content_type() != self.ct { + return Err(JsonPayloadError::ContentType) + } + + let limit = self.limit; + let fut = req.payload_mut().readany() + .from_err() + .fold(BytesMut::new(), 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.fut.as_mut().expect("JsonBody could not be used second time").poll() + } +} + #[cfg(test)] mod tests { use super::*; - use http::{header, Method}; - use application::Application; + use bytes::Bytes; + use http::header; + use futures::Async; - #[derive(Serialize)] - struct MyObj { - name: &'static str, + impl PartialEq for JsonPayloadError { + fn eq(&self, other: &JsonPayloadError) -> bool { + match *self { + JsonPayloadError::Overflow => match *other { + JsonPayloadError::Overflow => true, + _ => false, + }, + JsonPayloadError::ContentType => match *other { + JsonPayloadError::ContentType => true, + _ => false, + }, + _ => false, + } + } + } + + #[derive(Serialize, Deserialize, PartialEq, Debug)] + struct MyObject { + name: String, } #[test] fn test_json() { - let json = Json(MyObj{name: "test"}); + let json = Json(MyObject{name: "test".to_owned()}); let resp = json.respond_to(HttpRequest::default()).unwrap(); assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "application/json"); } + #[test] + fn test_json_body() { + let mut req = HttpRequest::default(); + let mut json = req.json::(); + assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); + + let mut json = req.json::().content_type("text/json"); + req.headers_mut().insert(header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json")); + assert_eq!(json.poll().err().unwrap(), JsonPayloadError::ContentType); + + let mut json = req.json::().limit(100); + req.headers_mut().insert(header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json")); + req.headers_mut().insert(header::CONTENT_LENGTH, + header::HeaderValue::from_static("10000")); + assert_eq!(json.poll().err().unwrap(), JsonPayloadError::Overflow); + + req.headers_mut().insert(header::CONTENT_LENGTH, + header::HeaderValue::from_static("16")); + req.payload_mut().unread_data(Bytes::from_static(b"{\"name\": \"test\"}")); + let mut json = req.json::(); + assert_eq!(json.poll().ok().unwrap(), Async::Ready(MyObject{name: "test".to_owned()})); + } + } diff --git a/src/lib.rs b/src/lib.rs index 81b9fc2f6..56240a8cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,7 @@ pub use json::{Json}; pub use application::Application; pub use httprequest::HttpRequest; pub use httpresponse::HttpResponse; -pub use handler::{Reply, Responder, NormalizePath}; +pub use handler::{Reply, Responder, NormalizePath, AsyncResponder}; pub use route::Route; pub use resource::Resource; pub use server::HttpServer; @@ -166,6 +166,7 @@ pub mod dev { pub use body::BodyStream; pub use info::ConnectionInfo; pub use handler::Handler; + pub use json::JsonBody; pub use router::{Router, Pattern}; pub use pipeline::Pipeline; pub use channel::{HttpChannel, HttpHandler, IntoHttpHandler}; diff --git a/src/payload.rs b/src/payload.rs index eda81c755..7c921070c 100644 --- a/src/payload.rs +++ b/src/payload.rs @@ -433,7 +433,7 @@ impl Inner { fn unread_data(&mut self, data: Bytes) { self.len += data.len(); - self.items.push_front(data) + self.items.push_front(data); } fn capacity(&self) -> usize {