背景描述

事情发生在一年前,彼时我正通过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 方法:

但是谁知道两年多过去了,浏览器厂商们懒得改。那既然厂商不改,就标准改吧。 在 1999 年 6 月的 RFC 2616 中,增加了 303 与 307,与此同时 302 被更名为 Found。标准中提到:

302 标准就被那么放着了。直到2014 年 6 月的 RFC 7231 中,修改了对 302 的定义:

在之前的标准中,这句话中的 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 Method,然后还要重定向,Apache HttpClient对此有所限制。如果你还想允许 POST(但不允许 PUT 或 DELETE),可以通过类似的方式进行切换:

HttpClientBuilder hcb = HttpClients.custom();
hcb.setRedirectStrategy(new LaxRedirectStrategy());
HttpClient client = hcb.build(); 

如果你还想跟随 PUT 和 DELETE,你将不得不实现一个自定义策略(注意:我们在 HttpClient 中遇到了一个错误,它似乎在我们这样做时试图添加第二个 Content-Length 标头,所以我们手动移除了它。因人而异)。通过使用这种策略,HttpClient 还将支持 308 重定向,而 Apache 团队甚至懒得包含这个功能。

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;
        }
    }
    
}

同样的你需要选择这个自定义的重定向策略

hcb.setRedirectStrategy(new LaxRedirectStrategy());

最后

如果你使用Apache HttpClient遇到重定向问题,仅仅看最后一部分即可。
文章前部分对于解决问题益处不大,但是我还是保留了,因为文章也是记录给我自己看的?,就当作知海拾贝吧。

HTTP 中的 301、302、303、307、308 响应状态码

whats-the-difference-between-http-301-and-308-status-codes

apache-http-client-doesnt-respect-http-status-code-307

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]