1
0
mirror of https://github.com/actix/actix-extras.git synced 2024-11-24 07:53:00 +01:00
This commit is contained in:
dowwie 2019-04-01 15:10:49 -04:00
commit 6d169f4c9c
14 changed files with 294 additions and 27 deletions

View File

@ -8,6 +8,7 @@ cache:
matrix: matrix:
include: include:
- rust: 1.31.0
- rust: stable - rust: stable
- rust: beta - rust: beta
- rust: nightly-2019-03-02 - rust: nightly-2019-03-02

View File

@ -113,3 +113,7 @@ flate2 = "1.0.2"
lto = true lto = true
opt-level = 3 opt-level = 3
codegen-units = 1 codegen-units = 1
[patch.crates-io]
actix = { git = "https://github.com/actix/actix.git" }
actix-http = { path = "actix-http" }

View File

@ -566,7 +566,10 @@ mod tests {
use bytes::BytesMut; use bytes::BytesMut;
use super::*; 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::test::{self, TestRequest};
use actix_web::App; use actix_web::App;
@ -683,7 +686,6 @@ mod tests {
#[test] #[test]
fn test_named_file_image_attachment() { fn test_named_file_image_attachment() {
use header::{ContentDisposition, DispositionParam, DispositionType};
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename(String::from("test.png"))], parameters: vec![DispositionParam::Filename(String::from("test.png"))],

View File

@ -1,5 +1,12 @@
# Changes # Changes
## [0.1.0-alpha.3] - 2019-04-xx
### Fixed
* Rust 1.31.0 compatibility
## [0.1.0-alpha.2] - 2019-03-29 ## [0.1.0-alpha.2] - 2019-03-29
### Added ### Added

View File

@ -59,7 +59,7 @@ mod parse;
#[macro_use] #[macro_use]
mod secure; mod secure;
#[cfg(feature = "secure-cookies")] #[cfg(feature = "secure-cookies")]
pub use secure::*; pub use self::secure::*;
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt; use std::fmt;
@ -68,11 +68,11 @@ use std::str::FromStr;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET};
use time::{Duration, Tm}; use time::{Duration, Tm};
pub use builder::CookieBuilder; pub use self::builder::CookieBuilder;
pub use draft::*; pub use self::draft::*;
pub use jar::{CookieJar, Delta, Iter}; pub use self::jar::{CookieJar, Delta, Iter};
use parse::parse_cookie; use self::parse::parse_cookie;
pub use parse::ParseError; pub use self::parse::ParseError;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum CookieStr { enum CookieStr {

View File

@ -1,5 +1,22 @@
# Changes # Changes
## [0.1.0-alpha.3] - 2019-04-xx
### Added
* Added `Deref<Target = RequestHead>` 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 ## [0.1.0-alpha.2] - 2019-03-29
### Added ### Added

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "0.1.0-alpha.2" version = "0.1.0-alpha.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http client." description = "Actix http client."
readme = "README.md" readme = "README.md"
@ -44,6 +44,7 @@ bytes = "0.4"
derive_more = "0.14" derive_more = "0.14"
futures = "0.1.25" futures = "0.1.25"
log =" 0.4" log =" 0.4"
mime = "0.3"
percent-encoding = "1.0" percent-encoding = "1.0"
rand = "0.6" rand = "0.6"
serde = "1.0" serde = "1.0"
@ -62,6 +63,5 @@ actix-server = { version = "0.4.1", features=["ssl"] }
brotli2 = { version="0.3.2" } brotli2 = { version="0.3.2" }
flate2 = { version="1.0.2" } flate2 = { version="1.0.2" }
env_logger = "0.6" env_logger = "0.6"
mime = "0.3"
rand = "0.6" rand = "0.6"
tokio-tcp = "0.1" tokio-tcp = "0.1"

View File

@ -4,6 +4,9 @@ pub use actix_http::error::PayloadError;
pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::HandshakeError as WsHandshakeError;
pub use actix_http::ws::ProtocolError as WsProtocolError; 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 actix_http::http::{header::HeaderValue, Error as HttpError, StatusCode};
use derive_more::{Display, From}; use derive_more::{Display, From};
@ -47,3 +50,27 @@ impl From<HttpError> for WsClientError {
WsClientError::SendRequest(err.into()) 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)
}
}

View File

@ -39,7 +39,7 @@ pub mod ws;
pub use self::builder::ClientBuilder; pub use self::builder::ClientBuilder;
pub use self::request::ClientRequest; pub use self::request::ClientRequest;
pub use self::response::ClientResponse; pub use self::response::{ClientResponse, JsonBody, MessageBody};
use self::connect::{Connect, ConnectorWrapper}; use self::connect::{Connect, ConnectorWrapper};

View File

@ -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 { impl fmt::Debug for ClientRequest {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!( writeln!(

View File

@ -4,13 +4,14 @@ use std::fmt;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::{Future, Poll, Stream}; 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::header::{CONTENT_LENGTH, SET_COOKIE};
use actix_http::http::{HeaderMap, StatusCode, Version}; use actix_http::http::{HeaderMap, StatusCode, Version};
use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead};
use serde::de::DeserializeOwned;
use actix_http::cookie::Cookie; use crate::error::JsonPayloadError;
use actix_http::error::CookieParseError;
/// Client Response /// Client Response
pub struct ClientResponse<S = PayloadStream> { pub struct ClientResponse<S = PayloadStream> {
@ -104,10 +105,21 @@ impl<S> ClientResponse<S>
where where
S: Stream<Item = Bytes, Error = PayloadError> + 'static, S: Stream<Item = Bytes, Error = PayloadError> + 'static,
{ {
/// Load http response's body. /// Loads http response's body.
pub fn body(self) -> MessageBody<S> { pub fn body(&mut self) -> MessageBody<S> {
MessageBody::new(self) MessageBody::new(self)
} }
/// Loads and parse `application/json` encoded body.
/// Return `JsonBody<T>` future. It resolves to a `T` value.
///
/// Returns error:
///
/// * content type is not `application/json`
/// * content length is greater than 256k
pub fn json<T: DeserializeOwned>(&mut self) -> JsonBody<S, T> {
JsonBody::new(self)
}
} }
impl<S> Stream for ClientResponse<S> impl<S> Stream for ClientResponse<S>
@ -137,7 +149,7 @@ impl<S> fmt::Debug for ClientResponse<S> {
pub struct MessageBody<S> { pub struct MessageBody<S> {
limit: usize, limit: usize,
length: Option<usize>, length: Option<usize>,
stream: Option<ClientResponse<S>>, stream: Option<Payload<S>>,
err: Option<PayloadError>, err: Option<PayloadError>,
fut: Option<Box<Future<Item = Bytes, Error = PayloadError>>>, fut: Option<Box<Future<Item = Bytes, Error = PayloadError>>>,
} }
@ -147,7 +159,7 @@ where
S: Stream<Item = Bytes, Error = PayloadError> + 'static, S: Stream<Item = Bytes, Error = PayloadError> + 'static,
{ {
/// Create `MessageBody` for request. /// Create `MessageBody` for request.
pub fn new(res: ClientResponse<S>) -> MessageBody<S> { pub fn new(res: &mut ClientResponse<S>) -> MessageBody<S> {
let mut len = None; let mut len = None;
if let Some(l) = res.headers().get(CONTENT_LENGTH) { if let Some(l) = res.headers().get(CONTENT_LENGTH) {
if let Ok(s) = l.to_str() { if let Ok(s) = l.to_str() {
@ -164,7 +176,7 @@ where
MessageBody { MessageBody {
limit: 262_144, limit: 262_144,
length: len, length: len,
stream: Some(res), stream: Some(res.take_payload()),
fut: None, fut: None,
err: 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<S, U> {
limit: usize,
length: Option<usize>,
stream: Payload<S>,
err: Option<JsonPayloadError>,
fut: Option<Box<Future<Item = U, Error = JsonPayloadError>>>,
}
impl<S, U> JsonBody<S, U>
where
S: Stream<Item = Bytes, Error = PayloadError> + 'static,
U: DeserializeOwned,
{
/// Create `JsonBody` for request.
pub fn new(req: &mut ClientResponse<S>) -> 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::<usize>() {
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<T, U> Future for JsonBody<T, U>
where
T: Stream<Item = Bytes, Error = PayloadError> + 'static,
U: DeserializeOwned + 'static,
{
type Item = U;
type Error = JsonPayloadError;
fn poll(&mut self) -> Poll<U, JsonPayloadError> {
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::<U>(&body)?));
self.fut = Some(Box::new(fut));
self.poll()
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures::Async; use futures::Async;
use serde::{Deserialize, Serialize};
use crate::{http::header, test::TestResponse}; use crate::{http::header, test::block_on, test::TestResponse};
#[test] #[test]
fn test_body() { 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() { match req.body().poll().err().unwrap() {
PayloadError::UnknownLength => (), PayloadError::UnknownLength => (),
_ => unreachable!("error"), _ => 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() { match req.body().poll().err().unwrap() {
PayloadError::Overflow => (), PayloadError::Overflow => (),
_ => unreachable!("error"), _ => unreachable!("error"),
} }
let req = TestResponse::default() let mut req = TestResponse::default()
.set_payload(Bytes::from_static(b"test")) .set_payload(Bytes::from_static(b"test"))
.finish(); .finish();
match req.body().poll().ok().unwrap() { match req.body().poll().ok().unwrap() {
@ -259,7 +375,7 @@ mod tests {
_ => unreachable!("error"), _ => unreachable!("error"),
} }
let req = TestResponse::default() let mut req = TestResponse::default()
.set_payload(Bytes::from_static(b"11111111111111")) .set_payload(Bytes::from_static(b"11111111111111"))
.finish(); .finish();
match req.body().limit(5).poll().err().unwrap() { match req.body().limit(5).poll().err().unwrap() {
@ -267,4 +383,73 @@ mod tests {
_ => unreachable!("error"), _ => 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()
}
);
}
} }

View File

@ -6,6 +6,8 @@ use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue};
use actix_http::http::{HeaderName, HttpTryFrom, Version}; use actix_http::http::{HeaderName, HttpTryFrom, Version};
use actix_http::{h1, Payload, ResponseHead}; use actix_http::{h1, Payload, ResponseHead};
use bytes::Bytes; use bytes::Bytes;
#[cfg(test)]
use futures::Future;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::{percent_encode, USERINFO_ENCODE_SET};
use crate::ClientResponse; use crate::ClientResponse;
@ -18,7 +20,7 @@ thread_local! {
} }
#[cfg(test)] #[cfg(test)]
pub fn run_on<F, R>(f: F) -> R pub(crate) fn run_on<F, R>(f: F) -> R
where where
F: Fn() -> R, F: Fn() -> R,
{ {
@ -29,6 +31,14 @@ where
.unwrap() .unwrap()
} }
#[cfg(test)]
pub(crate) fn block_on<F>(f: F) -> Result<F::Item, F::Error>
where
F: Future,
{
RT.with(move |rt| rt.borrow_mut().block_on(f))
}
/// Test `ClientResponse` builder /// Test `ClientResponse` builder
pub struct TestResponse { pub struct TestResponse {
head: ResponseHead, head: ResponseHead,

View File

@ -79,7 +79,7 @@ pub enum JsonPayloadError {
Payload(PayloadError), Payload(PayloadError),
} }
/// Return `BadRequest` for `UrlencodedError` /// Return `BadRequest` for `JsonPayloadError`
impl ResponseError for JsonPayloadError { impl ResponseError for JsonPayloadError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
match *self { match *self {

View File

@ -62,7 +62,7 @@
//! * SSL support with OpenSSL or `native-tls` //! * SSL support with OpenSSL or `native-tls`
//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) //! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`)
//! * Supports [Actix actor framework](https://github.com/actix/actix) //! * 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 //! ## Package feature
//! //!