253f99d921
Project CI / AI game creator shell Rust crates (push) Successful in 1m30s
Project CI / AI game creator shell Rust smoke (push) Successful in 1m59s
Project CI / AI game creator shell Rust lane 2/2 (push) Failing after 5m26s
Project CI / Backend tests (push) Successful in 4m13s
Project CI / Frontend tests (push) Has been cancelled
Project CI / Native shell tests (push) Has been cancelled
Project CI / AI game creator shell web tests (push) Has been cancelled
Project CI / AI game creator shell Rust lane 1/2 (push) Has been cancelled
Project CI / Repository checks (push) Has been cancelled
- api-server 的错误响应改用 attach_request_context 建立的 CURRENT_REQUEST_CONTEXT(tokio task_local),错误 envelope 与成功 envelope 一样带 meta.requestId / meta.operation,脱离请求任务时退回原形状 - 越权隔离脚本新增成功/失败 envelope 断言:修复前错误响应 requestId 为空让断言变红,修复后 23 项 PASS - 媒体链路脚本扩展审核治理段:普通作者不能提交审核动作、更新待审与被拒都不改变在线旧版 URL 与可玩内容、审核结论可在后台追溯,44 项 PASS - 游戏分发里程碑阶段 A 第 6 条、阶段 B 第 1/2 条改为已勾选并写入取证,阶段 B 其余条目列出具体缺口 - decision-log 补记错误 envelope 同一份 meta 与「按可消费取证」两条口径
152 lines
4.3 KiB
Rust
152 lines
4.3 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use axum::{
|
|
extract::Request,
|
|
http::{HeaderValue, Request as HttpRequest, header::HeaderName},
|
|
middleware::Next,
|
|
response::Response,
|
|
};
|
|
use shared_contracts::api::API_RESPONSE_ENVELOPE_HEADER;
|
|
use uuid::Uuid;
|
|
|
|
pub use shared_contracts::api::X_REQUEST_ID_HEADER;
|
|
|
|
tokio::task_local! {
|
|
/// 当前请求的上下文:`AppError::into_response` 这类拿不到 extensions 的转换也要按同一份
|
|
/// meta 口径给出 requestId / operation,错误 envelope 与成功 envelope 才不会各写一套。
|
|
pub static CURRENT_REQUEST_CONTEXT: RequestContext;
|
|
}
|
|
|
|
// 当前阶段先把请求级元信息统一挂到 extensions,后续响应头、envelope 与错误处理中间件继续复用。
|
|
#[derive(Clone, Debug)]
|
|
pub struct RequestContext {
|
|
request_id: String,
|
|
operation: String,
|
|
request_started_at: Instant,
|
|
wants_envelope: bool,
|
|
external_call_deadline: Option<Instant>,
|
|
}
|
|
|
|
impl RequestContext {
|
|
pub fn new(
|
|
request_id: String,
|
|
operation: String,
|
|
elapsed_seed: Duration,
|
|
wants_envelope: bool,
|
|
) -> Self {
|
|
Self {
|
|
request_id,
|
|
operation,
|
|
request_started_at: Instant::now()
|
|
.checked_sub(elapsed_seed)
|
|
.unwrap_or_else(Instant::now),
|
|
wants_envelope,
|
|
external_call_deadline: None,
|
|
}
|
|
}
|
|
|
|
pub fn with_external_call_deadline(mut self, deadline: Instant) -> Self {
|
|
self.external_call_deadline = Some(deadline);
|
|
self
|
|
}
|
|
|
|
pub fn request_id(&self) -> &str {
|
|
&self.request_id
|
|
}
|
|
|
|
pub fn operation(&self) -> &str {
|
|
&self.operation
|
|
}
|
|
|
|
pub fn wants_envelope(&self) -> bool {
|
|
self.wants_envelope
|
|
}
|
|
|
|
pub fn external_call_deadline(&self) -> Option<Instant> {
|
|
self.external_call_deadline
|
|
}
|
|
|
|
pub fn elapsed(&self) -> u64 {
|
|
self.request_started_at
|
|
.elapsed()
|
|
.as_millis()
|
|
.min(u64::MAX as u128) as u64
|
|
}
|
|
}
|
|
|
|
pub async fn attach_request_context(mut request: Request, next: Next) -> Response {
|
|
let wants_envelope = wants_api_envelope(&request);
|
|
let request_id = request
|
|
.headers()
|
|
.get(X_REQUEST_ID_HEADER)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
|
let operation = format!("{} {}", request.method(), request.uri());
|
|
|
|
let context = RequestContext::new(
|
|
request_id.clone(),
|
|
operation,
|
|
Duration::ZERO,
|
|
wants_envelope,
|
|
);
|
|
let context_for_scope = context.clone();
|
|
request.extensions_mut().insert(context);
|
|
|
|
// 统一把 request_id 写回请求头,方便后续 tracing、响应头与 envelope 层读取同一来源。
|
|
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
|
|
request
|
|
.headers_mut()
|
|
.insert(HeaderName::from_static(X_REQUEST_ID_HEADER), header_value);
|
|
}
|
|
|
|
CURRENT_REQUEST_CONTEXT
|
|
.scope(context_for_scope, async move { next.run(request).await })
|
|
.await
|
|
}
|
|
|
|
pub fn resolve_request_id<B>(request: &HttpRequest<B>) -> Option<String> {
|
|
request
|
|
.extensions()
|
|
.get::<RequestContext>()
|
|
.map(|context| context.request_id().to_string())
|
|
.or_else(|| {
|
|
request
|
|
.headers()
|
|
.get(X_REQUEST_ID_HEADER)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(str::trim)
|
|
.filter(|value| !value.is_empty())
|
|
.map(ToOwned::to_owned)
|
|
})
|
|
}
|
|
|
|
fn wants_api_envelope<B>(request: &HttpRequest<B>) -> bool {
|
|
request
|
|
.headers()
|
|
.get(API_RESPONSE_ENVELOPE_HEADER)
|
|
.and_then(|value| value.to_str().ok())
|
|
.map(str::trim)
|
|
.map(str::to_lowercase)
|
|
.is_some_and(|value| matches!(value.as_str(), "1" | "true" | "v1" | "envelope"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn request_context_has_no_external_call_deadline_by_default() {
|
|
let context = RequestContext::new(
|
|
"request-1".to_string(),
|
|
"GET /healthz".to_string(),
|
|
Duration::ZERO,
|
|
false,
|
|
);
|
|
|
|
assert_eq!(context.external_call_deadline(), None);
|
|
}
|
|
}
|