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