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

Zero-length responses break connection reuse #555

Closed
njsmith opened this issue Nov 17, 2022 · 0 comments · Fixed by #565
Closed

Zero-length responses break connection reuse #555

njsmith opened this issue Nov 17, 2022 · 0 comments · Fixed by #565

Comments

@njsmith
Copy link

njsmith commented Nov 17, 2022

Here's the code for handling responses with fixed-length bodies:

ureq/src/response.rs

Lines 325 to 343 in 28d667a

// Responses with a content-length header means we should limit the reading
// of the body to the number of bytes in the header. Once done, we can
// return the underlying stream to the connection pool.
(false, Some(len)) => {
let mut pooler =
PoolReturnRead::new(&unit.agent, &unit.url, LimitedRead::new(stream, len));
if len <= buffer_len {
debug!("Body entirely buffered (length: {})", len);
let mut buf = vec![0; len];
pooler
.read_exact(&mut buf)
.expect("failed to read exact buffer length from stream");
Box::new(Cursor::new(buf))
} else {
debug!("Streaming body until content-length: {}", len);
Box::new(pooler)
}
}

We set up the PoolReturnRead, and then if the body is short enough, we call .read_exact on the PoolReturnRead to both fetch the body and, as a side-effect, force PoolReturnRead to notice that the body has been consumed and that it's free to return the connection to the pool.

However, if the response body is empty (e.g. on a 304 Not Modified), then len is 0, and we call read_exact on a zero-length buffer. In this case, the default implementation of std::io::Read::read_exact exits immediately. In particular, it does not call PoolReturnRead::read at all, so PoolReturnRead has no chance to notice that the stream has been exhausted. Then we drop the PoolReturnRead, and since it believes the body hasn't been exhausted, it drops the underlying connection and on our next request we'll have to set up a new connection from scratch.

(Ironically, this seems to make my program slower on a warm cache than a cold cache, because in many cases fetching the data is cheaper than restarting the connection.)

njsmith added a commit to njsmith/ureq that referenced this issue Nov 17, 2022
For empty response bodies, there's no guarantee that
`PoolReturnRead::read` will ever be called, so we might never notice
that the body is exhausted and can be returned to the pool. Solve this
by adding an exhaustion check to `PoolReturnRead::new`. Annoyingly, this
requires changing its API (adding a new trait bound + making `new`
fallible), but fortunately it's an internal API and the callers are easy
to update.

Fixes algesten#555
This was referenced Nov 26, 2022
@jsha jsha closed this as completed in #565 Dec 4, 2022
jsha added a commit that referenced this issue Dec 4, 2022
Introduce PoolReturner, a handle on an agent and a PoolKey that is
capable of returning a Stream to a Pool. Make Streams keep track of
their own PoolReturner, instead of having PoolReturnRead keep track of
that information.

For the LimitedRead code path, get rid of PoolReturnRead. Instead,
LimitedRead is responsible for returning its Stream to the Pool after
its second-to-last read. In other words, LimitedRead will return the
stream if the next read is guaranteed to return Ok(0).

Constructing a LimitedRead of size 0 is always wrong, because we could
always just return the stream immediately. Change the size argument to
NonZeroUsize to enforce that.

Remove the Done trait, which was only used for LimitedRead. It was used
to try and make sure we returned the stream to the pool on exact reads,
but was not reliable.

This does not yet move the ChunkDecoder code path away from
PoolReturnRead. That requires a little more work.

Part 1 of #559.  Fixes #555.
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 a pull request may close this issue.

1 participant