Skip to main content

serde_qs/
actix.rs

1//! Actix-web integration for `serde_qs`.
2//!
3//! This module provides extractors for deserializing querystrings and form data
4//! with support for nested structures in Actix-web applications.
5//!
6//! ## Features
7//!
8//! - `actix4`: Support for actix-web 4.x
9//! - `actix3`: Support for actix-web 3.x
10//!
11//! ## Why use this over actix-web's built-in extractors?
12//!
13//! Actix-web's `web::Query` and `web::Form` only support flat structures.
14//! These extractors support nested objects and arrays using bracket notation.
15
16use crate::Config as QsConfig;
17use crate::error::Error as QsError;
18
19#[cfg(feature = "actix3")]
20use actix_web3 as actix_web;
21#[cfg(feature = "actix4")]
22use actix_web4 as actix_web;
23
24#[cfg(feature = "actix3")]
25use actix_web::HttpResponse;
26use actix_web::dev::Payload;
27use actix_web::{Error as ActixError, FromRequest, HttpRequest, ResponseError, web};
28use futures::StreamExt;
29use futures::future::{FutureExt, LocalBoxFuture, Ready, ready};
30use serde::de;
31use serde::de::DeserializeOwned;
32use std::fmt::Debug;
33use std::sync::Arc;
34
35#[cfg(feature = "actix3")]
36impl ResponseError for QsError {
37    fn error_response(&self) -> HttpResponse {
38        HttpResponse::BadRequest().finish()
39    }
40}
41
42#[cfg(feature = "actix4")]
43impl ResponseError for QsError {
44    fn status_code(&self) -> actix_web::http::StatusCode {
45        actix_web::http::StatusCode::BAD_REQUEST
46    }
47}
48
49/// Extract typed information from the request's query string.
50///
51/// A drop-in replacement for `actix_web::web::Query` that supports nested structures.
52/// Use this when you need to deserialize querystrings with arrays or nested objects.
53///
54/// ## Example
55///
56/// ```rust
57/// # #[cfg(feature = "actix4")]
58/// # use actix_web4 as actix_web;
59/// # #[cfg(feature = "actix3")]
60/// # use actix_web3 as actix_web;
61/// use actix_web::{web, App, HttpResponse};
62/// use serde::Deserialize;
63/// use serde_qs::actix::QsQuery;
64///
65/// #[derive(Deserialize)]
66/// pub struct UsersFilter {
67///    id: Vec<u64>,
68/// }
69///
70/// // Use `QsQuery` extractor for query information.
71/// // The correct request for this handler would be `/users?id[]=1124&id[]=88"`
72/// async fn filter_users(info: QsQuery<UsersFilter>) -> HttpResponse {
73///     HttpResponse::Ok().body(
74///         info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ")
75///     )
76/// }
77///
78/// fn main() {
79///     let app = App::new().service(
80///        web::resource("/users")
81///            .route(web::get().to(filter_users)));
82/// }
83/// ```
84pub use crate::web::QsQuery;
85
86impl<T> FromRequest for QsQuery<T>
87where
88    T: de::DeserializeOwned,
89{
90    type Error = ActixError;
91    type Future = Ready<Result<Self, ActixError>>;
92    #[cfg(feature = "actix3")]
93    type Config = QsQueryConfig;
94
95    #[inline]
96    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
97        let query_config = req
98            .app_data::<QsQueryConfig>()
99            .unwrap_or(&DEFAULT_QUERY_CONFIG);
100
101        let res = query_config
102            .qs_config
103            .deserialize_str::<T>(req.query_string())
104            .map(|val| Ok(QsQuery(val)))
105            .unwrap_or_else(move |e| {
106                let e = if let Some(error_handler) = &query_config.ehandler {
107                    (error_handler)(e, req)
108                } else {
109                    e.into()
110                };
111
112                Err(e)
113            });
114        ready(res)
115    }
116}
117
118/// Extract typed information from the request's form data.
119///
120/// A drop-in replacement for `actix_web::web::Form` that supports nested structures.
121/// Use this when you need to deserialize form data with arrays or nested objects.
122///
123/// ## Example
124///
125/// ```rust
126/// # #[cfg(feature = "actix4")]
127/// # use actix_web4 as actix_web;
128/// # #[cfg(feature = "actix3")]
129/// # use actix_web3 as actix_web;
130/// use actix_web::{web, App, HttpResponse};
131/// use serde::Deserialize;
132/// use serde_qs::actix::QsForm;
133///
134/// #[derive(Debug, Deserialize)]
135/// pub struct UsersFilter {
136///    id: Vec<u64>,
137/// }
138///
139/// // Use `QsForm` extractor for Form information.
140/// // Content-Type: application/x-www-form-urlencoded
141/// // The correct request payload for this handler would be `id[]=1124&id[]=88`
142/// async fn filter_users(info: QsForm<UsersFilter>) -> HttpResponse {
143///     HttpResponse::Ok().body(
144///         info.id.iter().map(|i| i.to_string()).collect::<Vec<String>>().join(", ")
145///     )
146/// }
147///
148/// fn main() {
149///     let app = App::new().service(
150///        web::resource("/users")
151///            .route(web::get().to(filter_users)));
152/// }
153/// ```
154pub use crate::web::QsForm;
155
156impl<T> FromRequest for QsForm<T>
157where
158    T: DeserializeOwned + Debug,
159{
160    type Error = ActixError;
161    type Future = LocalBoxFuture<'static, Result<Self, ActixError>>;
162    #[cfg(feature = "actix3")]
163    type Config = QsQueryConfig;
164
165    fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
166        let mut stream = payload.take();
167        let req_clone = req.clone();
168
169        let query_config: QsQueryConfig = req
170            .app_data::<QsQueryConfig>()
171            .unwrap_or(&DEFAULT_FORM_CONFIG)
172            .clone();
173        async move {
174            let mut bytes = web::BytesMut::new();
175
176            while let Some(item) = stream.next().await {
177                bytes.extend_from_slice(&item.unwrap());
178            }
179
180            query_config
181                .qs_config
182                .deserialize_bytes::<T>(&bytes)
183                .map(|val| Ok(QsForm(val)))
184                .unwrap_or_else(|e| {
185                    let e = if let Some(error_handler) = &query_config.ehandler {
186                        (error_handler)(e, &req_clone)
187                    } else {
188                        e.into()
189                    };
190
191                    Err(e)
192                })
193        }
194        .boxed_local()
195    }
196}
197
198type ActixErrorHandler = Option<Arc<dyn Fn(QsError, &HttpRequest) -> ActixError + Send + Sync>>;
199
200/// Configuration for query and form extractors.
201///
202/// Allows customization of error handling and serialization parameters
203/// for both `QsQuery` and `QsForm` extractors.
204///
205/// ```rust
206/// # #[cfg(feature = "actix4")]
207/// # use actix_web4 as actix_web;
208/// # #[cfg(feature = "actix3")]
209/// # use actix_web3 as actix_web;
210/// use actix_web::{error, web, App, FromRequest, HttpResponse};
211/// use serde::Deserialize;
212/// use serde_qs::actix::QsQuery;
213/// use serde_qs::Config as QsConfig;
214/// use serde_qs::actix::QsQueryConfig;
215///
216/// #[derive(Deserialize)]
217/// struct Info {
218///     username: String,
219/// }
220///
221/// /// deserialize `Info` from request's querystring
222/// async fn index(info: QsQuery<Info>) -> HttpResponse {
223///     HttpResponse::Ok().body(
224///         format!("Welcome {}!", info.username)
225///     )
226/// }
227///
228/// fn main() {
229/// let qs_config = QsQueryConfig::default()
230///     .error_handler(|err, req| {  // <- create custom error response
231///     error::InternalError::from_response(
232///         err, HttpResponse::Conflict().finish()).into()
233///     })
234///     .qs_config(QsConfig::default());
235///
236/// let app = App::new().service(
237///         web::resource("/index.html").app_data(qs_config)
238///             .route(web::post().to(index))
239///     );
240/// }
241/// ```
242#[derive(Clone, Default)]
243pub struct QsQueryConfig {
244    ehandler: ActixErrorHandler,
245    qs_config: QsConfig,
246}
247
248static DEFAULT_QUERY_CONFIG: QsQueryConfig = QsQueryConfig {
249    ehandler: None,
250    qs_config: QsConfig::new(),
251};
252
253static DEFAULT_FORM_CONFIG: QsQueryConfig = QsQueryConfig {
254    ehandler: None,
255    qs_config: QsConfig::new().use_form_encoding(true),
256};
257
258impl QsQueryConfig {
259    /// Set custom error handler
260    pub fn error_handler<F>(mut self, f: F) -> Self
261    where
262        F: Fn(QsError, &HttpRequest) -> ActixError + Send + Sync + 'static,
263    {
264        self.ehandler = Some(Arc::new(f));
265        self
266    }
267
268    /// Set custom serialization parameters
269    pub fn qs_config(mut self, config: QsConfig) -> Self {
270        self.qs_config = config;
271        self
272    }
273}