Fix truncated values when the peer closes mid-response#77
Merged
Conversation
ianks
force-pushed
the
ik/exact-read-fixed-length-responses
branch
3 times, most recently
from
July 6, 2026 20:39
1756f5d to
6efed70
Compare
ianks
marked this pull request as ready for review
July 6, 2026 20:41
oosidat
approved these changes
Jul 6, 2026
mrattle
reviewed
Jul 6, 2026
mrattle
left a comment
There was a problem hiding this comment.
Just a small comment on how we're raising but otherwise this looks good to me.
| buffer | ||
| end | ||
|
|
||
| def raise_read_eof(expected, received) |
There was a problem hiding this comment.
The read and read_exact methods already rescue EOFError and then call error_on_request! in their rescue blocks. Can we just raise EOFError here instead and leverage that existing rescue?
down! (source) called from inside error_on_request! uses $ERROR_INFO for context, which we might be losing by raising with the raw string here.
Author
ianks
force-pushed
the
ik/exact-read-fixed-length-responses
branch
from
July 7, 2026 15:35
6efed70 to
d582ac5
Compare
Previously ConnectionManager#read / #read_exact did a single @sock.read(count) and used whatever came back. IO#read(count) on a blocking socket blocks until it has count bytes, accumulating across TCP chunks internally, and only hands back a shorter (non-nil) buffer or nil when the stream hits EOF. So if the server closed the connection partway through a response body (memcached restart, proxy timeout, connection reset), we read a truncated body and surfaced it as a valid, decodable value that then got returned/cached. read and read_exact now go through a read_bytes helper that raises EOFError on a short or nil read. That flows through their existing rescue EOFError => error_on_request!, which closes the dirty socket and retries on a fresh connection. Raising EOFError (rather than calling error_on_request! with a raw string) preserves the $ERROR_INFO context that down! relies on. No accumulation loop is needed: IO#read already fills to count or EOFs, so a short read unambiguously means the peer closed mid-response.
ianks
force-pushed
the
ik/exact-read-fixed-length-responses
branch
from
July 7, 2026 15:41
d582ac5 to
3dec4fe
Compare
mrattle
approved these changes
Jul 7, 2026
nherson
approved these changes
Jul 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Previously,
ConnectionManager#read/#read_exactdid a single@sock.read(count).IO#read(maxlen)is fread-like: it returns exactlymaxlenbytes unless the stream hits EOF, in which case it hands back a shorter (non-nil) buffer with no exception. So if the server closes the connection partway through a response body (memcached restart, proxy timeout, connection reset), we'd read a truncated body and surface it as a valid value, a decodable-but-wrong value that then gets returned/cached. This PR fixes that by reading until we have exactlycountbytes, and raising on a premature EOF so the dirty socket gets closed and retried instead of reused.The happy path is unchanged and allocation-free: a healthy socket fills
countin a single read, so we return that buffer directly. Only a short read (the mid-response EOF case) falls through to the accumulation loop.How it works
readandread_exactboth go through aread_byteshelper@sock.read(count), returned directly when it's already the full lengthnil/empty chunk beforecountbytes raises, which closes the socket and lets the client retry on a fresh connectionThe integration test uses a real TCP server that truncates the first response and closes the socket, then serves the full body on the retry. Before this change it returned the truncated value, now the client transparently retries and gets the right one.