]> git.ipfire.org Git - thirdparty/tornado.git/commitdiff
simple_httpclient: Only convert POST requests to GETs on redirect 2440/head
authorBen Darnell <ben@bendarnell.com>
Tue, 1 Jan 2019 17:55:58 +0000 (12:55 -0500)
committerBen Darnell <ben@bendarnell.com>
Tue, 1 Jan 2019 17:55:58 +0000 (12:55 -0500)
Convert POST to GET on 301 in addition to 302 and 303.

tornado/simple_httpclient.py
tornado/test/httpclient_test.py

index 6af9dd15c3e9060a3fd22b765790e1d1663583a3..6e075aa639d23fef03fedebcd1e7256d1362be29 100644 (file)
@@ -626,14 +626,17 @@ class _HTTPConnection(httputil.HTTPMessageDelegate):
             )
             new_request.max_redirects = self.request.max_redirects - 1
             del new_request.headers["Host"]
-            # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.4
-            # Client SHOULD make a GET request after a 303.
-            # According to the spec, 302 should be followed by the same
-            # method as the original request, but in practice browsers
-            # treat 302 the same as 303, and many servers use 302 for
-            # compatibility with pre-HTTP/1.1 user agents which don't
-            # understand the 303 status.
-            if self.code in (302, 303):
+            # https://tools.ietf.org/html/rfc7231#section-6.4
+            #
+            # The original HTTP spec said that after a 301 or 302
+            # redirect, the request method should be preserved.
+            # However, browsers implemented this by changing the
+            # method to GET, and the behavior stuck. 303 redirects
+            # always specified this POST-to-GET behavior (arguably 303
+            # redirects should change *all* requests to GET, but
+            # libcurl only does this for POST so we follow their
+            # example).
+            if self.code in (301, 302, 303) and self.request.method == "POST":
                 new_request.method = "GET"
                 new_request.body = None
                 for h in [
index 9adbcea46d4d152cc99046dc9d6df0729a8436d6..26a1ad7fc8a7eadb381f0c309ed82a68213f7bae 100644 (file)
@@ -126,7 +126,7 @@ class AllMethodsHandler(RequestHandler):
     def method(self):
         self.write(self.request.method)
 
-    get = post = put = delete = options = patch = other = method  # type: ignore
+    get = head = post = put = delete = options = patch = other = method  # type: ignore
 
 
 class SetHeaderHandler(RequestHandler):
@@ -322,6 +322,32 @@ Transfer-Encoding: chunked
         )
         self.assertEqual(response.body, b"Put body: ")
 
+    def test_method_after_redirect(self):
+        # Legacy redirect codes (301, 302) convert POST requests to GET.
+        for status in [301, 302, 303]:
+            url = "/redirect?url=/all_methods&status=%d" % status
+            resp = self.fetch(url, method="POST", body=b"")
+            self.assertEqual(b"GET", resp.body)
+
+            # Other methods are left alone.
+            for method in ["GET", "OPTIONS", "PUT", "DELETE"]:
+                resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
+                self.assertEqual(utf8(method), resp.body)
+            # HEAD is different so check it separately.
+            resp = self.fetch(url, method="HEAD")
+            self.assertEqual(200, resp.code)
+            self.assertEqual(b"", resp.body)
+
+        # Newer redirects always preserve the original method.
+        for status in [307, 308]:
+            url = "/redirect?url=/all_methods&status=307"
+            for method in ["GET", "OPTIONS", "POST", "PUT", "DELETE"]:
+                resp = self.fetch(url, method=method, allow_nonstandard_methods=True)
+                self.assertEqual(method, to_unicode(resp.body))
+            resp = self.fetch(url, method="HEAD")
+            self.assertEqual(200, resp.code)
+            self.assertEqual(b"", resp.body)
+
     def test_credentials_in_url(self):
         url = self.get_url("/auth").replace("http://", "http://me:secret@")
         response = self.fetch(url)