github.com/Kolosok86/http@v0.1.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 "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 // jsFetchDisabled will be true if the "process" global is present. 48 // We use this as an indicator that we're running in Node.js. We 49 // want to disable the Fetch API in Node.js because it breaks 50 // our wasm tests. See https://go.dev/issue/57613 for more information. 51 var jsFetchDisabled = !js.Global().Get("process").IsUndefined() 52 53 // Determine whether the JS runtime supports streaming request bodies. 54 // Courtesy: https://developer.chrome.com/articles/fetch-streaming-requests/#feature-detection 55 func supportsPostRequestStreams() bool { 56 requestOpt := js.Global().Get("Object").New() 57 requestBody := js.Global().Get("ReadableStream").New() 58 59 requestOpt.Set("method", "POST") 60 requestOpt.Set("body", requestBody) 61 62 // There is quite a dance required to define a getter if you do not have the { get property() { ... } } 63 // syntax available. However, it is possible: 64 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#defining_a_getter_on_existing_objects_using_defineproperty 65 duplexCalled := false 66 duplexGetterObj := js.Global().Get("Object").New() 67 duplexGetterFunc := js.FuncOf(func(this js.Value, args []js.Value) any { 68 duplexCalled = true 69 return "half" 70 }) 71 defer duplexGetterFunc.Release() 72 duplexGetterObj.Set("get", duplexGetterFunc) 73 js.Global().Get("Object").Call("defineProperty", requestOpt, "duplex", duplexGetterObj) 74 75 // Slight difference here between the aforementioned example: Non-browser-based runtimes 76 // do not have a non-empty API Base URL (https://html.spec.whatwg.org/multipage/webappapis.html#api-base-url) 77 // so we have to supply a valid URL here. 78 requestObject := js.Global().Get("Request").New("https://www.example.org", requestOpt) 79 80 hasContentTypeHeader := requestObject.Get("headers").Call("has", "Content-Type").Bool() 81 82 return duplexCalled && !hasContentTypeHeader 83 } 84 85 // RoundTrip implements the RoundTripper interface using the WHATWG Fetch API. 86 func (t *Transport) RoundTrip(req *Request) (*Response, error) { 87 // The Transport has a documented contract that states that if the DialContext or 88 // DialTLSContext functions are set, they will be used to set up the connections. 89 // If they aren't set then the documented contract is to use Dial or DialTLS, even 90 // though they are deprecated. Therefore, if any of these are set, we should obey 91 // the contract and dial using the regular round-trip instead. Otherwise, we'll try 92 // to fall back on the Fetch API, unless it's not available. 93 if t.Dial != nil || t.DialContext != nil || t.DialTLS != nil || t.DialTLSContext != nil || jsFetchMissing || jsFetchDisabled { 94 return t.roundTrip(req) 95 } 96 97 ac := js.Global().Get("AbortController") 98 if !ac.IsUndefined() { 99 // Some browsers that support WASM don't necessarily support 100 // the AbortController. See 101 // https://developer.mozilla.org/en-US/docs/Web/API/AbortController#Browser_compatibility. 102 ac = ac.New() 103 } 104 105 opt := js.Global().Get("Object").New() 106 // See https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch 107 // for options available. 108 opt.Set("method", req.Method) 109 opt.Set("credentials", "same-origin") 110 if h := req.Header.Get(jsFetchCreds); h != "" { 111 opt.Set("credentials", h) 112 req.Header.Del(jsFetchCreds) 113 } 114 if h := req.Header.Get(jsFetchMode); h != "" { 115 opt.Set("mode", h) 116 req.Header.Del(jsFetchMode) 117 } 118 if h := req.Header.Get(jsFetchRedirect); h != "" { 119 opt.Set("redirect", h) 120 req.Header.Del(jsFetchRedirect) 121 } 122 if !ac.IsUndefined() { 123 opt.Set("signal", ac.Get("signal")) 124 } 125 headers := js.Global().Get("Headers").New() 126 for key, values := range req.Header { 127 for _, value := range values { 128 headers.Call("append", key, value) 129 } 130 } 131 opt.Set("headers", headers) 132 133 var readableStreamStart, readableStreamPull, readableStreamCancel js.Func 134 if req.Body != nil { 135 if !supportsPostRequestStreams() { 136 body, err := io.ReadAll(req.Body) 137 if err != nil { 138 req.Body.Close() // RoundTrip must always close the body, including on errors. 139 return nil, err 140 } 141 if len(body) != 0 { 142 buf := uint8Array.New(len(body)) 143 js.CopyBytesToJS(buf, body) 144 opt.Set("body", buf) 145 } 146 } else { 147 readableStreamCtorArg := js.Global().Get("Object").New() 148 readableStreamCtorArg.Set("type", "bytes") 149 readableStreamCtorArg.Set("autoAllocateChunkSize", t.writeBufferSize()) 150 151 readableStreamPull = js.FuncOf(func(this js.Value, args []js.Value) any { 152 controller := args[0] 153 byobRequest := controller.Get("byobRequest") 154 if byobRequest.IsNull() { 155 controller.Call("close") 156 } 157 158 byobRequestView := byobRequest.Get("view") 159 160 bodyBuf := make([]byte, byobRequestView.Get("byteLength").Int()) 161 readBytes, readErr := io.ReadFull(req.Body, bodyBuf) 162 if readBytes > 0 { 163 buf := uint8Array.New(byobRequestView.Get("buffer")) 164 js.CopyBytesToJS(buf, bodyBuf) 165 byobRequest.Call("respond", readBytes) 166 } 167 168 if readErr == io.EOF || readErr == io.ErrUnexpectedEOF { 169 controller.Call("close") 170 } else if readErr != nil { 171 readErrCauseObject := js.Global().Get("Object").New() 172 readErrCauseObject.Set("cause", readErr.Error()) 173 readErr := js.Global().Get("Error").New("io.ReadFull failed while streaming POST body", readErrCauseObject) 174 controller.Call("error", readErr) 175 } 176 // Note: This a return from the pull callback of the controller and *not* RoundTrip(). 177 return nil 178 }) 179 readableStreamCtorArg.Set("pull", readableStreamPull) 180 181 opt.Set("body", js.Global().Get("ReadableStream").New(readableStreamCtorArg)) 182 // There is a requirement from the WHATWG fetch standard that the duplex property of 183 // the object given as the options argument to the fetch call be set to 'half' 184 // when the body property of the same options object is a ReadableStream: 185 // https://fetch.spec.whatwg.org/#dom-requestinit-duplex 186 opt.Set("duplex", "half") 187 } 188 } 189 190 fetchPromise := js.Global().Call("fetch", req.URL.String(), opt) 191 var ( 192 respCh = make(chan *Response, 1) 193 errCh = make(chan error, 1) 194 success, failure js.Func 195 ) 196 success = js.FuncOf(func(this js.Value, args []js.Value) any { 197 success.Release() 198 failure.Release() 199 readableStreamCancel.Release() 200 readableStreamPull.Release() 201 readableStreamStart.Release() 202 203 req.Body.Close() 204 205 result := args[0] 206 header := Header{} 207 // https://developer.mozilla.org/en-US/docs/Web/API/Headers/entries 208 headersIt := result.Get("headers").Call("entries") 209 for { 210 n := headersIt.Call("next") 211 if n.Get("done").Bool() { 212 break 213 } 214 pair := n.Get("value") 215 key, value := pair.Index(0).String(), pair.Index(1).String() 216 ck := CanonicalHeaderKey(key) 217 header[ck] = append(header[ck], value) 218 } 219 220 contentLength := int64(0) 221 clHeader := header.Get("Content-Length") 222 switch { 223 case clHeader != "": 224 cl, err := strconv.ParseInt(clHeader, 10, 64) 225 if err != nil { 226 errCh <- fmt.Errorf("net/http: ill-formed Content-Length header: %v", err) 227 return nil 228 } 229 if cl < 0 { 230 // Content-Length Values less than 0 are invalid. 231 // See: https://datatracker.ietf.org/doc/html/rfc2616/#section-14.13 232 errCh <- fmt.Errorf("net/http: invalid Content-Length header: %q", clHeader) 233 return nil 234 } 235 contentLength = cl 236 default: 237 // If the response length is not declared, set it to -1. 238 contentLength = -1 239 } 240 241 b := result.Get("body") 242 var body io.ReadCloser 243 // The body is undefined when the browser does not support streaming response bodies (Firefox), 244 // and null in certain error cases, i.e. when the request is blocked because of CORS settings. 245 if !b.IsUndefined() && !b.IsNull() { 246 body = &streamReader{stream: b.Call("getReader")} 247 } else { 248 // Fall back to using ArrayBuffer 249 // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer 250 body = &arrayReader{arrayPromise: result.Call("arrayBuffer")} 251 } 252 253 code := result.Get("status").Int() 254 respCh <- &Response{ 255 Status: fmt.Sprintf("%d %s", code, StatusText(code)), 256 StatusCode: code, 257 Header: header, 258 ContentLength: contentLength, 259 Body: body, 260 Request: req, 261 } 262 263 return nil 264 }) 265 failure = js.FuncOf(func(this js.Value, args []js.Value) any { 266 success.Release() 267 failure.Release() 268 readableStreamCancel.Release() 269 readableStreamPull.Release() 270 readableStreamStart.Release() 271 272 req.Body.Close() 273 274 err := args[0] 275 // The error is a JS Error type 276 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error 277 // We can use the toString() method to get a string representation of the error. 278 errMsg := err.Call("toString").String() 279 // Errors can optionally contain a cause. 280 if cause := err.Get("cause"); !cause.IsUndefined() { 281 // The exact type of the cause is not defined, 282 // but if it's another error, we can call toString() on it too. 283 if !cause.Get("toString").IsUndefined() { 284 errMsg += ": " + cause.Call("toString").String() 285 } else if cause.Type() == js.TypeString { 286 errMsg += ": " + cause.String() 287 } 288 } 289 errCh <- fmt.Errorf("net/http: fetch() failed: %s", errMsg) 290 return nil 291 }) 292 293 fetchPromise.Call("then", success, failure) 294 select { 295 case <-req.Context().Done(): 296 if !ac.IsUndefined() { 297 // Abort the Fetch request. 298 ac.Call("abort") 299 } 300 return nil, req.Context().Err() 301 case resp := <-respCh: 302 return resp, nil 303 case err := <-errCh: 304 return nil, err 305 } 306 } 307 308 var errClosed = errors.New("net/http: reader is closed") 309 310 // streamReader implements an io.ReadCloser wrapper for ReadableStream. 311 // See https://fetch.spec.whatwg.org/#readablestream for more information. 312 type streamReader struct { 313 pending []byte 314 stream js.Value 315 err error // sticky read error 316 } 317 318 func (r *streamReader) Read(p []byte) (n int, err error) { 319 if r.err != nil { 320 return 0, r.err 321 } 322 if len(r.pending) == 0 { 323 var ( 324 bCh = make(chan []byte, 1) 325 errCh = make(chan error, 1) 326 ) 327 success := js.FuncOf(func(this js.Value, args []js.Value) any { 328 result := args[0] 329 if result.Get("done").Bool() { 330 errCh <- io.EOF 331 return nil 332 } 333 value := make([]byte, result.Get("value").Get("byteLength").Int()) 334 js.CopyBytesToGo(value, result.Get("value")) 335 bCh <- value 336 return nil 337 }) 338 defer success.Release() 339 failure := js.FuncOf(func(this js.Value, args []js.Value) any { 340 // Assumes it's a TypeError. See 341 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError 342 // for more information on this type. See 343 // https://streams.spec.whatwg.org/#byob-reader-read for the spec on 344 // the read method. 345 errCh <- errors.New(args[0].Get("message").String()) 346 return nil 347 }) 348 defer failure.Release() 349 r.stream.Call("read").Call("then", success, failure) 350 select { 351 case b := <-bCh: 352 r.pending = b 353 case err := <-errCh: 354 r.err = err 355 return 0, err 356 } 357 } 358 n = copy(p, r.pending) 359 r.pending = r.pending[n:] 360 return n, nil 361 } 362 363 func (r *streamReader) Close() error { 364 // This ignores any error returned from cancel method. So far, I did not encounter any concrete 365 // situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close(). 366 // If there's a need to report error here, it can be implemented and tested when that need comes up. 367 r.stream.Call("cancel") 368 if r.err == nil { 369 r.err = errClosed 370 } 371 return nil 372 } 373 374 // arrayReader implements an io.ReadCloser wrapper for ArrayBuffer. 375 // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer. 376 type arrayReader struct { 377 arrayPromise js.Value 378 pending []byte 379 read bool 380 err error // sticky read error 381 } 382 383 func (r *arrayReader) Read(p []byte) (n int, err error) { 384 if r.err != nil { 385 return 0, r.err 386 } 387 if !r.read { 388 r.read = true 389 var ( 390 bCh = make(chan []byte, 1) 391 errCh = make(chan error, 1) 392 ) 393 success := js.FuncOf(func(this js.Value, args []js.Value) any { 394 // Wrap the input ArrayBuffer with a Uint8Array 395 uint8arrayWrapper := uint8Array.New(args[0]) 396 value := make([]byte, uint8arrayWrapper.Get("byteLength").Int()) 397 js.CopyBytesToGo(value, uint8arrayWrapper) 398 bCh <- value 399 return nil 400 }) 401 defer success.Release() 402 failure := js.FuncOf(func(this js.Value, args []js.Value) any { 403 // Assumes it's a TypeError. See 404 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError 405 // for more information on this type. 406 // See https://fetch.spec.whatwg.org/#concept-body-consume-body for reasons this might error. 407 errCh <- errors.New(args[0].Get("message").String()) 408 return nil 409 }) 410 defer failure.Release() 411 r.arrayPromise.Call("then", success, failure) 412 select { 413 case b := <-bCh: 414 r.pending = b 415 case err := <-errCh: 416 return 0, err 417 } 418 } 419 if len(r.pending) == 0 { 420 return 0, io.EOF 421 } 422 n = copy(p, r.pending) 423 r.pending = r.pending[n:] 424 return n, nil 425 } 426 427 func (r *arrayReader) Close() error { 428 if r.err == nil { 429 r.err = errClosed 430 } 431 return nil 432 }