- 阅读本篇可以收获Http重定向相关规范
- 实际解决了一些Apache HttpClient关于重定向的问题
背景描述
事情发生在一年前,彼时我正通过HttpClient调用合作方接口,屡屡遭遇报错。我反复核验代码逻辑,确认调用方式分毫不差。当时为赶项目工期,只得用潦草的方式解决,未曾深究根源------直到数日前,某个寻常夜晚,这个被遗忘在记忆角落的问题突然又鬼使神差地浮上心头,遂成此文。
不过时过境迁,我已经忘记了当时的具体场景,但是经过复盘我还是有一些收获。多年以后,当我深夜排查事故时,准会想起多年前那个潦草处理HttpClient报错、未及深究便匆匆绕过的遥远午后。
Http重定向规范
RFC 7231定义如下状态
- 301 Moved Permanently
- 302 Found
- 303 See Other
- 307 Temporary Redirect
RFC 7538新增如下状态
- 308 Permanent Redirect
见名知意,301、308 是永久重定向,剩下三个不一定能从名字看出来作用是什么,干脆记住它们都是临时重定向。
临时重定向
302 Found 的定义
302 状态码表示目标资源临时移动到了另一个 URI 上。由于重定向是临时发生的,所以客户端在之后的请求中还应该使用原本的 URI。
服务器会在响应 Header 的 Location 字段中放上这个不同的 URI。浏览器可以使用 Location 中的 URI 进行自动重定向。
注意:由于历史原因 ,用户代理可能会在重定向后的请求中把 POST 方法改为 GET 方法。如果不想这样,应该使用 307(Temporary Redirect) 状态码。(之后我们会详细叙述历史原因)
303 See Other 的定义
303 状态码表示服务器要将浏览器重定向到另一个资源,这个资源的 URI 会被写在响应 Header 的 Location 字段。从语义上讲,重定向到的资源并不是你所请求的资源,而是对你所请求资源的一些描述。
303 常用于将 POST 请求重定向到 GET 请求,比如你上传了一份个人信息,服务器发回一个 303 响应,将你导向一个"上传成功"页面。不管原请求是什么方法,重定向请求的方法都是 GET(或 HEAD,不常用)。303 和 302 的作用很类似,除去语义差别,似乎是 302 包含了 303 的情况。确实,这是由历史原因导致的。
307 Temporary Redirect 的定义
307 的定义实际上和 302 是一致的,唯一的区别在于,307 状态码不允许浏览器将原本为 POST 的请求重定向到 GET 请求上。
三者关系
从实际效果看:302 允许各种各样的重定向,一般情况下都会实现为到 GET 的重定向,但是不能确保 POST 会重定向为 POST;而 303 只允许任意请求到 GET 的重定向;307 和 302 一样,除了不允许 POST 到 GET 的重定向。
历史包袱
在 1995 年 6 月的 RFC 1945 HTTP 1.0 标准,302 被称为 Moved Temporarily,而不是现在的 Found。标准中提到,有些浏览器收到了 302 状态码,在自动重定向时候会错误的把 POST 方法转为 GET 方法:
Note: When automatically redirecting a POST request after
receiving a 302 status code, some existing user agents will
erroneously change it into a GET request. 这个错误在 1997 年 1 月的 RFC 2068 HTTP 1.1 标准提出时,仍然没有被修正。此时标准中依然只有 302 Moved Temporarily。
但是谁知道两年多过去了,浏览器厂商们懒得改。那既然厂商不改,就标准改吧。 在 1999 年 6 月的 RFC 2616 中,增加了 303 与 307,与此同时 302 被更名为 Found。标准中提到:
Note: RFC 1945 and RFC 2068 specify that the client is not allowed
to change the method on the redirected request. However, most
existing user agent implementations treat 302 as if it were a 303
response, performing a GET on the Location field-value regardless
of the original request method. The status codes 303 and 307 have
been added for servers that wish to make unambiguously clear which
kind of reaction is expected of the client. 简单来说,就是之前的标准都写了不允许重定向的时候改写方法,但是大多数浏览器还把 302 当成 303 那样处理。那干脆把两种不同的行为区分成 303 和 307。
302 标准就被那么放着了。直到2014 年 6 月的 RFC 7231 中,修改了对 302 的定义:
The user agent MAY use the Location field value for automatic redirection.
在之前的标准中,这句话中的 MAY 都是 MUST NOT。标准妥协了,既然现在大多数浏览器都支持了 307 和 303,那 302 的标准也就改了吧。
永久重定向
301 Moved Permanently 的定义
301 状态码表明目标资源被永久的移动到了一个新的 URI,任何未来对这个资源的引用都应该使用新的 URI。
308 Permanent Redirect 的定义
308 的定义实际上和 301 是一致的,唯一的区别在于,308 状态码不允许浏览器将原本为 POST 的请求重定向到 GET 请求上。
历史包袱
和 302 一样,301 在浏览器中的实现和标准是不同的,这个时间一直延续到 2014 年的 RFC 7231,301 定义中的 Note 还是提到了这个问题。直到 2015 年 4 月,RFC 7538 提出了 308 的标准,类似 307 Temporary Redirect 之于 302 Found 的存在,308 成为了 301 的补充。
Apache HttpClient中的重定向处理
默认重定向行为
DefaultRedirectStrategy
仅允许自动重定向 GET 和 HEAD。如果你还想允许 POST(但不允许 PUT 或 DELETE),可以通过类似的方式进行切换:
java
HttpClientBuilder hcb = HttpClients.custom();
hcb.setRedirectStrategy(new LaxRedirectStrategy());
HttpClient client = hcb.build();
如果你还想跟随 PUT 和 DELETE,你将不得不实现一个自定义策略(注意:我们在 HttpClient 中遇到了一个错误,它似乎在我们这样做时试图添加第二个 Content-Length 标头,所以我们手动移除了它。因人而异)。通过使用这种策略,HttpClient 还将支持 308 重定向,而 Apache 团队甚至懒得包含这个功能。
java
class ConsumeRedirectStrategy extends DefaultRedirectStrategy {
public boolean isRedirected(
HttpRequest request, HttpResponse response, HttpContext context
) throws ProtocolException {
Args.notNull(request, "HTTP request");
Args.notNull(response, "HTTP response");
int statusCode = response.getStatusLine().getStatusCode();
switch(statusCode) {
case 301:
case 307:
case 302:
case 308:
case 303:
return true;
case 304:
case 305:
case 306:
default:
return false;
}
}
public HttpUriRequest getRedirect(
HttpRequest request, HttpResponse response, HttpContext context
) throws ProtocolException {
URI uri = this.getLocationURI(request, response, context);
String method = request.getRequestLine().getMethod();
if(method.equalsIgnoreCase("HEAD")) {
return new HttpHead(uri);
} else if(method.equalsIgnoreCase("GET")) {
return new HttpGet(uri);
} else {
int status = response.getStatusLine().getStatusCode();
HttpUriRequest toReturn = null;
if(status == 307 || status == 308) {
toReturn = RequestBuilder.copy(request).setUri(uri).build();
toReturn.removeHeaders("Content-Length"); //Workaround for an apparent bug in HttpClient
} else {
toReturn = new HttpGet(uri);
}
return toReturn;
}
}
}
同样的你需要选择这个自定义的重定向策略
java
hcb.setRedirectStrategy(new LaxRedirectStrategy());
最后
如果你使用Apache HttpClient遇到重定向问题,仅仅看最后一部分即可。
文章前部分对于解决问题益处不大,但是我还是保留了,因为文章也是记录给我自己看的📝,就当作知海拾贝吧。
HTTP 中的 301、302、303、307、308 响应状态码