搬运请求标识与头
主线位置:作用点主线的前两个时刻。
SetRequestId、AddExtension、SetSensitiveRequestHeaders只在call时改请求;PropagateRequestId、PropagateHeader在call时从请求里读一个值存进 future,响应 future 完成时写进响应。这一页的几个层都很小,要讲清楚的是它们之间的先后。
flowchart TB
in["请求进来"] --> set{"SetRequestId:请求头里有 id 吗"}
set -->|有| ext1["只补 RequestId 扩展"]
set -->|没有| gen["MakeRequestId 造一个,写进请求头和扩展"]
ext1 --> prop["PropagateRequestId:call 时读请求头,存进 future"]
gen --> prop
prop --> h[handler]
h --> back{"响应里已经有这个头吗"}
back -->|有| keep["保留响应的,只补扩展"]
back -->|没有| copy["把存下的 id 写进响应头和扩展"]
SetRequestId:有就信,没有才造
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
if let Some(request_id) = req.headers().get(&self.header_name) {
if req.extensions().get::<RequestId>().is_none() {
let request_id = request_id.clone();
req.extensions_mut().insert(RequestId::new(request_id));
}
} else if let Some(request_id) = self.make_request_id.make_request_id(&req) {
req.extensions_mut().insert(request_id.clone());
req.headers_mut()
.insert(self.header_name.clone(), request_id.0);
}
self.inner.call(req)
}
- 请求头里已经有
x-request-id(或者你配的别的名字),就原样采用,不校验格式、不加前缀。这适合放在网关后面、由网关统一生成 id 的部署;直接面对公网时,任何客户端都能把自己的字符串塞进你的日志 id,需要的话在它外面先用SetRequestHeaderLayer::overriding覆盖掉,或者自己实现一个会检查的MakeRequestId。 - 同一个 id 放两处:请求头(给下游服务、给
PropagateRequestId)和扩展RequestId(给 handler,用req.extensions().get::<RequestId>()取)。 - 扩展已经有、请求头也有时,扩展不被覆盖。两者可能不一致:外层另一个中间件改了头却没改扩展,handler 读到的是旧扩展。
MakeRequestId 和上一页的 MakeHeaderValue 一样拿 &mut self、可以返回 None(返回 None 时这个请求就没有 id)。现成的只有 MakeRequestUuid,一个零大小类型,每次 Uuid::new_v4(),这也是 request-id feature 会拉进 uuid 的原因。RequestId 本身只是 HeaderValue 的新类型;0.7.1 起它和各个 layer、service 的构造函数、访问器都是 const fn(#716),可以写成 const LAYER: PropagateRequestIdLayer = PropagateRequestIdLayer::x_request_id();。
PropagateRequestId:call 时就读,所以要在里面
PropagateRequestId::call 在把请求交给内层之前,把请求头里的 id 克隆一份存进 future;poll 在响应回来时决定写不写:
if let Some(current_id) = response.headers().get(&*this.header_name) {
if response.extensions().get::<RequestId>().is_none() {
let current_id = current_id.clone();
response.extensions_mut().insert(RequestId::new(current_id));
}
} else if let Some(request_id) = this.request_id.take() {
response
.headers_mut()
.insert(this.header_name.clone(), request_id.0.clone());
response.extensions_mut().insert(request_id);
}
两条规则:它读的是请求头,不是 RequestId 扩展;响应里已经有同名头时,响应的赢。第一条决定了层的顺序:SetRequestId 生成的 id 是在它自己的 call 里写进请求头的,PropagateRequestId 要在那之后才 call,也就是在 SetRequestId 里面。ServiceBuilderExt 的 set_x_request_id().propagate_x_request_id() 正是这个顺序(先写的在外层)。反过来会怎样:
tower-http = { version = "=0.7.1", features = ["request-id", "propagate-header", "add-extension", "sensitive-headers", "trace", "util"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tracing = "0.1"
tracing-subscriber = "0.3"
use bytes::Bytes;
use http::{header, HeaderName, HeaderValue, Request, Response};
use http_body_util::Full;
use std::convert::Infallible;
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc,
};
use tower::{ServiceBuilder, ServiceExt};
use tower_http::{
request_id::{MakeRequestId, MakeRequestUuid, PropagateRequestIdLayer, RequestId, SetRequestIdLayer},
sensitive_headers::SetSensitiveRequestHeadersLayer,
trace::{DefaultMakeSpan, TraceLayer},
};
// 可预测的 id:req-1、req-2……
#[derive(Clone, Default)]
struct Seq(Arc<AtomicUsize>);
impl MakeRequestId for Seq {
fn make_request_id<B>(&mut self, _: &Request<B>) -> Option<RequestId> {
let n = self.0.fetch_add(1, Ordering::SeqCst) + 1;
Some(RequestId::new(HeaderValue::from_str(&format!("req-{n}")).unwrap()))
}
}
async fn handler(req: Request<Full<Bytes>>) -> Result<Response<Full<Bytes>>, Infallible> {
let ext = req.extensions().get::<RequestId>().map(|id| id.header_value().clone());
println!(" handler sees header={:?} extension={:?}", req.headers().get("x-request-id"), ext);
let mut res = Response::new(Full::default());
if req.uri().path() == "/own" {
res.headers_mut().insert("x-request-id", HeaderValue::from_static("handler-made"));
}
Ok(res)
}
fn req(path: &str, id: Option<&str>) -> Request<Full<Bytes>> {
let mut b = Request::get(path);
if let Some(id) = id {
b = b.header("x-request-id", id);
}
b.body(Full::default()).unwrap()
}
#[tokio::main(flavor = "current_thread")]
async fn main() {
let x = HeaderName::from_static("x-request-id");
let right = ServiceBuilder::new()
.layer(SetRequestIdLayer::new(x.clone(), Seq::default()))
.layer(PropagateRequestIdLayer::new(x.clone()))
.service_fn(handler);
let wrong = ServiceBuilder::new()
.layer(PropagateRequestIdLayer::new(x.clone()))
.layer(SetRequestIdLayer::new(x.clone(), Seq::default()))
.service_fn(handler);
println!("Set outside Propagate");
for (path, id) in [("/", None), ("/", Some("from-client")), ("/own", None)] {
let res = right.clone().oneshot(req(path, id)).await.unwrap();
println!(" {path:<4} client sent {id:?} -> response {:?}", res.headers().get("x-request-id"));
}
println!("Propagate outside Set");
for (path, id) in [("/", None), ("/", Some("from-client")), ("/own", None)] {
let res = wrong.clone().oneshot(req(path, id)).await.unwrap();
println!(" {path:<4} client sent {id:?} -> response {:?}", res.headers().get("x-request-id"));
}
let mut uuid = MakeRequestUuid;
let id = uuid.make_request_id(&req("/", None)).unwrap();
println!("MakeRequestUuid: {} bytes, version char {:?}", id.header_value().len(), &id.header_value().to_str().unwrap()[14..15]);
// 敏感头:同一个 Trace(打印请求头),标记层放在里面和外面各一次
tracing_subscriber::fmt().without_time().with_target(false).with_ansi(false).with_max_level(tracing::Level::DEBUG).init();
for outside in [false, true] {
println!("SetSensitiveRequestHeaders {} Trace", if outside { "outside" } else { "inside" });
let trace = TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::new().include_headers(true));
let auth = header::AUTHORIZATION;
let r = Request::get("/").header(&auth, "Bearer s3cr3t").body(Full::<Bytes>::default()).unwrap();
if outside {
let svc = ServiceBuilder::new().layer(SetSensitiveRequestHeadersLayer::new([auth])).layer(trace).service_fn(|_: Request<Full<Bytes>>| async { Ok::<_, Infallible>(Response::new(Full::<Bytes>::default())) });
svc.oneshot(r).await.unwrap();
} else {
let svc = ServiceBuilder::new().layer(trace).layer(SetSensitiveRequestHeadersLayer::new([auth])).service_fn(|_: Request<Full<Bytes>>| async { Ok::<_, Infallible>(Response::new(Full::<Bytes>::default())) });
svc.oneshot(r).await.unwrap();
}
}
let mut v = HeaderValue::from_static("Bearer s3cr3t");
v.set_sensitive(true);
println!("Debug of a sensitive HeaderValue: {:?}; as_bytes still {:?}", v, v.as_bytes());
}
输出(M2 Max、rustc 1.99.0,跑 3 次相同):
Set outside Propagate
handler sees header=Some("req-1") extension=Some("req-1")
/ client sent None -> response Some("req-1")
handler sees header=Some("from-client") extension=Some("from-client")
/ client sent Some("from-client") -> response Some("from-client")
handler sees header=Some("req-2") extension=Some("req-2")
/own client sent None -> response Some("handler-made")
Propagate outside Set
handler sees header=Some("req-1") extension=Some("req-1")
/ client sent None -> response None
handler sees header=Some("from-client") extension=Some("from-client")
/ client sent Some("from-client") -> response Some("from-client")
handler sees header=Some("req-2") extension=Some("req-2")
/own client sent None -> response Some("handler-made")
MakeRequestUuid: 36 bytes, version char "4"
SetSensitiveRequestHeaders inside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: started processing request
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": "Bearer s3cr3t"}}: finished processing request latency=0 ms status=200
SetSensitiveRequestHeaders outside Trace
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: started processing request
DEBUG request{method=GET uri=/ version=HTTP/1.1 headers={"authorization": Sensitive}}: finished processing request latency=0 ms status=200
(最后一行 Debug of a sensitive HeaderValue 放到下一节说。)
- 顺序对时:生成的
req-1回到响应头;客户端带来的from-client原样采用;handler 自己写了x-request-id,响应的赢,handler-made留下。 - 顺序反了:handler 照样看到
req-1(SetRequestId还在起作用),但响应里没有 id。PropagateRequestId::call跑在前面,那时请求头里什么都没有,future 里存的是None。客户端自带 id 的请求不受影响,所以只靠「带着 id 发请求」来测试,发现不了这个问题。 MakeRequestUuid生成 36 字节的带连字符的 v4 UUID。
PropagateHeader 与 AddExtension
PropagateHeader 是 PropagateRequestId 去掉扩展的通用版:call 时克隆请求里那个头的第一个值,future 完成时 insert 进响应。区别在于它无条件覆盖:响应里已经有同名头,也会被请求里的值替换掉;请求里没有这个头,响应就原样不动。
AddExtension 只有一行有效代码:req.extensions_mut().insert(self.value.clone())。每个请求克隆一次值,所以放进去的一般是 Arc<State>。axum 有自己的 State 提取器,AddExtension 更多用在纯 hyper / tower 栈上,或者给 axum 的 Extension 提取器供值。同类型的扩展只能有一个,内层再 insert 同类型的值会覆盖外层放的。
SetSensitiveHeaders:只是打个标记
SetSensitiveRequestHeaders::call 对配置里的每个头,把它的每个值调一次 HeaderValue::set_sensitive(true),然后照常交给内层。头还在、值没变,handler 和下游拿到的字节一模一样。标记的效果全在 http 和用它的库那里:
HeaderValue的Debug输出变成Sensitive,上面实验的最后一行:Debug of a sensitive HeaderValue: Sensitive; as_bytes still [66, 101, 97, ...]。TraceLayer的DefaultMakeSpan::include_headers(true)用?request.headers()打印,于是日志里看不到原值。- h2 的 HPACK 编码器对敏感值不进动态表,按「never indexed」字面量发出(h2 0.4.20
hpack/table.rs里if header.is_sensitive()的两处提前返回),中间的代理也被要求不要索引它。
所以它和 TraceLayer 的相对位置决定日志里藏不藏得住:Trace 在 call 时就创建 span、打印请求头,标记必须在那之前打上,即请求侧标记层放在 Trace 外面;响应头是在响应 future 完成时由 Trace 打印(DefaultOnResponse::include_headers(true)),标记要在那之前完成,即响应侧标记层放在 Trace 里面。上面实验里标记层在里面时 Bearer s3cr3t 出现在 DEBUG 日志中,放到外面就变成了 Sensitive。这条规则在 Trace 的内部一页也提过,模块文档给的示例正是「请求侧在外、TraceLayer、响应侧在里」三层。
于是那个「两侧一起标」的 SetSensitiveHeadersLayer 有点尴尬。它生成的 SetSensitiveHeaders<S> 甚至不是结构体,只是一个类型别名:
pub type SetSensitiveHeaders<S> = SetSensitiveRequestHeaders<SetSensitiveResponseHeaders<S>>;
两层贴在一起,无论放在 Trace 的哪一边,总有一侧的标记来不及。和 Trace 一起用时,要拆成两个 layer 分别放。三个 layer 都用 Arc<[HeaderName]> 存头名,from_shared 让两侧共用一份列表。
问: 客户端用 AddAuthorizationLayer 加的 Authorization,要不要再套一层 SetSensitiveRequestHeadersLayer?
答: 不用套层,AddAuthorizationLayer::as_sensitive(true) 直接让它插入的那个值带着标记,见校验与鉴权。要注意的是默认值是不标记。反过来,如果 Authorization 是调用方自己写的,AddAuthorization 又不在栈里,就只能靠 SetSensitiveRequestHeadersLayer,并且同样要放在客户端栈的 Trace 外面。
小结
SetRequestId:请求头里有 id 就原样采用(只补RequestId扩展),没有才用MakeRequestId造,同时写进请求头和扩展;MakeRequestUuid是唯一现成实现,拉进uuid。0.7.1 起构造函数是const fn。PropagateRequestId在call时读请求头,所以必须放在SetRequestId里面;顺序反了,生成的 id 不会回到响应里,客户端自带 id 的请求却一切正常。响应里已有同名头时响应的赢。PropagateHeader无条件覆盖响应里的同名头;AddExtension每个请求克隆一次值。SetSensitiveHeaders只调set_sensitive(true),影响Debug与 HPACK,不改字节;请求侧标记层放在Trace外面、响应侧放在里面才藏得住,两侧合一的SetSensitiveHeadersLayer(一个类型别名)做不到这一点。