diff --git a/examples/form.rs b/examples/form.rs new file mode 100644 index 00000000..4076b183 --- /dev/null +++ b/examples/form.rs @@ -0,0 +1,54 @@ +use http::Request; +use serde::Deserialize; +use std::net::SocketAddr; +use tower_web::prelude::*; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt::init(); + + // build our application with some routes + let app = route("/", get(show_form).post(accept_form)) + .layer(tower_http::trace::TraceLayer::new_for_http()); + + // run it with hyper + let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + tracing::debug!("listening on {}", addr); + app.serve(&addr).await.unwrap(); +} + +async fn show_form(_req: Request) -> response::Html<&'static str> { + response::Html( + r#" + + + + +
+ + + + + +
+ + + "#, + ) +} + +#[derive(Deserialize, Debug)] +struct Input { + name: String, + email: String, +} + +async fn accept_form(extract::Form(input): extract::Form) { + dbg!(&input); +} diff --git a/src/extract/mod.rs b/src/extract/mod.rs index 7aa55445..8567d639 100644 --- a/src/extract/mod.rs +++ b/src/extract/mod.rs @@ -132,11 +132,12 @@ use crate::{body::Body, response::IntoResponse}; use async_trait::async_trait; -use bytes::Bytes; +use bytes::{Buf, Bytes}; use http::{header, HeaderMap, Method, Request, Response, Uri, Version}; use rejection::{ - BodyAlreadyExtracted, FailedToBufferBody, InvalidJsonBody, InvalidUrlParam, InvalidUtf8, - LengthRequired, MissingExtension, MissingJsonContentType, MissingRouteParams, PayloadTooLarge, + BodyAlreadyExtracted, FailedToBufferBody, FailedToDeserializeQueryString, + InvalidFormContentType, InvalidJsonBody, InvalidUrlParam, InvalidUtf8, LengthRequired, + MissingExtension, MissingJsonContentType, MissingRouteParams, PayloadTooLarge, QueryStringMissing, RequestAlreadyExtracted, UrlParamsAlreadyExtracted, }; use serde::de::DeserializeOwned; @@ -192,10 +193,11 @@ where /// /// // ... /// } +/// /// let app = route("/list_things", get(list_things)); /// ``` /// -/// If the query string cannot be parsed it will reject the request with a `404 +/// If the query string cannot be parsed it will reject the request with a `400 /// Bad Request` response. #[derive(Debug, Clone, Copy, Default)] pub struct Query(pub T); @@ -205,15 +207,89 @@ impl FromRequest for Query where T: DeserializeOwned, { - type Rejection = QueryStringMissing; + type Rejection = Response; async fn from_request(req: &mut Request) -> Result { - let query = req.uri().query().ok_or(QueryStringMissing)?; - let value = serde_urlencoded::from_str(query).map_err(|_| QueryStringMissing)?; + let query = req + .uri() + .query() + .ok_or(QueryStringMissing) + .map_err(IntoResponse::into_response)?; + let value = serde_urlencoded::from_str(query) + .map_err(FailedToDeserializeQueryString::new::) + .map_err(IntoResponse::into_response)?; Ok(Query(value)) } } +/// Extractor that deserializes `application/x-www-form-urlencoded` requests +/// into some type. +/// +/// `T` is expected to implement [`serde::Deserialize`]. +/// +/// # Example +/// +/// ```rust,no_run +/// use tower_web::prelude::*; +/// use serde::Deserialize; +/// +/// #[derive(Deserialize)] +/// struct SignUp { +/// username: String, +/// password: String, +/// } +/// +/// async fn accept_form(form: extract::Form) { +/// let sign_up: SignUp = form.0; +/// +/// // ... +/// } +/// +/// let app = route("/sign_up", post(accept_form)); +/// ``` +/// +/// Note that `Content-Type: multipart/form-data` requests are not supported. +#[derive(Debug, Clone, Copy, Default)] +pub struct Form(pub T); + +#[async_trait] +impl FromRequest for Form +where + T: DeserializeOwned, +{ + type Rejection = Response; + + #[allow(warnings)] + async fn from_request(req: &mut Request) -> Result { + if !has_content_type(&req, "application/x-www-form-urlencoded") { + return Err(InvalidFormContentType.into_response()); + } + + if req.method() == Method::GET { + let query = req + .uri() + .query() + .ok_or(QueryStringMissing) + .map_err(IntoResponse::into_response)?; + let value = serde_urlencoded::from_str(query) + .map_err(FailedToDeserializeQueryString::new::) + .map_err(IntoResponse::into_response)?; + Ok(Form(value)) + } else { + let body = take_body(req).map_err(IntoResponse::into_response)?; + let chunks = hyper::body::aggregate(body) + .await + .map_err(FailedToBufferBody::from_err) + .map_err(IntoResponse::into_response)?; + let value = serde_urlencoded::from_reader(chunks.reader()) + .map_err(FailedToDeserializeQueryString::new::) + .map_err(IntoResponse::into_response)?; + + Ok(Form(value)) + } + } +} + /// Extractor that deserializes request bodies into some type. /// /// `T` is expected to implement [`serde::Deserialize`]. @@ -239,7 +315,7 @@ where /// let app = route("/users", post(create_user)); /// ``` /// -/// If the query string cannot be parsed it will reject the request with a `404 +/// If the query string cannot be parsed it will reject the request with a `400 /// Bad Request` response. /// /// The request is required to have a `Content-Type: application/json` header. diff --git a/src/extract/rejection.rs b/src/extract/rejection.rs index ef961cab..4e156aaf 100644 --- a/src/extract/rejection.rs +++ b/src/extract/rejection.rs @@ -1,5 +1,7 @@ //! Rejection response types. +use tower::BoxError; + use super::IntoResponse; use crate::body::Body; @@ -147,6 +149,13 @@ define_rejection! { pub struct RequestAlreadyExtracted; } +define_rejection! { + #[status = BAD_REQUEST] + #[body = "Form requests must have `Content-Type: x-www-form-urlencoded`"] + /// Rejection type used if you try and extract the request more than once. + pub struct InvalidFormContentType; +} + /// Rejection type for [`UrlParams`](super::UrlParams) if the capture route /// param didn't have the expected type. #[derive(Debug)] @@ -172,3 +181,34 @@ impl IntoResponse for InvalidUrlParam { res } } + +/// Rejection type for extractors that deserialize query strings if the input +/// couldn't be deserialized into the target type. +#[derive(Debug)] +pub struct FailedToDeserializeQueryString { + error: BoxError, + type_name: &'static str, +} + +impl FailedToDeserializeQueryString { + pub(super) fn new(error: E) -> Self + where + E: Into, + { + FailedToDeserializeQueryString { + error: error.into(), + type_name: std::any::type_name::(), + } + } +} + +impl IntoResponse for FailedToDeserializeQueryString { + fn into_response(self) -> http::Response { + let mut res = http::Response::new(Body::from(format!( + "Failed to deserialize query string. Expected something of type `{}`. Error: {}", + self.type_name, self.error, + ))); + *res.status_mut() = http::StatusCode::BAD_REQUEST; + res + } +}