...

Source file src/pkg/net/http/roundtrip_js.go

     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	// +build js,wasm
     6	
     7	package http
     8	
     9	import (
    10		"errors"
    11		"fmt"
    12		"io"
    13		"io/ioutil"
    14		"strconv"
    15		"syscall/js"
    16	)
    17	
    18	var uint8Array = js.Global().Get("Uint8Array")
    19	
    20	// jsFetchMode is a Request.Header map key that, if present,
    21	// signals that the map entry is actually an option to the Fetch API mode setting.
    22	// Valid values are: "cors", "no-cors", "same-origin", "navigate"
    23	// The default is "same-origin".
    24	//
    25	// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
    26	const jsFetchMode = "js.fetch:mode"
    27	
    28	// jsFetchCreds is a Request.Header map key that, if present,
    29	// signals that the map entry is actually an option to the Fetch API credentials setting.
    30	// Valid values are: "omit", "same-origin", "include"
    31	// The default is "same-origin".
    32	//
    33	// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
    34	const jsFetchCreds = "js.fetch:credentials"
    35	
    36	// jsFetchRedirect is a Request.Header map key that, if present,
    37	// signals that the map entry is actually an option to the Fetch API redirect setting.
    38	// Valid values are: "follow", "error", "manual"
    39	// The default is "follow".
    40	//
    41	// Reference: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters
    42	const jsFetchRedirect = "js.fetch:redirect"
    43	
    44	var useFakeNetwork = js.Global().Get("fetch") == js.Undefined()
    45	
    46	// RoundTrip implements the RoundTripper interface using the WHATWG Fetch API.
    47	func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    48		if useFakeNetwork {
    49			return t.roundTrip(req)
    50		}
    51	
    52		ac := js.Global().Get("AbortController")
    53		if ac != js.Undefined() {
    54			// Some browsers that support WASM don't necessarily support
    55			// the AbortController. See
    56			// https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility.
    57			ac = ac.New()
    58		}
    59	
    60		opt := js.Global().Get("Object").New()
    61		// See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch
    62		// for options available.
    63		opt.Set("method", req.Method)
    64		opt.Set("credentials", "same-origin")
    65		if h := req.Header.Get(jsFetchCreds); h != "" {
    66			opt.Set("credentials", h)
    67			req.Header.Del(jsFetchCreds)
    68		}
    69		if h := req.Header.Get(jsFetchMode); h != "" {
    70			opt.Set("mode", h)
    71			req.Header.Del(jsFetchMode)
    72		}
    73		if h := req.Header.Get(jsFetchRedirect); h != "" {
    74			opt.Set("redirect", h)
    75			req.Header.Del(jsFetchRedirect)
    76		}
    77		if ac != js.Undefined() {
    78			opt.Set("signal", ac.Get("signal"))
    79		}
    80		headers := js.Global().Get("Headers").New()
    81		for key, values := range req.Header {
    82			for _, value := range values {
    83				headers.Call("append", key, value)
    84			}
    85		}
    86		opt.Set("headers", headers)
    87	
    88		if req.Body != nil {
    89			// TODO(johanbrandhorst): Stream request body when possible.
    90			// See https://bugs.chromium.org/p/chromium/issues/detail?id=688906 for Blink issue.
    91			// See https://bugzilla.mozilla.org/show_bug.cgi?id=1387483 for Firefox issue.
    92			// See https://github.com/web-platform-tests/wpt/issues/7693 for WHATWG tests issue.
    93			// See https://developer.mozilla.org/en-US/docs/Web/API/Streams_API for more details on the Streams API
    94			// and browser support.
    95			body, err := ioutil.ReadAll(req.Body)
    96			if err != nil {
    97				req.Body.Close() // RoundTrip must always close the body, including on errors.
    98				return nil, err
    99			}
   100			req.Body.Close()
   101			buf := uint8Array.New(len(body))
   102			js.CopyBytesToJS(buf, body)
   103			opt.Set("body", buf)
   104		}
   105		respPromise := js.Global().Call("fetch", req.URL.String(), opt)
   106		var (
   107			respCh = make(chan *Response, 1)
   108			errCh  = make(chan error, 1)
   109		)
   110		success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   111			result := args[0]
   112			header := Header{}
   113			// https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries
   114			headersIt := result.Get("headers").Call("entries")
   115			for {
   116				n := headersIt.Call("next")
   117				if n.Get("done").Bool() {
   118					break
   119				}
   120				pair := n.Get("value")
   121				key, value := pair.Index(0).String(), pair.Index(1).String()
   122				ck := CanonicalHeaderKey(key)
   123				header[ck] = append(header[ck], value)
   124			}
   125	
   126			contentLength := int64(0)
   127			if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil {
   128				contentLength = cl
   129			}
   130	
   131			b := result.Get("body")
   132			var body io.ReadCloser
   133			// The body is undefined when the browser does not support streaming response bodies (Firefox),
   134			// and null in certain error cases, i.e. when the request is blocked because of CORS settings.
   135			if b != js.Undefined() && b != js.Null() {
   136				body = &streamReader{stream: b.Call("getReader")}
   137			} else {
   138				// Fall back to using ArrayBuffer
   139				// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer
   140				body = &arrayReader{arrayPromise: result.Call("arrayBuffer")}
   141			}
   142	
   143			code := result.Get("status").Int()
   144			select {
   145			case respCh <- &Response{
   146				Status:        fmt.Sprintf("%d %s", code, StatusText(code)),
   147				StatusCode:    code,
   148				Header:        header,
   149				ContentLength: contentLength,
   150				Body:          body,
   151				Request:       req,
   152			}:
   153			case <-req.Context().Done():
   154			}
   155	
   156			return nil
   157		})
   158		defer success.Release()
   159		failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   160			err := fmt.Errorf("net/http: fetch() failed: %s", args[0].String())
   161			select {
   162			case errCh <- err:
   163			case <-req.Context().Done():
   164			}
   165			return nil
   166		})
   167		defer failure.Release()
   168		respPromise.Call("then", success, failure)
   169		select {
   170		case <-req.Context().Done():
   171			if ac != js.Undefined() {
   172				// Abort the Fetch request
   173				ac.Call("abort")
   174			}
   175			return nil, req.Context().Err()
   176		case resp := <-respCh:
   177			return resp, nil
   178		case err := <-errCh:
   179			return nil, err
   180		}
   181	}
   182	
   183	var errClosed = errors.New("net/http: reader is closed")
   184	
   185	// streamReader implements an io.ReadCloser wrapper for ReadableStream.
   186	// See https://fetch.spec.whatwg.org/#readablestream for more information.
   187	type streamReader struct {
   188		pending []byte
   189		stream  js.Value
   190		err     error // sticky read error
   191	}
   192	
   193	func (r *streamReader) Read(p []byte) (n int, err error) {
   194		if r.err != nil {
   195			return 0, r.err
   196		}
   197		if len(r.pending) == 0 {
   198			var (
   199				bCh   = make(chan []byte, 1)
   200				errCh = make(chan error, 1)
   201			)
   202			success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   203				result := args[0]
   204				if result.Get("done").Bool() {
   205					errCh <- io.EOF
   206					return nil
   207				}
   208				value := make([]byte, result.Get("value").Get("byteLength").Int())
   209				js.CopyBytesToGo(value, result.Get("value"))
   210				bCh <- value
   211				return nil
   212			})
   213			defer success.Release()
   214			failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   215				// Assumes it's a TypeError. See
   216				// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
   217				// for more information on this type. See
   218				// https://streams.spec.whatwg.org/#byob-reader-read for the spec on
   219				// the read method.
   220				errCh <- errors.New(args[0].Get("message").String())
   221				return nil
   222			})
   223			defer failure.Release()
   224			r.stream.Call("read").Call("then", success, failure)
   225			select {
   226			case b := <-bCh:
   227				r.pending = b
   228			case err := <-errCh:
   229				r.err = err
   230				return 0, err
   231			}
   232		}
   233		n = copy(p, r.pending)
   234		r.pending = r.pending[n:]
   235		return n, nil
   236	}
   237	
   238	func (r *streamReader) Close() error {
   239		// This ignores any error returned from cancel method. So far, I did not encounter any concrete
   240		// situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close().
   241		// If there's a need to report error here, it can be implemented and tested when that need comes up.
   242		r.stream.Call("cancel")
   243		if r.err == nil {
   244			r.err = errClosed
   245		}
   246		return nil
   247	}
   248	
   249	// arrayReader implements an io.ReadCloser wrapper for ArrayBuffer.
   250	// https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer.
   251	type arrayReader struct {
   252		arrayPromise js.Value
   253		pending      []byte
   254		read         bool
   255		err          error // sticky read error
   256	}
   257	
   258	func (r *arrayReader) Read(p []byte) (n int, err error) {
   259		if r.err != nil {
   260			return 0, r.err
   261		}
   262		if !r.read {
   263			r.read = true
   264			var (
   265				bCh   = make(chan []byte, 1)
   266				errCh = make(chan error, 1)
   267			)
   268			success := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   269				// Wrap the input ArrayBuffer with a Uint8Array
   270				uint8arrayWrapper := uint8Array.New(args[0])
   271				value := make([]byte, uint8arrayWrapper.Get("byteLength").Int())
   272				js.CopyBytesToGo(value, uint8arrayWrapper)
   273				bCh <- value
   274				return nil
   275			})
   276			defer success.Release()
   277			failure := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
   278				// Assumes it's a TypeError. See
   279				// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
   280				// for more information on this type.
   281				// See https://fetch.spec.whatwg.org/#concept-body-consume-body for reasons this might error.
   282				errCh <- errors.New(args[0].Get("message").String())
   283				return nil
   284			})
   285			defer failure.Release()
   286			r.arrayPromise.Call("then", success, failure)
   287			select {
   288			case b := <-bCh:
   289				r.pending = b
   290			case err := <-errCh:
   291				return 0, err
   292			}
   293		}
   294		if len(r.pending) == 0 {
   295			return 0, io.EOF
   296		}
   297		n = copy(p, r.pending)
   298		r.pending = r.pending[n:]
   299		return n, nil
   300	}
   301	
   302	func (r *arrayReader) Close() error {
   303		if r.err == nil {
   304			r.err = errClosed
   305		}
   306		return nil
   307	}
   308	

View as plain text