Skip to content

Commit f5c43b9

Browse files
committed
net/http: add func NewRequestWithContext, Request.Clone
Fixes #23544 Change-Id: Iaa31d76c4cda8ce22412d73c9025fc57e4fb1967 Reviewed-on: https://go-review.googlesource.com/c/go/+/174324 Reviewed-by: Andrew Bonventre <[email protected]>
1 parent 5e404b3 commit f5c43b9

File tree

4 files changed

+122
-21
lines changed

4 files changed

+122
-21
lines changed

src/net/http/client_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -317,8 +317,7 @@ func TestClientRedirectContext(t *testing.T) {
317317
return errors.New("redirected request's context never expired after root request canceled")
318318
}
319319
}
320-
req, _ := NewRequest("GET", ts.URL, nil)
321-
req = req.WithContext(ctx)
320+
req, _ := NewRequestWithContext(ctx, "GET", ts.URL, nil)
322321
_, err := c.Do(req)
323322
ue, ok := err.(*url.Error)
324323
if !ok {

src/net/http/clone.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2019 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package http
6+
7+
import (
8+
"mime/multipart"
9+
"net/textproto"
10+
"net/url"
11+
)
12+
13+
func cloneURLValues(v url.Values) url.Values {
14+
if v == nil {
15+
return nil
16+
}
17+
// http.Header and url.Values have the same representation, so temporarily
18+
// treat it like http.Header, which does have a clone:
19+
return url.Values(Header(v).Clone())
20+
}
21+
22+
func cloneURL(u *url.URL) *url.URL {
23+
if u == nil {
24+
return nil
25+
}
26+
u2 := new(url.URL)
27+
*u2 = *u
28+
if u.User != nil {
29+
u2.User = new(url.Userinfo)
30+
*u2.User = *u.User
31+
}
32+
return u2
33+
}
34+
35+
func cloneMultipartForm(f *multipart.Form) *multipart.Form {
36+
if f == nil {
37+
return nil
38+
}
39+
f2 := &multipart.Form{
40+
Value: (map[string][]string)(Header(f.Value).Clone()),
41+
}
42+
if f.File != nil {
43+
m := make(map[string][]*multipart.FileHeader)
44+
for k, vv := range f.File {
45+
vv2 := make([]*multipart.FileHeader, len(vv))
46+
for i, v := range vv {
47+
vv2[i] = cloneMultipartFileHeader(v)
48+
}
49+
m[k] = vv2
50+
}
51+
f2.File = m
52+
}
53+
return f2
54+
}
55+
56+
func cloneMultipartFileHeader(fh *multipart.FileHeader) *multipart.FileHeader {
57+
if fh == nil {
58+
return nil
59+
}
60+
fh2 := new(multipart.FileHeader)
61+
*fh2 = *fh
62+
fh2.Header = textproto.MIMEHeader(Header(fh.Header).Clone())
63+
return fh2
64+
}

src/net/http/httputil/reverseproxy.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,11 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
196196
}()
197197
}
198198

199-
outreq := req.WithContext(ctx) // includes shallow copies of maps, but okay
199+
outreq := req.Clone(ctx)
200200
if req.ContentLength == 0 {
201201
outreq.Body = nil // Issue 16036: nil Body for http.Transport retries
202202
}
203203

204-
outreq.Header = req.Header.Clone()
205-
206204
p.Director(outreq)
207205
outreq.Close = false
208206

