1// +build js
2
3package http
4
5import (
6	"errors"
7	"fmt"
8	"io"
9	"io/ioutil"
10	"strconv"
11
12	"github.com/gopherjs/gopherjs/js"
13)
14
15// streamReader implements an io.ReadCloser wrapper for ReadableStream of https://fetch.spec.whatwg.org/.
16type streamReader struct {
17	pending []byte
18	stream  *js.Object
19}
20
21func (r *streamReader) Read(p []byte) (n int, err error) {
22	if len(r.pending) == 0 {
23		var (
24			bCh   = make(chan []byte)
25			errCh = make(chan error)
26		)
27		r.stream.Call("read").Call("then",
28			func(result *js.Object) {
29				if result.Get("done").Bool() {
30					errCh <- io.EOF
31					return
32				}
33				bCh <- result.Get("value").Interface().([]byte)
34			},
35			func(reason *js.Object) {
36				// Assumes it's a DOMException.
37				errCh <- errors.New(reason.Get("message").String())
38			},
39		)
40		select {
41		case b := <-bCh:
42			r.pending = b
43		case err := <-errCh:
44			return 0, err
45		}
46	}
47	n = copy(p, r.pending)
48	r.pending = r.pending[n:]
49	return n, nil
50}
51
52func (r *streamReader) Close() error {
53	// This ignores any error returned from cancel method. So far, I did not encounter any concrete
54	// situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close().
55	// If there's a need to report error here, it can be implemented and tested when that need comes up.
56	r.stream.Call("cancel")
57	return nil
58}
59
60// fetchTransport is a RoundTripper that is implemented using Fetch API. It supports streaming
61// response bodies.
62type fetchTransport struct{}
63
64func (t *fetchTransport) RoundTrip(req *Request) (*Response, error) {
65	headers := js.Global.Get("Headers").New()
66	for key, values := range req.Header {
67		for _, value := range values {
68			headers.Call("append", key, value)
69		}
70	}
71	opt := map[string]interface{}{
72		"method":      req.Method,
73		"headers":     headers,
74		"credentials": "same-origin",
75	}
76	if req.Body != nil {
77		// TODO: Find out if request body can be streamed into the fetch request rather than in advance here.
78		//       See BufferSource at https://fetch.spec.whatwg.org/#body-mixin.
79		body, err := ioutil.ReadAll(req.Body)
80		if err != nil {
81			req.Body.Close() // RoundTrip must always close the body, including on errors.
82			return nil, err
83		}
84		req.Body.Close()
85		opt["body"] = body
86	}
87	respPromise := js.Global.Call("fetch", req.URL.String(), opt)
88
89	var (
90		respCh = make(chan *Response)
91		errCh  = make(chan error)
92	)
93	respPromise.Call("then",
94		func(result *js.Object) {
95			header := Header{}
96			result.Get("headers").Call("forEach", func(value, key *js.Object) {
97				ck := CanonicalHeaderKey(key.String())
98				header[ck] = append(header[ck], value.String())
99			})
100
101			contentLength := int64(-1)
102			if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil {
103				contentLength = cl
104			}
105
106			select {
107			case respCh <- &Response{
108				Status:        result.Get("status").String() + " " + StatusText(result.Get("status").Int()),
109				StatusCode:    result.Get("status").Int(),
110				Header:        header,
111				ContentLength: contentLength,
112				Body:          &streamReader{stream: result.Get("body").Call("getReader")},
113				Request:       req,
114			}:
115			case <-req.Context().Done():
116			}
117		},
118		func(reason *js.Object) {
119			select {
120			case errCh <- fmt.Errorf("net/http: fetch() failed: %s", reason.String()):
121			case <-req.Context().Done():
122			}
123		},
124	)
125	select {
126	case <-req.Context().Done():
127		// TODO: Abort request if possible using Fetch API.
128		return nil, errors.New("net/http: request canceled")
129	case resp := <-respCh:
130		return resp, nil
131	case err := <-errCh:
132		return nil, err
133	}
134}
135