spikard_http/server/
request_extraction.rs

1//! Request parsing and data extraction utilities
2
3use crate::handler_trait::RequestData;
4use crate::query_parser::{parse_query_pairs_to_json, parse_query_string};
5use axum::body::Body;
6use http_body_util::BodyExt;
7use serde_json::Value;
8use std::collections::HashMap;
9use std::sync::Arc;
10use std::sync::OnceLock;
11
12#[derive(Debug, Clone, Copy)]
13pub struct WithoutBodyExtractionOptions {
14    pub include_raw_query_params: bool,
15    pub include_query_params_json: bool,
16    pub include_headers: bool,
17    pub include_cookies: bool,
18}
19
20fn extract_query_params_and_raw(
21    uri: &axum::http::Uri,
22    include_raw_query_params: bool,
23    include_query_params_json: bool,
24) -> (Value, HashMap<String, Vec<String>>) {
25    let query_string = uri.query().unwrap_or("");
26    if query_string.is_empty() {
27        return (Value::Object(serde_json::Map::new()), HashMap::new());
28    }
29
30    match (include_raw_query_params, include_query_params_json) {
31        (false, false) => (Value::Null, HashMap::new()),
32        (false, true) => (
33            crate::query_parser::parse_query_string_to_json(query_string.as_bytes(), true),
34            HashMap::new(),
35        ),
36        (true, false) => {
37            let raw =
38                parse_query_string(query_string.as_bytes(), '&')
39                    .into_iter()
40                    .fold(HashMap::new(), |mut acc, (k, v)| {
41                        acc.entry(k).or_insert_with(Vec::new).push(v);
42                        acc
43                    });
44            (Value::Null, raw)
45        }
46        (true, true) => {
47            let pairs = parse_query_string(query_string.as_bytes(), '&');
48            let json = parse_query_pairs_to_json(&pairs, true);
49            let raw = pairs.into_iter().fold(HashMap::new(), |mut acc, (k, v)| {
50                acc.entry(k).or_insert_with(Vec::new).push(v);
51                acc
52            });
53            (json, raw)
54        }
55    }
56}
57
58/// Extract and parse query parameters from request URI
59pub fn extract_query_params(uri: &axum::http::Uri) -> Value {
60    let query_string = uri.query().unwrap_or("");
61    if query_string.is_empty() {
62        Value::Object(serde_json::Map::new())
63    } else {
64        crate::query_parser::parse_query_string_to_json(query_string.as_bytes(), true)
65    }
66}
67
68/// Extract raw query parameters as strings (no type conversion)
69/// Used for validation error messages to show the actual input values
70pub fn extract_raw_query_params(uri: &axum::http::Uri) -> HashMap<String, Vec<String>> {
71    let query_string = uri.query().unwrap_or("");
72    if query_string.is_empty() {
73        HashMap::new()
74    } else {
75        parse_query_string(query_string.as_bytes(), '&')
76            .into_iter()
77            .fold(HashMap::new(), |mut acc, (k, v)| {
78                acc.entry(k).or_insert_with(Vec::new).push(v);
79                acc
80            })
81    }
82}
83
84/// Extract headers from request
85pub fn extract_headers(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
86    let mut map = HashMap::with_capacity(headers.len());
87    for (name, value) in headers.iter() {
88        if let Ok(val_str) = value.to_str() {
89            // `HeaderName::as_str()` is already normalized to lowercase.
90            map.insert(name.as_str().to_string(), val_str.to_string());
91        }
92    }
93    map
94}
95
96fn extract_content_type_header(headers: &axum::http::HeaderMap) -> Arc<HashMap<String, String>> {
97    let Some(value) = headers
98        .get(axum::http::header::CONTENT_TYPE)
99        .and_then(|h| h.to_str().ok())
100    else {
101        return empty_string_map();
102    };
103
104    let mut map = HashMap::with_capacity(1);
105    map.insert("content-type".to_string(), value.to_string());
106    Arc::new(map)
107}
108
109/// Extract cookies from request headers
110pub fn extract_cookies(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
111    let mut cookies = HashMap::new();
112
113    if let Some(cookie_str) = headers.get(axum::http::header::COOKIE).and_then(|h| h.to_str().ok()) {
114        for cookie in cookie::Cookie::split_parse(cookie_str).flatten() {
115            cookies.insert(cookie.name().to_string(), cookie.value().to_string());
116        }
117    }
118
119    cookies
120}
121
122fn empty_string_map() -> Arc<HashMap<String, String>> {
123    static EMPTY: OnceLock<Arc<HashMap<String, String>>> = OnceLock::new();
124    Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
125}
126
127fn empty_raw_query_map() -> Arc<HashMap<String, Vec<String>>> {
128    static EMPTY: OnceLock<Arc<HashMap<String, Vec<String>>>> = OnceLock::new();
129    Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
130}
131
132/// Create RequestData from request parts (for requests without body)
133///
134/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
135pub fn create_request_data_without_body(
136    uri: &axum::http::Uri,
137    method: &axum::http::Method,
138    headers: &axum::http::HeaderMap,
139    path_params: HashMap<String, String>,
140    options: WithoutBodyExtractionOptions,
141) -> RequestData {
142    let (query_params, raw_query_params) =
143        extract_query_params_and_raw(uri, options.include_raw_query_params, options.include_query_params_json);
144    RequestData {
145        path_params: Arc::new(path_params),
146        query_params,
147        raw_query_params: if raw_query_params.is_empty() {
148            empty_raw_query_map()
149        } else {
150            Arc::new(raw_query_params)
151        },
152        validated_params: None,
153        headers: if options.include_headers {
154            Arc::new(extract_headers(headers))
155        } else {
156            empty_string_map()
157        },
158        cookies: if options.include_cookies {
159            Arc::new(extract_cookies(headers))
160        } else {
161            empty_string_map()
162        },
163        body: Value::Null,
164        raw_body: None,
165        method: method.as_str().to_string(),
166        path: uri.path().to_string(),
167        #[cfg(feature = "di")]
168        dependencies: None,
169    }
170}
171
172/// Create RequestData from request parts (for requests with body)
173///
174/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
175/// Performance optimization: stores raw body bytes without parsing JSON.
176/// JSON parsing is deferred until actually needed (e.g., for validation).
177pub async fn create_request_data_with_body(
178    parts: &axum::http::request::Parts,
179    path_params: HashMap<String, String>,
180    body: Body,
181    include_raw_query_params: bool,
182    include_query_params_json: bool,
183    include_headers: bool,
184    include_cookies: bool,
185) -> Result<RequestData, (axum::http::StatusCode, String)> {
186    let body_bytes = if let Some(pre_read) = parts.extensions.get::<crate::middleware::PreReadBody>() {
187        pre_read.0.clone()
188    } else {
189        body.collect()
190            .await
191            .map_err(|e| {
192                (
193                    axum::http::StatusCode::BAD_REQUEST,
194                    format!("Failed to read body: {}", e),
195                )
196            })?
197            .to_bytes()
198    };
199
200    let (query_params, raw_query_params) =
201        extract_query_params_and_raw(&parts.uri, include_raw_query_params, include_query_params_json);
202
203    let body_value = parts
204        .extensions
205        .get::<crate::middleware::PreParsedJson>()
206        .map(|parsed| parsed.0.clone())
207        .unwrap_or(Value::Null);
208
209    Ok(RequestData {
210        path_params: Arc::new(path_params),
211        query_params,
212        raw_query_params: if raw_query_params.is_empty() {
213            empty_raw_query_map()
214        } else {
215            Arc::new(raw_query_params)
216        },
217        validated_params: None,
218        headers: if include_headers {
219            Arc::new(extract_headers(&parts.headers))
220        } else {
221            extract_content_type_header(&parts.headers)
222        },
223        cookies: if include_cookies {
224            Arc::new(extract_cookies(&parts.headers))
225        } else {
226            empty_string_map()
227        },
228        body: body_value,
229        raw_body: if body_bytes.is_empty() { None } else { Some(body_bytes) },
230        method: parts.method.as_str().to_string(),
231        path: parts.uri.path().to_string(),
232        #[cfg(feature = "di")]
233        dependencies: None,
234    })
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use axum::http::{HeaderMap, HeaderValue, Method, Uri};
241    use futures_util::stream;
242    use serde_json::json;
243
244    const OPTIONS_ALL: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
245        include_raw_query_params: true,
246        include_query_params_json: true,
247        include_headers: true,
248        include_cookies: true,
249    };
250
251    const OPTIONS_SKIP_HEADERS: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
252        include_raw_query_params: true,
253        include_query_params_json: true,
254        include_headers: false,
255        include_cookies: true,
256    };
257
258    const OPTIONS_SKIP_COOKIES: WithoutBodyExtractionOptions = WithoutBodyExtractionOptions {
259        include_raw_query_params: true,
260        include_query_params_json: true,
261        include_headers: true,
262        include_cookies: false,
263    };
264
265    #[test]
266    fn test_extract_query_params_empty() {
267        let uri: Uri = "/path".parse().unwrap();
268        let result = extract_query_params(&uri);
269        assert_eq!(result, json!({}));
270    }
271
272    #[test]
273    fn test_extract_query_params_single_param() {
274        let uri: Uri = "/path?name=value".parse().unwrap();
275        let result = extract_query_params(&uri);
276        assert_eq!(result, json!({"name": "value"}));
277    }
278
279    #[test]
280    fn test_extract_query_params_multiple_params() {
281        let uri: Uri = "/path?foo=1&bar=2".parse().unwrap();
282        let result = extract_query_params(&uri);
283        assert_eq!(result, json!({"foo": 1, "bar": 2}));
284    }
285
286    #[test]
287    fn test_extract_query_params_array_params() {
288        let uri: Uri = "/path?tags=rust&tags=http".parse().unwrap();
289        let result = extract_query_params(&uri);
290        assert_eq!(result, json!({"tags": ["rust", "http"]}));
291    }
292
293    #[test]
294    fn test_extract_query_params_mixed_array_and_single() {
295        let uri: Uri = "/path?tags=rust&tags=web&id=123".parse().unwrap();
296        let result = extract_query_params(&uri);
297        assert_eq!(result, json!({"tags": ["rust", "web"], "id": 123}));
298    }
299
300    #[test]
301    fn test_extract_query_params_url_encoded() {
302        let uri: Uri = "/path?email=test%40example.com&name=john+doe".parse().unwrap();
303        let result = extract_query_params(&uri);
304        assert_eq!(result, json!({"email": "test@example.com", "name": "john doe"}));
305    }
306
307    #[test]
308    fn test_extract_query_params_boolean_values() {
309        let uri: Uri = "/path?active=true&enabled=false".parse().unwrap();
310        let result = extract_query_params(&uri);
311        assert_eq!(result, json!({"active": true, "enabled": false}));
312    }
313
314    #[test]
315    fn test_extract_query_params_null_value() {
316        let uri: Uri = "/path?value=null".parse().unwrap();
317        let result = extract_query_params(&uri);
318        assert_eq!(result, json!({"value": null}));
319    }
320
321    #[test]
322    fn test_extract_query_params_empty_string_value() {
323        let uri: Uri = "/path?key=".parse().unwrap();
324        let result = extract_query_params(&uri);
325        assert_eq!(result, json!({"key": ""}));
326    }
327
328    #[test]
329    fn test_extract_raw_query_params_empty() {
330        let uri: Uri = "/path".parse().unwrap();
331        let result = extract_raw_query_params(&uri);
332        assert!(result.is_empty());
333    }
334
335    #[test]
336    fn test_extract_raw_query_params_single() {
337        let uri: Uri = "/path?name=value".parse().unwrap();
338        let result = extract_raw_query_params(&uri);
339        assert_eq!(result.get("name"), Some(&vec!["value".to_string()]));
340    }
341
342    #[test]
343    fn test_extract_raw_query_params_multiple_values() {
344        let uri: Uri = "/path?tag=rust&tag=http".parse().unwrap();
345        let result = extract_raw_query_params(&uri);
346        assert_eq!(result.get("tag"), Some(&vec!["rust".to_string(), "http".to_string()]));
347    }
348
349    #[test]
350    fn test_extract_raw_query_params_mixed() {
351        let uri: Uri = "/path?id=123&tags=rust&tags=web&active=true".parse().unwrap();
352        let result = extract_raw_query_params(&uri);
353        assert_eq!(result.get("id"), Some(&vec!["123".to_string()]));
354        assert_eq!(result.get("tags"), Some(&vec!["rust".to_string(), "web".to_string()]));
355        assert_eq!(result.get("active"), Some(&vec!["true".to_string()]));
356    }
357
358    #[test]
359    fn test_extract_raw_query_params_url_encoded() {
360        let uri: Uri = "/path?email=test%40example.com".parse().unwrap();
361        let result = extract_raw_query_params(&uri);
362        assert_eq!(result.get("email"), Some(&vec!["test@example.com".to_string()]));
363    }
364
365    #[test]
366    fn test_extract_headers_empty() {
367        let headers = HeaderMap::new();
368        let result = extract_headers(&headers);
369        assert!(result.is_empty());
370    }
371
372    #[test]
373    fn test_extract_headers_single() {
374        let mut headers = HeaderMap::new();
375        headers.insert("content-type", HeaderValue::from_static("application/json"));
376        let result = extract_headers(&headers);
377        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
378    }
379
380    #[test]
381    fn test_extract_headers_multiple() {
382        let mut headers = HeaderMap::new();
383        headers.insert("content-type", HeaderValue::from_static("application/json"));
384        headers.insert("user-agent", HeaderValue::from_static("test-agent"));
385        headers.insert("authorization", HeaderValue::from_static("Bearer token123"));
386
387        let result = extract_headers(&headers);
388        assert_eq!(result.len(), 3);
389        assert_eq!(result.get("content-type"), Some(&"application/json".to_string()));
390        assert_eq!(result.get("user-agent"), Some(&"test-agent".to_string()));
391        assert_eq!(result.get("authorization"), Some(&"Bearer token123".to_string()));
392    }
393
394    #[test]
395    fn test_extract_headers_case_insensitive() {
396        let mut headers = HeaderMap::new();
397        headers.insert("Content-Type", HeaderValue::from_static("text/html"));
398        headers.insert("USER-Agent", HeaderValue::from_static("chrome"));
399
400        let result = extract_headers(&headers);
401        assert_eq!(result.get("content-type"), Some(&"text/html".to_string()));
402        assert_eq!(result.get("user-agent"), Some(&"chrome".to_string()));
403    }
404
405    #[test]
406    fn test_extract_headers_with_dashes() {
407        let mut headers = HeaderMap::new();
408        headers.insert("x-custom-header", HeaderValue::from_static("custom-value"));
409        headers.insert("x-request-id", HeaderValue::from_static("req-12345"));
410
411        let result = extract_headers(&headers);
412        assert_eq!(result.get("x-custom-header"), Some(&"custom-value".to_string()));
413        assert_eq!(result.get("x-request-id"), Some(&"req-12345".to_string()));
414    }
415
416    #[test]
417    fn test_extract_cookies_no_cookie_header() {
418        let headers = HeaderMap::new();
419        let result = extract_cookies(&headers);
420        assert!(result.is_empty());
421    }
422
423    #[test]
424    fn test_extract_cookies_single() {
425        let mut headers = HeaderMap::new();
426        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc123"));
427
428        let result = extract_cookies(&headers);
429        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
430    }
431
432    #[test]
433    fn test_extract_cookies_multiple() {
434        let mut headers = HeaderMap::new();
435        headers.insert(
436            axum::http::header::COOKIE,
437            HeaderValue::from_static("session=abc123; user_id=42; theme=dark"),
438        );
439
440        let result = extract_cookies(&headers);
441        assert_eq!(result.len(), 3);
442        assert_eq!(result.get("session"), Some(&"abc123".to_string()));
443        assert_eq!(result.get("user_id"), Some(&"42".to_string()));
444        assert_eq!(result.get("theme"), Some(&"dark".to_string()));
445    }
446
447    #[test]
448    fn test_extract_cookies_with_spaces() {
449        let mut headers = HeaderMap::new();
450        headers.insert(
451            axum::http::header::COOKIE,
452            HeaderValue::from_static("session = abc123 ; theme = light"),
453        );
454
455        let result = extract_cookies(&headers);
456        assert!(result.len() >= 1);
457    }
458
459    #[test]
460    fn test_extract_cookies_empty_value() {
461        let mut headers = HeaderMap::new();
462        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("empty="));
463
464        let result = extract_cookies(&headers);
465        assert_eq!(result.get("empty"), Some(&String::new()));
466    }
467
468    #[test]
469    fn test_create_request_data_without_body_minimal() {
470        let uri: Uri = "/test".parse().unwrap();
471        let method = Method::GET;
472        let headers = HeaderMap::new();
473        let path_params = HashMap::new();
474
475        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
476
477        assert_eq!(result.method, "GET");
478        assert_eq!(result.path, "/test");
479        assert!(result.path_params.is_empty());
480        assert_eq!(result.query_params, json!({}));
481        assert!(result.raw_query_params.is_empty());
482        assert!(result.headers.is_empty());
483        assert!(result.cookies.is_empty());
484        assert_eq!(result.body, Value::Null);
485        assert!(result.raw_body.is_none());
486    }
487
488    #[test]
489    fn test_create_request_data_without_body_with_path_params() {
490        let uri: Uri = "/users/42".parse().unwrap();
491        let method = Method::GET;
492        let headers = HeaderMap::new();
493        let mut path_params = HashMap::new();
494        path_params.insert("user_id".to_string(), "42".to_string());
495
496        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
497
498        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
499    }
500
501    #[test]
502    fn test_create_request_data_without_body_with_query_params() {
503        let uri: Uri = "/search?q=rust&limit=10".parse().unwrap();
504        let method = Method::GET;
505        let headers = HeaderMap::new();
506        let path_params = HashMap::new();
507
508        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
509
510        assert_eq!(result.query_params, json!({"q": "rust", "limit": 10}));
511        assert_eq!(result.raw_query_params.get("q"), Some(&vec!["rust".to_string()]));
512        assert_eq!(result.raw_query_params.get("limit"), Some(&vec!["10".to_string()]));
513    }
514
515    #[test]
516    fn test_create_request_data_without_body_with_headers() {
517        let uri: Uri = "/test".parse().unwrap();
518        let method = Method::POST;
519        let mut headers = HeaderMap::new();
520        headers.insert("content-type", HeaderValue::from_static("application/json"));
521        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
522        let path_params = HashMap::new();
523
524        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
525
526        assert_eq!(
527            result.headers.get("content-type"),
528            Some(&"application/json".to_string())
529        );
530        assert_eq!(result.headers.get("authorization"), Some(&"Bearer token".to_string()));
531    }
532
533    #[test]
534    fn test_create_request_data_without_body_with_cookies() {
535        let uri: Uri = "/test".parse().unwrap();
536        let method = Method::GET;
537        let mut headers = HeaderMap::new();
538        headers.insert(
539            axum::http::header::COOKIE,
540            HeaderValue::from_static("session=xyz; theme=dark"),
541        );
542        let path_params = HashMap::new();
543
544        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
545
546        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
547        assert_eq!(result.cookies.get("theme"), Some(&"dark".to_string()));
548    }
549
550    #[test]
551    fn test_create_request_data_without_body_skip_headers() {
552        let uri: Uri = "/test".parse().unwrap();
553        let method = Method::GET;
554        let mut headers = HeaderMap::new();
555        headers.insert("authorization", HeaderValue::from_static("Bearer token"));
556        let path_params = HashMap::new();
557
558        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_SKIP_HEADERS);
559
560        assert!(result.headers.is_empty());
561    }
562
563    #[test]
564    fn test_create_request_data_without_body_skip_cookies() {
565        let uri: Uri = "/test".parse().unwrap();
566        let method = Method::GET;
567        let mut headers = HeaderMap::new();
568        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=xyz"));
569        let path_params = HashMap::new();
570
571        let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_SKIP_COOKIES);
572
573        assert!(result.cookies.is_empty());
574    }
575
576    #[test]
577    fn test_create_request_data_without_body_different_methods() {
578        let uri: Uri = "/resource".parse().unwrap();
579        let headers = HeaderMap::new();
580        let path_params = HashMap::new();
581
582        for method in &[Method::GET, Method::POST, Method::PUT, Method::DELETE, Method::PATCH] {
583            let result = create_request_data_without_body(&uri, method, &headers, path_params.clone(), OPTIONS_ALL);
584            assert_eq!(result.method, method.as_str());
585        }
586    }
587
588    #[tokio::test]
589    async fn test_create_request_data_with_body_empty() {
590        let parts = axum::http::request::Request::builder()
591            .method(Method::POST)
592            .uri("/test")
593            .body(Body::empty())
594            .unwrap()
595            .into_parts();
596
597        let body = Body::empty();
598        let path_params = HashMap::new();
599
600        let result = create_request_data_with_body(&parts.0, path_params, body, true, true, true, true)
601            .await
602            .unwrap();
603
604        assert_eq!(result.method, "POST");
605        assert_eq!(result.path, "/test");
606        assert_eq!(result.body, Value::Null);
607        assert!(result.raw_body.is_none());
608    }
609
610    #[tokio::test]
611    async fn test_create_request_data_with_body_json() {
612        let request_body = Body::from(r#"{"key":"value"}"#);
613        let request = axum::http::request::Request::builder()
614            .method(Method::POST)
615            .uri("/test")
616            .body(Body::empty())
617            .unwrap();
618
619        let (parts, _) = request.into_parts();
620        let path_params = HashMap::new();
621
622        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
623            .await
624            .unwrap();
625
626        assert_eq!(result.method, "POST");
627        assert_eq!(result.body, Value::Null);
628        assert!(result.raw_body.is_some());
629        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), br#"{"key":"value"}"#);
630    }
631
632    #[tokio::test]
633    async fn test_create_request_data_with_body_with_query_params() {
634        let request_body = Body::from("test");
635        let request = axum::http::request::Request::builder()
636            .method(Method::POST)
637            .uri("/test?foo=bar&baz=qux")
638            .body(Body::empty())
639            .unwrap();
640
641        let (parts, _) = request.into_parts();
642        let path_params = HashMap::new();
643
644        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
645            .await
646            .unwrap();
647
648        assert_eq!(result.query_params, json!({"foo": "bar", "baz": "qux"}));
649    }
650
651    #[tokio::test]
652    async fn test_create_request_data_with_body_with_headers() {
653        let request_body = Body::from("test");
654        let request = axum::http::request::Request::builder()
655            .method(Method::POST)
656            .uri("/test")
657            .header("content-type", "application/json")
658            .header("x-request-id", "req123")
659            .body(Body::empty())
660            .unwrap();
661
662        let (parts, _) = request.into_parts();
663        let path_params = HashMap::new();
664
665        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
666            .await
667            .unwrap();
668
669        assert_eq!(
670            result.headers.get("content-type"),
671            Some(&"application/json".to_string())
672        );
673        assert_eq!(result.headers.get("x-request-id"), Some(&"req123".to_string()));
674    }
675
676    #[tokio::test]
677    async fn test_create_request_data_with_body_with_cookies() {
678        let request_body = Body::from("test");
679        let request = axum::http::request::Request::builder()
680            .method(Method::POST)
681            .uri("/test")
682            .header("cookie", "session=xyz; user=123")
683            .body(Body::empty())
684            .unwrap();
685
686        let (parts, _) = request.into_parts();
687        let path_params = HashMap::new();
688
689        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
690            .await
691            .unwrap();
692
693        assert_eq!(result.cookies.get("session"), Some(&"xyz".to_string()));
694        assert_eq!(result.cookies.get("user"), Some(&"123".to_string()));
695    }
696
697    #[tokio::test]
698    async fn test_create_request_data_with_body_large_payload() {
699        let large_json = json!({
700            "data": (0..100).map(|i| json!({"id": i, "value": format!("item-{}", i)})).collect::<Vec<_>>()
701        });
702        let json_str = serde_json::to_string(&large_json).unwrap();
703        let request_body = Body::from(json_str.clone());
704
705        let request = axum::http::request::Request::builder()
706            .method(Method::POST)
707            .uri("/test")
708            .body(Body::empty())
709            .unwrap();
710
711        let (parts, _) = request.into_parts();
712        let path_params = HashMap::new();
713
714        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
715            .await
716            .unwrap();
717
718        assert!(result.raw_body.is_some());
719        assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), json_str.as_bytes());
720    }
721
722    #[tokio::test]
723    async fn test_create_request_data_with_body_preserves_all_fields() {
724        let request_body = Body::from("request data");
725        let request = axum::http::request::Request::builder()
726            .method(Method::PUT)
727            .uri("/users/42?action=update")
728            .header("authorization", "Bearer token")
729            .header("cookie", "session=abc")
730            .body(Body::empty())
731            .unwrap();
732
733        let (parts, _) = request.into_parts();
734        let mut path_params = HashMap::new();
735        path_params.insert("user_id".to_string(), "42".to_string());
736
737        let result = create_request_data_with_body(&parts, path_params, request_body, true, true, true, true)
738            .await
739            .unwrap();
740
741        assert_eq!(result.method, "PUT");
742        assert_eq!(result.path, "/users/42");
743        assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
744        assert_eq!(result.query_params, json!({"action": "update"}));
745        assert!(result.headers.contains_key("authorization"));
746        assert!(result.cookies.contains_key("session"));
747        assert!(result.raw_body.is_some());
748    }
749
750    #[test]
751    fn test_arc_wrapping_for_cheap_cloning() {
752        let uri: Uri = "/test".parse().unwrap();
753        let method = Method::GET;
754        let mut headers = HeaderMap::new();
755        headers.insert(axum::http::header::COOKIE, HeaderValue::from_static("session=abc"));
756        let mut path_params = HashMap::new();
757        path_params.insert("id".to_string(), "1".to_string());
758
759        let request_data = create_request_data_without_body(&uri, &method, &headers, path_params.clone(), OPTIONS_ALL);
760
761        let cloned = request_data.clone();
762
763        assert!(Arc::ptr_eq(&request_data.path_params, &cloned.path_params));
764        assert!(Arc::ptr_eq(&request_data.headers, &cloned.headers));
765        assert!(Arc::ptr_eq(&request_data.cookies, &cloned.cookies));
766        assert!(Arc::ptr_eq(&request_data.raw_query_params, &cloned.raw_query_params));
767    }
768
769    #[tokio::test]
770    async fn create_request_data_with_body_returns_bad_request_when_body_stream_errors() {
771        let request = axum::http::Request::builder()
772            .method(Method::POST)
773            .uri("/path")
774            .body(Body::empty())
775            .unwrap();
776        let (parts, _) = request.into_parts();
777
778        let stream = stream::once(async move {
779            Err::<bytes::Bytes, std::io::Error>(std::io::Error::new(std::io::ErrorKind::Other, "boom"))
780        });
781        let body = Body::from_stream(stream);
782
783        let err = create_request_data_with_body(&parts, HashMap::new(), body, true, true, true, true)
784            .await
785            .unwrap_err();
786        assert_eq!(err.0, axum::http::StatusCode::BAD_REQUEST);
787        assert!(err.1.contains("Failed to read body:"));
788    }
789}