1
0
mirror of https://github.com/fafhrd91/actix-web synced 2024-11-23 16:21:06 +01:00

Query config (#839)

* add QueryConfig

* expose QueryConfig in web module

* fmt

* use associated type for QueryConfig

* update CHANGES.md
This commit is contained in:
Davide Di Carlo 2019-05-14 22:54:30 +02:00 committed by Nikolay Kim
parent f8af3b86e5
commit bba90d7f22
4 changed files with 120 additions and 4 deletions

View File

@ -1,5 +1,9 @@
# Changes
### Add
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
### Changes
* `JsonConfig` is now `Send + Sync`, this implies that `error_handler` must be `Send + Sync` too.

View File

@ -6,6 +6,7 @@ use url::ParseError as UrlParseError;
use crate::http::StatusCode;
use crate::HttpResponse;
use serde_urlencoded::de;
/// Errors which can occur when attempting to generate resource uri.
#[derive(Debug, PartialEq, Display, From)]
@ -91,6 +92,23 @@ impl ResponseError for JsonPayloadError {
}
}
/// A set of errors that can occur during parsing query strings
#[derive(Debug, Display, From)]
pub enum QueryPayloadError {
/// Deserialize error
#[display(fmt = "Query deserialize error: {}", _0)]
Deserialize(de::Error),
}
/// Return `BadRequest` for `QueryPayloadError`
impl ResponseError for QueryPayloadError {
fn error_response(&self) -> HttpResponse {
match *self {
QueryPayloadError::Deserialize(_) => HttpResponse::new(StatusCode::BAD_REQUEST),
}
}
}
/// Error type returned when reading body as lines.
#[derive(From, Display, Debug)]
pub enum ReadlinesError {
@ -143,6 +161,12 @@ mod tests {
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_query_payload_error() {
let resp: HttpResponse = QueryPayloadError::Deserialize(serde_urlencoded::from_str::<i32>("bad query").unwrap_err()).error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_readlines_error() {
let resp: HttpResponse = ReadlinesError::LimitOverflow.error_response();

View File

@ -11,4 +11,4 @@ pub use self::form::{Form, FormConfig};
pub use self::json::{Json, JsonConfig};
pub use self::path::Path;
pub use self::payload::{Payload, PayloadConfig};
pub use self::query::Query;
pub use self::query::{Query, QueryConfig};

View File

@ -1,5 +1,6 @@
//! Query extractor
use std::sync::Arc;
use std::{fmt, ops};
use actix_http::error::Error;
@ -9,6 +10,7 @@ use serde_urlencoded;
use crate::dev::Payload;
use crate::extract::FromRequest;
use crate::request::HttpRequest;
use crate::error::QueryPayloadError;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
/// Extract typed information from from the request's query.
@ -115,32 +117,103 @@ impl<T> FromRequest for Query<T>
where
T: de::DeserializeOwned,
{
type Config = ();
type Error = Error;
type Future = Result<Self, Error>;
type Config = QueryConfig;
#[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req
.app_data::<Self::Config>()
.map(|c| c.ehandler.clone())
.unwrap_or(None);
serde_urlencoded::from_str::<T>(req.query_string())
.map(|val| Ok(Query(val)))
.unwrap_or_else(|e| {
.unwrap_or_else(move |e| {
let e = QueryPayloadError::Deserialize(e);
log::debug!(
"Failed during Query extractor deserialization. \
Request path: {:?}",
req.path()
);
Err(e.into())
let e = if let Some(error_handler) = error_handler {
(error_handler)(e, req)
} else {
e.into()
};
Err(e)
})
}
}
/// Query extractor configuration
///
/// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{error, web, App, FromRequest, HttpResponse};
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's querystring
/// fn index(info: web::Query<Info>) -> String {
/// format!("Welcome {}!", info.username)
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html").data(
/// // change query extractor configuration
/// web::Query::<Info>::configure(|cfg| {
/// cfg.error_handler(|err, req| { // <- create custom error response
/// error::InternalError::from_response(
/// err, HttpResponse::Conflict().finish()).into()
/// })
/// }))
/// .route(web::post().to(index))
/// );
/// }
/// ```
#[derive(Clone)]
pub struct QueryConfig {
ehandler: Option<Arc<Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync>>,
}
impl QueryConfig {
/// Set custom error handler
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(QueryPayloadError, &HttpRequest) -> Error + Send + Sync + 'static,
{
self.ehandler = Some(Arc::new(f));
self
}
}
impl Default for QueryConfig {
fn default() -> Self {
QueryConfig {
ehandler: None,
}
}
}
#[cfg(test)]
mod tests {
use derive_more::Display;
use serde_derive::Deserialize;
use actix_http::http::StatusCode;
use super::*;
use crate::test::TestRequest;
use crate::error::InternalError;
use crate::HttpResponse;
#[derive(Deserialize, Debug, Display)]
struct Id {
@ -164,4 +237,19 @@ mod tests {
let s = s.into_inner();
assert_eq!(s.id, "test1");
}
#[test]
fn test_custom_error_responder() {
let req = TestRequest::with_uri("/name/user1/")
.data(QueryConfig::default().error_handler(|e, _| {
let resp = HttpResponse::UnprocessableEntity().finish();
InternalError::from_response(e, resp).into()
})).to_srv_request();
let (req, mut pl) = req.into_parts();
let query = Query::<Id>::from_request(&req, &mut pl);
assert!(query.is_err());
assert_eq!(query.unwrap_err().as_response_error().error_response().status(), StatusCode::UNPROCESSABLE_ENTITY);
}
}