-
Notifications
You must be signed in to change notification settings - Fork 570
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
compiler/natives/src/net/http: Implement http.RoundTripper on Fetch API. #454
Changes from 3 commits
d2ae28d
c11f3dd
f24a0ca
b1c7241
4f00876
9a99615
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
// +build js | ||
|
||
package http | ||
|
||
import ( | ||
"errors" | ||
"io" | ||
"io/ioutil" | ||
"strconv" | ||
|
||
"github.com/gopherjs/gopherjs/js" | ||
) | ||
|
||
// streamReader implements an io.ReadCloser wrapper for ReadableStream of https://fetch.spec.whatwg.org/. | ||
type streamReader struct { | ||
pending []byte | ||
stream *js.Object | ||
} | ||
|
||
func (r *streamReader) Read(p []byte) (n int, err error) { | ||
if len(r.pending) == 0 { | ||
var ( | ||
bCh = make(chan []byte) | ||
errCh = make(chan error) | ||
) | ||
r.stream.Call("read").Call("then", | ||
func(result *js.Object) { | ||
if result.Get("done").Bool() { | ||
errCh <- io.EOF | ||
return | ||
} | ||
bCh <- result.Get("value").Interface().([]byte) | ||
}, | ||
func(reason *js.Object) { | ||
// Assumes it's a DOMException. | ||
errCh <- errors.New(reason.Get("message").String()) | ||
}, | ||
) | ||
select { | ||
case b := <-bCh: | ||
r.pending = b | ||
case err := <-errCh: | ||
return 0, err | ||
} | ||
} | ||
n = copy(p, r.pending) | ||
r.pending = r.pending[n:] | ||
return n, nil | ||
} | ||
|
||
func (r *streamReader) Close() error { | ||
// TOOD: Cannot do this because it's a blocking call, and Close() is often called | ||
// via `defer resp.Body.Close()`, but GopherJS currently has an issue with supporting that. | ||
// See https://github.com/gopherjs/gopherjs/issues/381 and https://github.com/gopherjs/gopherjs/issues/426. | ||
/*ch := make(chan error) | ||
r.stream.Call("cancel").Call("then", | ||
func(result *js.Object) { | ||
if result != js.Undefined { | ||
ch <- errors.New(result.String()) // TODO: Verify this works, it probably doesn't and should be rewritten as result.Get("message").String() or something. | ||
return | ||
} | ||
ch <- nil | ||
}, | ||
) | ||
return <-ch*/ | ||
r.stream.Call("cancel") | ||
return nil | ||
} | ||
|
||
// fetchTransport is a RoundTripper that is implemented using Fetch API. It supports streaming | ||
// response bodies. | ||
type fetchTransport struct{} | ||
|
||
func (t *fetchTransport) RoundTrip(req *Request) (*Response, error) { | ||
headers := js.Global.Get("Headers").New() | ||
for key, values := range req.Header { | ||
for _, value := range values { | ||
headers.Call("append", key, value) | ||
} | ||
} | ||
opt := map[string]interface{}{ | ||
"method": req.Method, | ||
"headers": headers, | ||
} | ||
if req.Body != nil { | ||
// TODO: Find out if request body can be streamed into the fetch request rather than in advance here. | ||
// See BufferSource at https://fetch.spec.whatwg.org/#body-mixin. | ||
body, err := ioutil.ReadAll(req.Body) | ||
if err != nil { | ||
req.Body.Close() // RoundTrip must always close the body, including on errors. | ||
return nil, err | ||
} | ||
req.Body.Close() | ||
opt["body"] = body | ||
} | ||
respPromise := js.Global.Call("fetch", req.URL.String(), opt) | ||
|
||
var ( | ||
respCh = make(chan *Response) | ||
errCh = make(chan error) | ||
) | ||
respPromise.Call("then", | ||
func(result *js.Object) { | ||
header := Header{} | ||
result.Get("headers").Call("forEach", func(value, key *js.Object) { | ||
ck := CanonicalHeaderKey(key.String()) | ||
header[ck] = append(header[ck], value.String()) | ||
}) | ||
|
||
contentLength := int64(-1) | ||
if cl, err := strconv.ParseInt(header.Get("Content-Length"), 10, 64); err == nil { | ||
contentLength = cl | ||
} | ||
|
||
respCh <- &Response{ | ||
Status: result.Get("status").String() + " " + StatusText(result.Get("status").Int()), | ||
StatusCode: result.Get("status").Int(), | ||
Header: header, | ||
ContentLength: contentLength, | ||
Body: &streamReader{stream: result.Get("body").Call("getReader")}, | ||
Request: req, | ||
} | ||
}, | ||
func(reason *js.Object) { | ||
errCh <- errors.New("net/http: fetch() failed") | ||
}, | ||
) | ||
select { | ||
case resp := <-respCh: | ||
return resp, nil | ||
case err := <-errCh: | ||
return nil, err | ||
case <-req.Cancel: | ||
// TODO: Abort request if possible using Fetch API. | ||
return nil, errors.New("net/http: request canceled") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -13,18 +13,29 @@ import ( | |
"github.com/gopherjs/gopherjs/js" | ||
) | ||
|
||
var DefaultTransport RoundTripper = &XHRTransport{} | ||
var DefaultTransport = func() RoundTripper { | ||
if fetchAPI, streamsAPI := js.Global.Get("fetch"), js.Global.Get("ReadableStream"); fetchAPI != js.Undefined && streamsAPI != js.Undefined { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why the extra variables? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It checks that both Body: &streamReader{stream: result.Get("body").Call("getReader")}, For example, Firefox currently supports Fetch API but does not support streaming response bodies. So See There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I meant this:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, I overlooked that or didn't think of it. It looks simpler, so I'll do it. I think I initially tried to pass the variable into the transport, e.g.:
But reverted because it was a bad idea. It meant that zero value was no longer valid, and there's no performance gain in storing It might also have been because I wanted to document the name of API being checked for. |
||
return &fetchTransport{} | ||
} | ||
if xhrAPI := js.Global.Get("XMLHttpRequest"); xhrAPI != js.Undefined { | ||
return &XHRTransport{} | ||
} | ||
return noTransport{} | ||
}() | ||
|
||
// noTransport is used when neither Fetch API nor XMLHttpRequest API are available. It always fails. | ||
type noTransport struct{} | ||
|
||
func (noTransport) RoundTrip(req *Request) (*Response, error) { | ||
return nil, errors.New("net/http: neither of Fetch nor XMLHttpRequest APIs is available") | ||
} | ||
|
||
type XHRTransport struct { | ||
inflight map[*Request]*js.Object | ||
} | ||
|
||
func (t *XHRTransport) RoundTrip(req *Request) (*Response, error) { | ||
xhrConstructor := js.Global.Get("XMLHttpRequest") | ||
if xhrConstructor == js.Undefined { | ||
return nil, errors.New("net/http: XMLHttpRequest not available") | ||
} | ||
xhr := xhrConstructor.New() | ||
xhr := js.Global.Get("XMLHttpRequest").New() | ||
|
||
if t.inflight == nil { | ||
t.inflight = map[*Request]*js.Object{} | ||
|
@@ -79,8 +90,10 @@ func (t *XHRTransport) RoundTrip(req *Request) (*Response, error) { | |
var err error | ||
body, err = ioutil.ReadAll(req.Body) | ||
if err != nil { | ||
req.Body.Close() // RoundTrip must always close the body, including on errors. | ||
return nil, err | ||
} | ||
req.Body.Close() | ||
} | ||
xhr.Call("send", body) | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be ensured via a
defer
at the beginning on the function?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, but I don't think it'd be cleaner. You still have to check that
req.Body
is notnil
before doing it.There are no other
return
s before this section of code (the code above simply deals with setting up the fields needed for the request), soreq.Body.Close()
will always be called.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
K.