Skip to content

Commit

Permalink
merge upstream net/http: 2023-06-15(c54632)
Browse files Browse the repository at this point in the history
  • Loading branch information
imroc committed Jun 15, 2023
1 parent dee4e87 commit f417a9b
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 44 deletions.
7 changes: 6 additions & 1 deletion internal/transport/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ type Options struct {
// If Proxy is nil or returns a nil *URL, no proxy is used.
Proxy func(*http.Request) (*url.URL, error)

// OnProxyConnectResponse is called when the Transport gets an HTTP response from
// a proxy for a CONNECT request. It's called before the check for a 200 OK response.
// If it returns an error, the request fails with that error.
OnProxyConnectResponse func(ctx context.Context, proxyURL *url.URL, connectReq *http.Request, connectRes *http.Response) error

// DialContext specifies the dial function for creating unencrypted TCP connections.
// If DialContext is nil, then the transport dials using package net.
//
Expand Down Expand Up @@ -53,7 +58,7 @@ type Options struct {
// If non-nil, HTTP/2 support may not be enabled by default.
TLSClientConfig *tls.Config

// TLSHandshakeTimeout specifies the maximum amount of time waiting to
// TLSHandshakeTimeout specifies the maximum amount of time to
// wait for a TLS handshake. Zero means no timeout.
TLSHandshakeTimeout time.Duration

Expand Down
3 changes: 1 addition & 2 deletions roundtrip.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build !js || !wasm
// +build !js !wasm
//go:build !js

package req

Expand Down
142 changes: 121 additions & 21 deletions roundtrip_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.

//go:build js && wasm
// +build js,wasm

package req

Expand Down Expand Up @@ -46,6 +45,44 @@ const jsFetchRedirect = "js.fetch:redirect"
// the browser globals.
var jsFetchMissing = js.Global().Get("fetch").IsUndefined()

// jsFetchDisabled will be true if the "process" global is present.
// We use this as an indicator that we're running in Node.js. We
// want to disable the Fetch API in Node.js because it breaks
// our wasm tests. See https://go.dev/issue/57613 for more information.
var jsFetchDisabled = !js.Global().Get("process").IsUndefined()

// Determine whether the JS runtime supports streaming request bodies.
// Courtesy: https://developer.chrome.com/articles/fetch-streaming-requests/#feature-detection
func supportsPostRequestStreams() bool {
requestOpt := js.Global().Get("Object").New()
requestBody := js.Global().Get("ReadableStream").New()

requestOpt.Set("method", "POST")
requestOpt.Set("body", requestBody)

// There is quite a dance required to define a getter if you do not have the { get property() { ... } }
// syntax available. However, it is possible:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#defining_a_getter_on_existing_objects_using_defineproperty
duplexCalled := false
duplexGetterObj := js.Global().Get("Object").New()
duplexGetterFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
duplexCalled = true
return "half"
})
defer duplexGetterFunc.Release()
duplexGetterObj.Set("get", duplexGetterFunc)
js.Global().Get("Object").Call("defineProperty", requestOpt, "duplex", duplexGetterObj)

// Slight difference here between the aforementioned example: Non-browser-based runtimes
// do not have a non-empty API Base URL (https://html.spec.whatwg.org/multipage/webappapis.html#api-base-url)
// so we have to supply a valid URL here.
requestObject := js.Global().Get("Request").New("https://www.example.org", requestOpt)

hasContentTypeHeader := requestObject.Get("headers").Call("has", "Content-Type").Bool()

return duplexCalled && !hasContentTypeHeader
}

// RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
// The Transport has a documented contract that states that if the DialContext or
Expand All @@ -54,7 +91,7 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
// though they are deprecated. Therefore, if any of these are set, we should obey
// the contract and dial using the regular round-trip instead. Otherwise, we'll try
// to fall back on the Fetch API, unless it's not available.
if t.DialContext != nil || t.DialTLSContext != nil || jsFetchMissing {
if t.DialContext != nil || t.DialTLSContext != nil || jsFetchMissing || jsFetchDisabled {
return t.roundTrip(req)
}

Expand Down Expand Up @@ -94,23 +131,60 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
}
opt.Set("headers", headers)

