PHP:设置Location导致状态码被修改为302

最近在试着做RESTful API,学了很多平时用不到的HTTP知识。在一个API上,我想用409 Conflict表示要创建的资源已经存在。这是一个挺冷门的状态码,在B/S结构中基本上用不到。RFC 2616对它的解释是:

409 Conflict

The request could not be completed due to a conflict with the current
state of the resource. This code is only allowed in situations where
it is expected that the user might be able to resolve the conflict and
resubmit the request. The response body SHOULD include enough
information for the user to recognize the source of the conflict.
Ideally, the response entity would include enough information for the
user or user agent to fix the problem; however, that might not be
possible and is not required.

《RESTful Web APIs》这本书在409 Conflict的解释中写到:

响应报头:如果该冲突是由于某些其他资源的存在(比如,客户端尝试创建的某个特定的资源已经存在了)而造成的话,那么Location报头应该链接到该资源的URL:也就是说,冲突的来源。

因此我在要创建的资源已存在时,将状态码设置为409,并将Location报头设置成已经存在的那个资源的URL。我用的是Slim Framework,伪代码如下:

$response->status(409);
$response->header('Location', $url);

然后奇怪的事情就发生了,客户端收到的状态码不是409,而是302。


开始我以为这是Slim做的处理,但是看了一下Slim的源码,并没有找到这个处理。Slim的资源并不多,Google上也找不到解释,换了好多关键词,最后发现Laravel上出现过同样的问题。但是,问题并不在框架上,而在语言上。

简而言之,在PHP中,设置header时会检查是否有Location,并据此修改状态码,在PHP源码中可以看到:

} else if (!strcasecmp(header_line, "Location")) {
    if ((SG(sapi_headers).http_response_code < 300 ||
        SG(sapi_headers).http_response_code > 399) &&
        SG(sapi_headers).http_response_code != 201) {
        /* Return a Found Redirect if one is not already specified */
        if (http_response_code) { /* user specified redirect code */
            sapi_update_response_code(http_response_code);
        } else if (SG(request_info).proto_num > 1000 &&
           SG(request_info).request_method &&
           strcmp(SG(request_info).request_method, "HEAD") &&
           strcmp(SG(request_info).request_method, "GET")) {
            sapi_update_response_code(303);
        } else {
            sapi_update_response_code(302);
        }
    }   

当设置的header是Location,且状态码不是3xx,也不是201时,状态码就会被改成302。


如果是用的原生PHP,可以把代码从

header("HTTP/1.1 409 Conflict");
header("Location: ".$url);

改成

header("Location: ".$url);
header("HTTP/1.1 409 Conflict");

也就是在状态码被修改成302之后再设置一次状态码。


但是如果使用了框架,例如Laravel和Slim,它们发送header的顺序并不是用户设置header的顺序。例如在Slim Framework中:

//Send headers
if (headers_sent() === false) {
    //Send status
    if (strpos(PHP_SAPI, 'cgi') === 0) {
        header(sprintf('Status: %s', \Slim\Http\Response::getMessageForCode($status)));
    } else {
        header(sprintf('HTTP/%s %s', $this->config('http.version'), \Slim\Http\Response::getMessageForCode($status)));
    }

    //Send headers
    foreach ($headers as $name => $value) {
        $hValues = explode("\n", $value);
        foreach ($hValues as $hVal) {
            header("$name: $hVal", false);
        }
    }
}

状态码总是最先被发送的。

最后,考虑到解决这个问题需要做一些dirty hack,我选择了把冲突来源的URL放在body里。

标签: php, http

仅有一条评论

  1. 你的这篇文章对我有帮助哦,感谢!

添加新评论