src/net/http/request.go

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ type Request struct {
304304
//
305305
// For server requests, this field is not applicable.
306306
//
307-
// Deprecated: Use the Context and WithContext methods
307+
// Deprecated: Set the Request's context with NewRequestWithContext
308308
// instead. If a Request's Cancel field and context are both
309309
// set, it is undefined whether Cancel is respected.
310310
Cancel <-chan struct{}
@@ -345,23 +345,50 @@ func (r *Request) Context() context.Context {
345345
// For outgoing client request, the context controls the entire
346346
// lifetime of a request and its response: obtaining a connection,
347347
// sending the request, and reading the response headers and body.
348+
//
349+
// To create a new request with a context, use NewRequestWithContext.
350+
// To change the context of a request (such as an incoming) you then
351+
// also want to modify to send back out, use Request.Clone. Between
352+
// those two uses, it's rare to need WithContext.
348353
func (r *Request) WithContext(ctx context.Context) *Request {
349354
if ctx == nil {
350355
panic("nil context")
351356
}
352357
r2 := new(Request)
353358
*r2 = *r
354359
r2.ctx = ctx
360+
r2.URL = cloneURL(r.URL) // legacy behavior; TODO: try to remove. Issue 23544
361+
return r2
362+
}
355363

356-
// Deep copy the URL because it isn't
357-
// a map and the URL is mutable by users
358-
// of WithContext.
359-
if r.URL != nil {
360-
r2URL := new(url.URL)
361-
*r2URL = *r.URL
362-
r2.URL = r2URL
364+
// Clone returns a deep copy of r with its context changed to ctx.
365+
// The provided ctx must be non-nil.
366+
//
367+
// For an outgoing client request, the context controls the entire
368+
// lifetime of a request and its response: obtaining a connection,
369+
// sending the request, and reading the response headers and body.
370+
func (r *Request) Clone(ctx context.Context) *Request {
371+
if ctx == nil {
372+
panic("nil context")
363373
}
364-
374+
r2 := new(Request)
375+
*r2 = *r
376+
r2.ctx = ctx
377+
r2.URL = cloneURL(r.URL)
378+
if r.Header != nil {
379+
r2.Header = r.Header.Clone()
380+
}
381+
if r.Trailer != nil {
382+
r2.Trailer = r.Trailer.Clone()
383+
}
384+
if s := r.TransferEncoding; s != nil {
385+
s2 := make([]string, len(s))
386+
copy(s2, s)
387+
r2.TransferEncoding = s
388+
}
389+
r2.Form = cloneURLValues(r.Form)
390+
r2.PostForm = cloneURLValues(r.PostForm)
391+
r2.MultipartForm = cloneMultipartForm(r.MultipartForm)
365392
return r2
366393
}
367394

@@ -781,25 +808,34 @@ func validMethod(method string) bool {
781808
return len(method) > 0 && strings.IndexFunc(method, isNotToken) == -1
782809
}
783810

784-
// NewRequest returns a new Request given a method, URL, and optional body.
811+
// NewRequest wraps NewRequestWithContext using the background context.
812+
func NewRequest(method, url string, body io.Reader) (*Request, error) {
813+
return NewRequestWithContext(context.Background(), method, url, body)
814+
}
815+
816+
// NewRequestWithContext returns a new Request given a method, URL, and
817+
// optional body.
785818
//
786819
// If the provided body is also an io.Closer, the returned
787820
// Request.Body is set to body and will be closed by the Client
788821
// methods Do, Post, and PostForm, and Transport.RoundTrip.
789822
//
790-
// NewRequest returns a Request suitable for use with Client.Do or
791-
// Transport.RoundTrip. To create a request for use with testing a
792-
// Server Handler, either use the NewRequest function in the
823+
// NewRequestWithContext returns a Request suitable for use with
824+
// Client.Do or Transport.RoundTrip. To create a request for use with
825+
// testing a Server Handler, either use the NewRequest function in the
793826
// net/http/httptest package, use ReadRequest, or manually update the
794-
// Request fields. See the Request type's documentation for the
795-
// difference between inbound and outbound request fields.
827+
// Request fields. For an outgoing client request, the context
828+
// controls the entire lifetime of a request and its response:
829+
// obtaining a connection, sending the request, and reading the
830+
// response headers and body. See the Request type's documentation for
831+
// the difference between inbound and outbound request fields.
796832
//
797833
// If body is of type *bytes.Buffer, *bytes.Reader, or
798834
// *strings.Reader, the returned request's ContentLength is set to its
799835
// exact value (instead of -1), GetBody is populated (so 307 and 308
800836
// redirects can replay the body), and Body is set to NoBody if the
801837
// ContentLength is 0.
802-
func NewRequest(method, url string, body io.Reader) (*Request, error) {
838+
func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {
803839
if method == "" {
804840
// We document that "" means "GET" for Request.Method, and people have
805841
// relied on that from NewRequest, so keep that working.
@@ -809,6 +845,9 @@ func NewRequest(method, url string, body io.Reader) (*Request, error) {
809845
if !validMethod(method) {
810846
return nil, fmt.Errorf("net/http: invalid method %q", method)
811847
}
848+
if ctx == nil {
849+
return nil, errors.New("net/http: nil Context")
850+
}
812851
u, err := parseURL(url) // Just url.Parse (url is shadowed for godoc).
813852
if err != nil {
814853
return nil, err
@@ -820,6 +859,7 @@ func NewRequest(method, url string, body io.Reader) (*Request, error) {
820859
// The host's colon:port should be normalized. See Issue 14836.
821860
u.Host = removeEmptyPort(u.Host)
822861
req := &Request{
862+
ctx: ctx,
823863
Method: method,
824864
URL: u,
825865
Proto: "HTTP/1.1",

0 commit comments

Comments
 (0)