-
-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
301/302 redirects for non-POST with body #3757
301/302 redirects for non-POST with body #3757
Conversation
def test_http_301_for_redirectable_methods(self, httpbin, method, body, expected): | ||
"""Tests all methods except OPTIONS for expected redirect behaviour. | ||
|
||
OPTIONS isn't a redirectable method and will always return values for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was a hasty comment. There isn't anything in the RFC that states this, and it seems servers from major platforms treat the request differently (Facebook treats it as a GET??). I think the problem here is that it's unpredictable how a server may respond. I'll clarify the comment in the next review pass.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is no reason to assume that OPTIONS cannot be redirected, and nothing in the Requests codebase forbids it.
56f8854
to
9f84319
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally this looks really good. There's some confusion around OPTIONS that needs to be cleaned up, and I'd like some more explanatory comments, but mostly this seems fab.
def test_http_301_for_redirectable_methods(self, httpbin, method, body, expected): | ||
"""Tests all methods except OPTIONS for expected redirect behaviour. | ||
|
||
OPTIONS isn't a redirectable method and will always return values for |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there is no reason to assume that OPTIONS cannot be redirected, and nothing in the Requests codebase forbids it.
assert r.status_code == 200 | ||
assert r.history == [] | ||
assert r.request.method == 'OPTIONS' | ||
assert 'Allow' in r.headers |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really only a test of httpbin's particular behaviour.
"""Tests all methods except OPTIONS for expected redirect behaviour. | ||
|
||
OPTIONS isn't a redirectable method and will always return values for | ||
the original URI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same note re OPTIONS here.
"""Tests all methods except OPTIONS for expected redirect behaviour. | ||
|
||
OPTIONS isn't a redirectable method and will always return values for | ||
the original URI. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same note re OPTIONS.
# This bizarre behaviour is explained in Issue 1704. | ||
if response.status_code == codes.moved and method == 'POST': | ||
# If a POST is responded to with a 301 or 302, turn it into a GET. | ||
if response.status_code in (codes.found, codes.moved) and method == 'POST': |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We may want to flesh this comment out a bit here to point out that there is a lack of consistency over what exactly happens here. Some browsers have historically changed other methods (I can find some citations of Chrome changing DELETE to GET), but ultimately most browsers rarely ever issue methods outside of POST and GET.
Our best option, then, is to mostly emulate what curl does, and curl restricts this behaviour to POST only. That seems like the most consistent position to take. But we should aim to cite our sources. ;)
9f84319
to
77cae3f
Compare
Alright, comments updated.
Thoughts on mucking with this? If not, I think we're good to go. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open to changing the signature of rebuild_method
.
to them. For that reason they aren't included here. | ||
""" | ||
s = requests.Session() | ||
params = {'url': '/%s' % method.lower(), 'status_code': '301'} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the URL parameter not be expected
?
s = requests.Session() | ||
params = {'url': '/%s' % method.lower(), 'status_code': '301'} | ||
req = requests.Request(method, httpbin('redirect-to'), data=body, params=params) | ||
r = s.send(req.prepare()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using the PreparedRequest
flow?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So I went with the PreparedRequest
flow because I felt it was easier to understand. We could use the API mappings by writing the test as:
params = {'url': '/%s' % method.lower(), 'status_code': '301'}
req = getattr(requests, method)
req(httpbin('redirect-to'), data=body, params=params)
but that's probably not a paradigm to encourage and makes the test kind of odd for the sake of saving 2 lines.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it not easier just to use requests.request
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First method in the api and I don't know if I knew that existed before now. That's definitely simpler, I'll get that updated.
s = requests.Session() | ||
params = {'url': '/%s' % method.lower()} | ||
req = requests.Request(method, httpbin('redirect-to'), data=body, params=params) | ||
r = s.send(req.prepare()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same notes as above.
s = requests.Session() | ||
params = {'url': '/%s' % method.lower(), 'status_code': '303'} | ||
req = requests.Request(method, httpbin('redirect-to'), data=body, params=params) | ||
r = s.send(req.prepare()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same notes here.
77cae3f
to
a3b5539
Compare
old_method = prepared_request.method | ||
self.rebuild_method(prepared_request, response) | ||
new_method = prepared_request.method | ||
method_changed = self.rebuild_method(prepared_request, response) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Eh, so I guess I'm indifferent on this now that I'm really looking at it. Pulling the logic from the conditional into it's own function adds code without significantly reducing the resolve_redirects
footprint.
The return value for rebuild_method
provides an op vs no-op check but isn't particularly useful. I threw it in there as a slight readability improvement, but we can ditch it if you aren't feeling it.
if response.status_code not in (codes.temporary_redirect, | ||
codes.permanent_redirect): | ||
# https://github.com/kennethreitz/requests/issues/3490 | ||
# https://github.com/kennethreitz/requests/issues/2590 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should probably keep old issue references if only for historical context.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
""" | ||
method = prepared_request.method | ||
original_method = prepared_request.method |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: This might be less confusing if we did
method = original_method = prepared_request.method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, I like that better too.
a3b5539
to
326bb4a
Compare
326bb4a
to
3bb25a1
Compare
@Lukasa, I think this is ready for a peek whenever you've got a moment :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking pretty good! Got a note about the testing layout, but not too much otherwise.
assert r.history[0].status_code == 301 | ||
assert r.history[0].is_redirect | ||
assert r.request.body == 'test body' | ||
assert r.json()['data'] == 'test body' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test looks pretty close to a duplicate of the one below. Any reason we wouldn't just move the check on status_code
into the test below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You're right, this one was written before test_http_301_for_redirectable_methods
was, following the styling above. We can actually collapse the three tests above into the one below, but I left them to ensure we were always testing for those specific cases. The thought being if test_http_301_for_redirectable_methods
is later modified it would be nice for those to still fail, but I understand if you'd like to see them squashed together.
As for the status code, I left that out of the test below because /head doesn't exist in httpbin (because it's non-sensical). The test logic becomes more complex trying to code around the HEAD case and the status code isn't needed confirm we got what we wanted from the test, only that httpbin is functioning properly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's no reason to keep them separate. =)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I'll squash them all down then.
Ok, 301-303 individual tests removed in favor of the big ones. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cool, I like this. Thanks for the work @nateprewitt!
This patch is an attempt to address #2590 by relaxing the way Requests handles redirects.
Taking a look at how we’re handling stripping bodies, I modified the conditional to avoid an overly complicated check. The only requests we want to strip bodies off of are ones that have been modified from the redirect. That means the method will have already been changed to GET in
rebuild_method
and we can perform a uniform stripping for only GET requests. Personally, I'd like to haverebuild_method
return a value, either a boolean or the new method, and use that as the parameter for the check. Then move the code inside the conditional into it's own function. This will help a bit with the overall size ofresolve_redirects
.I also removed the 302 catch-all which was put in place in #1704 when RFC 2616 was still the standard. The previous mandates for 302 aren’t included in RFC 7231 6.4.3 and don’t appear to be reproducible in Chrome or Firefox with any tools supporting PUT, PATCH, or DELETE. (Note: if there’s a repro for this that I’m missing please let me know and I’ll gladly revert this. I’m just going off of the information available.)
The "test all methods" tests may be a bit overboard, I added them primarily for my own assurance to make sure I wasn't missing edge cases. I can remove those if they're deemed unnecessary.