Skip to content

Commit fc6d68a

Browse files
committed
net/http: optimize Transport request body writes
net.TCPConn has the ability to send data out using system calls such as sendfile when the source data comes from an *os.File. However, the way that I/O has been laid out in the transport means that the File is actually wrapped behind two outer io.Readers, and as such the TCP stack cannot properly type-assert the reader, ensuring that it falls back to genericReadFrom. This commit does the following: * Removes transferBodyReader and moves its functionality to a new doBodyCopy helper. This is not an io.Reader implementation, but no functionality is lost this way, and it allows us to unwrap one layer from the body. * The second layer of the body is unwrapped if the original writer was wrapped with ioutil.NopCloser, which is what NewRequest wraps the body in if it's not a ReadCloser on its own. The unwrap operation passes through the existing body if there's no nopCloser. Note that this depends on change https://golang.org/cl/163737 to properly function, as the lack of ReaderFrom implementation otherwise means that this functionality is essentially walled off. Updates #30377.
1 parent 01f34cb commit fc6d68a

File tree

2 files changed

+220
-19
lines changed

2 files changed

+220
-19
lines changed

src/net/http/transfer.go

+34-19
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,6 @@ func (br *byteReader) Read(p []byte) (n int, err error) {
5353
return 1, io.EOF
5454
}
5555