var readableStreamStart, readableStreamPull, readableStreamCancel js.Func
if req.Body != nil {
// TODO(johanbrandhorst): Stream request body when possible.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue.
// See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue.
// See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue.
// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API
// and browser support.
body, err := io.ReadAll(req.Body)
if err != nil {
req.Body.Close() // RoundTrip must always close the body, including on errors.
return nil, err
}
req.Body.Close()
if len(body) != 0 {
buf := uint8Array.New(len(body))
js.CopyBytesToJS(buf, body)
opt.Set("body", buf)
if !supportsPostRequestStreams() {
body, err := io.ReadAll(req.Body)
if err != nil {
req.Body.Close() // RoundTrip must always close the body, including on errors.
return nil, err
}
if len(body) != 0 {
buf := uint8Array.New(len(body))
js.CopyBytesToJS(buf, body)
opt.Set("body", buf)
}
} else {
readableStreamCtorArg := js.Global().Get("Object").New()
readableStreamCtorArg.Set("type", "bytes")
readableStreamCtorArg.Set("autoAllocateChunkSize", t.writeBufferSize())

readableStreamPull = js.FuncOf(func(this js.Value, args []js.Value) any {
controller := args[0]
byobRequest := controller.Get("byobRequest")
if byobRequest.IsNull() {
controller.Call("close")
}

byobRequestView := byobRequest.Get("view")

bodyBuf := make([]byte, byobRequestView.Get("byteLength").Int())
readBytes, readErr := io.ReadFull(req.Body, bodyBuf)
if readBytes > 0 {
buf := uint8Array.New(byobRequestView.Get("buffer"))
js.CopyBytesToJS(buf, bodyBuf)
byobRequest.Call("respond", readBytes)
}

if readErr == io.EOF || readErr == io.ErrUnexpectedEOF {
controller.Call("close")
} else if readErr != nil {
readErrCauseObject := js.Global().Get("Object").New()
readErrCauseObject.Set("cause", readErr.Error())
readErr := js.Global().Get("Error").New("io.ReadFull failed while streaming POST body", readErrCauseObject)
controller.Call("error", readErr)
}
// Note: This a return from the pull callback of the controller and *not* RoundTrip().
return nil
})
readableStreamCtorArg.Set("pull", readableStreamPull)

opt.Set("body", js.Global().Get("ReadableStream").New(readableStreamCtorArg))
// There is a requirement from the WHATWG fetch standard that the duplex property of
// the object given as the options argument to the fetch call be set to 'half'
// when the body property of the same options object is a ReadableStream:
// https://fetch.spec.whatwg.org/#dom-requestinit-duplex
opt.Set("duplex", "half")
}
}

Expand All @@ -120,9 +194,14 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {
errCh = make(chan error, 1)
success, failure js.Func
)
success = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
success = js.FuncOf(func(this js.Value, args []js.Value) any {
success.Release()
failure.Release()
readableStreamCancel.Release()
readableStreamPull.Release()
readableStreamStart.Release()

req.Body.Close()

result := args[0]
header := http.Header{}
Expand Down Expand Up @@ -184,10 +263,31 @@ func (t *Transport) RoundTrip(req *http.Request) (*http.Response, error) {

return nil
})
failure = js.FuncOf(func(this js.Value, args []js.Value) interface{} {
failure = js.FuncOf(func(this js.Value, args []js.Value) any {
success.Release()
failure.Release()
errCh <- fmt.Errorf("net/http: fetch() failed: %s", args[0].Get("message").String())
readableStreamCancel.Release()
readableStreamPull.Release()
readableStreamStart.Release()

req.Body.Close()

err := args[0]
// The error is a JS Error type
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
// We can use the toString() method to get a string representation of the error.
errMsg := err.Call("toString").String()
// Errors can optionally contain a cause.
if cause := err.Get("cause"); !cause.IsUndefined() {
// The exact type of the cause is not defined,
// but if it's another error, we can call toString() on it too.
if !cause.Get("toString").IsUndefined() {
errMsg += ": " + cause.Call("toString").String()
} else if cause.Type() == js.TypeString {
errMsg += ": " + cause.String()
}
}
errCh <- fmt.Errorf("net/http: fetch() failed: %s", errMsg)
return nil
})

Expand Down
16 changes: 8 additions & 8 deletions transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -405,7 +405,7 @@ func (t *transferWriter) doBodyCopy(dst io.Writer, src io.Reader) (n int64, err
return
}

// unwrapBodyReader unwraps the body's inner reader if it's a
// unwrapBody unwraps the body's inner reader if it's a
// nopCloser. This is to ensure that body writes sourced from local
// files (*os.File types) are properly optimized.
//
Expand Down Expand Up @@ -520,7 +520,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
// or close connection when finished, since multipart is not supported yet
switch {
case t.Chunked:
if noResponseBodyExpected(t.RequestMethod) || !bodyAllowedForStatus(t.StatusCode) {
if isResponse && (noResponseBodyExpected(t.RequestMethod) || !bodyAllowedForStatus(t.StatusCode)) {
t.Body = NoBody
} else {
t.Body = &body{src: internal.NewChunkedReader(r), hdr: msg, r: r, closing: t.Close}
Expand Down Expand Up @@ -555,7 +555,7 @@ func readTransfer(msg interface{}, r *bufio.Reader) (err error) {
return nil
}

// Checks whether chunked is part of the encodings stack
// Checks whether chunked is part of the encodings stack.
func chunked(te []string) bool { return len(te) > 0 && te[0] == "chunked" }

// Checks whether the encoding is explicitly "identity".
Expand Down Expand Up @@ -590,7 +590,7 @@ func (t *transferReader) parseTransferEncoding() error {
if len(raw) != 1 {
return &unsupportedTEError{fmt.Sprintf("too many transfer encodings: %q", raw)}
}
if !ascii.EqualFold(textproto.TrimString(raw[0]), "chunked") {
if !ascii.EqualFold(raw[0], "chunked") {
return &unsupportedTEError{fmt.Sprintf("unsupported transfer encoding: %q", raw[0])}
}

Expand Down Expand Up @@ -638,7 +638,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header http.He
}

// Logic based on response type or status
if noResponseBodyExpected(requestMethod) {
if isResponse && noResponseBodyExpected(requestMethod) {
return 0, nil
}
if status/100 == 1 {
Expand Down Expand Up @@ -673,7 +673,7 @@ func fixLength(isResponse bool, status int, requestMethod string, header http.He

// Determine whether to hang up after sending a request and body, or
// receiving a response and body
// 'header' is the request headers
// 'header' is the request headers.
func shouldClose(major, minor int, header http.Header, removeCloseHeader bool) bool {
if major < 1 {
return true
Expand All @@ -692,7 +692,7 @@ func shouldClose(major, minor int, header http.Header, removeCloseHeader bool) b
return hasClose
}

// Parse the trailer header
// Parse the trailer header.
func fixTrailer(header http.Header, chunked bool) (http.Header, error) {
vv, ok := header["Trailer"]
if !ok {
Expand Down Expand Up @@ -1010,7 +1010,7 @@ var nopCloserWriterToType = reflect.TypeOf(io.NopCloser(struct {
}{}))

// unwrapNopCloser return the underlying reader and true if r is a NopCloser
// else it return false
// else it return false.
func unwrapNopCloser(r io.Reader) (underlyingReader io.Reader, isNopCloser bool) {
switch reflect.TypeOf(r) {
case nopCloserType, nopCloserWriterToType:
Expand Down
Loading

0 comments on commit f417a9b

Please sign in to comment.