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 "net/http/internal/ascii"
14 "strconv"
15 "bytes"
16 "syscall/js"
17 )
18 19 var 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
27 const 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
35 const 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
43 const jsFetchRedirect = "js.fetch:redirect"
44 45 // jsFetchMissing will be true if the Fetch API is not present in
46 // the browser globals.
47 var 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.
56 var jsFetchDisabled = js.Global().Get("process").Type() == js.TypeObject &&
57 bytes.HasPrefix(js.Global().Get("process").Get("argv0").String(), "node")
58 59 // RoundTrip implements the [RoundTripper] interface using the WHATWG Fetch API.
60 func (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 = chan *Response{1}
132 errCh = 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 // Wait for fetch promise to be rejected prior to exiting. See
241 // https://github.com/golang/go/issues/57098 for more details.
242 select {
243 case resp := <-respCh:
244 resp.Body.Close()
245 case <-errCh:
246 }
247 }
248 return nil, req.Context().Err()
249 case resp := <-respCh:
250 return resp, nil
251 case err := <-errCh:
252 return nil, err
253 }
254 }
255 256 var errClosed = errors.New("net/http: reader is closed")
257 258 // streamReader implements an io.ReadCloser wrapper for ReadableStream.
259 // See https://fetch.spec.whatwg.org/#readablestream for more information.
260 type streamReader struct {
261 pending []byte
262 stream js.Value
263 err error // sticky read error
264 }
265 266 func (r *streamReader) Read(p []byte) (n int, err error) {
267 if r.err != nil {
268 return 0, r.err
269 }
270 if len(r.pending) == 0 {
271 var (
272 bCh = chan []byte{1}
273 errCh = chan error{1}
274 )
275 success := js.FuncOf(func(this js.Value, args []js.Value) any {
276 result := args[0]
277 if result.Get("done").Bool() {
278 errCh <- io.EOF
279 return nil
280 }
281 value := []byte{:result.Get("value").Get("byteLength").Int()}
282 js.CopyBytesToGo(value, result.Get("value"))
283 bCh <- value
284 return nil
285 })
286 defer success.Release()
287 failure := js.FuncOf(func(this js.Value, args []js.Value) any {
288 // Assumes it's a TypeError. See
289 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
290 // for more information on this type. See
291 // https://streams.spec.whatwg.org/#byob-reader-read for the spec on
292 // the read method.
293 errCh <- errors.New(args[0].Get("message").String())
294 return nil
295 })
296 defer failure.Release()
297 r.stream.Call("read").Call("then", success, failure)
298 select {
299 case b := <-bCh:
300 r.pending = b
301 case err := <-errCh:
302 r.err = err
303 return 0, err
304 }
305 }
306 n = copy(p, r.pending)
307 r.pending = r.pending[n:]
308 return n, nil
309 }
310 311 func (r *streamReader) Close() error {
312 // This ignores any error returned from cancel method. So far, I did not encounter any concrete
313 // situation where reporting the error is meaningful. Most users ignore error from resp.Body.Close().
314 // If there's a need to report error here, it can be implemented and tested when that need comes up.
315 r.stream.Call("cancel")
316 if r.err == nil {
317 r.err = errClosed
318 }
319 return nil
320 }
321 322 // arrayReader implements an io.ReadCloser wrapper for ArrayBuffer.
323 // https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer.
324 type arrayReader struct {
325 arrayPromise js.Value
326 pending []byte
327 read bool
328 err error // sticky read error
329 }
330 331 func (r *arrayReader) Read(p []byte) (n int, err error) {
332 if r.err != nil {
333 return 0, r.err
334 }
335 if !r.read {
336 r.read = true
337 var (
338 bCh = chan []byte{1}
339 errCh = chan error{1}
340 )
341 success := js.FuncOf(func(this js.Value, args []js.Value) any {
342 // Wrap the input ArrayBuffer with a Uint8Array
343 uint8arrayWrapper := uint8Array.New(args[0])
344 value := []byte{:uint8arrayWrapper.Get("byteLength").Int()}
345 js.CopyBytesToGo(value, uint8arrayWrapper)
346 bCh <- value
347 return nil
348 })
349 defer success.Release()
350 failure := js.FuncOf(func(this js.Value, args []js.Value) any {
351 // Assumes it's a TypeError. See
352 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError
353 // for more information on this type.
354 // See https://fetch.spec.whatwg.org/#concept-body-consume-body for reasons this might error.
355 errCh <- errors.New(args[0].Get("message").String())
356 return nil
357 })
358 defer failure.Release()
359 r.arrayPromise.Call("then", success, failure)
360 select {
361 case b := <-bCh:
362 r.pending = b
363 case err := <-errCh:
364 return 0, err
365 }
366 }
367 if len(r.pending) == 0 {
368 return 0, io.EOF
369 }
370 n = copy(p, r.pending)
371 r.pending = r.pending[n:]
372 return n, nil
373 }
374 375 func (r *arrayReader) Close() error {
376 if r.err == nil {
377 r.err = errClosed
378 }
379 return nil
380 }
381