Rust in Everything
基于 tower-http 0.7.1

搬运请求标识与头

主线位置:作用点主线的前两个时刻。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:有就信,没有才造

SetRequestId::call:

    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 让两侧共用一份列表。

📝NOTE
没有蠢问题

问: 客户端用 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(一个类型别名)做不到这一点。

💬 关联讨论

发起讨论
加载中...