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