Skip to main content

apimock_server/
parsed_request.rs

1//! Server-side helpers for `apimock_routing::ParsedRequest`.
2//!
3//! # Why this file exists after the 5.0 split
4//!
5//! `ParsedRequest` (the data) now lives in `apimock-routing` so the
6//! matcher crate can depend on it without pulling in hyper/body I/O.
7//! The two operations that *touch* HTTP — building a `ParsedRequest`
8//! from an incoming hyper request, and logging one to stdout — are
9//! server-layer activities, so they stay here as free functions.
10
11use apimock_config::config::log_config::verbose_config::VerboseConfig;
12use apimock_routing::{ParsedRequest, util::http::normalize_url_path};
13use console::style;
14use http_body_util::BodyExt;
15use hyper::header::ORIGIN;
16use hyper::{Version, body::Incoming};
17use serde_json::{Value, to_string_pretty};
18
19use std::time::{SystemTime, UNIX_EPOCH};
20
21use crate::http_util::content_type_is_application_json;
22
23/// Consume an incoming hyper request into a `ParsedRequest` the matcher
24/// can use.
25///
26/// # Why a non-JSON body is logged but not rejected
27///
28/// Some rule sets key only on URL path or headers and don't inspect the
29/// body at all. Failing the whole request because an operator sent a
30/// form-encoded payload would be more aggressive than needed; we log a
31/// warning and continue so the URL-path-only rules still apply. Only
32/// *claimed* JSON (`Content-Type: application/json`) that fails to
33/// parse becomes a hard `Err` — that is a real client bug.
34pub async fn parsed_request_from(
35    request: hyper::Request<Incoming>,
36) -> Result<ParsedRequest, String> {
37    let (component_parts, body) = request.into_parts();
38
39    let body_bytes = match body.boxed().collect().await {
40        Ok(x) => Some(x.to_bytes()),
41        Err(err) => {
42            log::warn!("failed to collect request incoming body: {}", err);
43            None
44        }
45    };
46
47    let has_body = body_bytes.as_ref().map(|b| !b.is_empty()).unwrap_or(false);
48
49    let body_json = if has_body {
50        let bytes = body_bytes
51            .as_ref()
52            .expect("body_bytes presence checked by has_body");
53        let raw_body_json = serde_json::from_slice::<Option<Value>>(bytes);
54
55        match (
56            content_type_is_application_json(&component_parts.headers),
57            raw_body_json,
58        ) {
59            // declared application/json but body didn't parse → hard error
60            (Some(true), Err(err)) => {
61                return Err(format!(
62                    "failed to get json value from request body: {}",
63                    err
64                ));
65            }
66            (Some(true), Ok(v)) => v,
67            (_, Ok(v)) => {
68                if matches!(
69                    content_type_is_application_json(&component_parts.headers),
70                    Some(false)
71                ) {
72                    log::warn!("request has body but its content-type is not application/json");
73                } else if content_type_is_application_json(&component_parts.headers).is_none() {
74                    log::warn!("request has body but doesn't have content-type");
75                }
76                v
77            }
78            (_, Err(_)) => None,
79        }
80    } else {
81        None
82    };
83
84    let url_path = normalize_url_path(component_parts.uri.path(), None);
85
86    Ok(ParsedRequest {
87        url_path,
88        component_parts,
89        body_json,
90    })
91}
92
93/// Emit a single log line describing the request.
94pub fn capture_in_log(request: &ParsedRequest, verbose: VerboseConfig) {
95    let now = SystemTime::now()
96        .duration_since(UNIX_EPOCH)
97        .map(|d| d.as_secs())
98        .unwrap_or_default();
99    let hours = (now / 3600) % 24;
100    let minutes = (now / 60) % 60;
101    let seconds = now % 60;
102    let timestamp = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
103
104    let version = match request.component_parts.version {
105        Version::HTTP_3 => "HTTP/3",
106        Version::HTTP_2 => "HTTP/2",
107        Version::HTTP_11 => "HTTP/1.1",
108        _ => "HTTP/1.0 or earlier, or HTTP/4 or later",
109    };
110
111    let origin = request
112        .component_parts
113        .headers
114        .get(ORIGIN)
115        .and_then(|v| v.to_str().ok());
116
117    let mut printed = format!(
118        "<- {}\n   [{}]",
119        style(request.url_path.as_str()).yellow(),
120        request.component_parts.method,
121    );
122    if let Some(origin) = origin {
123        printed.push_str(&format!(" [ORIGIN {}]", origin));
124    }
125    printed.push_str(&format!(
126        " [{}] request received (at {} UTC)",
127        version, timestamp
128    ));
129
130    if verbose.header || verbose.body {
131        printed.push_str("\n");
132    }
133    if verbose.header {
134        let headers = request
135            .component_parts
136            .headers
137            .iter()
138            .map(|(name, value)| format!("\n{}: {}", name, value.to_str().unwrap_or("<non-utf8>")))
139            .collect::<String>();
140        printed.push_str(&format!(
141            "   [request.headers]{}\n",
142            style(headers).magenta()
143        ));
144    }
145
146    let mut is_verbose_body = false;
147    if verbose.body {
148        let query = request.component_parts.uri.query();
149        if let Some(query) = query {
150            printed.push_str(&format!("   [request.query] {}\n", query));
151            is_verbose_body = true;
152        }
153
154        if let Some(request_body_json_value) = &request.body_json {
155            printed.push_str("   [request.body.json]\n");
156
157            let body_str = match to_string_pretty(request_body_json_value) {
158                Ok(x) => x,
159                Err(err) => {
160                    log::warn!(
161                        "failed to prettify JSON: {} ({})",
162                        request_body_json_value,
163                        err
164                    );
165                    request_body_json_value.to_string()
166                }
167            };
168            let styled_body_str = body_str
169                .split("\n")
170                .map(|s| style(s).green().to_string())
171                .collect::<Vec<String>>()
172                .join("\n");
173            printed.push_str(styled_body_str.as_str());
174
175            is_verbose_body = true;
176        }
177    }
178    if verbose.header || is_verbose_body {
179        printed.push_str("\n");
180    }
181
182    log::info!("{}", printed);
183}