-
Notifications
You must be signed in to change notification settings - Fork 430
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
Improve Redirect Interceptor #388
Conversation
Codecov Report
@@ Coverage Diff @@
## master #388 +/- ##
============================================
+ Coverage 75.92% 76.09% +0.16%
- Complexity 194 195 +1
============================================
Files 33 33
Lines 943 958 +15
Branches 166 174 +8
============================================
+ Hits 716 729 +13
- Misses 139 142 +3
+ Partials 88 87 -1
Continue to review full report at Codecov.
|
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 am happy to approve if you feel these ideas for change are not an improvement
|
||
val newMethod = when (response.statusCode) { | ||
HttpsURLConnection.HTTP_MOVED_PERM, | ||
HttpsURLConnection.HTTP_MOVED_TEMP, |
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.
would it be worth having these in a collection, so if you add them it doesn't get bigger and bigger, also the collection of valid redirects could be reused in other parts of the code base?
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.
Agree! Will do!
} | ||
) | ||
|
||
if (redirectedUrl != null && redirectedUrl.isNotEmpty()) { |
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.
would you consider redirectedUrl?.isNotEmpty() = true
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.
Aw right, will do.
} | ||
} | ||
|
||
if (response.statusCode in HttpsURLConnection.HTTP_MULT_CHOICE..HTTP_PERMANENT_REDIRECT) { |
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 this range would be useful for reuse, would you consider defining it in HttpsUrlConnection?
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.
But we don't have HttpsUrlConnection object here though. When you said defining it in HttpsUrlConnection?
it means extenstion of HttpsUrlConnection
?
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.
yes unless you feel that is an error?
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.
Hmm, an extension HttpsUrlConnection needs an object of HttpsUrlConnection, doesn't it?
Let's say we have
val HttpsUrlConnection.redirectStatusRange
get () = HttpsURLConnection.HTTP_MULT_CHOICE..HTTP_PERMANENT_REDIRECT
How do we use this?
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.
sorry I see what you mean
@markGilchrist not at all, sir! I have fixed most of them but #388 (comment). Please advice 🥇 |
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 am happy to merge as my suggestion wasn't completely thought out
I wanna have @SleeplessByte to see this PR and hopefully say some few wise words 😄 |
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.
Overall muchos better @kittinunf. Kudos on the work <3
private val redirectStatusWithGets = listOf(HttpsURLConnection.HTTP_MOVED_PERM, | ||
HttpsURLConnection.HTTP_MOVED_TEMP, | ||
HttpsURLConnection.HTTP_SEE_OTHER) | ||
|
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.
For whomever is interested. This list is correct as per:
Note: For historical reasons, a user agent MAY change the request
method from POST to GET for the subsequent request. If this
behavior is undesired, the 307 (Temporary Redirect) status code
can be used instead.
Note: For historical reasons, a user agent MAY change the request
method from POST to GET for the subsequent request. If this
behavior is undesired, the 307 (Temporary Redirect) status code
can be used instead.
A user agent can perform a retrieval request targeting that URI (a GET
or HEAD request if using HTTP), which might also be redirected, and
present the eventual result as an answer to the original request.
This list MUST NOT contain the following:
Note: This status code is similar to 302 (Found), except that it
does not allow changing the request method from POST to GET. This
specification defines no equivalent counterpart for 301 (Moved
Permanently) ([RFC7238], however, defines the status code 308
(Permanent Redirect) for this purpose).
Note: This status code is similar to 301 (Moved Permanently)
([RFC7231], Section 6.4.2), except that it does not allow changing
the request method from POST to GET.
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 the highest quality comment on a pr I have ever seen
} | ||
} | ||
|
||
if (response.statusCode in HttpsURLConnection.HTTP_MULT_CHOICE..HTTP_PERMANENT_REDIRECT) { |
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 may consider simplyfying this by targeting everything in the 3xx class (and using 300..399), although this seems fine to me. However you probably want redirection to be optional, as the HTTP spec also defines it as optional! (note to unsafe requests).
https://tools.ietf.org/html/rfc7231#section-6.4
The 3xx (Redirection) class of status code indicates that further
action needs to be taken by the user agent in order to fulfill the
request. If a Location header field (Section 7.1.2) is provided, the
user agent MAY automatically redirect its request to the URI
referenced by the Location field value, even if the specific status
code is not understood. Automatic redirection needs to done with
care for methods not known to be safe, as defined in Section 4.2.1,
since the user might not wish to redirect an unsafe request.
I would name the range .redirectionClass
or .STATUS_REDIRECTION
and if you do, maybe also define the .informationalClass
/ .STATUS_INFORMATIONAL
, .successfulClass
/ .STATUS_SUCCESSFUL
, .clientErrorClass
/ .STATUS_CLIENT_ERROR
and .serverErrorClass
/ .STATUS_SERVER_ERROR
.
There is no such thing as a redirect status, thus redirect status implies range.
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.
Nice comment. @SleeplessByte Thanks so much for your insightful knowledge on this topic.
I do have one question when you said the redirection should be optional, what does the implementation should look like exactly? In what condition it is optional, in what condition the redirection should fall through?
Maybe if the location
target is not there, we do nothing?
Regarding the status class, it is a great idea. I think I will try to do as you suggested. 👍
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.
Almost all libraries have a simple flag "follow redirections" which is almost always opt-in. cURL uses -L I think, Android's HttpGet has setFollowRedirects(bool)
I think a correct way of handling is doing both
- adding a flag
- checking for the presence of the location URL (and the followability)
Now following redirects is optional.
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 fixed by b345f5b#diff-c6f14509edd9ece29c446e94eb74871bR33
URL(request.url, newUrl) | ||
}).toString()) | ||
|
||
// redirect | ||
next(request, manager.request(encoding).response().second) |
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.
What happens with all the other headers? If you only take the "default manager headers" that contract should be defined very explicitly. I think this is the wrong behaviour. For example Range-requests break when being redirected.
This is a good collection of comments around this subject. Note: it is NOT defined in any RFC yet, however, libcurl has a solid implementation that seems to make a lot of sense. It boils down to:
- Copy all headers if you're redirecting to the same host
- Copy all headers except for Authorization if you're redirecting to a different host.
I suggest implementing the same behaviour; either in this PR or a follow-up PR.
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.
As per https://tools.ietf.org/html/rfc7231#section-6.4, you SHOULD implement a method to detect infinite loops. I suggest the arbitrary value of 10 hops as maximum, as 5 is deemed too little.
A client SHOULD detect and intervene in cyclical redirections (i.e.,
"infinite" redirection loops).
Note: An earlier version of this specification recommended a
maximum of five redirections ([RFC2068], Section 10.3). Content
developers need to be aware that some clients might implement such
a fixed limitation.
Currently, with this implementation, infinite loops would go on infinitely and slowely but surely deplete both your data plan as well as the battery.
//error | ||
val error = FuelError(RedirectException(), response.data, response) | ||
// error | ||
val error = FuelError( |
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.
No. Redirection itself is always a MAY which means the behaviour is optional. Providing a Location
is SHOULD and not MUST. It is not an error if there is no URL.
- 300 responses do not have a
Location
header, if there is no preferred response (see: Agent driven content-negotiation) - 304 responses do not have defined that they SHOULD have a location.
Instead I would return the response with the 3xx status response and optionally the data. Not as an error.
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.
Nice, will do.
assertThat(data, nullValue()) | ||
assertThat(data, not(containsString("http://httpbin.org/get"))) | ||
assertThat(error, notNullValue()) | ||
} | ||
} |
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.
Probably want to test that POST stays POST (307, 308) and POST becomes GET (301, 302)
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.
Fixed by e90c13e
55f4045
to
0b510bc
Compare
val (_, _, result) = manager.request(Method.GET, | ||
"http://httpbin.org/redirect-to", | ||
listOf("url" to "/get")) | ||
.allowRedirect(false) |
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.
@SleeplessByte this is a flag to stop follow the redirection if the client wishes so. Do you think this makes sense?
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.
Yah, anything like that works fine. Since it will follow up to max number of redirects, you might want to change it to plural. Can you set that at manager level as well?
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.
Will work on the plural!
Can you set that at manager level as well?
I have thought about it and decided (by myself) that it is more flexible to do at the request level. Do you see any potential misuse or inconvenience?
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.
People are lazy; but it's soooo trivial to pipe requests (using extension methods) through my own set to always set allowRedirect(false) or whatever, so I really couldn't care less. I think it's a fine design decision; as long as it's documented how to disallow redirects :)
|
||
val (data, error) = result | ||
|
||
// TODO: This is current based on the current behavior, however we need to fix this as it should handle 100 - 399 gracefully not httpException |
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 should be fixed together with #379. Based on the current Fuel
behavior, it is correct! However, it is wrong 😄.
I will be addressed in the different PR.
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.
Sounds good!
There is one thing I haven't addressed which is the discussion in #388 (comment) I have created this #395 so that we can comeback to this later. |
b5af7dc
to
446eddd
Compare
I wanna get this thing rolling. Hopefully, @SleeplessByte is mildly satisfied with this. 🙃 Thanks so much for the reviews. |
Does this work with |
@hudson155 yes, these changes are in the core, so is used by anything. As long as the redirection interceptor is in your interceptors this will work. |
What's in this PR?
The original PR by @ParkerK is here #208. However, due to lack of my attention over the time, it has been stale without me taking much care about it. 🙏 😢 😭
I think it might be best to revive it. And improve it a little along the way. With the suggestion from @SleeplessByte, I think we will have the right Redirect interceptor this time.
This PR should supersede #208. All credits go to him 🍺 🥂