Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions compiler/natives/src/net/http/fetch.go
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.
Copy link
Member

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?

Copy link
Member Author

@dmitshur dmitshur May 22, 2016

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 not nil before doing it.

There are no other returns before this section of code (the code above simply deals with setting up the fields needed for the request), so req.Body.Close() will always be called.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

K.

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")
}
}
25 changes: 19 additions & 6 deletions compiler/natives/src/net/http/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why the extra variables?

Copy link
Member Author

@dmitshur dmitshur May 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It checks that both fetch API and ReadableStream is available. The former is needed to make Fetch requests, and the latter is to be able to execute this line successfully:

Body: &streamReader{stream: result.Get("body").Call("getReader")},

For example, Firefox currently supports Fetch API but does not support streaming response bodies. So result.Get("body") would be nil, and using Fetch over XHR is not very advantageous.

See readonly attribute ReadableStream? body; at https://fetch.spec.whatwg.org/#response-class.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant this:

if js.Global.Get("fetch") != js.Undefined && js.Global.Get("ReadableStream") != js.Undefined {

Copy link
Member Author

@dmitshur dmitshur May 22, 2016

Choose a reason for hiding this comment

The 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.:

if xhrAPI := js.Global.Get("XMLHttpRequest"); xhrAPI != js.Undefined {
    return &XHRTransport{xhrAPI}
}

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 js.Global.Get("XMLHttpRequest") in a variable.

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{}
Expand Down Expand Up @@ -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)

Expand Down