Skip to content
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

Merged
merged 4 commits into from
Dec 12, 2016

Conversation

nateprewitt
Copy link
Member

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 have rebuild_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 of resolve_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.

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
Copy link
Member Author

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.

Copy link
Member

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.

@nateprewitt nateprewitt force-pushed the 2590_redirected_bodies branch from 56f8854 to 9f84319 Compare December 9, 2016 04:21
Copy link
Member

@Lukasa Lukasa left a 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
Copy link
Member

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
Copy link
Member

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.
Copy link
Member

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.
Copy link
Member

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':
Copy link
Member

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. ;)

@nateprewitt nateprewitt force-pushed the 2590_redirected_bodies branch from 9f84319 to 77cae3f Compare December 9, 2016 18:43
@nateprewitt
Copy link
Member Author

Alright, comments updated.

Personally, I'd like to have rebuild_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 of resolve_redirects.

Thoughts on mucking with this? If not, I think we're good to go.

Copy link
Member

@Lukasa Lukasa left a 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'}
Copy link
Member

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())
Copy link
Member

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?

Copy link
Member Author

@nateprewitt nateprewitt Dec 9, 2016

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.

Copy link
Member

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?

Copy link
Member Author

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())
Copy link
Member

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())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same notes here.

@nateprewitt nateprewitt force-pushed the 2590_redirected_bodies branch from 77cae3f to a3b5539 Compare December 9, 2016 22:40
old_method = prepared_request.method
self.rebuild_method(prepared_request, response)
new_method = prepared_request.method
method_changed = self.rebuild_method(prepared_request, response)
Copy link
Member Author

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.

@nateprewitt nateprewitt changed the title [WIP] 301/302 redirects for non-POST with body 301/302 redirects for non-POST with body Dec 9, 2016
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
Copy link
Contributor

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.

Copy link
Member Author

@nateprewitt nateprewitt Dec 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think #3490 still makes sense to have, but #1084 is relating to code that is being removed here. We can keep it in if you think it's still providing value, but I feel it will end up as more of a red herring. Maybe making a note in #2590 with the link would be more useful if it's needed?

"""
method = prepared_request.method
original_method = prepared_request.method
Copy link
Contributor

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

Copy link
Member Author

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.

@nateprewitt nateprewitt force-pushed the 2590_redirected_bodies branch from a3b5539 to 326bb4a Compare December 10, 2016 19:02
@nateprewitt nateprewitt force-pushed the 2590_redirected_bodies branch from 326bb4a to 3bb25a1 Compare December 11, 2016 05:02
@nateprewitt
Copy link
Member Author

nateprewitt commented Dec 12, 2016

@Lukasa, I think this is ready for a peek whenever you've got a moment :)

Copy link
Member

@Lukasa Lukasa left a 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'
Copy link
Member

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?

Copy link
Member Author

@nateprewitt nateprewitt Dec 12, 2016

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.

Copy link
Member

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. =)

Copy link
Member Author

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.

@nateprewitt
Copy link
Member Author

Ok, 301-303 individual tests removed in favor of the big ones.

Copy link
Member

@Lukasa Lukasa left a 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!

@Lukasa Lukasa merged commit 0b4302b into psf:proposed/3.0.0 Dec 12, 2016
@nateprewitt nateprewitt deleted the 2590_redirected_bodies branch December 12, 2016 16:09
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Sep 8, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants