From: Ben Darnell Date: Tue, 1 Jan 2019 17:55:58 +0000 (-0500) Subject: simple_httpclient: Only convert POST requests to GETs on redirect X-Git-Tag: v6.0.0b1~7^2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=c443fb7bf8a87ba8ab02b9a6af9e140cabc0ab0d;p=thirdparty%2Ftornado.git simple_httpclient: Only convert POST requests to GETs on redirect Convert POST to GET on 301 in addition to 302 and 303. --- diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index 6af9dd15c..6e075aa63 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -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 [ diff --git a/tornado/test/httpclient_test.py b/tornado/test/httpclient_test.py index 9adbcea46..26a1ad7fc 100644 --- a/tornado/test/httpclient_test.py +++ b/tornado/test/httpclient_test.py @@ -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)