Skip to main content

ullun/
request.rs

1//! Request types and extractors
2
3use crate::cookies::Cookies;
4use crate::error::{Error, Result};
5use serde::de::DeserializeOwned;
6use std::cell::OnceCell;
7use std::collections::HashMap;
8
9/// Path parameters extracted from route
10#[derive(Debug, Clone)]
11pub struct Params {
12    inner: HashMap<String, String>,
13}
14
15impl Params {
16    pub fn new() -> Self {
17        Self {
18            inner: HashMap::new(),
19        }
20    }
21
22    pub fn insert(&mut self, key: String, value: String) {
23        self.inner.insert(key, value);
24    }
25
26    pub fn get(&self, key: &str) -> Result<&str> {
27        self.inner
28            .get(key)
29            .map(|s| s.as_str())
30            .ok_or_else(|| Error::bad_request(format!("Missing param: {}", key)))
31    }
32
33    pub fn get_opt(&self, key: &str) -> Option<&str> {
34        self.inner.get(key).map(|s| s.as_str())
35    }
36
37    /// Parse a path parameter into any `FromStr` type, e.g. `params.parse::<u64>("id")?`
38    ///
39    /// Answers `400` naming the parameter when it does not parse.
40    pub fn parse<T: std::str::FromStr>(&self, key: &str) -> Result<T> {
41        self.get(key)?
42            .parse()
43            .map_err(|_| Error::bad_request(format!("Invalid path parameter '{}'", key)))
44    }
45}
46
47impl Default for Params {
48    fn default() -> Self {
49        Self::new()
50    }
51}
52
53/// Query parameters from URL (also used for `application/x-www-form-urlencoded` bodies)
54#[derive(Debug, Clone)]
55pub struct Query {
56    inner: HashMap<String, String>,
57    raw: String,
58}
59
60impl Query {
61    pub fn new() -> Self {
62        Self {
63            inner: HashMap::new(),
64            raw: String::new(),
65        }
66    }
67
68    /// Parse `a=1&b=two`. Keys and values are percent-decoded and `+` is a
69    /// space. Pairs without `=` are kept with an empty value; if a key repeats,
70    /// the last value wins.
71    pub fn parse_query_string(query: &str) -> Self {
72        let mut q = Query {
73            inner: HashMap::new(),
74            raw: query.to_string(),
75        };
76        for pair in query.split('&').filter(|p| !p.is_empty()) {
77            let (key, value) = pair.split_once('=').unwrap_or((pair, ""));
78            q.inner
79                .insert(decode_component(key), decode_component(value));
80        }
81        q
82    }
83
84    pub fn get(&self, key: &str) -> Option<&str> {
85        self.inner.get(key).map(|s| s.as_str())
86    }
87
88    pub fn contains(&self, key: &str) -> bool {
89        self.inner.contains_key(key)
90    }
91
92    pub fn is_empty(&self) -> bool {
93        self.inner.is_empty()
94    }
95
96    /// Deserialize into a struct; numbers, booleans and `Option` are converted
97    /// from their string form.
98    pub fn parse<T: DeserializeOwned>(&self) -> Result<T> {
99        serde_urlencoded::from_str(&self.raw)
100            .map_err(|e| Error::bad_request(format!("Invalid query params: {}", e)))
101    }
102}
103
104fn decode_component(s: &str) -> String {
105    let s = s.replace('+', " ");
106    urlencoding::decode(&s).map(|c| c.into_owned()).unwrap_or(s)
107}
108
109impl Default for Query {
110    fn default() -> Self {
111        Self::new()
112    }
113}
114
115/// Type-keyed storage for shared application state
116#[derive(Default)]
117pub(crate) struct StateMap {
118    values: HashMap<std::any::TypeId, Box<dyn std::any::Any + Send + Sync>>,
119}
120
121impl StateMap {
122    pub(crate) fn insert<T: Send + Sync + 'static>(&mut self, value: T) {
123        self.values
124            .insert(std::any::TypeId::of::<T>(), Box::new(value));
125    }
126
127    pub(crate) fn get<T: Send + Sync + 'static>(&self) -> Option<&T> {
128        self.values
129            .get(&std::any::TypeId::of::<T>())
130            .and_then(|v| v.downcast_ref::<T>())
131    }
132}
133
134/// HTTP Request wrapper
135pub struct Request {
136    pub method: http::Method,
137    pub uri: http::Uri,
138    pub headers: http::HeaderMap,
139    pub body: bytes::Bytes,
140    pub params: Params,
141    pub query: Query,
142    /// Typed per-request data; middleware can attach values for handlers
143    /// (authenticated user, request id, ...).
144    pub extensions: http::Extensions,
145    /// Address of the connected peer (the proxy when behind one)
146    pub remote_addr: Option<std::net::SocketAddr>,
147    pub(crate) state: Option<std::sync::Arc<StateMap>>,
148    cookies: OnceCell<Cookies>,
149}
150
151impl Request {
152    pub fn new(
153        method: http::Method,
154        uri: http::Uri,
155        headers: http::HeaderMap,
156        body: bytes::Bytes,
157    ) -> Self {
158        let query = uri
159            .query()
160            .map(Query::parse_query_string)
161            .unwrap_or_default();
162
163        Self {
164            method,
165            uri,
166            headers,
167            body,
168            params: Params::new(),
169            query,
170            extensions: http::Extensions::new(),
171            remote_addr: None,
172            state: None,
173            cookies: OnceCell::new(),
174        }
175    }
176
177    /// Shared application state registered with `App::state`
178    ///
179    /// Answers `500` when no value of type `T` was registered: that is a
180    /// programming error, not something the client can fix.
181    pub fn state<T: Send + Sync + 'static>(&self) -> Result<&T> {
182        self.try_state().ok_or_else(|| {
183            Error::internal(format!(
184                "State of type {} is not registered",
185                std::any::type_name::<T>()
186            ))
187        })
188    }
189
190    /// Like [`Request::state`], `None` when the type is not registered
191    pub fn try_state<T: Send + Sync + 'static>(&self) -> Option<&T> {
192        self.state.as_ref()?.get::<T>()
193    }
194
195    /// Get cookies from the request
196    pub fn cookies(&self) -> &Cookies {
197        self.cookies.get_or_init(|| {
198            if let Some(cookie_header) = self.headers.get("cookie") {
199                if let Ok(cookie_str) = cookie_header.to_str() {
200                    Cookies::parse(cookie_str)
201                } else {
202                    Cookies::new()
203                }
204            } else {
205                Cookies::new()
206            }
207        })
208    }
209
210    /// Parse an `application/x-www-form-urlencoded` body into a struct
211    ///
212    /// Answers `415` for other content types and `400` for values that do not
213    /// fit the target type.
214    pub fn form<T: DeserializeOwned>(&self) -> Result<T> {
215        self.require_form_content_type()?;
216        serde_urlencoded::from_bytes(&self.body)
217            .map_err(|e| Error::bad_request(format!("Invalid form data: {}", e)))
218    }
219
220    /// The same body as decoded key/value access (`+` and `%XX` decoded)
221    pub fn form_data(&self) -> Result<Query> {
222        self.require_form_content_type()?;
223        let text = std::str::from_utf8(&self.body)
224            .map_err(|_| Error::bad_request("Form data is not valid UTF-8"))?;
225        Ok(Query::parse_query_string(text))
226    }
227
228    fn require_form_content_type(&self) -> Result<()> {
229        let is_form = self
230            .headers
231            .get(http::header::CONTENT_TYPE)
232            .and_then(|v| v.to_str().ok())
233            .map(|v| {
234                v.split(';')
235                    .next()
236                    .unwrap_or("")
237                    .trim()
238                    .eq_ignore_ascii_case("application/x-www-form-urlencoded")
239            })
240            .unwrap_or(false);
241        if is_form {
242            Ok(())
243        } else {
244            Err(Error::custom(
245                415,
246                "Expected content-type application/x-www-form-urlencoded",
247            ))
248        }
249    }
250
251    /// Parse JSON body
252    pub fn json<T: DeserializeOwned>(&self) -> Result<T> {
253        serde_json::from_slice(&self.body)
254            .map_err(|e| Error::bad_request(format!("Invalid JSON: {}", e)))
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261
262    fn form_request(content_type: Option<&str>, body: &'static str) -> Request {
263        let mut headers = http::HeaderMap::new();
264        if let Some(ct) = content_type {
265            headers.insert(http::header::CONTENT_TYPE, ct.parse().unwrap());
266        }
267        Request::new(
268            http::Method::POST,
269            "/".parse().unwrap(),
270            headers,
271            bytes::Bytes::from_static(body.as_bytes()),
272        )
273    }
274
275    #[derive(Debug, serde::Deserialize, PartialEq)]
276    struct Signup {
277        name: String,
278        age: u8,
279        newsletter: Option<bool>,
280    }
281
282    #[test]
283    fn params_parse_typed_values() {
284        let mut p = Params::new();
285        p.insert("id".into(), "42".into());
286        p.insert("name".into(), "x".into());
287        assert_eq!(p.parse::<u64>("id").unwrap(), 42);
288        assert_eq!(p.parse::<String>("name").unwrap(), "x");
289        let bad = p.parse::<u64>("name").unwrap_err();
290        assert_eq!(bad.status_code(), 400);
291        assert_eq!(bad.message(), "Invalid path parameter 'name'");
292        assert_eq!(p.parse::<u64>("missing").unwrap_err().status_code(), 400);
293        assert!(p.parse::<u8>("id").is_ok());
294        p.insert("big".into(), "300".into());
295        assert!(p.parse::<u8>("big").is_err());
296    }
297
298    #[test]
299    fn query_decodes_keys_values_and_plus() {
300        let q = Query::parse_query_string("q=rust+web&na%20me=a%26b&flag&&x=1");
301        assert_eq!(q.get("q"), Some("rust web"));
302        assert_eq!(q.get("na me"), Some("a&b"));
303        assert_eq!(q.get("flag"), Some(""));
304        assert!(q.contains("x") && !q.is_empty());
305    }
306
307    #[test]
308    fn query_parse_converts_types() {
309        let q = Query::parse_query_string("name=Ann&age=31&newsletter=true");
310        assert_eq!(
311            q.parse::<Signup>().unwrap(),
312            Signup {
313                name: "Ann".into(),
314                age: 31,
315                newsletter: Some(true)
316            }
317        );
318        let bad = Query::parse_query_string("name=Ann&age=old");
319        assert_eq!(bad.parse::<Signup>().unwrap_err().status_code(), 400);
320    }
321
322    #[test]
323    fn form_typed_and_untyped() {
324        let req = form_request(
325            Some("application/x-www-form-urlencoded; charset=UTF-8"),
326            "name=Ann+Lee&age=31",
327        );
328        let signup: Signup = req.form().unwrap();
329        assert_eq!(
330            signup,
331            Signup {
332                name: "Ann Lee".into(),
333                age: 31,
334                newsletter: None
335            }
336        );
337        assert_eq!(req.form_data().unwrap().get("name"), Some("Ann Lee"));
338    }
339
340    #[test]
341    fn form_rejects_wrong_content_type_and_bad_values() {
342        let json = form_request(Some("application/json"), "name=Ann&age=1");
343        assert_eq!(json.form::<Signup>().unwrap_err().status_code(), 415);
344        let none = form_request(None, "name=Ann&age=1");
345        assert_eq!(none.form_data().unwrap_err().status_code(), 415);
346
347        let bad = form_request(
348            Some("application/x-www-form-urlencoded"),
349            "name=Ann&age=300",
350        );
351        assert_eq!(bad.form::<Signup>().unwrap_err().status_code(), 400);
352    }
353}