Rust in Everything
基于 tower-http 0.7.1

if and else have incompatible types:expected UnsyncBoxBody, found ResponseBody<UnsyncBoxBody>

主线位置:不在主线上,在编译期。每一层都可能换掉响应 body 的类型,所以层的组合不同,整个服务的响应类型就不同。body 模块 用「有没有这一层」演示过同一个错误;这一页是另一种常见来源:层都一样,只是顺序不同。

flowchart TB
  subgraph a["限额在内"]
    a1["RequestDecompression:响应 body 装箱成 UnsyncBoxBody"] --> a2["RequestBodyLimit:limit::ResponseBody&lt;Full&gt;"]
  end
  subgraph b["限额在外"]
    b1["RequestBodyLimit:limit::ResponseBody&lt;UnsyncBoxBody&gt;"] --> b2["RequestDecompression:UnsyncBoxBody"]
  end
  a --> ta["Response&lt;UnsyncBoxBody&gt;"]
  b --> tb["Response&lt;ResponseBody&lt;UnsyncBoxBody&gt;&gt;"]

复现

限了 1 MiB,handler 还是读到了 10 MiB 讲了这两层谁在外面结果完全不同。假设想做成一个开关,在两种顺序之间切换做对比:

tower-http = { version = "=0.7.1", features = ["limit", "decompression-gzip", "map-response-body"] }
tower = { version = "=0.5.3", features = ["util"] }
tokio = { version = "=1.54.1", features = ["macros", "rt"] }
http = "1"
http-body = "1"
http-body-util = "0.1"
bytes = "1"
use bytes::Bytes;
use http::{Request, Response};
use http_body_util::{BodyExt, Full};
use std::convert::Infallible;
use tower::{BoxError, ServiceBuilder, ServiceExt};
use tower_http::{decompression::RequestDecompressionLayer, limit::RequestBodyLimitLayer};

async fn handler<B>(req: Request<B>) -> Result<Response<Full<Bytes>>, Infallible>
where
  B: http_body::Body,
  B::Error: Into<BoxError>,
{
  let n = req.into_body().collect().await.map(|c| c.to_bytes().len()).unwrap_or(0);
  Ok(Response::new(Full::from(format!("read {n} bytes"))))
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
  let limit_inside = std::env::args().count() > 1;
  let svc = if limit_inside {
    ServiceBuilder::new()
      .layer(RequestDecompressionLayer::new())
      .layer(RequestBodyLimitLayer::new(1 << 20))
      .service_fn(handler)
      .boxed_clone()
  } else {
    ServiceBuilder::new()
      .layer(RequestBodyLimitLayer::new(1 << 20))
      .layer(RequestDecompressionLayer::new())
      .service_fn(handler)
      .boxed_clone()
  };
  let res = svc.oneshot(Request::new(Full::new(Bytes::from_static(b"hello")))).await.unwrap();
  println!("{}", String::from_utf8_lossy(&res.into_body().collect().await.unwrap().to_bytes()));
}

rustc 1.99.0:

error[E0308]: `if` and `else` have incompatible types
  --> src/main.rs:27:5
   |
20 |       let svc = if limit_inside {
   |  _______________-
21 | | /     ServiceBuilder::new()
22 | | |       .layer(RequestDecompressionLayer::new())
23 | | |       .layer(RequestBodyLimitLayer::new(1 << 20))
24 | | |       .service_fn(handler)
25 | | |       .boxed_clone()
   | | |____________________- expected because of this
26 | |     } else {
27 | | /     ServiceBuilder::new()
28 | | |       .layer(RequestBodyLimitLayer::new(1 << 20))
29 | | |       .layer(RequestDecompressionLayer::new())
30 | | |       .service_fn(handler)
31 | | |       .boxed_clone()
   | | |____________________^ expected `UnsyncBoxBody<Bytes, Box<_>>`, found `ResponseBody<UnsyncBoxBody<Bytes, _>>`
32 | |     };
   | |_____- `if` and `else` have incompatible types
   |
   = note: expected struct `BoxCloneService<Request<_>, Response<UnsyncBoxBody<_, _>>, _>`
              found struct `BoxCloneService<Request<_>, Response<ResponseBody<UnsyncBoxBody<_, _>>>, _>`

为什么

两个分支都 .boxed_clone() 了,服务类型已经抹掉,剩下的差别在 BoxCloneService 的第二个类型参数,也就是响应类型。看谁在最外层:

  • 解压在外:RequestDecompression 把内层的响应(不管是什么)统一装箱成 UnsyncBoxBody<Bytes, BoxError>,因为它短路回 415 时要和正常响应用同一个类型。所以整个服务的响应 body 是 UnsyncBoxBody。
  • 限额在外:RequestBodyLimit 把内层的 body 包进 limit::ResponseBody<B>,一个「内层 body 或者 413 正文」的两变体枚举。内层是解压,所以是 ResponseBody<UnsyncBoxBody<..>>。

最外层那一层的 body 包装决定了服务的响应类型,顺序一换,外层换了,类型就换了。tower 的 option_layer / Either 帮不上忙,Either 要求两边 Response 相同。

修法

两个分支在最外面各加一层 MapResponseBodyLayer,把 body 擦成同一个类型,再装箱:

use tower_http::{
  body::UnsyncBoxBody, decompression::RequestDecompressionLayer, limit::RequestBodyLimitLayer,
  map_response_body::MapResponseBodyLayer,
};

  let svc = if limit_inside {
    ServiceBuilder::new()
      .layer(MapResponseBodyLayer::new(UnsyncBoxBody::new))
      .layer(RequestDecompressionLayer::new())
      .layer(RequestBodyLimitLayer::new(1 << 20))
      .service_fn(handler)
      .boxed_clone()
  } else {
    ServiceBuilder::new()
      .layer(MapResponseBodyLayer::new(UnsyncBoxBody::new))
      .layer(RequestBodyLimitLayer::new(1 << 20))
      .layer(RequestDecompressionLayer::new())
      .service_fn(handler)
      .boxed_clone()
  };

两种参数各跑一次,都输出 read 5 bytes。tower_http::body::UnsyncBoxBody::new 是 0.7.0 才公开的(body.rs 第 124 行),0.6.x 里用 http_body_util::combinators::UnsyncBoxBody 和 BodyExt::boxed_unsync 做同样的事。解压在外那个分支会多装一次箱,这是统一类型的代价。

在 axum 里不会遇到:Router::layer 替你把每一层的响应 body 转回 axum::body::Body(见 tower-http 解决什么问题)。纯 tower、hyper 服务和自己拼的客户端才会撞上。

还有一种相关的编译错误不是 E0308:有些层要求内层 body 实现 Default,层的顺序会让它满足或不满足,见 四件套 的「Default 让层的顺序影响编译」。

小结

  • 装箱抹掉服务类型,抹不掉响应类型;最外层的 body 包装决定响应类型,换顺序就换类型。
  • RequestDecompression 在外是 UnsyncBoxBody,RequestBodyLimit 在外是 limit::ResponseBody<..>。
  • 每个分支最外面加 MapResponseBodyLayer::new(UnsyncBoxBody::new) 再装箱。

💬 关联讨论

发起讨论
加载中...