IEInternals

A look at Internet Explorer from the inside out. @EricLaw left Microsoft in 2012, but was named an IE MVP in '13 & an IE userAgent (http://useragents.ie) in '14

HTTP Methods and Redirect Status Codes

HTTP Methods and Redirect Status Codes

  • Comments 9

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

  • Hi Eric, you've pulled out the "Note" on the definition of 302 to explain IE9's behavior, but you left out the immediately preceding sentence:

    "If the 302 status code is received in response to a request other than GET or HEAD, the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user, since this might change the conditions under which the request was issued."

    While the developer was wrong in relying on DELETE's conversion to GET on a 302, IE9 is still wrong in not requiring confirmation. Cheers, Mike

    [EricLaw: Hi, Mike! You should probably go ahead and read the entire post. There's an entire section on that topic. Thanks! ]

  • Mozilla bug tracker entry for this: bugzilla.mozilla.org/show_bug.cgi

  • Thanks for the clarification. Just out of curiosity, was this a candidate for changing in IE9 or IE10 simply for compatibility w/ all other modern browsers? (E.g. maybe just in IE9/IE10 standards modes?)

  • @Aseem: I'm interested to see what HTTPBIS specifies here. The current behavior of preserving the method is spec-compliant and it has been this way for over a decade.

    While matching behavior is appealing, the notion that a 301 or 302 response should turn a HEAD into a GET doesn't make much sense to me.

  • HTTPbis WG issue: trac.tools.ietf.org/.../160

    The current plan is to keep the definitions of 301/302, except by noting that rewriting POST-to-GET is ok (ut not for other methods).

  • HTTPbis draft 17 will *allow* rewriting of POST (and only POST) to GET for the status codes 301 and 302. See trac.tools.ietf.org/.../1428

  • Hi, I am using a 307 redirect in a htaccess file, like this:

    RewriteRule ^add/(.*)/$ /_ucp/moduls/$1/add/add.php [L,R=307]

    your browser now sends a post request, recives a 307 and then starts a GET request with the new URL. I tracked it in the F12 developer tools, any tipps, explanations ?

    EricLaw: This seems more likely to either be a DevTools bug (a POST is really sent) or a bug in your server (you're not really sending a 307). Please email me (using Help > Send Feedback in Fiddler) a SAZ file captured using Fiddler that demonstrates this problem.

  • My application initiates a POST request from JavaScript and the server sends a redirect new url and code 307.  IE even though receieves 307, it initiates a GET request. Why?? Where I could have gone wrong?

    EricLaw: Please provide a repro URL? Did you verify using Fiddler that it's really returning a 307?

  • By Fiddler I have verified. it is getting the 307 status code. I have both the request i.e. original POST request and the Redirected POST request, both are having the same header except the request method. Because of this I am losing the POST parameters. the same thing is working on Firefox and Chrome.

    EricLaw: Please email me a repro URL. Use Help > About in Fiddler.

Page 1 of 1 (9 items)
Leave a Comment
  • Please add 5 and 6 and type the answer here:
  • Post