fix(api): resolve the admin request id independently of the header name

The request-id layer injects under the configured api.request_id_header,
but the admin extractor read the hardcoded x-request-id name, so pointing
that setting at any other header made every authenticated admin request
fail with an internal error and took the admin interface down. Read the
name-agnostic RequestId extension the layer also populates, keeping the
header lookup as a fallback for callers that run without the layer.

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
zyycn
2026-07-27 01:09:49 +08:00
co-authored by Claude
parent 1bd40ffa67
commit 8226089ade
3 changed files with 43 additions and 9 deletions
+18 -7
View File
@@ -14,6 +14,7 @@ use gateway_admin::{
model::auth::{AdminPrincipal, AdminRequestContext, LoginCommand, LoginError},
};
use serde::{Deserialize, Serialize};
use tower_http::request_id::RequestId;
use super::{AdminEnvelope, AdminError, AdminResponse};
@@ -51,22 +52,32 @@ where
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
let principal = require_admin_auth(state, &parts.headers).await?;
let request_id = parts
.headers
.get(REQUEST_ID_HEADER)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
let request_id = admin_request_id(parts)
.ok_or_else(|| AdminError::internal("Missing admin request context"))?;
Ok(Self {
context: AdminRequestContext {
principal,
request_id: request_id.to_owned(),
request_id,
},
})
}
}
/// request-id 层按配置的 header 名注入,同时写入与名字无关的扩展;
/// 优先读扩展,使自定义 header 名不会让管理请求失去请求上下文。
/// header 回退覆盖未装配该层的调用方。
fn admin_request_id(parts: &Parts) -> Option<String> {
parts
.extensions
.get::<RequestId>()
.map(RequestId::header_value)
.or_else(|| parts.headers.get(REQUEST_ID_HEADER))
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
pub async fn require_admin_session<S>(state: &S, headers: &HeaderMap) -> Result<String, AdminError>
where
S: AdminSessionState + Send + Sync,
@@ -213,3 +213,26 @@ async fn settings_should_accept_admin_api_key_header() {
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test]
async fn admin_auth_should_accept_a_configured_request_id_header_name() {
use axum::http::HeaderName;
use tower_http::request_id::{MakeRequestUuid, SetRequestIdLayer};
let fixture = AdminTestFixture::new().await;
fixture.auth.insert_session("valid-session");
// 部署把 api.request_id_header 改名后,注入的 header 不再叫 x-request-id;
// 管理请求仍须拿到请求上下文,而不是退化为 500。
let custom = HeaderName::from_static("x-trace-id");
let app = app(fixture.state()).layer(SetRequestIdLayer::new(custom, MakeRequestUuid));
let unlabelled = Request::builder()
.method(Method::GET)
.uri("/api/admin/settings")
.header(header::COOKIE, "cpr_admin_session=valid-session")
.body(Body::empty())
.expect("build settings request");
let response = app.oneshot(unlabelled).await.expect("settings response");
assert_eq!(response.status(), StatusCode::OK);
}
+2 -2
View File
@@ -23,8 +23,8 @@ Client Key 固定绑定一个 Provider:`openai` 或 `xai`。同一次请求不
- `x-api-key: <admin-api-key>`。
请求无需自带 `x-request-id`;缺失时服务端自动生成 UUID 并在响应头回传同一 request ID。
`api.request_id_header` 只改变注入与回传的 header 名,而管理端鉴权固定读取 `x-request-id`,
因此启用管理端时该配置必须保持默认值。管理端响应统一带 `Cache-Control: no-store`。
`api.request_id_header` 可改变注入与回传的 header 名,管理端鉴权不依赖该名字。
管理端响应统一带 `Cache-Control: no-store`。
配置了 CORS 白名单 origin 时,跨域请求以凭据模式放行,仅允许 `GET`/`POST` 方法和
`authorization`、`content-type`、`x-api-key` 与 request ID 四个请求头,不使用通配符。