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

Conversation

dmitshur
Copy link
Member

@dmitshur dmitshur commented May 8, 2016

This is a WIP PR in order to get initial review comments, to communicate and avoid duplicate efforts. Known TODOs are listed in code.

The motivation to use Fetch API is because it's lower level, and allows implementing an http.RoundTripper more efficiently (less conversions between strings and []byte). It also supports streaming body responses efficiently.

References:

Tested in stable channel of Chrome, latest Safari, Firefox (developer edition).

Dynamically determine which of XHR, Fetch APIs are available and gracefully fallback to the best available API.

Here's a quick demo, using a local instance of https://http2.golang.org/reqinfo and https://http2.golang.org/clockstream endpoints. I've asked @bradfitz if it's possible to enable CORS on http2.golang.org itself, and if so, you could try this out in a playground.

func streamingDemo() error {
    resp, err := http.Get("https://localhost:4430/clockstream")
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    _, err = io.Copy(os.Stdout, resp.Body)
    return err
}

Results

(It's a video, click it to view.)

image

@dmitshur
Copy link
Member Author

dmitshur commented May 10, 2016

Ok, I've setup an instance of h2demo with CORS enabled, so now it's possible to try out this change in the GopherJS Playground:

http://www.gopherjs.org/playground/#/5NfiqqGZCs

Wait for it to load, press run, and wait a few seconds as it compiles the program and runs it!

It will make a request to /reqinfo, then another to /clockstream and copy (streaming) first 1500 bytes before returning.

It should work in latest stable Chrome and any other browser that supports Fetch and Streams APIs (see http://caniuse.com/#search=Fetch).

@neelance
Copy link
Member

Nice!

header := Header{}
result.Get("headers").Call("forEach", func(value, key *js.Object) {
header[CanonicalHeaderKey(key.String())] = []string{value.String()} // TODO: Support multiple values.
})
Copy link
Member Author

Choose a reason for hiding this comment

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

Suggestions for how to do this better are very welcome.

What's a good way to iterate all keys (and values) of a JS Iterable?

In JS, one could do something like:

for (let value of ["a", "b", "c"]) {
    console.log(value);
}

But what's a good way in GopherJS?

…API.

The motivation to use Fetch API is because it's lower level, and allows
implementing an http.RoundTripper more efficiently (less conversions
between strings and []byte). It also supports streaming body responses
efficiently.

Includes known TODOs.

References:

- https://fetch.spec.whatwg.org/
- https://streams.spec.whatwg.org/
- http://caniuse.com/#search=Fetch

Tested in stable channel of Chrome, latest Safari, Firefox (developer
edition).

Dynamically determine which of XHR, Fetch APIs are available and
gracefully fallback to the best available API.
…lity.

Add support for multiple same-key headers for request and response.

Send request body.

Close request body in RoundTrip to satisfy the interface contract. Also
apply this fix to XHRTransport.

Partial support for req.Cancel, remove CancelRequest(*http.Request)
support since that method is deprecated.

Fix a bug in streamReader due to not using pointer receiver Read method.
@dmitshur
Copy link
Member Author

dmitshur commented May 22, 2016

@neelance I've completed the critical functionality (closing response body, full request & response header support, and sending request body) in c11f3dd. PTAL.

This should be functionally ready to be merged. There are still some style and minor TODOs to clean up. Also, before merging, this should be squashed into logical commit(s) with clean commit message, and gopherjs/compiler/natives/fs_vfsdata.go needs to be regenerated.

> But [Content-Length header] doesn't seem to be set even for
non-streaming responses.

That wasn't true. It is set for non-streaming responses. It's just that
CORS needs to allow that header to be accessed by the frontend code, by
setting w.Header().Set("Access-Control-Expose-Headers",
"Content-Length"), etc.

I wish we could use "redirect": "manual", since http.RoundTripper is
not supposed to follow redirects, but that can't be done. Due to
security reasons (/cc @mholt), browsers don't allow frontend code
access to see intermediate redirect URLs, because that might allow for
cross-site scripting attacks (according to
https://fetch.spec.whatwg.org/#atomic-http-redirect-handling).
XHRTransport suffers from the same problem/limitation. So, it's better
to at least succeed and finish the request rather than not being able
to follow redirects at all.

The no body case is not applicable, I added that when I accidentally
confused request body for response body. It should always be set since
we're checking for ReadableStream support before opting in to use
fetchTransport. If that's not the case, we'll need to adjust that logic.
@dmitshur dmitshur changed the title WIP: compiler/natives/net/http: Implement http.RoundTripper on Fetch API. compiler/natives/net/http: Implement http.RoundTripper on Fetch API. May 22, 2016
@dmitshur dmitshur changed the title compiler/natives/net/http: Implement http.RoundTripper on Fetch API. compiler/natives/src/net/http: Implement http.RoundTripper on Fetch API. May 22, 2016
dmitshur added a commit to goxjs/glfw that referenced this pull request May 22, 2016
This allows the browser implementation to return the underlying
io.ReaderCloser directly, without having to first cache the entire
thing. That enables streaming of the underlying source to be possible
(which is possible in browser after Fetch Transport, see
gopherjs/gopherjs#454.

If seeking is desired, it can be added by the user on top of the
returned io.ReadCloser. In many cases, seeking is not very critical and
easy to avoid, and definitely not worth the expensive overhead of
always doing supporting it.

This is a slightly breaking API change if you depend on seeking, but
easy to work around in client code.
dmitshur added a commit to goxjs/glfw that referenced this pull request May 22, 2016
This allows the browser implementation to return the underlying
io.ReaderCloser directly, without having to first cache the entire
thing. That enables streaming of the underlying source to be possible
(which is possible in browser after Fetch Transport, see
gopherjs/gopherjs#454.

If seeking is desired, it can be added by the user on top of the
returned io.ReadCloser. In many cases, seeking is not very critical and
easy to avoid, and definitely not worth the expensive overhead of
always supporting it.

This is a slightly breaking API change if you depend on seeking, but
easy to work around in client code.
@@ -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.

// 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.

No need for fetchAPI, streamsAPI, xhrAPI local variables since the API
names can be easily inferred from the JS object being checked.
@dmitshur
Copy link
Member Author

dmitshur commented May 22, 2016

In case it's helpful, I've tested the implementation significantly during development using https://github.com/shurcooL/play/blob/master/192/go-js-fetch/http/test/main.go. You can run that suite by go get -u -d github.com/shurcooL/play/192/go-js-fetch/http/test and gopherjs serveing it.

@neelance
Copy link
Member

LGTM

It does not belong to `return &fetchTransport{}` line, so move it
closer to the relevant line.

Instead of putting it above, put it on the side. This is because this
is an internal detail that you'll only care about if editing or trying
to understand that line better. It's not needed for general readability.

So having to scroll to the right to see it is very fitting IMO.

This is modeled after Bret Victor's style of putting footnotes on the
right hand side of the article (side notes?). They can be skipped
unless the reader wants to learn more. See
http://virtivia.com:27080/5o1yhnwvjrvw.png.
@dmitshur dmitshur closed this in db27c7c May 22, 2016
@dmitshur
Copy link
Member Author

dmitshur commented May 22, 2016

Squashed and merged as 00254cb and db27c7c (with logically separated changes, clean commit messages).

I did not force-push to the fetch-transport branch of this PR, so that we can still see the development history here. Instead, I just added "Closes #454." to the commit message of the squashed commit. But its content is identical to the diff in this PR.

@dmitshur dmitshur deleted the fetch-transport branch May 22, 2016 23:13
@dmitshur
Copy link
Member Author

dmitshur commented May 23, 2016

For anyone interested in an updated demo, I've updated the playground to use latest GopherJS compiler and created this simple demo that uses http.Get:

http://www.gopherjs.org/playground/#/okdxAaGlfF

dmitshur added a commit that referenced this pull request Jun 12, 2016
Followup to #454. Mention that Fetch API can be used.

Also mention that by default, without third party polyfill modules for XHR or Fetch APIs, node.js will not support net/http.
dmitshur added a commit that referenced this pull request Jun 12, 2016
Followup to #454. Mention that Fetch API can be used.

Mention that only net/http client is supported, server is not.

Also mention that by default, without third party polyfill modules for XHR or Fetch APIs, node.js will not support net/http.
dmitshur added a commit that referenced this pull request Jul 4, 2016
This makes the behavior of http.DefaultTransport using Fetch API more
similar as when using XHR API.

It's unfortunate to have to do this, since it's kinda magic, but this
seems to be the most reasonable default. It's consistent with the
previous behavior of XHR implementation.

There is no equivalent property in http.Request as far as I can tell.
The only other way to set credentials would be to use req.AddCookie,
but then one would need to get the cookie in JavaScript, which is not
possible if that cookie has HttpOnly flag set.

It appears this is an unfortunate reality that we have to deal with to
provide a useful http.DefaultTransport implementation on frontend,
because of restrictions put into place due to security concerns on the web.

For reference, see:

https://fetch.spec.whatwg.org/#concept-request-credentials-mode
https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials

> A request has an associated credentials mode, which is "omit",
> "same-origin", or "include". Unless stated otherwise, it is "omit".

https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/withCredentials

> Setting withCredentials has no effect on same-site requests.

So default XHR behavior without withCredentials set is like Fetch
with "same-origin" credentials mode.

Updates #454.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants