Skip to main content

tako_rs_extractors/
form.rs

1//! Form data extraction from HTTP request bodies.
2//!
3//! This module provides the [`Form`](crate::form::Form) extractor for parsing `application/x-www-form-urlencoded`
4//! request bodies into strongly-typed Rust structures. It uses serde for deserialization,
5//! allowing automatic parsing of form data into any type that implements `DeserializeOwned`.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use tako::extractors::form::Form;
11//! use serde::Deserialize;
12//!
13//! #[derive(Deserialize)]
14//! struct LoginForm {
15//!     username: String,
16//!     password: String,
17//! }
18//!
19//! async fn login_handler(Form(form): Form<LoginForm>) {
20//!     println!("Username: {}", form.username);
21//!     // Handle login logic...
22//! }
23//! ```
24
25use http::StatusCode;
26use http_body_util::BodyExt;
27use serde::de::DeserializeOwned;
28use tako_rs_core::extractors::FromRequest;
29use tako_rs_core::responder::Responder;
30use tako_rs_core::types::Request;
31
32/// Represents a form extracted from an HTTP request body.
33///
34/// This generic struct wraps the deserialized form data of type `T`. It automatically
35/// parses `application/x-www-form-urlencoded` request bodies and deserializes them
36/// into the specified type using serde.
37///
38/// # Examples
39///
40/// ```rust
41/// use tako::extractors::form::Form;
42/// use serde::Deserialize;
43///
44/// #[derive(Deserialize)]
45/// struct ContactForm {
46///     name: String,
47///     email: String,
48///     message: String,
49/// }
50///
51/// async fn contact_handler(Form(contact): Form<ContactForm>) {
52///     println!("Received message from {} ({}): {}",
53///              contact.name, contact.email, contact.message);
54/// }
55/// ```
56#[doc(alias = "form")]
57pub struct Form<T>(pub T);
58
59/// Error type for Form extraction.
60///
61/// Represents various failure modes that can occur when extracting and parsing
62/// form data from HTTP request bodies. This error type implements
63/// `std::error::Error` for integration with error handling libraries.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum FormError {
66  /// Request content type is not `application/x-www-form-urlencoded`.
67  InvalidContentType,
68  /// Failed to read the request body.
69  BodyReadError(String),
70  /// Request body contains invalid UTF-8 sequences.
71  InvalidUtf8,
72  /// Failed to parse the form data format.
73  ParseError(String),
74  /// Failed to deserialize form data into the target type.
75  DeserializationError(String),
76}
77
78impl std::fmt::Display for FormError {
79  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80    match self {
81      Self::InvalidContentType => {
82        write!(
83          f,
84          "invalid content type; expected application/x-www-form-urlencoded"
85        )
86      }
87      Self::BodyReadError(err) => write!(f, "failed to read request body: {err}"),
88      Self::InvalidUtf8 => write!(f, "request body contains invalid UTF-8"),
89      Self::ParseError(err) => write!(f, "failed to parse form data: {err}"),
90      Self::DeserializationError(err) => write!(f, "failed to deserialize form data: {err}"),
91    }
92  }
93}
94
95impl std::error::Error for FormError {}
96
97impl Responder for FormError {
98  /// Converts the error into an HTTP response.
99  ///
100  /// Maps form extraction errors to appropriate HTTP status codes with descriptive
101  /// error messages. All errors result in `400 Bad Request` as they indicate
102  /// client-side issues with the request format or content.
103  ///
104  /// # Examples
105  ///
106  /// ```rust
107  /// use tako::extractors::form::FormError;
108  /// use tako::responder::Responder;
109  /// use http::StatusCode;
110  ///
111  /// let error = FormError::InvalidContentType;
112  /// let response = error.into_response();
113  /// assert_eq!(response.status(), StatusCode::BAD_REQUEST);
114  ///
115  /// let error = FormError::InvalidUtf8;
116  /// let response = error.into_response();
117  /// assert_eq!(response.status(), StatusCode::BAD_REQUEST);
118  /// ```
119  fn into_response(self) -> tako_rs_core::types::Response {
120    match self {
121      FormError::InvalidContentType => (
122        StatusCode::BAD_REQUEST,
123        "Invalid content type; expected application/x-www-form-urlencoded",
124      )
125        .into_response(),
126      FormError::BodyReadError(err) => (
127        StatusCode::BAD_REQUEST,
128        format!("Failed to read request body: {err}"),
129      )
130        .into_response(),
131      FormError::InvalidUtf8 => (
132        StatusCode::BAD_REQUEST,
133        "Request body contains invalid UTF-8",
134      )
135        .into_response(),
136      FormError::ParseError(err) => (
137        StatusCode::BAD_REQUEST,
138        format!("Failed to parse form data: {err}"),
139      )
140        .into_response(),
141      FormError::DeserializationError(err) => (
142        StatusCode::BAD_REQUEST,
143        format!("Failed to deserialize form data: {err}"),
144      )
145        .into_response(),
146    }
147  }
148}
149
150impl<'a, T> FromRequest<'a> for Form<T>
151where
152  T: DeserializeOwned + Send + 'static,
153{
154  type Error = FormError;
155
156  fn from_request(
157    req: &'a mut Request,
158  ) -> impl core::future::Future<Output = core::result::Result<Self, Self::Error>> + Send + 'a {
159    async move {
160      // Check content type. `starts_with` matches both the bare media type
161      // and the common `; charset=utf-8` variant; the zero-copy `FormBorrowed`
162      // already used the looser check, so anchoring both extractors at the
163      // same string keeps clients from getting random InvalidContentType
164      // rejects based on which extractor a route picked.
165      let content_type = req
166        .headers()
167        .get(http::header::CONTENT_TYPE)
168        .and_then(|v| v.to_str().ok())
169        .unwrap_or("");
170
171      if !content_type.starts_with("application/x-www-form-urlencoded") {
172        return Err(FormError::InvalidContentType);
173      }
174
175      // Read the request body
176      let body_bytes = req
177        .body_mut()
178        .collect()
179        .await
180        .map_err(|e| FormError::BodyReadError(e.to_string()))?
181        .to_bytes();
182
183      // Convert to string
184      let body_str = std::str::from_utf8(&body_bytes).map_err(|_| FormError::InvalidUtf8)?;
185
186      // Deserialize directly from URL-encoded form data
187      let form_data = serde_urlencoded::from_str::<T>(body_str)
188        .map_err(|e| FormError::DeserializationError(e.to_string()))?;
189
190      Ok(Form(form_data))
191    }
192  }
193}