torch_web/request.rs
1//! # HTTP Request Handling
2//!
3//! This module provides the [`Request`] struct, which wraps HTTP requests and provides
4//! convenient methods for accessing request data like headers, body, path parameters,
5//! and query parameters.
6
7use std::collections::HashMap;
8use std::any::{Any, TypeId};
9use std::sync::Arc;
10use http::{HeaderMap, Method, Uri, Version};
11use http_body_util::BodyExt;
12use hyper::body::Incoming;
13use crate::extractors::state::StateMap;
14
15/// HTTP request wrapper that provides convenient access to request data.
16///
17/// The `Request` struct encapsulates all the information about an incoming HTTP request,
18/// including the method, URI, headers, body, and extracted path parameters. It provides
19/// a high-level API for accessing this data in handlers.
20///
21/// # Examples
22///
23/// ## Basic Usage in Handlers
24///
25/// ```rust
26/// use torch_web::{App, Request, Response};
27///
28/// let app = App::new()
29/// .get("/", |req: Request| async move {
30/// println!("Method: {}", req.method());
31/// println!("Path: {}", req.path());
32/// Response::ok().body("Hello!")
33/// });
34/// ```
35///
36/// ## Accessing Headers
37///
38/// ```rust
39/// use torch_web::{App, Request, Response};
40///
41/// let app = App::new()
42/// .post("/api/data", |req: Request| async move {
43/// if let Some(content_type) = req.header("content-type") {
44/// println!("Content-Type: {}", content_type);
45/// }
46///
47/// if let Some(auth) = req.header("authorization") {
48/// println!("Authorization: {}", auth);
49/// }
50///
51/// Response::ok().body("Received")
52/// });
53/// ```
54///
55/// ## Working with Request Body
56///
57/// ```rust
58/// use torch_web::{App, Request, Response};
59///
60/// let app = App::new()
61/// .post("/upload", |req: Request| async move {
62/// let body_bytes = req.body();
63/// println!("Received {} bytes", body_bytes.len());
64///
65/// if let Ok(body_text) = req.body_string() {
66/// println!("Body: {}", body_text);
67/// }
68///
69/// Response::ok().body("Upload complete")
70/// });
71/// ```
72///
73/// ## Path Parameters
74///
75/// ```rust
76/// use torch_web::{App, Request, Response};
77///
78/// let app = App::new()
79/// .get("/users/:id/posts/:post_id", |req: Request| async move {
80/// let user_id = req.param("id").unwrap_or("unknown");
81/// let post_id = req.param("post_id").unwrap_or("unknown");
82///
83/// Response::ok().body(format!("User {} Post {}", user_id, post_id))
84/// });
85/// ```
86#[derive(Debug)]
87pub struct Request {
88 method: Method,
89 uri: Uri,
90 version: Version,
91 headers: HeaderMap,
92 body: Vec<u8>,
93 params: HashMap<String, String>,
94 query: HashMap<String, String>,
95 extensions: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
96}
97
98impl Request {
99 /// Creates a new empty request with default values.
100 ///
101 /// This is primarily used for testing and internal purposes. In normal operation,
102 /// requests are created from incoming HTTP requests via `from_hyper`.
103 ///
104 /// # Examples
105 ///
106 /// ```rust
107 /// use torch_web::Request;
108 ///
109 /// let req = Request::new();
110 /// assert_eq!(req.path(), "/");
111 /// assert_eq!(req.method().as_str(), "GET");
112 /// ```
113 pub fn new() -> Self {
114 Self {
115 method: Method::GET,
116 uri: "/".parse().unwrap(),
117 version: Version::HTTP_11,
118 headers: HeaderMap::new(),
119 body: Vec::new(),
120 params: HashMap::new(),
121 query: HashMap::new(),
122 extensions: HashMap::new(),
123 }
124 }
125
126 /// Creates a new Request from Hyper's request parts and body.
127 ///
128 /// This is an internal method used by the framework to convert incoming
129 /// Hyper requests into Torch Request objects. It reads the entire request
130 /// body into memory and parses query parameters.
131 ///
132 /// # Parameters
133 ///
134 /// * `parts` - The HTTP request parts (method, URI, headers, etc.)
135 /// * `body` - The request body stream
136 ///
137 /// # Returns
138 ///
139 /// Returns a `Result` containing the constructed `Request` or an error if
140 /// the body cannot be read or the request is malformed.
141 ///
142 /// # Examples
143 ///
144 /// ```rust,no_run
145 /// use torch_web::Request;
146 /// use hyper::body::Incoming;
147 /// use http::request::Parts;
148 ///
149 /// async fn handle_hyper_request(parts: Parts, body: Incoming) {
150 /// match Request::from_hyper(parts, body).await {
151 /// Ok(req) => {
152 /// println!("Received request to {}", req.path());
153 /// }
154 /// Err(e) => {
155 /// eprintln!("Failed to parse request: {}", e);
156 /// }
157 /// }
158 /// }
159 /// ```
160 pub async fn from_hyper(
161 parts: http::request::Parts,
162 body: Incoming,
163 ) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
164 let body_bytes = body.collect().await?.to_bytes().to_vec();
165
166 let query = Self::parse_query_string(parts.uri.query().unwrap_or(""));
167
168 Ok(Request {
169 method: parts.method,
170 uri: parts.uri,
171 version: parts.version,
172 headers: parts.headers,
173 body: body_bytes,
174 params: HashMap::new(),
175 query,
176 extensions: HashMap::new(),
177 })
178 }
179
180 /// Returns the HTTP method of the request.
181 ///
182 /// # Examples
183 ///
184 /// ```rust
185 /// use torch_web::{App, Request, Response, Method};
186 ///
187 /// let app = App::new()
188 /// .route(Method::POST, "/api/data", |req: Request| async move {
189 /// match req.method() {
190 /// &Method::POST => Response::ok().body("POST request"),
191 /// &Method::GET => Response::ok().body("GET request"),
192 /// _ => Response::method_not_allowed().body("Method not allowed"),
193 /// }
194 /// });
195 /// ```
196 pub fn method(&self) -> &Method {
197 &self.method
198 }
199
200 /// Returns the complete URI of the request.
201 ///
202 /// This includes the path, query string, and fragment (if present).
203 ///
204 /// # Examples
205 ///
206 /// ```rust
207 /// use torch_web::{App, Request, Response};
208 ///
209 /// let app = App::new()
210 /// .get("/debug", |req: Request| async move {
211 /// let uri = req.uri();
212 /// Response::ok().body(format!("Full URI: {}", uri))
213 /// });
214 /// ```
215 pub fn uri(&self) -> &Uri {
216 &self.uri
217 }
218
219 /// Returns the path portion of the request URI.
220 ///
221 /// This excludes the query string and fragment, returning only the path component.
222 ///
223 /// # Examples
224 ///
225 /// ```rust
226 /// use torch_web::{App, Request, Response};
227 ///
228 /// let app = App::new()
229 /// .get("/users/:id", |req: Request| async move {
230 /// println!("Request path: {}", req.path()); // "/users/123"
231 /// Response::ok().body("User page")
232 /// });
233 /// ```
234 pub fn path(&self) -> &str {
235 self.uri.path()
236 }
237
238 /// Returns the HTTP version of the request.
239 ///
240 /// # Examples
241 ///
242 /// ```rust
243 /// use torch_web::{App, Request, Response};
244 /// use http::Version;
245 ///
246 /// let app = App::new()
247 /// .get("/version", |req: Request| async move {
248 /// let version = match req.version() {
249 /// Version::HTTP_09 => "HTTP/0.9",
250 /// Version::HTTP_10 => "HTTP/1.0",
251 /// Version::HTTP_11 => "HTTP/1.1",
252 /// Version::HTTP_2 => "HTTP/2.0",
253 /// Version::HTTP_3 => "HTTP/3.0",
254 /// _ => "Unknown",
255 /// };
256 /// Response::ok().body(format!("HTTP Version: {}", version))
257 /// });
258 /// ```
259 pub fn version(&self) -> Version {
260 self.version
261 }
262
263 /// Returns a reference to the request headers.
264 ///
265 /// # Examples
266 ///
267 /// ```rust
268 /// use torch_web::{App, Request, Response};
269 ///
270 /// let app = App::new()
271 /// .post("/api/data", |req: Request| async move {
272 /// let headers = req.headers();
273 ///
274 /// for (name, value) in headers.iter() {
275 /// println!("{}: {:?}", name, value);
276 /// }
277 ///
278 /// Response::ok().body("Headers logged")
279 /// });
280 /// ```
281 pub fn headers(&self) -> &HeaderMap {
282 &self.headers
283 }
284
285 /// Returns the value of a specific header.
286 ///
287 /// This is a convenience method that looks up a header by name and converts
288 /// it to a string. Returns `None` if the header doesn't exist or contains
289 /// invalid UTF-8.
290 ///
291 /// # Parameters
292 ///
293 /// * `name` - The header name to look up (case-insensitive)
294 ///
295 /// # Examples
296 ///
297 /// ```rust
298 /// use torch_web::{App, Request, Response};
299 ///
300 /// let app = App::new()
301 /// .post("/api/upload", |req: Request| async move {
302 /// if let Some(content_type) = req.header("content-type") {
303 /// println!("Content-Type: {}", content_type);
304 ///
305 /// if content_type.starts_with("application/json") {
306 /// return Response::ok().body("JSON data received");
307 /// }
308 /// }
309 ///
310 /// Response::bad_request().body("Content-Type required")
311 /// });
312 /// ```
313 pub fn header(&self, name: &str) -> Option<&str> {
314 self.headers.get(name)?.to_str().ok()
315 }
316
317 /// Returns the request body as a byte slice.
318 ///
319 /// The entire request body is read into memory when the request is created,
320 /// so this method provides immediate access to the raw bytes.
321 ///
322 /// # Examples
323 ///
324 /// ```rust
325 /// use torch_web::{App, Request, Response};
326 ///
327 /// let app = App::new()
328 /// .post("/upload", |req: Request| async move {
329 /// let body_bytes = req.body();
330 /// println!("Received {} bytes", body_bytes.len());
331 ///
332 /// // Process binary data
333 /// if body_bytes.starts_with(b"PNG") {
334 /// Response::ok().body("PNG image received")
335 /// } else {
336 /// Response::ok().body("Data received")
337 /// }
338 /// });
339 /// ```
340 pub fn body(&self) -> &[u8] {
341 &self.body
342 }
343
344 /// Returns the request body as a UTF-8 string.
345 ///
346 /// This method attempts to convert the request body bytes into a valid UTF-8 string.
347 /// Returns an error if the body contains invalid UTF-8 sequences.
348 ///
349 /// # Returns
350 ///
351 /// Returns `Ok(String)` if the body is valid UTF-8, or `Err(FromUtf8Error)` otherwise.
352 ///
353 /// # Examples
354 ///
355 /// ```rust
356 /// use torch_web::{App, Request, Response};
357 ///
358 /// let app = App::new()
359 /// .post("/text", |req: Request| async move {
360 /// match req.body_string() {
361 /// Ok(text) => {
362 /// println!("Received text: {}", text);
363 /// Response::ok().body(format!("Echo: {}", text))
364 /// }
365 /// Err(_) => {
366 /// Response::bad_request().body("Invalid UTF-8 in request body")
367 /// }
368 /// }
369 /// });
370 /// ```
371 pub fn body_string(&self) -> Result<String, std::string::FromUtf8Error> {
372 String::from_utf8(self.body.clone())
373 }
374
375 /// Parse the request body as JSON (requires "json" feature)
376 #[cfg(feature = "json")]
377 pub async fn json<T>(&self) -> Result<T, serde_json::Error>
378 where
379 T: serde::de::DeserializeOwned,
380 {
381 serde_json::from_slice(&self.body)
382 }
383
384 /// Get a path parameter by name
385 pub fn param(&self, name: &str) -> Option<&str> {
386 self.params.get(name).map(|s| s.as_str())
387 }
388
389 /// Get all path parameters
390 pub fn params(&self) -> &HashMap<String, String> {
391 &self.params
392 }
393
394 /// Get all path parameters (for extractors)
395 pub fn path_params(&self) -> &HashMap<String, String> {
396 &self.params
397 }
398
399 /// Set a path parameter (used internally by the router)
400 pub(crate) fn set_param(&mut self, name: String, value: String) {
401 self.params.insert(name, value);
402 }
403
404 /// Get a reference to the request extensions
405 pub fn extensions(&self) -> &HashMap<TypeId, Box<dyn Any + Send + Sync>> {
406 &self.extensions
407 }
408
409 /// Get a mutable reference to the request extensions
410 pub fn extensions_mut(&mut self) -> &mut HashMap<TypeId, Box<dyn Any + Send + Sync>> {
411 &mut self.extensions
412 }
413
414 /// Insert a value into the request extensions
415 pub fn insert_extension<T: Send + Sync + 'static>(&mut self, value: T) {
416 self.extensions.insert(TypeId::of::<T>(), Box::new(value));
417 }
418
419 /// Get a value from the request extensions
420 pub fn get_extension<T: Send + Sync + 'static>(&self) -> Option<&T> {
421 self.extensions
422 .get(&TypeId::of::<T>())
423 .and_then(|boxed| boxed.downcast_ref())
424 }
425
426 /// Get a query parameter by name
427 pub fn query(&self, name: &str) -> Option<&str> {
428 self.query.get(name).map(|s| s.as_str())
429 }
430
431 /// Get all query parameters
432 pub fn query_params(&self) -> &HashMap<String, String> {
433 &self.query
434 }
435
436 /// Get the raw query string
437 pub fn query_string(&self) -> Option<&str> {
438 self.uri.query()
439 }
440
441 /// Get the request body as bytes (for extractors)
442 pub fn body_bytes(&self) -> &[u8] {
443 &self.body
444 }
445
446 /// Set the request body (for testing)
447 #[cfg(test)]
448 pub fn set_body(&mut self, body: Vec<u8>) {
449 self.body = body;
450 }
451
452 /// Get mutable access to headers (for extractors)
453 pub fn headers_mut(&mut self) -> &mut HeaderMap {
454 &mut self.headers
455 }
456
457 /// Parse query string into a HashMap
458 fn parse_query_string(query: &str) -> HashMap<String, String> {
459 let mut params = HashMap::new();
460
461 for pair in query.split('&') {
462 if let Some((key, value)) = pair.split_once('=') {
463 let key = urlencoding::decode(key).unwrap_or_else(|_| key.into()).into_owned();
464 let value = urlencoding::decode(value).unwrap_or_else(|_| value.into()).into_owned();
465 params.insert(key, value);
466 } else if !pair.is_empty() {
467 let key = urlencoding::decode(pair).unwrap_or_else(|_| pair.into()).into_owned();
468 params.insert(key, String::new());
469 }
470 }
471
472 params
473 }
474}
475
476/// Implementation of RequestStateExt for Request
477impl crate::extractors::state::RequestStateExt for Request {
478 fn get_state(&self, type_id: TypeId) -> Option<&Arc<dyn Any + Send + Sync>> {
479 // Check if we have a StateMap stored in extensions
480 if let Some(state_map_any) = self.extensions.get(&TypeId::of::<StateMap>()) {
481 if let Some(state_map) = state_map_any.downcast_ref::<StateMap>() {
482 return state_map.get_by_type_id(type_id);
483 }
484 }
485 None
486 }
487
488 fn set_state_map(&mut self, state_map: StateMap) {
489 self.extensions.insert(TypeId::of::<StateMap>(), Box::new(state_map));
490 }
491
492 fn state_map(&self) -> Option<&StateMap> {
493 self.extensions
494 .get(&TypeId::of::<StateMap>())
495 .and_then(|state_map_any| state_map_any.downcast_ref::<StateMap>())
496 }
497}
498
499#[cfg(test)]
500mod tests {
501 use super::*;
502 // Imports for potential future test use
503
504 #[test]
505 fn test_parse_query_string() {
506 let query = "name=John&age=30&city=New%20York";
507 let params = Request::parse_query_string(query);
508
509 assert_eq!(params.get("name"), Some(&"John".to_string()));
510 assert_eq!(params.get("age"), Some(&"30".to_string()));
511 assert_eq!(params.get("city"), Some(&"New York".to_string()));
512 }
513
514 #[test]
515 fn test_parse_empty_query_string() {
516 let params = Request::parse_query_string("");
517 assert!(params.is_empty());
518 }
519}