HTTP Methods and Redirect Status Codes

This crossed my Twitter stream earlier today:

image

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 (http://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"] = 'http://'+ 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:

image

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:

image

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.

image

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:

image

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:

image

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.

image

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:

Firefox prompts on 307 to a POST

Opera's redirect confirmation prompt ( note: Doesn't seem to matter which of the three buttons you click )

Opera prompts on a 307 response to a POST but will not reissue the request

 

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