Files
Genarrative/server-rs/apps/api-server/src/app.rs

156 lines
5.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use axum::{body::Body, extract::Extension, http::Request, middleware, routing::get, Router};
use tower_http::trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{info_span, Level};
use crate::{
error_middleware::normalize_error_response,
health::health_check,
request_context::{attach_request_context, resolve_request_id},
response_headers::propagate_request_id_header,
state::AppState,
};
// 统一由这里构造 Axum 路由树,后续再逐项挂接中间件与业务路由。
pub fn build_router(state: AppState) -> Router {
Router::new()
.route(
"/healthz",
get(|Extension(request_context): Extension<_>| async move {
health_check(Extension(request_context)).await
}),
)
// 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。
.layer(middleware::from_fn(normalize_error_response))
// 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。
.layer(middleware::from_fn(propagate_request_id_header))
// 当前阶段先统一挂接 HTTP tracing后续 request_id、响应头与错误中间件继续在这里扩展。
.layer(
TraceLayer::new_for_http()
.make_span_with(|request: &Request<Body>| {
let request_id =
resolve_request_id(request).unwrap_or_else(|| "unknown".to_string());
info_span!(
"http.request",
method = %request.method(),
uri = %request.uri(),
request_id = %request_id,
)
})
.on_request(DefaultOnRequest::new().level(Level::INFO))
.on_response(DefaultOnResponse::new().level(Level::INFO))
.on_failure(DefaultOnFailure::new().level(Level::ERROR)),
)
// request_id 中间件先进入请求链,确保后续 tracing、错误处理和响应头层都能复用同一份请求标识。
.layer(middleware::from_fn(attach_request_context))
.with_state(state)
}
#[cfg(test)]
mod tests {
use axum::{
body::Body,
http::{Request, StatusCode},
};
use http_body_util::BodyExt;
use serde_json::Value;
use tower::ServiceExt;
use crate::{config::AppConfig, state::AppState};
use super::build_router;
#[tokio::test]
async fn healthz_returns_legacy_compatible_payload_and_headers() {
let app = build_router(AppState::new(AppConfig::default()));
let response = app
.oneshot(
Request::builder()
.uri("/healthz")
.header("x-request-id", "req-health-legacy")
.body(Body::empty())
.expect("healthz request should build"),
)
.await
.expect("healthz request should succeed");
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get("x-request-id")
.and_then(|value| value.to_str().ok()),
Some("req-health-legacy")
);
assert_eq!(
response
.headers()
.get("x-api-version")
.and_then(|value| value.to_str().ok()),
Some("2026-04-08")
);
assert_eq!(
response
.headers()
.get("x-route-version")
.and_then(|value| value.to_str().ok()),
Some("2026-04-08")
);
assert!(response.headers().contains_key("x-response-time-ms"));
let body = response
.into_body()
.collect()
.await
.expect("healthz body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("healthz body should be valid json");
assert_eq!(payload["ok"], Value::Bool(true));
assert_eq!(
payload["service"],
Value::String("genarrative-node-server".to_string())
);
}
#[tokio::test]
async fn healthz_returns_standard_envelope_when_requested() {
let app = build_router(AppState::new(AppConfig::default()));
let response = app
.oneshot(
Request::builder()
.uri("/healthz")
.header("x-request-id", "req-health-envelope")
.header("x-genarrative-response-envelope", "v1")
.body(Body::empty())
.expect("healthz request should build"),
)
.await
.expect("healthz request should succeed");
assert_eq!(response.status(), StatusCode::OK);
let body = response
.into_body()
.collect()
.await
.expect("healthz body should collect")
.to_bytes();
let payload: Value =
serde_json::from_slice(&body).expect("healthz body should be valid json");
assert_eq!(payload["ok"], Value::Bool(true));
assert_eq!(
payload["data"]["service"],
Value::String("genarrative-node-server".to_string())
);
assert_eq!(
payload["meta"]["requestId"],
Value::String("req-health-envelope".to_string())
);
}
}