arcly_http/web/
boundary.rs1use axum::body::Body;
11use axum::extract::{RawPathParams, Request, State};
12use axum::http::request::Parts;
13use axum::response::Response;
14use axum::routing::{on, MethodFilter, MethodRouter};
15use smallvec::SmallVec;
16use smol_str::SmolStr;
17
18use crate::auth::extract::extract_auth;
19use crate::core::engine::{FrozenDiContainer, RouteDescriptor, RouteSpec};
20use crate::observability::lean_telemetry::on_request_start;
21use crate::observability::propagation::extract_trace_context;
22use crate::web::context::RequestContext;
23
24const MAX_BODY: usize = 8 * 1024 * 1024;
26
27pub(crate) struct InFlightGuard;
31impl InFlightGuard {
32 #[inline]
33 pub(crate) fn new() -> Self {
34 metrics::gauge!("http_requests_in_flight").increment(1.0);
35 Self
36 }
37}
38impl Drop for InFlightGuard {
39 #[inline]
40 fn drop(&mut self) {
41 metrics::gauge!("http_requests_in_flight").decrement(1.0);
42 }
43}
44
45pub(crate) async fn assemble_context(
52 parts: Parts,
53 body: Body,
54 params: SmallVec<[(SmolStr, SmolStr); 4]>,
55 container: &'static FrozenDiContainer,
56 route_pattern: &'static str,
57 route_spec: Option<&'static RouteSpec>,
58) -> RequestContext {
59 let bytes = axum::body::to_bytes(body, MAX_BODY)
60 .await
61 .unwrap_or_default();
62
63 let trace = extract_trace_context(&parts.headers);
64 let auth = extract_auth(&parts.headers, container).await;
65 let tenant = container
67 .try_get::<crate::web::tenant::TenantRegistry>()
68 .and_then(|tr| tr.resolve(&parts.headers));
69
70 RequestContext::__new(
71 parts.method,
72 SmolStr::new(parts.uri.path()),
73 SmolStr::new(parts.uri.query().unwrap_or("")),
74 params,
75 parts.headers,
76 bytes,
77 trace.trace_id,
78 trace.span_id,
79 trace.parent_span_id,
80 container,
81 route_pattern,
82 route_spec,
83 )
84 .__with_claims(auth.claims)
85 .__with_session(auth.session)
86 .__with_tenant(tenant)
87}
88
89pub fn adapt(rt: &'static RouteDescriptor) -> MethodRouter<&'static FrozenDiContainer> {
93 let filter = MethodFilter::try_from(axum::http::Method::from(rt.method))
94 .expect("Arcly: unsupported HTTP method");
95
96 let handler = move |State(container): State<&'static FrozenDiContainer>,
97 raw_params: RawPathParams,
98 req: Request| async move {
99 let params: SmallVec<[(SmolStr, SmolStr); 4]> = raw_params
100 .iter()
101 .map(|(k, v)| (SmolStr::new(k), SmolStr::new(v)))
102 .collect();
103
104 let (parts, body) = req.into_parts();
105 let ctx = assemble_context(parts, body, params, container, rt.path, Some(rt.spec)).await;
106
107 let _guard = on_request_start();
108 let _in_flight = InFlightGuard::new();
109 let resp: Response = (rt.handler)(ctx).await;
110 drop(_guard);
111
112 let (mut p, b) = resp.into_parts();
113 if !rt.spec.sunset.is_empty() {
116 p.headers
117 .insert("deprecation", axum::http::HeaderValue::from_static("true"));
118 if let Ok(v) = axum::http::HeaderValue::from_str(rt.spec.sunset) {
119 p.headers.insert("sunset", v);
120 }
121 }
122 Response::from_parts(p, Body::new(b))
123 };
124
125 on(filter, handler)
126}