307 Temporary Redirect 原样回到调用方
主线位置:作用点主线的第二个时刻。
FollowRedirect在响应 future 完成时决定再发一次还是把响应交出去,「不跟」和「跟丢了」都是正常返回一个 3xx,不是错误。Policy的三个方法和组合子见 FollowRedirect 的策略,那一页用service_fn模拟过 307;这一页对着一个真实的 axum 服务,把两种「拿到 3xx」的原因放在一起。
flowchart TB
res["内层回了 3xx"] --> code{"状态码"}
code -->|"301/302 的 POST、303"| empty["body 换成空的,不用复制"]
code -->|"307/308"| take{"BodyRepr::take"}
take -->|"留了一份,或确定为空"| pol{"policy.redirect"}
take -->|"None:没留"| out1["把 307 交给调用方"]
empty --> pol
pol -->|Follow| again["再发一次"]
pol -->|"Stop:Limited 用完"| out2["把 3xx 交给调用方"]
复现
axum 服务三个路由:POST /form 回 Redirect::temporary("/done")(307),/done 报告收到多少字节,GET /loop 回 Redirect::to("/loop")(axum 的 to 是 303)跳回自己并计数。客户端是 hyper-util 的 legacy Client 套 FollowRedirectLayer,请求 body 用 Full<Bytes>。
tower-http = { version = "=0.7.1", features = ["follow-redirect"] }
axum = { version = "=0.8.9", default-features = false, features = ["tokio", "http1"] }
tower = { version = "=0.5.3", features = ["util"] }
hyper = "1"
hyper-util = { version = "=0.1.21", features = ["client-legacy", "http1", "tokio"] }
http = "1"
http-body-util = "0.1"
bytes = "1"
tokio = { version = "=1.54.1", features = ["macros", "rt", "net"] }
use axum::response::Redirect;
use axum::routing::{any, get, post};
use axum::Router;
use bytes::Bytes;
use http::Request;
use http_body_util::{BodyExt, Full};
use hyper_util::{client::legacy::Client, rt::TokioExecutor};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use tower::{ServiceBuilder, ServiceExt};
use tower_http::follow_redirect::{policy, policy::PolicyExt, FollowRedirectLayer, RequestUri};
#[tokio::main(flavor = "current_thread")]
async fn main() {
let hits = Arc::new(AtomicUsize::new(0));
let h = hits.clone();
let app = Router::new()
.route("/form", post(|| async { Redirect::temporary("/done") }))
.route("/done", any(|body: Bytes| async move { format!("done, got {} bytes", body.len()) }))
.route("/loop", get(move || {
h.fetch_add(1, Ordering::SeqCst);
async { Redirect::to("/loop") }
}));
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
tokio::spawn(async move { axum::serve(listener, app).await.unwrap() });
let client = Client::builder(TokioExecutor::new()).build_http::<Full<Bytes>>();
// 打印最终状态码、最后一跳的地址(RequestUri 扩展)、Location 和 body
let send = |name: &'static str, svc, req: Request<Full<Bytes>>| async move {
let svc: tower::util::BoxCloneService<_, http::Response<hyper::body::Incoming>, _> = svc;
let res = svc.oneshot(req).await.unwrap();
let uri = res.extensions().get::<RequestUri>().unwrap().0.path().to_owned();
let loc = res.headers().get("location").map(|v| v.to_str().unwrap().to_owned());
let status = res.status();
let body = res.into_body().collect().await.unwrap().to_bytes();
println!("{name:<34} {status} last={uri} location={loc:?} {body:?}");
};
let standard = ServiceBuilder::new().layer(FollowRedirectLayer::new()).service(client.clone()).boxed_clone();
let cloning = ServiceBuilder::new()
.layer(FollowRedirectLayer::with_policy(
policy::Standard::default().and::<_, Full<Bytes>, hyper_util::client::legacy::Error>(policy::clone_body_fn(|b: &Full<Bytes>| Some(b.clone()))),
))
.service(client.clone())
.boxed_clone();
let form = || Request::post(format!("http://{addr}/form")).body(Full::from("a=1&b=2")).unwrap();
let empty_form = || Request::post(format!("http://{addr}/form")).body(Full::default()).unwrap();
send("Standard, POST /form (7 bytes)", standard.clone(), form()).await;
send("Standard, POST /form (empty)", standard.clone(), empty_form()).await;
send("Standard + clone_body_fn, POST", cloning, form()).await;
send("Standard, GET /loop", standard, Request::get(format!("http://{addr}/loop")).body(Full::default()).unwrap()).await;
println!("/loop handler ran {} times", hits.load(Ordering::SeqCst));
}
M2 Max、rustc 1.99.0,跑 3 次输出相同:
Standard, POST /form (7 bytes) 307 Temporary Redirect last=/form location=Some("/done") b""
Standard, POST /form (empty) 200 OK last=/done location=None b"done, got 0 bytes"
Standard + clone_body_fn, POST 200 OK last=/done location=None b"done, got 7 bytes"
Standard, GET /loop 303 See Other last=/loop location=Some("/loop") b""
/loop handler ran 21 times
原因一:307 要重发 body,而没人留一份
第一行:7 字节的表单遇到 307,FollowRedirect 没有跟随,调用方拿到 307 和 Location: /done,RequestUri 显示最后一跳还是 /form。同样的请求换成空 body(第二行)就跟过去了。
307 和 308 要求用原来的方法、原来的 body 重发。第一个请求发出去时 body 已经交给内层了,要重发只能事先复制一份,这件事由 Policy::clone_body 决定。follow_redirect/mod.rs 第 344 行:
let body = if let Some(body) = this.body.take() {
body
} else {
return Poll::Ready(Ok(res));
};
take() 只在两种情况下给出 body:事先留了一份,或者 body 的 size_hint 确定为 0,这时用 Default 造一个新的(所以空表单能跟)。Standard 策略不实现 clone_body,默认返回 None,非空 body 就没有副本,307 原样返回。Full<Bytes> 本身是 Clone 的也没用,Policy 上没有 B: Clone 的约束,它不知道。
修法是第三行:Standard 后面 .and(clone_body_fn(..)),自己告诉它怎么复制。clone_body_fn 的 redirect 永远是 Follow,放在 And 右边不会放宽 Standard 的判断。流式 body 复制不了,只能在拿到 307 后自己重建请求,reqwest 卷 重定向 讲了 reqwest 在这种情况下怎么做。
原因二:Limited 用完
第四行:/loop 一直 303 到自己。handler 跑了 21 次,第一次是原请求,后面 20 次是跟随;第 21 个 303 交回给调用方,没有错误。Standard 里的 Limited::redirect 额度用完只返回 Action::Stop,ResponseFuture::poll 对 Stop 的处理就是把当前响应交出去。
这和 reqwest、浏览器都不一样:它们超过次数会报「too many redirects」一类的错误。用 tower-http 自己拼客户端时,调用方要自己检查最终状态码是不是 3xx,否则一个重定向死循环看起来像是「服务端回了 303」。想要错误,自己写一个 Policy,在 redirect 里返回 Err。
怎么区分
两种情况都是 3xx,可以这样分:
- 最终响应是 3xx、
RequestUri等于原始地址:第一跳就没跟,多半是原因一(或者策略本身拒绝,比如SameOrigin)。 RequestUri已经变了:跟了若干跳以后停下,多半是原因二。
小结
FollowRedirect不跟随时返回 3xx,不返回错误。- 307/308 带非空 body 时,
Standard不复制 body,只能把 307 交回去;用clone_body_fn补上。 Limited默认 20 次,用完把第 21 个 3xx 交回去;调用方要自己检查状态码。