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