HTTP Methods and Redirect Status Codes
This crossed my Twitter stream earlier today:
I’m not sure why we need a public service announcement to notify folks that Internet Explorer is behaving properly, but I guess there’s no harm in that. However, based on the lack of information provided, and the implication that this is surprising, I think the original actually poster meant to say something else.
So let’s explore this a bit. The HTTP/1.0 specification, RFC1945 describes HTTP/301 as follows:
301 Moved Permanently
The requested resource has been assigned a new permanent URL and any future references to this resource should be done using that URL.// ...
Note: When automatically redirecting a POST request after receiving a 301 status code, some existing user agents will erroneously change it into a GET request.
The caveat in yellow also appears in the description for HTTP/302. Now, those “user agents” referred to in this remark included the popular browsers of the day, including Netscape Navigator and Internet Explorer. Arguably, this behavior is exactly what most websites wanted—after a successful POST, send the user to a different URL to show them something else. However, the POST-converted-to-GET behavior isn’t what the authors of HTTP had intended.
The authors of RFC2616 aimed to resolve the ambiguity by introducing two new redirection response codes in the HTTP/1.1 specification.
As noted inside the updated definition of HTTP/302:
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.
In defense of the implementers of the day, if RFC1945 and RFC2068 really do specify that the client user-agent is “not allowed to change the method”, they don’t do so in a very obvious manner. This expectation is not mentioned in the definition of the response code, nor is it mentioned in the preamble about redirects. Unfortunately, the current HTTPBIS Draft appears to suffer from the same limitation, although I believe that shortcoming is being tracked by a workitem in their list.
In RFC2616, the new HTTP/303 redirect code is defined thusly:
The response to the request can be found under a different URI and SHOULD be retrieved using a GET method on that resource. This method exists primarily to allow the output of a POST-activated script to redirect the user agent to a selected resource.
So, a 303 unambiguously means that the redirected request’s method should be changed to GET.
Unfortunately, the newly introduced definition of HTTP/307 failed to clearly specify that the original method was to be used-- the same problem that HTTP/302’s original definition suffered. However, it's obvious (based on the earlier text) that the intent of the HTTP/307 response code was to clearly indicate that the client should preserve the original method for the redirected request.
So, together, HTTP/303 and HTTP/307 allowed web developers to be specific about what they wanted to happen to the method (303->Convert to GET; 307->Keep original method).
Let’s see how well that has worked out...
Preserving Methods on Redirects
In this section, I’ll provide some cross-browser test results for a variety of scenarios. These are all generated using the XmlHttpRequest object and a MeddlerScript (https://webdbg.com/meddler/) file.
The MeddlerScript is as follows:
import Meddler;
import System;
import System.Net.Sockets;
import System.Windows.Forms;
class Handlers
{
static function OnConnection(oSession: Session)
{
if (oSession.ReadRequest()){
var oHeaders: ResponseHeaders = new ResponseHeaders();
oHeaders.Status = "200 OK";
oHeaders["Connection"] = "close";
var sRequestAsString = oSession.requestHeaders.ToString(true, true, true) + System.Text.Encoding.UTF8.GetString(oSession.requestBodyBytes);
if (oSession.urlContains('redir'))
{
MessageBox.Show(sRequestAsString, "Redirect Request");
oHeaders["Content-Type"] = "text/plain";
var s = oSession.GetQueryParams()["code"];
oHeaders.Status = s + " redirect";
oHeaders["Location"] = 'https://'+ oSession.requestHeaders["Host"] + '/final.cgi';
oHeaders["Cache-Control"] = "max-age=0";
oSession.WriteString(oHeaders.ToString());
oSession.WriteString("redirecting...\r\n");
}
else
if (oSession.urlContains('final'))
{
MessageBox.Show(sRequestAsString, "Final Request");
oHeaders["Content-Type"] = "text/plain";
oHeaders["Cache-Control"] = "max-age=0";
oSession.WriteString(oHeaders.ToString());
oSession.WriteString("Final Request was:<br />\r\n");
oSession.WriteString(sRequestAsString);
}
else
{
oHeaders["Content-Type"] = "text/html";
oSession.WriteString(oHeaders);
oSession.WriteString("<html><head><title>XMLHTTPRequest-based Redirect Test Harness</title>\n");
oSession.WriteString("<!doctype html>\r\n<html><head><script type='text/javascript'>\r\n");
oSession.WriteString("var xmlHttp; var i=0;");
oSession.WriteString("function doXHR(sMethod, sURI, sBody, iResponseCode){\r\n");
oSession.WriteString("i++; xmlHttp = new XMLHttpRequest();\r\n");
oSession.WriteString("\toutputdiv.innerHTML = '<pre>Beginning request #' + i + '...</pre><br />';\n");
oSession.WriteString("\txmlHttp.onreadystatechange = stateChanged;\n");
oSession.WriteString("\txmlHttp.open(sMethod,sURI+'?code='+iResponseCode,true);\n");
oSession.WriteString("\txmlHttp.setRequestHeader('CustomHeader', 'CustomValue');\n");
oSession.WriteString("\txmlHttp.send(sBody);}\n");
oSession.WriteString("function stateChanged(){\n\t\nif (xmlHttp.readyState==4){\n data = xmlHttp.responseText; ResponseHeaders.innerHTML = '[#' + i + ']Script-accessible response headers were:<br/><PRE>' + xmlHttp.getAllResponseHeaders() + data+ '</pre>';}\n}\n");
oSession.WriteString("function BuildRequest(){ doXHR(document.getElementById('txtMethod').value, document.getElementById('txtURI').value,document.getElementById('txtBody').value, document.getElementById('txtCode').value);\n}\n");
oSession.WriteString("</script></head><body><br></p><hr>Output: <div id='outputdiv'></div><br>Response: <span id='ResponseHeaders'></div><br></span>");
oSession.WriteString("<br /><input type=text id='txtMethod' value='GET' /><br />\n");
oSession.WriteString("<input type=text id='txtURI' value='/redir.cgi' /><br />\n");
oSession.WriteString("<input type=text id='txtBody' placeholder='Request body text...' value = '' /><br />\n");
oSession.WriteString("<input type=text id='txtCode' value='302' />\n<br />");
oSession.WriteString("<button onClick='BuildRequest();'>Send Request</button>");
oSession.WriteString("</body></html>\n");
}
}
oSession.CloseSocket();
}
}
In the first test, we examine HTTP/303 redirects:
IE 6-10 | DELETE converted to GET |
Chrome 13 | DELETE converted to GET |
Firefox 6 | DELETE converted to GET |
Safari 5.1 | DELETE converted to GET |
Opera 11.5 | DELETE converted to GET |
All browsers behave properly when receiving a HTTP/303 redirect code. Specifically, the client reissues the request after converting the DELETE method to a GET.
In the next test, we examine HTTP/307 redirects:
IE 6-10 | DELETE method is preserved |
Chrome 13 | DELETE method is preserved |
Firefox 6 | DELETE method is preserved (after prompt) |
Safari 5.1 | DELETE method is preserved |
Opera 11.5 | Fails to redirect (after prompt) |
All browsers (except Opera 11.5, which appears to be buggy) will correctly preserve the request method after receiving a HTTP/307.
Developers will run into cross-browser differences when using the older HTTP/301 and HTTP/302 methods.
IE 6-10 | DELETE method is preserved |
Chrome 13 | Converts to GET |
Firefox 6 | Converts to GET |
Safari 5.1 | Converts to GET |
Opera 11.5 | Converts to GET |
I believe that the highlighted result is what was surprising to the folks on Twitter. Only Internet Explorer avoids what RFC2616 calls "erroneous" behavior, by preserving the DELETE Method after the HTTP/302 redirection. Other browsers respond to a HTTP/301 or HTTP/302 as they do to HTTP/303, converting the method to GET.
But keep reading for another surprise...
This behavior mentioned in the last section was likely even more surprising because Internet Explorer does convert POST to GET for HTTP/301 and HTTP/302, like all other browsers:
IE 6-10 | Converts to GET |
Chrome 13 | Converts to GET |
Firefox 6 | Converts to GET |
Safari 5.1 | Converts to GET |
Opera 11.5 | Converts to GET |
As alluded to earlier, IE's POST->GET conversion for HTTP/301 and HTTP/302 is a historical artifact which was added in the 1990s for compatibility with the more popular web browsers of the time. Prior to the introduction of the XMLHttpRequest object, web pages could not really make use of other HTTP methods, which is why the method conversion is scoped to just the POST method.
Notably, browsers also behave differently in handling a 302 response to a HEAD request:
IE 6-10 | HEAD method is preserved |
Chrome 13 | HEAD converts to GET |
Firefox 6 | HEAD converts to GET |
Safari 5.1 | HEAD converts to GET |
Opera 11.5 | Redirect is not followed |
It seems like the unexpected conversion of HEAD into GET could lead to performance problems or functionality bugs.
Confirming Redirects
Now, RFC2616 also notes that redirects generally shouldn’t happen “automatically” except for idempotent / “safe” methods; the idea being that the user should have to confirm performing the same action on a different URL. In practice, a user is unlikely to have any idea what the prompt means, and thus most browsers ignore the requirement to confirm the redirection with the user.
Browser Behavior on Warn on POST –> 307 Redirect
IE 6-10 | Silent re-post |
Chrome 13 | Silent re-post |
Firefox 6 | Prompts (no target URL) before reposting |
Safari 5.1 | Silent re-post |
Opera 11.5 | Prompts (with target info) but neither Yes nor No results in a request. Bug? |
Firefox's redirect confirmation prompt:
Opera's redirect confirmation prompt ( note: Doesn't seem to matter which of the three buttons you click )
I hope this helps clear things up. In general, if you're using any method other than GET and you want to redirect, use HTTP/303 and HTTP/307 to be very explicit about what you want to have happen.
-Eric