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

Improve Redirect Interceptor #388

Merged
merged 16 commits into from
Jul 26, 2018
Merged

Conversation

kittinunf
Copy link
Owner

@kittinunf kittinunf commented Jul 16, 2018

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 🍺 🥂

@codecov
Copy link

codecov bot commented Jul 16, 2018

Codecov Report

Merging #388 into master will increase coverage by 0.16%.
The diff coverage is 76.92%.

Impacted file tree graph

@@             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
Impacted Files Coverage Δ Complexity Δ
...n/kotlin/com/github/kittinunf/fuel/core/Request.kt 78.41% <100%> (+0.47%) 60 <1> (+1) ⬆️
.../kotlin/com/github/kittinunf/fuel/core/Response.kt 68.88% <20%> (-6.12%) 20 <0> (ø)
...f/fuel/core/interceptors/RedirectionInterceptor.kt 82.6% <88.88%> (+20.1%) 0 <0> (ø) ⬇️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 1992bca...40e6d29. Read the comment docs.

Copy link
Collaborator

@markGilchrist markGilchrist left a 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,
Copy link
Collaborator

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?

Copy link
Owner Author

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

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

Copy link
Owner Author

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) {
Copy link
Collaborator

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?

Copy link
Owner Author

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 ?

Copy link
Collaborator

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?

Copy link
Owner Author

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?

Copy link
Collaborator

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

@kittinunf
Copy link
Owner Author

@markGilchrist not at all, sir! I have fixed most of them but #388 (comment). Please advice 🥇

Copy link
Collaborator

@markGilchrist markGilchrist left a 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

@kittinunf
Copy link
Owner Author

I wanna have @SleeplessByte to see this PR and hopefully say some few wise words 😄

Copy link
Collaborator

@SleeplessByte SleeplessByte left a 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)

Copy link
Collaborator

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:

301 Moved Permanently

  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.

302 Found

  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.

303 See Other

  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:

307 Temporary Redirect

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

308 Permanent Redirect

  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.

Copy link
Collaborator

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) {
Copy link
Collaborator

@SleeplessByte SleeplessByte Jul 17, 2018

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.

Copy link
Owner Author

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. 👍

Copy link
Collaborator

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.

Copy link
Owner Author

Choose a reason for hiding this comment

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

URL(request.url, newUrl)
}).toString())

// redirect
next(request, manager.request(encoding).response().second)
Copy link
Collaborator

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.

Copy link
Collaborator

@SleeplessByte SleeplessByte Jul 17, 2018

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(
Copy link
Collaborator

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.

Copy link
Owner Author

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

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)

Copy link
Owner Author

Choose a reason for hiding this comment

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

Fixed by e90c13e

@kittinunf kittinunf force-pushed the kv/improve-redirect-interceptor branch from 55f4045 to 0b510bc Compare July 24, 2018 04:03
val (_, _, result) = manager.request(Method.GET,
"http://httpbin.org/redirect-to",
listOf("url" to "/get"))
.allowRedirect(false)
Copy link
Owner Author

@kittinunf kittinunf Jul 24, 2018

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?

Copy link
Collaborator

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?

Copy link
Owner Author

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?

Copy link
Collaborator

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

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.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sounds good!

@kittinunf
Copy link
Owner Author

kittinunf commented Jul 24, 2018

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.

@kittinunf
Copy link
Owner Author

kittinunf commented Jul 26, 2018

I wanna get this thing rolling. Hopefully, @SleeplessByte is mildly satisfied with this. 🙃

Thanks so much for the reviews.

@kittinunf kittinunf merged commit 7fe0e28 into master Jul 26, 2018
@kittinunf kittinunf deleted the kv/improve-redirect-interceptor branch July 26, 2018 02:55
@kittinunf kittinunf mentioned this pull request Aug 1, 2018
@hudson155
Copy link

Does this work with FuelManager?

@SleeplessByte
Copy link
Collaborator

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants