roundtrip_js.mx raw

   1  // Copyright 2018 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  //go:build js && wasm
   6  
   7  package http
   8  
   9  import (
  10  	"errors"
  11  	"fmt"
  12  	"io"
  13  	"net/http/internal/ascii"
  14  	"strconv"
  15  	"bytes"
  16  	"syscall/js"
  17  )
  18  
  19  var uint8Array = js.Global().Get("Uint8Array")
  20  
  21  // jsFetchMode is a Request.Header map key that, if present,
  22  // signals that the map entry is actually an option to the Fetch API mode setting.
  23  // Valid values are: "cors", "no-cors", "same-origin", "navigate"
  24  // The default is "same-origin".
  25  //
  26  // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
  27  const jsFetchMode = "js.fetch:mode"
  28  
  29  // jsFetchCreds is a Request.Header map key that, if present,
  30  // signals that the map entry is actually an option to the Fetch API credentials setting.
  31  // Valid values are: "omit", "same-origin", "include"
  32  // The default is "same-origin".
  33  //
  34  // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
  35  const jsFetchCreds = "js.fetch:credentials"
  36  
  37  // jsFetchRedirect is a Request.Header map key that, if present,
  38  // signals that the map entry is actually an option to the Fetch API redirect setting.
  39  // Valid values are: "follow", "error", "manual"
  40  // The default is "follow".
  41  //
  42  // Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
  43  const jsFetchRedirect = "js.fetch:redirect"
  44  
  45  // jsFetchMissing will be true if the Fetch API is not present in
  46  // the browser globals.
  47  var jsFetchMissing = js.Global().Get("fetch").IsUndefined()
  48  
  49  // jsFetchDisabled controls whether the use of Fetch API is disabled.
  50  // It's set to true when we detect we're running in Node.js, so that
  51  // RoundTrip ends up talking over the same fake network the HTTP servers
  52  // currently use in various tests and examples. See go.dev/issue/57613.
  53  //
  54  // TODO(go.dev/issue/60810): See if it's viable to test the Fetch API
  55  // code path.
  56  var jsFetchDisabled = js.Global().Get("process").Type() == js.TypeObject &&
  57  	bytes.HasPrefix(js.Global().Get("process").Get("argv0").String(), "node")
  58  
  59  // RoundTrip implements the [RoundTripper] interface using the WHATWG Fetch API.
  60  func (t *Transport) RoundTrip(req *Request) (*Response, error) {
  61  	// The Transport has a documented contract that states that if the DialContext or
  62  	// DialTLSContext functions are set, they will be used to set up the connections.
  63  	// If they aren't set then the documented contract is to use Dial or DialTLS, even
  64  	// though they are deprecated. Therefore, if any of these are set, we should obey
  65  	// the contract and dial using the regular round-trip instead. Otherwise, we'll try
  66  	// to fall back on the Fetch API, unless it's not available.
  67  	if t.Dial != nil || t.DialContext != nil || t.DialTLS != nil || t.DialTLSContext != nil || jsFetchMissing || jsFetchDisabled {
  68  		return t.roundTrip(req)
  69  	}
  70  
  71  	ac := js.Global().Get("AbortController")
  72  	if !ac.IsUndefined() {
  73  		// Some browsers that support WASM don't necessarily support
  74  		// the AbortController. See
  75  		// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
  76  		ac = ac.New()
  77  	}
  78  
  79  	opt := js.Global().Get("Object").New()
  80  	// See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
  81  	// for options available.
  82  	opt.Set("method", req.Method)
  83  	opt.Set("credentials", "same-origin")
  84  	if h := req.Header.Get(jsFetchCreds); h != "" {
  85  		opt.Set("credentials", h)
  86  		req.Header.Del(jsFetchCreds)
  87  	}
  88  	if h := req.Header.Get(jsFetchMode); h != "" {
  89  		opt.Set("mode", h)
  90  		req.Header.Del(jsFetchMode)
  91  	}
  92  	if h := req.Header.Get(jsFetchRedirect); h != "" {
  93  		opt.Set("redirect", h)
  94  		req.Header.Del(jsFetchRedirect)
  95  	}
  96  	if !ac.IsUndefined() {
  97  		opt.Set("signal", ac.Get("signal"))
  98  	}
  99  	headers := js.Global().Get("Headers").New()
 100  	for key, values := range req.Header {
 101  		for _, value := range values {
 102  			headers.Call("append", key, value)
 103  		}
 104  	}
 105  	opt.Set("headers", headers)
 106  
 107  	if req.Body != nil {
 108  		// TODO(johanbrandhorst): Stream request body when possible.
 109  		// See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue.
 110  		// See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue.
 111  		// See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue.
 112  		// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API
 113  		// and browser support.
 114  		// NOTE(haruyama480): Ensure HTTP/1 fallback exists.
 115  		// See https://go.dev/issue/61889 for discussion.
 116  		body, err := io.ReadAll(req.Body)
 117  		if err != nil {
 118  			req.Body.Close() // RoundTrip must always close the body, including on errors.
 119  			return nil, err
 120  		}
 121  		req.Body.Close()
 122  		if len(body) != 0 {
 123  			buf := uint8Array.New(len(body))
 124  			js.CopyBytesToJS(buf, body)
 125  			opt.Set("body", buf)
 126  		}
 127  	}
 128  
 129  	fetchPromise := js.Global().Call("fetch", req.URL.String(), opt)
 130  	var (
 131  		respCh           = chan *Response{1}
 132  		errCh            = chan error{1}
 133  		success, failure js.Func
 134  	)
 135  	success = js.FuncOf(func(this js.Value, args []js.Value) any {
 136  		success.Release()
 137  		failure.Release()
 138  
 139  		result := args[0]
 140  		header := Header{}
 141  		// https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries
 142  		headersIt := result.Get("headers").Call("entries")
 143  		for {
 144  			n := headersIt.Call("next")
 145  			if n.Get("done").Bool() {
 146  				break
 147  			}
 148  			pair := n.Get("value")
 149  			key, value := pair.Index(0).String(), pair.Index(1).String()
 150  			ck := CanonicalHeaderKey(key)
 151  			header[ck] = append(header[ck], value)
 152  		}
 153  
 154  		contentLength := int64(0)
 155  		clHeader := header.Get("Content-Length")
 156  		switch {
 157  		case clHeader != "":
 158  			cl, err := strconv.ParseInt(clHeader, 10, 64)
 159  			if err != nil {
 160  				errCh <- fmt.Errorf("net/http: ill-formed Content-Length header: %v", err)
 161  				return nil
 162  			}
 163  			if cl < 0 {
 164  				// Content-Length values less than 0 are invalid.
 165  				// See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13
 166  				errCh <- fmt.Errorf("net/http: invalid Content-Length header: %q", clHeader)
 167  				return nil
 168  			}
 169  			contentLength = cl
 170  		default:
 171  			// If the response length is not declared, set it to -1.
 172  			contentLength = -1
 173  		}
 174  
 175  		b := result.Get("body")
 176  		var body io.ReadCloser
 177  		// The body is undefined when the browser does not support streaming response bodies (Firefox),
 178  		// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
 179  		if !b.IsUndefined() && !b.IsNull() {
 180  			body = &streamReader{stream: b.Call("getReader")}
 181  		} else {
 182  			// Fall back to using ArrayBuffer
 183  			// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer
 184  			body = &arrayReader{arrayPromise: result.Call("arrayBuffer")}
 185  		}
 186  
 187  		code := result.Get("status").Int()
 188  
 189  		uncompressed := false
 190  		if ascii.EqualFold(header.Get("Content-Encoding"), "gzip") {
 191  			// The fetch api will decode the gzip, but Content-Encoding not be deleted.
 192  			header.Del("Content-Encoding")
 193  			header.Del("Content-Length")
 194  			contentLength = -1
 195  			uncompressed = true
 196  		}
 197  
 198  		respCh <- &Response{
 199  			Status:        fmt.Sprintf("%d %s", code, StatusText(code)),
 200  			StatusCode:    code,
 201  			Header:        header,
 202  			ContentLength: contentLength,
 203  			Uncompressed:  uncompressed,
 204  			Body:          body,
 205  			Request:       req,
 206  		}
 207  
 208  		return nil
 209  	})
 210  	failure = js.FuncOf(func(this js.Value, args []js.Value) any {
 211  		success.Release()
 212  		failure.Release()
 213  
 214  		err := args[0]
 215  		// The error is a JS Error type
 216  		// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
 217  		// We can use the toString() method to get a string representation of the error.
 218  		errMsg := err.Call("toString").String()
 219  		// Errors can optionally contain a cause.
 220  		if cause := err.Get("cause"); !cause.IsUndefined() {
 221  			// The exact type of the cause is not defined,
 222  			// but if it's another error, we can call toString() on it too.
 223  			if !cause.Get("toString").IsUndefined() {
 224  				errMsg += ": " + cause.Call("toString").String()
 225  			} else if cause.Type() == js.TypeString {
 226  				errMsg += ": " + cause.String()
 227  			}
 228  		}
 229  		errCh <- fmt.Errorf("net/http: fetch() failed: %s", errMsg)
 230  		return nil
 231  	})
 232  
 233  	fetchPromise.Call("then", success, failure)
 234  	select {
 235  	case <-req.Context().Done():
 236  		if !ac.IsUndefined() {
 237  			// Abort the Fetch request.
 238  			ac.Call("abort")
 239  
 240  			// Wait for fetch promise to be rejected prior to exiting. See
 241  			// https://github.com/golang/go/issues/57098 for more details.
 242  			select {
 243  			case resp := <-respCh:
 244  				resp.Body.Close()
 245  			case <-errCh:
 246  			}
 247  		}
 248  		return nil, req.Context().Err()
 249  	case resp := <-respCh:
 250  		return resp, nil
 251  	case err := <-errCh:
 252  		return nil, err
 253  	}
 254  }
 255  
 256  var errClosed = errors.New("net/http: reader is closed")
 257  
 258  // streamReader implements an io.ReadCloser wrapper for ReadableStream.
 259  // See https://fetch.spec.whatwg.org/#readablestream for more information.
 260  type streamReader struct {
 261  	pending []byte
 262  	stream  js.Value
 263  	err     error // sticky read error
 264  }
 265  
 266  func (r *streamReader) Read(p []byte) (n int, err error) {
 267  	if r.err != nil {
 268  		return 0, r.err
 269  	}
 270  	if len(r.pending) == 0 {
 271  		var (
 272  			bCh   = chan []byte{1}
 273  			errCh = chan error{1}
 274  		)
 275  		success := js.FuncOf(func(this js.Value, args []js.Value) any {
 276  			result := args[0]
 277  			if result.Get("done").Bool() {
 278  				errCh <- io.EOF
 279  				return nil
 280  			}
 281  			value := []byte{:result.Get("value").Get("byteLength").Int()}
 282  			js.CopyBytesToGo(value, result.Get("value"))
 283  			bCh <- value
 284  			return nil
 285  		})
 286  		defer success.Release()
 287  		failure := js.FuncOf(func(this js.Value, args []js.Value) any {
 288  			// Assumes it's a TypeError. See
 289  			// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
 290  			// for more information on this type. See
 291  			// https://streams.spec.whatwg.org/#byob-reader-read for the spec on
 292  			// the read method.
 293  			errCh <- errors.New(args[0].Get("message").String())
 294  			return nil
 295  		})
 296  		defer failure.Release()
 297  		r.stream.Call("read").Call("then", success, failure)
 298  		select {
 299  		case b := <-bCh:
 300  			r.pending = b
 301  		case err := <-errCh:
 302  			r.err = err
 303  			return 0, err
 304  		}
 305  	}
 306  	n = copy(p, r.pending)
 307  	r.pending = r.pending[n:]
 308  	return n, nil
 309  }
 310  
 311  func (r *streamReader) Close() error {
 312  	// This ignores any error returned from cancel method. So far, I did not encounter any concrete
 313  	// situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close().
 314  	// If there's a need to report error here, it can be implemented and tested when that need comes up.
 315  	r.stream.Call("cancel")
 316  	if r.err == nil {
 317  		r.err = errClosed
 318  	}
 319  	return nil
 320  }
 321  
 322  // arrayReader implements an io.ReadCloser wrapper for ArrayBuffer.
 323  // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer.
 324  type arrayReader struct {
 325  	arrayPromise js.Value
 326  	pending      []byte
 327  	read         bool
 328  	err          error // sticky read error
 329  }
 330  
 331  func (r *arrayReader) Read(p []byte) (n int, err error) {
 332  	if r.err != nil {
 333  		return 0, r.err
 334  	}
 335  	if !r.read {
 336  		r.read = true
 337  		var (
 338  			bCh   = chan []byte{1}
 339  			errCh = chan error{1}
 340  		)
 341  		success := js.FuncOf(func(this js.Value, args []js.Value) any {
 342  			// Wrap the input ArrayBuffer with a Uint8Array
 343  			uint8arrayWrapper := uint8Array.New(args[0])
 344  			value := []byte{:uint8arrayWrapper.Get("byteLength").Int()}
 345  			js.CopyBytesToGo(value, uint8arrayWrapper)
 346  			bCh <- value
 347  			return nil
 348  		})
 349  		defer success.Release()
 350  		failure := js.FuncOf(func(this js.Value, args []js.Value) any {
 351  			// Assumes it's a TypeError. See
 352  			// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
 353  			// for more information on this type.
 354  			// See https://fetch.spec.whatwg.org/#concept-body-consume-body for reasons this might error.
 355  			errCh <- errors.New(args[0].Get("message").String())
 356  			return nil
 357  		})
 358  		defer failure.Release()
 359  		r.arrayPromise.Call("then", success, failure)
 360  		select {
 361  		case b := <-bCh:
 362  			r.pending = b
 363  		case err := <-errCh:
 364  			return 0, err
 365  		}
 366  	}
 367  	if len(r.pending) == 0 {
 368  		return 0, io.EOF
 369  	}
 370  	n = copy(p, r.pending)
 371  	r.pending = r.pending[n:]
 372  	return n, nil
 373  }
 374  
 375  func (r *arrayReader) Close() error {
 376  	if r.err == nil {
 377  		r.err = errClosed
 378  	}
 379  	return nil
 380  }
 381