build: add api server response envelope helpers
This commit is contained in:
@@ -100,7 +100,8 @@
|
|||||||
交付物:[../server-rs/apps/api-server/src/request_context.rs](../server-rs/apps/api-server/src/request_context.rs)、[../server-rs/apps/api-server/src/app.rs](../server-rs/apps/api-server/src/app.rs)
|
交付物:[../server-rs/apps/api-server/src/request_context.rs](../server-rs/apps/api-server/src/request_context.rs)、[../server-rs/apps/api-server/src/app.rs](../server-rs/apps/api-server/src/app.rs)
|
||||||
- [x] 接入统一错误处理中间件
|
- [x] 接入统一错误处理中间件
|
||||||
交付物:[../server-rs/apps/api-server/src/http_error.rs](../server-rs/apps/api-server/src/http_error.rs)、[../server-rs/apps/api-server/src/error_middleware.rs](../server-rs/apps/api-server/src/error_middleware.rs)、[../server-rs/apps/api-server/src/app.rs](../server-rs/apps/api-server/src/app.rs)
|
交付物:[../server-rs/apps/api-server/src/http_error.rs](../server-rs/apps/api-server/src/http_error.rs)、[../server-rs/apps/api-server/src/error_middleware.rs](../server-rs/apps/api-server/src/error_middleware.rs)、[../server-rs/apps/api-server/src/app.rs](../server-rs/apps/api-server/src/app.rs)
|
||||||
- [ ] 接入当前项目兼容的 response envelope
|
- [x] 接入当前项目兼容的 response envelope
|
||||||
|
交付物:[../server-rs/apps/api-server/src/api_response.rs](../server-rs/apps/api-server/src/api_response.rs)、[../server-rs/apps/api-server/src/request_context.rs](../server-rs/apps/api-server/src/request_context.rs)、[../server-rs/apps/api-server/src/http_error.rs](../server-rs/apps/api-server/src/http_error.rs)
|
||||||
- [ ] 接入 `x-request-id`
|
- [ ] 接入 `x-request-id`
|
||||||
- [ ] 接入 `x-api-version`
|
- [ ] 接入 `x-api-version`
|
||||||
- [ ] 接入 `x-route-version`
|
- [ ] 接入 `x-route-version`
|
||||||
|
|||||||
53
server-rs/Cargo.lock
generated
53
server-rs/Cargo.lock
generated
@@ -24,6 +24,7 @@ dependencies = [
|
|||||||
"axum",
|
"axum",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -113,6 +114,15 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "deranged"
|
||||||
|
version = "0.5.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
|
||||||
|
dependencies = [
|
||||||
|
"powerfmt",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "equivalent"
|
name = "equivalent"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -386,6 +396,12 @@ dependencies = [
|
|||||||
"windows-sys",
|
"windows-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-conv"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "once_cell"
|
name = "once_cell"
|
||||||
version = "1.21.4"
|
version = "1.21.4"
|
||||||
@@ -404,6 +420,12 @@ version = "0.2.17"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "powerfmt"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prettyplease"
|
name = "prettyplease"
|
||||||
version = "0.2.37"
|
version = "0.2.37"
|
||||||
@@ -596,6 +618,37 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time"
|
||||||
|
version = "0.3.47"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
|
||||||
|
dependencies = [
|
||||||
|
"deranged",
|
||||||
|
"itoa",
|
||||||
|
"num-conv",
|
||||||
|
"powerfmt",
|
||||||
|
"serde_core",
|
||||||
|
"time-core",
|
||||||
|
"time-macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-core"
|
||||||
|
version = "0.1.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "time-macros"
|
||||||
|
version = "0.2.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
|
||||||
|
dependencies = [
|
||||||
|
"num-conv",
|
||||||
|
"time-core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.1"
|
version = "1.52.1"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ axum = "0.8"
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
|
tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] }
|
||||||
|
time = { version = "0.3", features = ["formatting"] }
|
||||||
tower-http = { version = "0.6", features = ["trace"] }
|
tower-http = { version = "0.6", features = ["trace"] }
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
1. [x] 接入统一日志与 tracing
|
1. [x] 接入统一日志与 tracing
|
||||||
2. [x] 接入 `request_id`
|
2. [x] 接入 `request_id`
|
||||||
3. [x] 接入统一错误处理中间件
|
3. [x] 接入统一错误处理中间件
|
||||||
4. [ ] 接入 response envelope
|
4. [x] 接入 response envelope
|
||||||
5. [ ] 接入 `/healthz`
|
5. [ ] 接入 `/healthz`
|
||||||
|
|
||||||
当前 tracing 约定:
|
当前 tracing 约定:
|
||||||
@@ -51,6 +51,12 @@
|
|||||||
2. 已经带 `content-type` 的业务错误响应不会被覆盖,避免抢走后续 response envelope 的职责。
|
2. 已经带 `content-type` 的业务错误响应不会被覆盖,避免抢走后续 response envelope 的职责。
|
||||||
3. 统一错误日志会复用当前请求的 `request_id`,便于后续和响应头、envelope 元信息串联。
|
3. 统一错误日志会复用当前请求的 `request_id`,便于后续和响应头、envelope 元信息串联。
|
||||||
|
|
||||||
|
当前 response envelope 约定:
|
||||||
|
|
||||||
|
1. `RequestContext` 已记录 `request_id`、请求开始时间、默认 `operation` 与 envelope 协商结果。
|
||||||
|
2. `json_success_body(...)` / `json_error_body(...)` 会根据 `x-genarrative-response-envelope` 自动在“裸数据 / 标准 envelope / legacy error + meta”之间切换。
|
||||||
|
3. `meta.apiVersion`、`meta.requestId`、`meta.routeVersion`、`meta.operation`、`meta.latencyMs`、`meta.timestamp` 已按当前前端契约生成,响应头回写仍留给后续独立任务。
|
||||||
|
|
||||||
## 3. 边界约束
|
## 3. 边界约束
|
||||||
|
|
||||||
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
1. `api-server` 负责 HTTP、SSE、Cookie、Header、路由与协议装配。
|
||||||
|
|||||||
129
server-rs/apps/api-server/src/api_response.rs
Normal file
129
server-rs/apps/api-server/src/api_response.rs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
use axum::Json;
|
||||||
|
use serde::Serialize;
|
||||||
|
use serde_json::{json, Value};
|
||||||
|
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
|
||||||
|
|
||||||
|
use crate::{http_error::ApiErrorPayload, request_context::RequestContext};
|
||||||
|
|
||||||
|
pub const API_VERSION: &str = "2026-04-08";
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
struct ApiResponseMeta {
|
||||||
|
#[serde(rename = "apiVersion")]
|
||||||
|
api_version: &'static str,
|
||||||
|
#[serde(rename = "requestId", skip_serializing_if = "Option::is_none")]
|
||||||
|
request_id: Option<String>,
|
||||||
|
#[serde(rename = "routeVersion")]
|
||||||
|
route_version: &'static str,
|
||||||
|
operation: Option<String>,
|
||||||
|
#[serde(rename = "latencyMs")]
|
||||||
|
latency_ms: u64,
|
||||||
|
timestamp: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 当前阶段先把成功响应 envelope helper 准备好,后续 `/healthz` 与业务 handler 会直接复用这里的输出逻辑。
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn json_success_body<T>(request_context: Option<&RequestContext>, data: T) -> Json<Value>
|
||||||
|
where
|
||||||
|
T: Serialize,
|
||||||
|
{
|
||||||
|
if let Some(context) = request_context {
|
||||||
|
if context.wants_envelope() {
|
||||||
|
return Json(json!({
|
||||||
|
"ok": true,
|
||||||
|
"data": data,
|
||||||
|
"error": null,
|
||||||
|
"meta": build_api_response_meta(Some(context)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(serde_json::to_value(data).unwrap_or(Value::Null))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn json_error_body(
|
||||||
|
request_context: Option<&RequestContext>,
|
||||||
|
error: &ApiErrorPayload,
|
||||||
|
) -> Json<Value> {
|
||||||
|
let meta = build_api_response_meta(request_context);
|
||||||
|
|
||||||
|
if request_context.is_some_and(RequestContext::wants_envelope) {
|
||||||
|
return Json(json!({
|
||||||
|
"ok": false,
|
||||||
|
"data": null,
|
||||||
|
"error": error,
|
||||||
|
"meta": meta,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
Json(json!({
|
||||||
|
"error": error,
|
||||||
|
"meta": meta,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_api_response_meta(request_context: Option<&RequestContext>) -> ApiResponseMeta {
|
||||||
|
ApiResponseMeta {
|
||||||
|
api_version: API_VERSION,
|
||||||
|
request_id: request_context.map(|context| context.request_id().to_string()),
|
||||||
|
route_version: API_VERSION,
|
||||||
|
operation: request_context.map(|context| context.operation().to_string()),
|
||||||
|
latency_ms: request_context
|
||||||
|
.map(RequestContext::elapsed)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
timestamp: OffsetDateTime::now_utc()
|
||||||
|
.format(&Rfc3339)
|
||||||
|
.unwrap_or_else(|_| "1970-01-01T00:00:00Z".to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::request_context::RequestContext;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
fn build_request_context(wants_envelope: bool) -> RequestContext {
|
||||||
|
RequestContext::new(
|
||||||
|
"req-test".to_string(),
|
||||||
|
"GET /test".to_string(),
|
||||||
|
Duration::from_millis(12),
|
||||||
|
wants_envelope,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn success_body_returns_standard_envelope_when_requested() {
|
||||||
|
let request_context = build_request_context(true);
|
||||||
|
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||||
|
|
||||||
|
assert_eq!(body["ok"], Value::Bool(true));
|
||||||
|
assert_eq!(body["data"]["ok"], Value::String("value".to_string()));
|
||||||
|
assert_eq!(body["meta"]["requestId"], Value::String("req-test".to_string()));
|
||||||
|
assert_eq!(body["meta"]["routeVersion"], Value::String(API_VERSION.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn success_body_returns_raw_payload_when_envelope_not_requested() {
|
||||||
|
let request_context = build_request_context(false);
|
||||||
|
let body = json_success_body(Some(&request_context), json!({ "ok": "value" })).0;
|
||||||
|
|
||||||
|
assert_eq!(body["ok"], Value::String("value".to_string()));
|
||||||
|
assert!(body.get("meta").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn error_body_returns_legacy_shape_without_envelope_header() {
|
||||||
|
let request_context = build_request_context(false);
|
||||||
|
let error = ApiErrorPayload {
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "资源不存在",
|
||||||
|
details: None,
|
||||||
|
};
|
||||||
|
let body = json_error_body(Some(&request_context), &error).0;
|
||||||
|
|
||||||
|
assert_eq!(body["error"]["code"], Value::String("NOT_FOUND".to_string()));
|
||||||
|
assert_eq!(body["meta"]["requestId"], Value::String("req-test".to_string()));
|
||||||
|
assert!(body.get("ok").is_none());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,18 @@ use axum::{
|
|||||||
extract::Request,
|
extract::Request,
|
||||||
http::header::CONTENT_TYPE,
|
http::header::CONTENT_TYPE,
|
||||||
middleware::Next,
|
middleware::Next,
|
||||||
response::{IntoResponse, Response},
|
response::Response,
|
||||||
};
|
};
|
||||||
use tracing::{error, warn};
|
use tracing::{error, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
http_error::AppError,
|
http_error::AppError,
|
||||||
request_context::resolve_request_id,
|
request_context::{resolve_request_id, RequestContext},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub async fn normalize_error_response(request: Request, next: Next) -> Response {
|
pub async fn normalize_error_response(request: Request, next: Next) -> Response {
|
||||||
let request_id = resolve_request_id(&request);
|
let request_id = resolve_request_id(&request);
|
||||||
|
let request_context = request.extensions().get::<RequestContext>().cloned();
|
||||||
let response = next.run(request).await;
|
let response = next.run(request).await;
|
||||||
|
|
||||||
if !should_normalize_error_response(&response) {
|
if !should_normalize_error_response(&response) {
|
||||||
@@ -39,7 +40,7 @@ pub async fn normalize_error_response(request: Request, next: Next) -> Response
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
app_error.into_response()
|
app_error.into_response_with_context(request_context.as_ref())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_normalize_error_response(response: &Response) -> bool {
|
fn should_normalize_error_response(response: &Response) -> bool {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
Json,
|
|
||||||
};
|
};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
|
use crate::{api_response::json_error_body, request_context::RequestContext};
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppError {
|
pub struct AppError {
|
||||||
status_code: StatusCode,
|
status_code: StatusCode,
|
||||||
@@ -14,17 +15,12 @@ pub struct AppError {
|
|||||||
details: Option<Value>,
|
details: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Clone, Debug, Serialize)]
|
||||||
struct LegacyApiErrorBody {
|
pub struct ApiErrorPayload {
|
||||||
error: LegacyApiErrorPayload,
|
pub code: &'static str,
|
||||||
}
|
pub message: &'static str,
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
|
||||||
struct LegacyApiErrorPayload {
|
|
||||||
code: &'static str,
|
|
||||||
message: &'static str,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
details: Option<Value>,
|
pub details: Option<Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AppError {
|
impl AppError {
|
||||||
@@ -42,19 +38,26 @@ impl AppError {
|
|||||||
pub fn code(&self) -> &'static str {
|
pub fn code(&self) -> &'static str {
|
||||||
self.code
|
self.code
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn into_response_with_context(self, request_context: Option<&RequestContext>) -> Response {
|
||||||
|
let status_code = self.status_code;
|
||||||
|
let payload = self.to_payload();
|
||||||
|
|
||||||
|
(status_code, json_error_body(request_context, &payload)).into_response()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_payload(&self) -> ApiErrorPayload {
|
||||||
|
ApiErrorPayload {
|
||||||
|
code: self.code,
|
||||||
|
message: self.message,
|
||||||
|
details: self.details.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoResponse for AppError {
|
impl IntoResponse for AppError {
|
||||||
fn into_response(self) -> Response {
|
fn into_response(self) -> Response {
|
||||||
let body = LegacyApiErrorBody {
|
self.into_response_with_context(None)
|
||||||
error: LegacyApiErrorPayload {
|
|
||||||
code: self.code,
|
|
||||||
message: self.message,
|
|
||||||
details: self.details,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
(self.status_code, Json(body)).into_response()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
mod app;
|
mod app;
|
||||||
|
mod api_response;
|
||||||
mod config;
|
mod config;
|
||||||
mod error_middleware;
|
mod error_middleware;
|
||||||
mod http_error;
|
mod http_error;
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::Request,
|
extract::Request,
|
||||||
http::{header::HeaderName, HeaderValue, Request as HttpRequest},
|
http::{header::HeaderName, HeaderValue, Request as HttpRequest},
|
||||||
@@ -6,25 +8,57 @@ use axum::{
|
|||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
pub const API_RESPONSE_ENVELOPE_HEADER: &str = "x-genarrative-response-envelope";
|
||||||
pub const X_REQUEST_ID_HEADER: &str = "x-request-id";
|
pub const X_REQUEST_ID_HEADER: &str = "x-request-id";
|
||||||
|
|
||||||
// 当前阶段先把请求级元信息统一挂到 extensions,后续响应头、envelope 与错误处理中间件继续复用。
|
// 当前阶段先把请求级元信息统一挂到 extensions,后续响应头、envelope 与错误处理中间件继续复用。
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RequestContext {
|
pub struct RequestContext {
|
||||||
request_id: String,
|
request_id: String,
|
||||||
|
operation: String,
|
||||||
|
request_started_at: Instant,
|
||||||
|
wants_envelope: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RequestContext {
|
impl RequestContext {
|
||||||
pub fn new(request_id: String) -> Self {
|
pub fn new(
|
||||||
Self { request_id }
|
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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn request_id(&self) -> &str {
|
pub fn request_id(&self) -> &str {
|
||||||
&self.request_id
|
&self.request_id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn operation(&self) -> &str {
|
||||||
|
&self.operation
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wants_envelope(&self) -> bool {
|
||||||
|
self.wants_envelope
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
pub async fn attach_request_context(mut request: Request, next: Next) -> Response {
|
||||||
|
let wants_envelope = wants_api_envelope(&request);
|
||||||
let request_id = request
|
let request_id = request
|
||||||
.headers()
|
.headers()
|
||||||
.get(X_REQUEST_ID_HEADER)
|
.get(X_REQUEST_ID_HEADER)
|
||||||
@@ -33,10 +67,14 @@ pub async fn attach_request_context(mut request: Request, next: Next) -> Respons
|
|||||||
.filter(|value| !value.is_empty())
|
.filter(|value| !value.is_empty())
|
||||||
.map(ToOwned::to_owned)
|
.map(ToOwned::to_owned)
|
||||||
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
.unwrap_or_else(|| Uuid::new_v4().to_string());
|
||||||
|
let operation = format!("{} {}", request.method(), request.uri());
|
||||||
|
|
||||||
request
|
request.extensions_mut().insert(RequestContext::new(
|
||||||
.extensions_mut()
|
request_id.clone(),
|
||||||
.insert(RequestContext::new(request_id.clone()));
|
operation,
|
||||||
|
Duration::ZERO,
|
||||||
|
wants_envelope,
|
||||||
|
));
|
||||||
|
|
||||||
// 统一把 request_id 写回请求头,方便后续 tracing、响应头与 envelope 层读取同一来源。
|
// 统一把 request_id 写回请求头,方便后续 tracing、响应头与 envelope 层读取同一来源。
|
||||||
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
|
if let Ok(header_value) = HeaderValue::from_str(&request_id) {
|
||||||
@@ -63,3 +101,13 @@ pub fn resolve_request_id<B>(request: &HttpRequest<B>) -> Option<String> {
|
|||||||
.map(ToOwned::to_owned)
|
.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"))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user