-
Notifications
You must be signed in to change notification settings - Fork 716
Description
When you use PutObject with an io.Reader which doesn't implement Seek (i.e. not a io.ReadSeeker) like bytes.Buffer, if you receive any kind of retry-able error on uploading the object you would see errors like:
net/http: HTTP/1.x transport connection broken: http: ContentLength=XXX with Body length 0
As far as I can tell this is because the Progress option even when nil is used to create a hookReader wrapping the original io.Reader which does implement io.ReadSeeker and results in attempts to Seek using the fall through logic of the hookReader which if wrapping a source which doesn't implement io.Seeker cannot actually seek to the appropriate offset (usually the beginning) used in the retry logic.
I took a look and I believe the original intention was to just disallow retries for io.Reader input which doesn't implement io.Seeker but with the progress wrapper, effectively the code path that disallows retries for non-io.Seeker implementations does not ever get triggered.
I think there are 3 problems here:
- It's not clear to a user (while admittedly being somewhat obvious after thinking about it for a bit) that using a non-
io.Seekeras input should result in no retries. - Using an input that doesn't implement
io.Seekerin fact does result in a retry which could never succeed and throws the very cryptic error message mentioned above. - Using the
SendContentMd5option wraps anyio.Readerwhich doesn't implementio.Seekerwith aio.ReadSeekerimplementation which effectively papers over the problem as a side-effect.
Reproduction example:
Details
// This code has to be run against something that will serve up a retry-able error for the PutObject calls to demonstrate the problem.
// this will retry the upload operation
func UploadWithReadSeeker(client *minio.Client, bucketName string, rawData []byte) error {
readSeeker, size := bytes.NewReader(rawData), int64(len(rawData))
_, err := client.PutObject(context.Background(), bucketName, "with-read-seeker", readSeeker, size, minio.PutObjectOptions{})
return errors.Wrap(err, "failed to upload object")
}
// this will attempt to retry the upload operation and immediately fail
func UploadWithReader(client *minio.Client, bucketName string, rawData []byte) error {
reader, size := bytes.NewBuffer(rawData), int64(len(rawData))
_, err := client.PutObject(context.Background(), bucketName, "with-reader", reader, size, minio.PutObjectOptions{})
return errors.Wrap(err, "failed to upload object")
}
// this will retry the upload operation
func UploadWithReaderWMd5Hash(client *minio.Client, bucketName string, rawData []byte) error {
reader, size := bytes.NewBuffer(rawData), int64(len(rawData))
_, err := client.PutObject(context.Background(), bucketName, "with-reader", reader, size, minio.PutObjectOptions{
SendContentMd5: true,
})
return errors.Wrap(err, "failed to upload object")
}Investigation notes:
Perhaps introduced accidentally in #1673
Having and not having progress hook shouldn't really influence the retry behavior. Perhaps it's best to implement two hookReader types, one which is a pure one-shot io.Reader and another which is a io.ReadSeeker and match according to the input type?