Skip to content

Commit 4fb7297

Browse files
authored
fix: adapt multipart stream to support go1.27 io.EOF change (#521)
Signed-off-by: Frederic BIDON <fredbi@yahoo.com>
1 parent d7ec5a2 commit 4fb7297

3 files changed

Lines changed: 54 additions & 2 deletions

File tree

‎.golangci.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ linters:
55
- depguard
66
- err113 # disabled temporarily: there are just too many issues to address
77
- exhaustruct
8+
- exhaustruct_v5
89
- funlen
910
- gochecknoglobals
1011
- gochecknoinits

‎multipart_stream.go‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,10 @@ func (s *MultipartFormStream) Drain() error {
342342
// particular, a net/http server request body may discard a limited amount of
343343
// unread data to allow connection reuse, so Close is not guaranteed to return
344344
// immediately. Call Drain when trailing form fields must be collected.
345+
//
346+
// Close returns the error reported by the request body, except io.EOF: a
347+
// net/http server body reports io.EOF once it discards that unread remainder,
348+
// which tells us the body ended, not that closing it failed.
345349
func (s *MultipartFormStream) Close() error {
346350
if s == nil || s.closed {
347351
return nil
@@ -357,7 +361,14 @@ func (s *MultipartFormStream) Close() error {
357361
return nil
358362
}
359363

360-
return s.request.Body.Close()
364+
// Since go1.27.0, net/http (*body).Close returns io.EOF when it discards
365+
// the unread remainder of the request itself. Reaching the end of the body
366+
// is not a close failure.
367+
if err := s.request.Body.Close(); err != nil && !stderrors.Is(err, io.EOF) {
368+
return err
369+
}
370+
371+
return nil
361372
}
362373

363374
func supportsMultipartFormStream(method string) bool {

‎multipart_stream_test.go‎

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,44 @@ func TestMultipartFormStreamDrainCollectsTrailingFieldsAndClosesBody(t *testing.
325325
require.ErrorIs(t, err, io.EOF)
326326
}
327327

328+
func TestMultipartFormStreamCloseIgnoresBodyCloseEOF(t *testing.T) {
329+
// Since go1.27.0, a net/http server request body reports io.EOF from Close
330+
// when it discards the unread remainder of the request. The stream must not
331+
// pass that on as a failure.
332+
body, contentType := orderedMultipartBody(t,
333+
orderedFile{field: testFieldFile, filename: streamedFilename, content: "payload"},
334+
orderedField{name: "after", value: "value"},
335+
)
336+
trackedBody := &observableReadCloser{Reader: body, closeErr: io.EOF}
337+
request := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, nil)
338+
request.Body = trackedBody
339+
request.Header.Set(HeaderContentType, contentType)
340+
stream, err := NewMultipartFormStream(request)
341+
require.NoError(t, err)
342+
343+
_, err = stream.NextFile()
344+
require.NoError(t, err)
345+
require.NoError(t, stream.Drain())
346+
assert.EqualT(t, "value", request.Form.Get("after"))
347+
assert.TrueT(t, trackedBody.Closed())
348+
}
349+
350+
func TestMultipartFormStreamCloseReportsBodyCloseError(t *testing.T) {
351+
body, contentType := orderedMultipartBody(t,
352+
orderedFile{field: testFieldFile, filename: streamedFilename, content: "payload"},
353+
)
354+
closeErr := stderrors.New("close failed")
355+
trackedBody := &observableReadCloser{Reader: body, closeErr: closeErr}
356+
request := httptest.NewRequestWithContext(t.Context(), http.MethodPost, testUploadPath, nil)
357+
request.Body = trackedBody
358+
request.Header.Set(HeaderContentType, contentType)
359+
stream, err := NewMultipartFormStream(request)
360+
require.NoError(t, err)
361+
362+
require.ErrorIs(t, stream.Close(), closeErr)
363+
assert.TrueT(t, trackedBody.Closed())
364+
}
365+
328366
func TestMultipartFormStreamCloseAbortsWithoutDraining(t *testing.T) {
329367
body, contentType := orderedMultipartBody(t,
330368
orderedFile{field: testFieldFile, filename: streamedFilename, content: "payload"},
@@ -626,6 +664,8 @@ func orderedMultipartBody(t *testing.T, parts ...orderedMultipartPart) (*bytes.B
626664
type observableReadCloser struct {
627665
io.Reader
628666

667+
closeErr error
668+
629669
mu sync.Mutex
630670
closed bool
631671
}
@@ -635,7 +675,7 @@ func (r *observableReadCloser) Close() error {
635675
defer r.mu.Unlock()
636676
r.closed = true
637677

638-
return nil
678+
return r.closeErr
639679
}
640680

641681
func (r *observableReadCloser) Closed() bool {

0 commit comments

Comments
 (0)