1use std::{any::Any, fmt, time::Instant};
2
3use axum::{
4 Router,
5 body::Body,
6 extract::{FromRequestParts, MatchedPath, Request, State},
7 http::{
8 HeaderMap, HeaderName, HeaderValue, Method, StatusCode,
9 header::{
10 AUTHORIZATION, CACHE_CONTROL, CONTENT_TYPE, ETAG, IF_MATCH, LOCATION, RETRY_AFTER,
11 },
12 request::Parts,
13 },
14 middleware::{self, Next},
15 response::{IntoResponse, Response},
16};
17use metrics::Gauge;
18use opentelemetry::{
19 Context, global,
20 propagation::{Extractor, Injector},
21};
22use tokio::time;
23use tower::limit::ConcurrencyLimitLayer;
24use tower_http::{
25 catch_panic::CatchPanicLayer,
26 cors::{AllowOrigin, CorsLayer},
27 limit::RequestBodyLimitLayer,
28};
29use tracing::Instrument as _;
30use tracing_opentelemetry::OpenTelemetrySpanExt as _;
31use uuid::Uuid;
32
33use crate::{ApiError, HttpOptions, IDEMPOTENCY_KEY, ResponseCachePolicy};
34
35pub const X_REQUEST_ID: HeaderName = HeaderName::from_static("x-request-id");
37
38const TRACEPARENT: HeaderName = HeaderName::from_static("traceparent");
39const TRACESTATE: HeaderName = HeaderName::from_static("tracestate");
40const RATE_LIMIT_LIMIT: HeaderName = HeaderName::from_static("ratelimit-limit");
41const RATE_LIMIT_REMAINING: HeaderName = HeaderName::from_static("ratelimit-remaining");
42const RATE_LIMIT_RESET: HeaderName = HeaderName::from_static("ratelimit-reset");
43const PRIVATE_NO_STORE: HeaderValue = HeaderValue::from_static("private, no-store");
44const UNMATCHED_ROUTE: &str = "unmatched";
45
46pub const HTTP_REQUESTS_TOTAL: &str = "http_requests_total";
48pub const HTTP_REQUEST_DURATION_SECONDS: &str = "http_request_duration_seconds";
50pub const HTTP_REQUESTS_IN_FLIGHT: &str = "http_requests_in_flight";
52
53pub const DURATION_BUCKETS: &[f64] = &[
60 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0,
61];
62
63tokio::task_local! {
64 static CURRENT_REQUEST_ID: RequestId;
65}
66
67#[derive(Clone, Debug, Eq, Hash, PartialEq)]
72pub struct RequestId(String);
73
74impl RequestId {
75 fn from_headers(headers: &HeaderMap) -> Self {
76 headers
77 .get(&X_REQUEST_ID)
78 .and_then(|value| value.to_str().ok())
79 .filter(|value| !value.is_empty())
80 .map_or_else(Self::generate, |value| Self(value.to_owned()))
81 }
82
83 fn generate() -> Self {
84 Self(Uuid::new_v4().to_string())
85 }
86
87 #[must_use]
89 pub fn as_str(&self) -> &str {
90 &self.0
91 }
92
93 pub(crate) fn header_value(
94 &self,
95 ) -> Result<HeaderValue, axum::http::header::InvalidHeaderValue> {
96 HeaderValue::from_str(&self.0)
97 }
98}
99
100impl fmt::Display for RequestId {
101 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
102 formatter.write_str(&self.0)
103 }
104}
105
106impl<S> FromRequestParts<S> for RequestId
107where
108 S: Send + Sync,
109{
110 type Rejection = ApiError;
111
112 async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
113 parts.extensions.get::<Self>().cloned().ok_or_else(|| {
114 ApiError::internal(std::io::Error::other(
115 "RequestId extractor used without baukit_http::layers",
116 ))
117 })
118 }
119}
120
121#[must_use]
123pub fn extract_trace_context(headers: &HeaderMap) -> Context {
124 global::get_text_map_propagator(|propagator| propagator.extract(&HeaderExtractor(headers)))
125}
126
127pub fn inject_trace_context(context: &Context, headers: &mut HeaderMap) {
132 global::get_text_map_propagator(|propagator| {
133 propagator.inject_context(context, &mut HeaderInjector(headers));
134 });
135}
136
137pub fn inject_current_trace_context(headers: &mut HeaderMap) {
139 inject_trace_context(&tracing::Span::current().context(), headers);
140}
141
142pub fn layers<S>(router: Router<S>, options: HttpOptions) -> Router<S>
148where
149 S: Clone + Send + Sync + 'static,
150{
151 let options = options.validate().unwrap_or_default();
152 let cors = cors_layer(&options);
153 let body_size_limit = options.body_size_limit;
154 let concurrency_limit = options.concurrency_limit;
155
156 router
157 .layer(RequestBodyLimitLayer::new(body_size_limit))
158 .layer(ConcurrencyLimitLayer::new(concurrency_limit))
159 .layer(cors)
160 .layer(CatchPanicLayer::custom(panic_response))
161 .layer(middleware::from_fn_with_state(options, request_lifecycle))
162}
163
164fn cors_layer(options: &HttpOptions) -> CorsLayer {
165 let allowed_headers = with_additional_headers(
166 vec![
167 AUTHORIZATION,
168 CONTENT_TYPE,
169 IF_MATCH,
170 IDEMPOTENCY_KEY,
171 X_REQUEST_ID,
172 TRACEPARENT,
173 TRACESTATE,
174 ],
175 &options.additional_allowed_headers,
176 );
177 let exposed_headers = with_additional_headers(
178 vec![
179 X_REQUEST_ID,
180 TRACEPARENT,
181 TRACESTATE,
182 RETRY_AFTER,
183 RATE_LIMIT_LIMIT,
184 RATE_LIMIT_REMAINING,
185 RATE_LIMIT_RESET,
186 ETAG,
187 LOCATION,
188 ],
189 &options.additional_exposed_headers,
190 );
191 let mut cors = CorsLayer::new()
192 .allow_methods([
193 Method::GET,
194 Method::HEAD,
195 Method::POST,
196 Method::PUT,
197 Method::PATCH,
198 Method::DELETE,
199 Method::OPTIONS,
200 ])
201 .allow_headers(allowed_headers)
202 .expose_headers(exposed_headers);
203 if !options.allowed_origins.is_empty() {
204 cors = cors.allow_origin(AllowOrigin::list(options.allowed_origins.clone()));
205 }
206 if options.allow_credentials {
207 cors = cors.allow_credentials(true);
208 }
209 cors
210}
211
212fn with_additional_headers(
213 mut defaults: Vec<HeaderName>,
214 additional: &[HeaderName],
215) -> Vec<HeaderName> {
216 for header in additional {
217 if !defaults.contains(header) {
218 defaults.push(header.clone());
219 }
220 }
221 defaults
222}
223
224fn apply_cache_policy(policy: ResponseCachePolicy, headers: &mut HeaderMap) {
225 if policy == ResponseCachePolicy::PrivateNoStore && !headers.contains_key(CACHE_CONTROL) {
226 headers.insert(CACHE_CONTROL, PRIVATE_NO_STORE);
227 }
228}
229
230async fn request_lifecycle(
231 State(options): State<HttpOptions>,
232 mut request: Request,
233 next: Next,
234) -> Response {
235 let request_id = RequestId::from_headers(request.headers());
236 request.extensions_mut().insert(request_id.clone());
237 request
238 .extensions_mut()
239 .insert(options.json_rejection_mode.clone());
240 CURRENT_REQUEST_ID
241 .scope(request_id, lifecycle_inner(options, request, next))
242 .await
243}
244
245async fn lifecycle_inner(options: HttpOptions, request: Request, next: Next) -> Response {
246 let request_id = current_request_id();
247 let method = bounded_method(request.method());
248 let route = request
249 .extensions()
250 .get::<MatchedPath>()
251 .map_or(UNMATCHED_ROUTE, MatchedPath::as_str)
252 .to_owned();
253 let parent_context = extract_trace_context(request.headers());
254 let span_name = format!("{method} {route}");
255 let span = tracing::info_span!(
256 "http.request",
257 otel.name = %span_name,
258 http.request.method = method,
259 http.route = %route,
260 http.response.status_code = tracing::field::Empty,
261 request_id = %request_id,
262 );
263 if let Err(error) = span.set_parent(parent_context) {
264 tracing::debug!(error = %error, "could not attach inbound trace context");
265 }
266
267 async move {
268 let started = Instant::now();
269 let in_flight = metrics::gauge!(
270 HTTP_REQUESTS_IN_FLIGHT,
271 "method" => method,
272 "route" => route.clone()
273 );
274 in_flight.increment(1.0);
275 let _in_flight = InFlightGuard(in_flight);
276
277 let mut response = match time::timeout(options.request_timeout, next.run(request)).await {
278 Ok(mut response) => {
279 if response.extensions_mut().remove::<PanicCaught>().is_some() {
280 tracing::error!("request handler panicked");
281 ApiError::internal_without_cause().into_response()
282 } else if response.status() == StatusCode::PAYLOAD_TOO_LARGE {
283 ApiError::configured_payload_too_large(&options.json_rejection_mode)
284 .into_response()
285 } else {
286 response
287 }
288 }
289 Err(_) => {
290 tracing::warn!("request timed out");
291 ApiError::timeout().into_response()
292 }
293 };
294
295 let status = response.status();
296 let status_label = status.as_u16().to_string();
297 tracing::Span::current().record("http.response.status_code", status.as_u16());
298 metrics::counter!(
299 HTTP_REQUESTS_TOTAL,
300 "method" => method,
301 "route" => route.clone(),
302 "status" => status_label.clone()
303 )
304 .increment(1);
305 metrics::histogram!(
306 HTTP_REQUEST_DURATION_SECONDS,
307 "method" => method,
308 "route" => route,
309 "status" => status_label
310 )
311 .record(started.elapsed().as_secs_f64());
312
313 response.headers_mut().insert(
314 X_REQUEST_ID,
315 current_request_id()
316 .header_value()
317 .expect("request IDs originate from a valid header or UUID"),
318 );
319 inject_current_trace_context(response.headers_mut());
320 apply_cache_policy(options.response_cache_policy, response.headers_mut());
321 response
322 }
323 .instrument(span)
324 .await
325}
326
327pub(crate) fn current_request_id() -> RequestId {
328 CURRENT_REQUEST_ID
329 .try_with(Clone::clone)
330 .unwrap_or_else(|_| RequestId::generate())
331}
332
333fn bounded_method(method: &Method) -> &'static str {
334 match *method {
335 Method::CONNECT => "CONNECT",
336 Method::DELETE => "DELETE",
337 Method::GET => "GET",
338 Method::HEAD => "HEAD",
339 Method::OPTIONS => "OPTIONS",
340 Method::PATCH => "PATCH",
341 Method::POST => "POST",
342 Method::PUT => "PUT",
343 Method::TRACE => "TRACE",
344 _ => "OTHER",
345 }
346}
347
348#[derive(Clone, Copy, Debug)]
349struct PanicCaught;
350
351fn panic_response(_panic: Box<dyn Any + Send + 'static>) -> Response<Body> {
352 let mut response = StatusCode::INTERNAL_SERVER_ERROR.into_response();
353 response.extensions_mut().insert(PanicCaught);
354 response
355}
356
357struct InFlightGuard(Gauge);
358
359impl Drop for InFlightGuard {
360 fn drop(&mut self) {
361 self.0.decrement(1.0);
362 }
363}
364
365struct HeaderExtractor<'a>(&'a HeaderMap);
366
367impl Extractor for HeaderExtractor<'_> {
368 fn get(&self, key: &str) -> Option<&str> {
369 self.0.get(key).and_then(|value| value.to_str().ok())
370 }
371
372 fn keys(&self) -> Vec<&str> {
373 self.0.keys().map(HeaderName::as_str).collect()
374 }
375}
376
377struct HeaderInjector<'a>(&'a mut HeaderMap);
378
379impl Injector for HeaderInjector<'_> {
380 fn set(&mut self, key: &str, value: String) {
381 if let (Ok(name), Ok(value)) = (HeaderName::from_bytes(key.as_bytes()), value.parse()) {
382 self.0.insert(name, value);
383 }
384 }
385}