Skip to content

Commit b2d3a2b

Browse files
Add Transporter for wasm js target (#42)
* added js target specific roundtrip * roundtrip_js dial parameter fix
1 parent 90d59cb commit b2d3a2b

3 files changed

Lines changed: 387 additions & 5 deletions

File tree

http/roundtrip.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// TINYGO: The following is copied and modified from Go 1.21.4 official implementation.
2+
3+
//go:build !js
4+
5+
package http
6+
7+
// RoundTrip implements a RoundTripper over HTTP.
8+
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
9+
return roundTrip(req)
10+
}

http/roundtrip_js.go

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

http/transport.go

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,3 @@ type readTrackingBody struct {
2525
type Transport struct{}
2626

2727
var DefaultTransport RoundTripper = &Transport{}
28-
29-
// roundTrip implements a RoundTripper over HTTP.
30-
func (t *Transport) RoundTrip(req *Request) (*Response, error) {
31-
return roundTrip(req)
32-
}

0 commit comments

Comments
 (0)