-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
io: ReadFull should not forward io.ErrUnexpectedEOF from underlying reader #47677
Comments
CC @ianlancetaylor, @rsc. |
I agree that we can't change this at this point: I'm curious how you are using io.ReadFull that this matters, though. Looking at this from a different perspective, it's really only ever expected that So maybe the problem is in the specific Reader you are using: |
We had a (pooled) buffer to read into, and a data source of unknown size. We wanted to read as much as possible into the buffer, until either the buffer was full (oops, grab a bigger buffer) or we reached EOF. I believe the code in question was using ReadFull because it was the most obvious convenience function in package io that let you bring a buffer. And it worked fine, modulo this issue. The fix was to write our own custom read loop. That was probably always the right answer, but it was a surprise to find a little footgun lurking in ReadFull, thus this issue. It might be a nice addition to package io. (The semantics of Read means that read helpers are required for ~all uses of io.Reader.)
In the case that inspired this issue, the underlying reader is net/http.Request.Body. If the connection is closed during a read, it returns ErrUnexpectedEOF. We can’t change that either. |
The code that @josharian is describing looked something like: var buf []byte
switch n1, err := io.ReadFull(r, smallBuf); err {
case io.EOF, io.ErrUnexpectedEOF:
buf = smallBuf[:n]
case nil:
switch n2, err := io.ReadFull(r, bigBuf[n1:]); err {
case io.EOF, io.ErrUnexpectedEOF:
copy(bigBuf[:n1], smallBuf[:n1])
buf = bigBuf[:n1+n2]
case nil:
return errors.New("input too large")
default:
return err
}
default:
return err
} This code is really weird because the error conditions are entirely flipped (i.e., nil is an error, while We ended up writing our own // ReadUntilEOF continually reads r into b until io.EOF.
// It returns nil if io.EOF is encountered.
// It returns io.ErrShortBuffer if the entirety of the buffer is filled,
// which indicates that a larger buffer is needed.
func ReadUntilEOF(r io.Reader, b []byte) (n int, err error) {
for n < len(b) && err == nil {
var nn int
nn, err = r.Read(b[n:])
n += nn
}
switch {
case err == io.EOF:
return n, nil
case n == len(buf):
return n, io.ErrShortBuffer
default:
return n, err
}
} Using var buf []byte
switch n1, err := ReadUntilEOF(r, smallBuf); err {
case nil:
buf = smallBuf[:n]
case io.ErrShortBuffer:
switch n2, err := io.ReadFull(bigBuf[n1:]); err {
case nil:
copy(bigBuf[:n1], smallBuf[:n1])
buf = bigBuf[:n1+n2]
case io.ErrShortBuffer:
return errors.New("input too large")
default:
return err
}
default:
return err
} I doubt our use-case of needing to read as much of |
The logic for
io.ReadFull
returns errors from the underlyingio.Reader
verbatim.In the case where
io.Reader
returnsio.ErrUnexpectedEOF
, it is impossible for the caller ofio.ReadFull
to distinguish between whether:io.Reader
abruptly ended and is in a truncated state, orio.Reader
ended correctly and there is no more data.To avoid this ambiguity, we should wrap any
io.ErrUnexpectedEOF
from the underlyingio.Reader
.However, I suspect that this is a breaking change and the best that we can hope for is to just document this.
\cc @josharian @catzkorn
The text was updated successfully, but these errors were encountered: