From 6c84963297c156d5f2f4d05fb0c7a0a28cd74117 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Fri, 28 Aug 2026 20:52:15 +0100 Subject: [PATCH] readers: make NoCloser pass through WriteTo so io.Copy keeps its fast path NoCloser hides the Close method of an io.Reader but in doing so it also hid io.WriterTo if the underlying reader implemented it, forcing io.Copy to fall back to a buffered Read loop. Following io.NopCloser, return a variant which forwards WriteTo when the wrapped reader supports it. --- lib/readers/noclose.go | 18 +++++++++++++++++- lib/readers/noclose_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/readers/noclose.go b/lib/readers/noclose.go index dc36e8be2..7516ef317 100644 --- a/lib/readers/noclose.go +++ b/lib/readers/noclose.go @@ -7,16 +7,29 @@ type noClose struct { in io.Reader } -// Read implements io.Closer by passing it straight on +// Read implements io.Reader by passing it straight on func (nc noClose) Read(p []byte) (n int, err error) { return nc.in.Read(p) } +// noCloseWriterTo is a noClose which also forwards io.WriterTo +type noCloseWriterTo struct { + noClose +} + +// WriteTo implements io.WriterTo by passing it straight on +func (nc noCloseWriterTo) WriteTo(w io.Writer) (n int64, err error) { + return nc.in.(io.WriterTo).WriteTo(w) +} + // NoCloser makes sure that the io.Reader passed in can't upgraded to // an io.Closer. // // This is for use with http.NewRequest to make sure the body doesn't // get upgraded to an io.Closer and the body closed unexpectedly. +// +// If in implements io.WriterTo then the returned reader does too so +// that io.Copy can still use the more efficient path. func NoCloser(in io.Reader) io.Reader { if in == nil { return in @@ -25,5 +38,8 @@ func NoCloser(in io.Reader) io.Reader { if _, canClose := in.(io.Closer); !canClose { return in } + if _, canWriteTo := in.(io.WriterTo); canWriteTo { + return noCloseWriterTo{noClose{in: in}} + } return noClose{in: in} } diff --git a/lib/readers/noclose_test.go b/lib/readers/noclose_test.go index e954d9c72..6bfa43b34 100644 --- a/lib/readers/noclose_test.go +++ b/lib/readers/noclose_test.go @@ -26,6 +26,14 @@ func (readClose) Close() (err error) { return io.EOF } +type readCloseWriteTo struct { + readClose +} + +func (readCloseWriteTo) WriteTo(w io.Writer) (n int64, err error) { + return 42, errRead +} + func TestNoCloser(t *testing.T) { assert.Equal(t, nil, NoCloser(nil)) @@ -41,4 +49,20 @@ func TestNoCloser(t *testing.T) { _, err := nc.Read(nil) assert.Equal(t, errRead, err) + + _, hasWriteTo := nc.(io.WriterTo) + assert.False(t, hasWriteTo) + + rcw := readCloseWriteTo{} + ncw := NoCloser(rcw) + assert.NotEqual(t, ncw, rcw) + + _, hasClose = ncw.(io.Closer) + assert.False(t, hasClose) + + wt, hasWriteTo := ncw.(io.WriterTo) + assert.True(t, hasWriteTo) + n, err := wt.WriteTo(nil) + assert.Equal(t, int64(42), n) + assert.Equal(t, errRead, err) }