56-
// transferBodyReader is an io.Reader that reads from tw.Body
57-
// and records any non-EOF error in tw.bodyReadError.
58-
// It is exactly 1 pointer wide to avoid allocations into interfaces.
59-
type transferBodyReader struct{ tw *transferWriter }
60-
61-
func (br transferBodyReader) Read(p []byte) (n int, err error) {
62-
n, err = br.tw.Body.Read(p)
63-
if err != nil && err != io.EOF {
64-
br.tw.bodyReadError = err
65-
}
66-
return
67-
}
68-
6956
// transferWriter inspects the fields of a user-supplied Request or Response,
7057
// sanitizes them without changing the user object and provides methods for
7158
// writing the respective header, body and trailer in wire format.
@@ -347,15 +334,18 @@ func (t *transferWriter) writeBody(w io.Writer) error {
347334
var err error
348335
var ncopy int64
349336

350-
// Write body
337+
// Write body. We "unwrap" the body first if it was wrapped in a
338+
// nopCloser. This is to ensure that we can take advantage of
339+
// OS-level optimizations in the event that the body is an
340+
// *os.File.
351341
if t.Body != nil {
352-
var body = transferBodyReader{t}
342+
var body = t.unwrapBody()
353343
if chunked(t.TransferEncoding) {
354344
if bw, ok := w.(*bufio.Writer); ok && !t.IsResponse {
355345
w = &internal.FlushAfterChunkWriter{Writer: bw}
356346
}
357347
cw := internal.NewChunkedWriter(w)
358-
_, err = io.Copy(cw, body)
348+
_, err = t.doBodyCopy(cw, body)
359349
if err == nil {
360350
err = cw.Close()
361351
}
@@ -364,14 +354,14 @@ func (t *transferWriter) writeBody(w io.Writer) error {
364354
if t.Method == "CONNECT" {
365355
dst = bufioFlushWriter{dst}
366356
}
367-
ncopy, err = io.Copy(dst, body)
357+
ncopy, err = t.doBodyCopy(dst, body)
368358
} else {
369-
ncopy, err = io.Copy(w, io.LimitReader(body, t.ContentLength))
359+
ncopy, err = t.doBodyCopy(w, io.LimitReader(body, t.ContentLength))
370360
if err != nil {
371361
return err
372362
}
373363
var nextra int64
374-
nextra, err = io.Copy(ioutil.Discard, body)
364+
nextra, err = t.doBodyCopy(ioutil.Discard, body)
375365
ncopy += nextra
376366
}
377367
if err != nil {
@@ -402,6 +392,31 @@ func (t *transferWriter) writeBody(w io.Writer) error {
402392
return err
403393
}
404394

395+
// doBodyCopy wraps a copy operation, with any resulting error also
396+
// being saved in bodyReadError.
397+
//
398+
// This function is only intended for use in writeBody.
399+
func (t *transferWriter) doBodyCopy(dst io.Writer, src io.Reader) (n int64, err error) {
400+
n, err = io.Copy(dst, src)
401+
if err != nil && err != io.EOF {
402+
t.bodyReadError = err
403+
}
404+
return
405+
}
406+
407+
// unwrapBodyReader unwraps the body's inner reader if it's a
408+
// nopCloser. This is to ensure that body writes sourced from local
409+
// files (*os.File types) are properly optimized.
410+
//
411+
// This function is only intended for use in writeBody.
412+
func (t *transferWriter) unwrapBody() io.Reader {
413+
if reflect.TypeOf(t.Body) == nopCloserType {
414+
return reflect.ValueOf(t.Body).Field(0).Interface().(io.Reader)
415+
}
416+
417+
return t.Body
418+
}
419+
405420
type transferReader struct {
406421
// Input
407422
Header Header

src/net/http/transfer_test.go

+186
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"bytes"
1010
"io"
1111
"io/ioutil"
12+
"os"
13+
"reflect"
1214
"strings"
1315
"testing"
1416
)
@@ -90,3 +92,187 @@ func TestDetectInMemoryReaders(t *testing.T) {
9092
}
9193
}
9294
}
95+
96+
type mockTransferWriterBodyWriter struct {
97+
CalledReader io.Reader
98+
WriteCalled bool
99+
}
100+
101+
func (w *mockTransferWriterBodyWriter) ReadFrom(r io.Reader) (int64, error) {
102+
w.CalledReader = r
103+
return io.Copy(ioutil.Discard, r)
104+
}
105+
106+
func (w *mockTransferWriterBodyWriter) Write(p []byte) (int, error) {
107+
w.WriteCalled = true
108+
return ioutil.Discard.Write(p)
109+
}
110+
111+
func TestTransferWriterWriteBodyReaderTypes(t *testing.T) {
112+
fileTyp := reflect.TypeOf(&os.File{})
113+
bufferTyp := reflect.TypeOf(&bytes.Buffer{})
114+
115+
newFileFunc := func() (io.Reader, func(), error) {
116+
f, err := ioutil.TempFile("", "net-http-testtransferwriterwritebodyreadertypes")
117+
if err != nil {
118+
return nil, nil, err
119+
}
120+
121+
// 1K zeros just to get a file that we can read
122+
_, err = f.Write(make([]byte, 1024))
123+
f.Close()
124+
if err != nil {
125+
return nil, nil, err
126+
}
127+
128+
f, err = os.Open(f.Name())
129+
if err != nil {
130+
return nil, nil, err
131+
}
132+
133+
return f, func() {
134+
f.Close()
135+
os.Remove(f.Name())
136+
}, nil
137+
}
138+
139+
newBufferFunc := func() (io.Reader, func(), error) {
140+
return bytes.NewBuffer(make([]byte, 1024)), func() {}, nil
141+
}
142+
143+
cases := []struct {
144+
Name string
145+
BodyFunc func() (io.Reader, func(), error)
146+
Method string
147+
ContentLength int64
148+
TransferEncoding []string
149+
LimitedReader bool
150+
ExpectedReader reflect.Type
151+
ExpectedWrite bool
152+
}{
153+
{
154+
Name: "file, non-chunked, size set",
155+
BodyFunc: newFileFunc,
156+
Method: "PUT",
157+
ContentLength: 1024,
158+
LimitedReader: true,
159+
ExpectedReader: fileTyp,
160+
},
161+
{
162+
Name: "file, non-chunked, size set, nopCloser wrapped",
163+
Method: "PUT",
164+
BodyFunc: func() (io.Reader, func(), error) {
165+
r, cleanup, err := newFileFunc()
166+
return ioutil.NopCloser(r), cleanup, err
167+
},
168+
ContentLength: 1024,
169+
LimitedReader: true,
170+
ExpectedReader: fileTyp,
171+
},
172+
{
173+
Name: "file, non-chunked, negative size",
174+
Method: "PUT",
175+
BodyFunc: newFileFunc,
176+
ContentLength: -1,
177+
ExpectedReader: fileTyp,
178+
},
179+
{
180+
Name: "file, non-chunked, CONNECT, negative size",
181+
Method: "CONNECT",
182+
BodyFunc: newFileFunc,
183+
ContentLength: -1,
184+
ExpectedReader: fileTyp,
185+
},
186+
{
187+
Name: "file, chunked",
188+
Method: "PUT",
189+
BodyFunc: newFileFunc,
190+
TransferEncoding: []string{"chunked"},
191+
ExpectedWrite: true,
192+
},
193+
{
194+
Name: "buffer, non-chunked, size set",
195+
BodyFunc: newBufferFunc,
196+
Method: "PUT",
197+
ContentLength: 1024,
198+
LimitedReader: true,
199+
ExpectedReader: bufferTyp,
200+
},
201+
{
202+
Name: "buffer, non-chunked, size set, nopCloser wrapped",
203+
Method: "PUT",
204+
BodyFunc: func() (io.Reader, func(), error) {
205+
r, cleanup, err := newBufferFunc()
206+
return ioutil.NopCloser(r), cleanup, err
207+
},
208+
ContentLength: 1024,
209+
LimitedReader: true,
210+
ExpectedReader: bufferTyp,
211+
},
212+
{
213+
Name: "buffer, non-chunked, negative size",
214+
Method: "PUT",
215+
BodyFunc: newBufferFunc,
216+
ContentLength: -1,
217+
ExpectedWrite: true,
218+
},
219+
{
220+
Name: "buffer, non-chunked, CONNECT, negative size",
221+
Method: "CONNECT",
222+
BodyFunc: newBufferFunc,
223+
ContentLength: -1,
224+
ExpectedWrite: true,
225+
},
226+
{
227+
Name: "buffer, chunked",
228+
Method: "PUT",
229+
BodyFunc: newBufferFunc,
230+
TransferEncoding: []string{"chunked"},
231+
ExpectedWrite: true,
232+
},
233+
}
234+
235+
for _, tc := range cases {
236+
t.Run(tc.Name, func(t *testing.T) {
237+
body, cleanup, err := tc.BodyFunc()
238+
if err != nil {
239+
t.Fatal(err)
240+
}
241+
242+
defer cleanup()
243+
244+
mw := &mockTransferWriterBodyWriter{}
245+
tw := &transferWriter{
246+
Body: body,
247+
ContentLength: tc.ContentLength,
248+
TransferEncoding: tc.TransferEncoding,
249+
}
250+
251+
if err := tw.writeBody(mw); err != nil {
252+
t.Fatal(err)
253+
}
254+
255+
if tc.ExpectedReader != nil {
256+
if mw.CalledReader == nil {
257+
t.Fatal("expected ReadFrom to be called, but it wasn't")
258+
}
259+
260+
var actualReader reflect.Type
261+
lr, ok := mw.CalledReader.(*io.LimitedReader)
262+
if ok && tc.LimitedReader {
263+
actualReader = reflect.TypeOf(lr.R)
264+
} else {
265+
actualReader = reflect.TypeOf(mw.CalledReader)
266+
}
267+
268+
if tc.ExpectedReader != actualReader {
269+
t.Fatalf("expected reader to be %s, got %s", tc.ExpectedReader, actualReader)
270+
}
271+
}
272+
273+
if tc.ExpectedWrite && !mw.WriteCalled {
274+
t.Fatal("expected Read to be called, but it wasn't")
275+
}
276+
})
277+
}
278+
}

0 commit comments

Comments
 (0)