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