]> Cypherpunks.ru repositories - gostls13.git/blobdiff - src/net/http/roundtrip_js.go
net/http: remove Content-Encoding header in roundtrip_js
[gostls13.git] / src / net / http / roundtrip_js.go
index 6331351a8387a4a89bd587ee96f616a8f8a410b9..cbf978af182d5fb473f52de93ced64058293e1d2 100644 (file)
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
-// +build js,wasm
+//go:build js && wasm
 
 package http
 
@@ -10,8 +10,9 @@ import (
        "errors"
        "fmt"
        "io"
-       "io/ioutil"
+       "net/http/internal/ascii"
        "strconv"
+       "strings"
        "syscall/js"
 )
 
@@ -41,16 +42,34 @@ const jsFetchCreds = "js.fetch:credentials"
 // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
 const jsFetchRedirect = "js.fetch:redirect"
 
-var useFakeNetwork = js.Global().Get("fetch") == js.Undefined()
+// jsFetchMissing will be true if the Fetch API is not present in
+// the browser globals.
+var jsFetchMissing = js.Global().Get("fetch").IsUndefined()
+
+// jsFetchDisabled controls whether the use of Fetch API is disabled.
+// It's set to true when we detect we're running in Node.js, so that
+// RoundTrip ends up talking over the same fake network the HTTP servers
+// currently use in various tests and examples. See go.dev/issue/57613.
+//
+// TODO(go.dev/issue/60810): See if it's viable to test the Fetch API
+// code path.
+var jsFetchDisabled = js.Global().Get("process").Type() == js.TypeObject &&
+       strings.HasPrefix(js.Global().Get("process").Get("argv0").String(), "node")
 
 // RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
 func (t *Transport) RoundTrip(req *Request) (*Response, error) {
-       if useFakeNetwork {
+       // The Transport has a documented contract that states that if the DialContext or
+       // DialTLSContext functions are set, they will be used to set up the connections.
+       // If they aren't set then the documented contract is to use Dial or DialTLS, even
+       // 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.Dial != nil || t.DialContext != nil || t.DialTLS != nil || t.DialTLSContext != nil || jsFetchMissing || jsFetchDisabled {
                return t.roundTrip(req)
        }
 
        ac := js.Global().Get("AbortController")
-       if ac != js.Undefined() {
+       if !ac.IsUndefined() {
                // Some browsers that support WASM don't necessarily support
                // the AbortController. See
                // https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
@@ -74,7 +93,7 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
                opt.Set("redirect", h)
                req.Header.Del(jsFetchRedirect)
        }
-       if ac != js.Undefined() {
+       if !ac.IsUndefined() {
                opt.Set("signal", ac.Get("signal"))
        }
        headers := js.Global().Get("Headers").New()
@@ -92,22 +111,31 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
                // 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 := ioutil.ReadAll(req.Body)
+               // NOTE(haruyama480): Ensure HTTP/1 fallback exists.
+               // See https://go.dev/issue/61889 for discussion.
+               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()
-               buf := uint8Array.New(len(body))
-               js.CopyBytesToJS(buf, body)
-               opt.Set("body", buf)
+               if len(body) != 0 {
+                       buf := uint8Array.New(len(body))
+                       js.CopyBytesToJS(buf, body)
+                       opt.Set("body", buf)
+               }
        }
-       respPromise := js.Global().Call("fetch", req.URL.String(), opt)
+
+       fetchPromise := js.Global().Call("fetch", req.URL.String(), opt)
        var (
-               respCh = make(chan *Response, 1)
-               errCh  = make(chan error, 1)
+               respCh           = make(chan *Response, 1)
+               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()
+
                result := args[0]
                header := Header{}
                // https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries
@@ -124,15 +152,31 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
                }
 
                contentLength := int64(0)
-               if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil {
+               clHeader := header.Get("Content-Length")
+               switch {
+               case clHeader != "":
+                       cl, err := strconv.ParseInt(clHeader, 10, 64)
+                       if err != nil {
+                               errCh <- fmt.Errorf("net/http: ill-formed Content-Length header: %v", err)
+                               return nil
+                       }
+                       if cl < 0 {
+                               // Content-Length values less than 0 are invalid.
+                               // See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13
+                               errCh <- fmt.Errorf("net/http: invalid Content-Length header: %q", clHeader)
+                               return nil
+                       }
                        contentLength = cl
+               default:
+                       // If the response length is not declared, set it to -1.
+                       contentLength = -1
                }
 
                b := result.Get("body")
                var body io.ReadCloser
                // The body is undefined when the browser does not support streaming response bodies (Firefox),
                // and null in certain error cases, i.e. when the request is blocked because of CORS settings.
-               if b != js.Undefined() && b != js.Null() {
+               if !b.IsUndefined() && !b.IsNull() {
                        body = &streamReader{stream: b.Call("getReader")}
                } else {
                        // Fall back to using ArrayBuffer
@@ -141,35 +185,56 @@ func (t *Transport) RoundTrip(req *Request) (*Response, error) {
                }
 
                code := result.Get("status").Int()
-               select {
-               case respCh <- &Response{
+
+               uncompressed := false
+               if ascii.EqualFold(header.Get("Content-Encoding"), "gzip") {
+                       // The fetch api will decode the gzip, but Content-Encoding not be deleted.
+                       header.Del("Content-Encoding")
+                       header.Del("Content-Length")
+                       contentLength = -1
+                       uncompressed = true
+               }
+
+               respCh <- &Response{
                        Status:        fmt.Sprintf("%d %s", code, StatusText(code)),
                        StatusCode:    code,
                        Header:        header,
                        ContentLength: contentLength,
+                       Uncompressed:  uncompressed,
                        Body:          body,
                        Request:       req,
-               }:
-               case <-req.Context().Done():
                }
 
                return nil
        })
-       defer success.Release()
-       failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
-               err := fmt.Errorf("net/http: fetch() failed: %s", args[0].String())
-               select {
-               case errCh <- err:
-               case <-req.Context().Done():
+       failure = js.FuncOf(func(this js.Value, args []js.Value) any {
+               success.Release()
+               failure.Release()
+
+               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
        })
-       defer failure.Release()
-       respPromise.Call("then", success, failure)
+
+       fetchPromise.Call("then", success, failure)
        select {
        case <-req.Context().Done():
-               if ac != js.Undefined() {
-                       // Abort the Fetch request
+               if !ac.IsUndefined() {
+                       // Abort the Fetch request.
                        ac.Call("abort")
                }
                return nil, req.Context().Err()
@@ -199,7 +264,7 @@ func (r *streamReader) Read(p []byte) (n int, err error) {
                        bCh   = make(chan []byte, 1)
                        errCh = make(chan error, 1)
                )
-               success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+               success := js.FuncOf(func(this js.Value, args []js.Value) any {
                        result := args[0]
                        if result.Get("done").Bool() {
                                errCh <- io.EOF
@@ -211,7 +276,7 @@ func (r *streamReader) Read(p []byte) (n int, err error) {
                        return nil
                })
                defer success.Release()
-               failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+               failure := js.FuncOf(func(this js.Value, args []js.Value) any {
                        // Assumes it's a TypeError. See
                        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
                        // for more information on this type. See
@@ -265,7 +330,7 @@ func (r *arrayReader) Read(p []byte) (n int, err error) {
                        bCh   = make(chan []byte, 1)
                        errCh = make(chan error, 1)
                )
-               success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+               success := js.FuncOf(func(this js.Value, args []js.Value) any {
                        // Wrap the input ArrayBuffer with a Uint8Array
                        uint8arrayWrapper := uint8Array.New(args[0])
                        value := make([]byte, uint8arrayWrapper.Get("byteLength").Int())
@@ -274,7 +339,7 @@ func (r *arrayReader) Read(p []byte) (n int, err error) {
                        return nil
                })
                defer success.Release()
-               failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
+               failure := js.FuncOf(func(this js.Value, args []js.Value) any {
                        // Assumes it's a TypeError. See
                        // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
                        // for more information on this type.