Use Body::size_hint to determine whether a request is retryable#1345
Use Body::size_hint to determine whether a request is retryable#1345olix0r merged 10 commits intoeliza/fix-retry-clfrom
Conversation
This change rewrites `RetryPolicy::can_retry` as `RetryPolicy::body_too_big` and updates it to avoid reading `content-length` header, instead relying on the `Body::size_hint` value (which is set by the HTTP server if a `content-length` header is present. Renaming the function helps us express what the function actually checks to help disambiguate it from all of the other 'retry' related terminology in this file and is more explicit about what it does: we don't precicesly know if the request is retryable, rather we know whether the request is obviously too large to be retried. This also lets us simplify comments and logging as the flow is more self-explanatory. This change also adds unit tests for `RetryPolicy::body_too_big` to exercise the variety of scenarios this function handles.. Furthermore, this change ensures that all `Body` implementations correctly proxy `Body::size_hint`. `ReplayBody` is updated to cache the the inner body's size hint value in the shared state so it can be reused across all clones of the `ReplayBody` regardless of whether the current clone holds the body state or not.
…length... but that seems okay
| let has_body_header = req.headers().contains_key("transfer-encoding") | ||
| || req.headers().contains_key("content-length"); |
There was a problem hiding this comment.
I have no idea why this test--which only mentions transfer-encoding in its description--also checks for content-length... Now that we're properly preserving the size hint, we can set the content-length header to 0 if we know it's empty. This seems like a strictly good thing.
Currently, we look at each request's `content-length` header to determine whether a request is too large to buffer for retries. This is unnecessary, as hyper will already read the `content-length` and use it populate the `Body::size_hint` value. This change modifies the `ReplayBody` constructor (now `ReplayBody::try_new`) to check the body's size hint and fails to construct a `ReplayBody` if the lower bounds of the body size exceeds the maximum buffer size. In doing so, we update all `Body` implementations to ensure that `size_hint` is properly implemenented. We also update the `ReplayBody::size_hint` implementation to be more robust, returning the original size hint if a clone of `ReplayBody` does not currently hold the inner body.
Body::size_hint| .as_ref() | ||
| .map(|rest| { | ||
| let hint = rest.size_hint(); | ||
| (hint.lower(), hint.upper().unwrap_or(0)) |
There was a problem hiding this comment.
I think that hint.upper().unwrap_or(0) is a subtle bug -- if the lower bound is non-zero, we'll end up having an upper limit that is less than the lower limit, which is bad. It's better to omit the upper limit if the inner body does not specify it.
hawkw
left a comment
There was a problem hiding this comment.
yup, this seems good to me --- i hadn't realized that size_hint would be set from the request's content-length; i had assumed it was just the size of any currently-received body chunks. this definitely seems like the right approach since it will also let us make better decisions in the case where the whole body has been received but it didn't have a content-length.
i had some minor nits and suggestions, but no hard blockers --- let me know what you think?
| } | ||
| Either::B(req) | ||
| let (head, body) = req.into_parts(); | ||
| let replay_body = match ReplayBody::try_new(body, MAX_BUFFERED_BYTES) { |
There was a problem hiding this comment.
this is really not important at all, but i kind of feel like the prepare_request method should be responsible for checking the size_hint and ReplayBody should just have an unconditional new constructor...that kind of feels like a better separation of concerns to me, with the retry policy code being responsible for determining if the body is too large to retry, and ReplayBody just providing the buffering mechanism.
but, since ReplayBody does still need to be aware of the body size limit so that it can stop buffering if necessary, maybe this is fine...either way, it's not a big deal...
There was a problem hiding this comment.
I initially had it the way you suggest, but I think I liked to make it impossible to create a ReplayBody that is 'illegal' -- this means that the redundant check in clone_request becomes totally unnecessary: by virtue of having constructed a ReplayBody, we know that it doesn't violate our expectations on body size.
There was a problem hiding this comment.
fair enough, that's a good point!
| let replay_body = match ReplayBody::try_new(body, MAX_BUFFERED_BYTES) { | ||
| Ok(body) => body, | ||
| Err(body) => { | ||
| tracing::debug!( | ||
| size = body.size_hint().lower(), | ||
| "Body is too large to buffer" | ||
| ); | ||
| return Either::B(http::Request::from_parts(head, body)); | ||
| } | ||
| }; | ||
|
|
||
| // The body may still be too large to be buffered if the body's length was not known. | ||
| // `ReplayBody` handles this gracefully. | ||
| Either::A(http::Request::from_parts(head, replay_body)) |
There was a problem hiding this comment.
nit, take it or leave it: the early return here feels slightly convoluted, can't this just be
| let replay_body = match ReplayBody::try_new(body, MAX_BUFFERED_BYTES) { | |
| Ok(body) => body, | |
| Err(body) => { | |
| tracing::debug!( | |
| size = body.size_hint().lower(), | |
| "Body is too large to buffer" | |
| ); | |
| return Either::B(http::Request::from_parts(head, body)); | |
| } | |
| }; | |
| // The body may still be too large to be buffered if the body's length was not known. | |
| // `ReplayBody` handles this gracefully. | |
| Either::A(http::Request::from_parts(head, replay_body)) | |
| match ReplayBody::try_new(body, MAX_BUFFERED_BYTES) { | |
| Ok(replay_body) => { | |
| // The body may still be too large to be buffered if the body's length was not known. | |
| // `ReplayBody` handles this gracefully. | |
| Either::A(http::Request::from_parts(head, replay_body)) | |
| >, | |
| Err(body) => { | |
| tracing::debug!( | |
| size = body.size_hint().lower(), | |
| "Body is too large to buffer" | |
| ); | |
| Either::B(http::Request::from_parts(head, body)) | |
| } | |
| } |
There was a problem hiding this comment.
yeah, I had it that way initially. but it feels like the secondary case is exceptional, so it feels appropriate to early return. don't feel strongly either way, though.
| if let Some(rest_upper) = rest_hint.upper() { | ||
| hint.set_upper(buffered + rest_upper); | ||
| } |
There was a problem hiding this comment.
hmm, if rest doesn't have an upper bound, can't we just use the max buffered length as the upper bound, instead of not returning one?
There was a problem hiding this comment.
But we don't know that it's actually the upper limit of the body, do we? My understanding is that ReplayBody should gracefully handle the case when the inner body exceeds the buffer capacity?
There was a problem hiding this comment.
ah, yeah, it's only the upper limit if we're a subsequent clone of the body --- if we're the initial body, we will continue reading body chunks if we go past the limit; we'll just stop buffering them. we probably could add a check here and only return the max length if we didn't get an upper bound from rest_hint and we know we're not the original body...but, this is probably fine as-is and it may not be worth bothering with that...
Currently, we look at each request's
content-lengthheader todetermine whether a request is too large to buffer for retries. This is
unnecessary, as hyper will already read the
content-lengthand use itpopulate the
Body::size_hintvalue.This change modifies the
ReplayBodyconstructor (nowReplayBody::try_new) to check the body's size hint and fails toconstruct a
ReplayBodyif the lower bounds of the body size exceedsthe maximum buffer size.
In doing so, we update all
Bodyimplementations to ensure thatsize_hintis properly implemenented. We also update theReplayBody::size_hintimplementation to be more robust, returning theoriginal size hint if a clone of
ReplayBodydoes not currently holdthe inner body.