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