1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
use console::style;
use http_body_util::BodyExt;
use hyper::header::ORIGIN;
use hyper::http::request::Parts;
use hyper::{body::Incoming, Version};
use serde_json::{to_string_pretty, Value};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::core::{
config::log_config::verbose_config::VerboseConfig,
util::http::{content_type_is_application_json, normalize_url_path},
};
/// Request metadata and body, decoded once per request.
///
/// # Why we eagerly collect the body
///
/// Routing decisions depend on body contents (rule-set `body.json`
/// conditions, middleware evaluation). Rather than re-collecting the
/// body each time a matcher asks for it, we consume the `Incoming`
/// stream once here and keep the parsed JSON around for the lifetime of
/// the request. This is appropriate for a mock server where payloads
/// are small; a production proxy would want streaming instead.
#[derive(Debug)]
pub struct ParsedRequest {
pub url_path: String,
pub component_parts: Parts,
/// Parsed JSON body, if the request had one that parsed successfully.
/// `None` here means either "no body" or "body present but not JSON";
/// the two are indistinguishable at the matcher layer and we don't
/// currently need to distinguish them.
pub body_json: Option<Value>,
}
impl ParsedRequest {
/// Consume an incoming hyper request into a parsed form.
///
/// # Why a non-JSON body is logged but not rejected
///
/// Some rule sets key only on URL path or headers and don't inspect
/// the body at all. Failing the whole request because an operator
/// sent a form-encoded payload would be more aggressive than needed;
/// we log a warning and continue so the URL-path-only rules still
/// apply. Only *claimed* JSON (`Content-Type: application/json`) that
/// fails to parse becomes a hard `Err` — that is a real client bug.
pub async fn from(request: hyper::Request<Incoming>) -> Result<Self, String> {
let (component_parts, body) = request.into_parts();
let body_bytes = match body.boxed().collect().await {
Ok(x) => Some(x.to_bytes()),
Err(err) => {
log::warn!("failed to collect request incoming body: {}", err);
None
}
};
let has_body = body_bytes
.as_ref()
.map(|b| !b.is_empty())
.unwrap_or(false);
let body_json = if has_body {
// Safe: `has_body` implies `body_bytes.is_some()`.
let bytes = body_bytes.as_ref().expect("body_bytes presence checked by has_body");
let raw_body_json = serde_json::from_slice::<Option<Value>>(bytes);
match (
content_type_is_application_json(&component_parts.headers),
raw_body_json,
) {
// declared application/json but body didn't parse → hard error
(Some(true), Err(err)) => {
return Err(format!(
"failed to get json value from request body: {}",
err
));
}
// declared application/json and body parsed → use it
(Some(true), Ok(v)) => v,
// body parsed as JSON even without the declaration → use it
// (this is a common reality for lazy clients)
(_, Ok(v)) => {
if matches!(content_type_is_application_json(&component_parts.headers), Some(false)) {
log::warn!(
"request has body but its content-type is not application/json"
);
} else if content_type_is_application_json(&component_parts.headers).is_none() {
log::warn!("request has body but doesn't have content-type");
}
v
}
// body present but not JSON and not claimed as JSON → ignore
(_, Err(_)) => None,
}
} else {
None
};
let url_path = normalize_url_path(component_parts.uri.path(), None);
Ok(ParsedRequest {
url_path,
component_parts,
body_json,
})
}
/// Emit the request to the log.
///
/// # Why this is a method on `ParsedRequest` and not a logger plugin
///
/// The verbose log contains pretty-printed JSON body, which requires
/// an allocation. Doing it inside a dedicated method means we can
/// short-circuit before paying that cost when verbose mode is off —
/// the default for non-debug use — without the logger trait having
/// to know anything about request shape.
pub fn capture_in_log(&self, verbose: VerboseConfig) {
// server log (timestamp)
// `unwrap_or_default` here: if the system clock is before 1970
// we fall back to 0 rather than panicking; this is pure log
// cosmetics and should never take down the server.
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or_default();
let hours = (now / 3600) % 24;
let minutes = (now / 60) % 60;
let seconds = now % 60;
let timestamp = format!("{:02}:{:02}:{:02}", hours, minutes, seconds);
// request info (url path, origin etc.)
let version = match self.component_parts.version {
Version::HTTP_3 => "HTTP/3",
Version::HTTP_2 => "HTTP/2",
Version::HTTP_11 => "HTTP/1.1",
_ => "HTTP/1.0 or earlier, or HTTP/4 or later",
};
let origin = self
.component_parts
.headers
.get(ORIGIN)
.and_then(|v| v.to_str().ok());
// print
// - server info and request info
let mut printed = format!(
"<- {}\n [{}]",
style(self.url_path.as_str()).yellow(),
self.component_parts.method,
);
if let Some(origin) = origin {
printed.push_str(&format!(" [ORIGIN {}]", origin));
}
printed.push_str(&format!(
" [{}] request received (at {} UTC)",
version, timestamp
));
// - headers
if verbose.header || verbose.body {
printed.push_str("\n");
}
if verbose.header {
// Header values that aren't valid UTF-8 are rare but legal.
// We render them as `<non-utf8>` rather than panicking — a
// log line is not worth taking the request down over.
let headers = self
.component_parts
.headers
.iter()
.map(|(name, value)| {
format!("\n{}: {}", name, value.to_str().unwrap_or("<non-utf8>"))
})
.collect::<String>();
printed.push_str(&format!(
" [request.headers]{}\n",
style(headers).magenta()
));
}
// - body (url query, json params)
let mut is_verbose_body = false;
if verbose.body {
let query = self.component_parts.uri.query();
if let Some(query) = query {
printed.push_str(&format!(" [request.query] {}\n", query));
is_verbose_body = true;
}
if let Some(request_body_json_value) = &self.body_json {
printed.push_str(" [request.body.json]\n");
let body_str = match to_string_pretty(request_body_json_value) {
Ok(x) => x,
Err(err) => {
log::warn!(
"failed to prettify JSON: {} ({})",
request_body_json_value,
err
);
request_body_json_value.to_string()
}
};
let styled_body_str = body_str
.split("\n")
.map(|s| style(s).green().to_string())
.collect::<Vec<String>>()
.join("\n");
printed.push_str(styled_body_str.as_str());
is_verbose_body = true;
}
}
if verbose.header || is_verbose_body {
printed.push_str("\n");
}
log::info!("{}", printed);
}
}