use axum::extract::{Request, State};
use axum::middleware::Next;
use axum::response::Response;
use crate::server::state::AppState;
const USER_AGENT: &str = "user-agent";
const MAX_USER_AGENT_LEN: usize = 256;
const MAX_REQUEST_ID_LEN: usize = 200;
const CHANGE_CONTEXT: &str = "x-orion-change-context";
const MAX_CHANGE_CONTEXT_LEN: usize = 256;
#[derive(Debug, Clone, Default)]
pub struct RequestContext {
pub request_id: String,
pub client_ip: String,
pub user_agent: Option<String>,
pub change_context: Option<String>,
}
tokio::task_local! {
pub static REQUEST_CONTEXT: RequestContext;
}
pub fn request_id() -> Option<String> {
REQUEST_CONTEXT
.try_with(|ctx| ctx.request_id.clone())
.ok()
.filter(|id| !id.is_empty())
}
pub fn current() -> Option<RequestContext> {
REQUEST_CONTEXT.try_with(|ctx| ctx.clone()).ok()
}
fn ascii_header(req: &Request, name: &str, max_len: usize) -> Option<String> {
req.headers()
.get(name)
.and_then(|v| v.to_str().ok())
.filter(|v| !v.is_empty())
.map(|v| v[..v.len().min(max_len)].to_string())
}
pub async fn request_context_scope(
State(state): State<AppState>,
req: Request,
next: Next,
) -> Response {
let request_id = req
.headers()
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(|v| &v[..v.len().min(MAX_REQUEST_ID_LEN)])
.unwrap_or("")
.to_string();
let user_agent = ascii_header(&req, USER_AGENT, MAX_USER_AGENT_LEN);
let change_context = ascii_header(&req, CHANGE_CONTEXT, MAX_CHANGE_CONTEXT_LEN);
let client_ip = crate::server::rate_limit::extract_client_ip(&req, state.trusted_proxies());
let ctx = RequestContext {
request_id,
client_ip,
user_agent,
change_context,
};
REQUEST_CONTEXT.scope(ctx, next.run(req)).await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn request_id_is_none_outside_a_scope() {
assert!(request_id().is_none());
assert!(current().is_none());
}
#[tokio::test]
async fn empty_request_id_reads_as_absent() {
let ctx = RequestContext::default();
REQUEST_CONTEXT
.scope(ctx, async {
assert!(request_id().is_none(), "an empty id is not an id");
assert!(current().is_some(), "the context itself is still in scope");
})
.await;
}
}