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

Configurable Transports and Extraction of the Node HTTP Transport #265

Merged
merged 23 commits into from Oct 27, 2018

Conversation

jonny-improbable
Copy link
Contributor

@jonny-improbable jonny-improbable commented Oct 20, 2018

  • Allow the user to specify the default transport
  • Allow the user to customise individual Transports, beyond the common TransportOptions interface
  • Expose the underlying Fetch and XHR based transports
  • Expose proposed set of configuration options in FetchTransportInit
  • Refactor src/transport folder structure / package layout
  • Remove and extract the Node-HTTP Transport into its own npm module
  • Test coverage for setDefaultTransport
  • Revisit docs/transport.md

Change Overview

Following on from the conversation in #261 this PR allows the user to configure the behavior of a given Transport, separate from the pre-existing TransportOptions interface which deals with passing request state (ie: host, url, callbacks, etc).

To facilitate this change, the transport option passed to the unary, invoke and client functions is now expected to meet the new grpc.TransportFactory interface, ie: a func which can accept zero or more args and will return a grpc.Transport instance.

grpc.unary(BookService.GetBook, {
    request: getBookRequest,
    host: host,
    transport: grpc.HttpTransport({ credentials: 'include' }),
    onEnd: ( /*...*/ )
});

Note this results in a breaking change for anyone currently using the Websocket Transport; instead of specifying:

transport: gprc.WebsocketTransportFactory

They would instead now call:

transport: grpc.WebsocketTransport()

This change also spurred me to to fix #191 and #141 by extracting the 'Node HTTP' transport out from the gprc-web-client package and into a new module: grpc-web-node-http-client, which will be published to npm separately.

As one thing leads to another, extracting node-grpc-web-node-http-transport led to implement grpc.setDefaultTransport() which allows the user to specify which TransportFactory` should be .invoked if none is supplied in the options.

Breaking Changes

  • transport option passed to unary, invoke and client is now expected to by an instance of Transport, not a reference to a function which will return a Transport when invoked.
  • grpc-web-client no longer auto-selects a suitable transport in a NodeJS environment; users of grpc-web-client in a NodeJS environment will need to depend upon grpc-web-node-http-transport and make use of grpc.setDefaultTranpsport().

What's Next?

  • The whole npm linking stuff is disgusting and hard to maintain. I've played around with Lerna and I feel it would dramatically simplify things; however it does require us to move around a tonne of files, and I don't want to make this diff any bigger than it already is!
  • I don't feel that the name of the TransportOptions interface conveys the intention of this object well and could be confusing to future developers. Might be wise to rename it, however this would be a breaking change now we've exported it!

@MOZGIII
Copy link

MOZGIII commented Oct 20, 2018

This allows for setting credentials to include, but not to omit. Obviously, I know why that is, just want to point out this solutions only allows for one tiny use case to be fixed. Maybe it's worth it.
However, on the downside, to me it looks like this exposes a new API that can make it difficult to expose a low-level customization that I'd like to have available there (or it may not, depending on what API compatibility rules are followed across releases and if it'll seem reasonable to accept breaking changes).

That said, I like the idea, but I'd like to take a few extra steps there:

  1. Make Fetch constructor take RequestInit as a second argument.

  2. In here:

    fetch(this.options.url, {
    headers: this.metadata.toHeaders(),
    method: "POST",
    body: msgBytes,
    credentials: this.config.credentials,
    signal: this.controller && this.controller.signal
    }).then((res: Response) => {

    Allow setting arbitrary parameters:

    fetch(this.options.url, {
      credentials: "same-origin",
      ...this.config,
      headers: this.metadata.toHeaders(),
      method: "POST",
      body: msgBytes,
      signal: this.controller && this.controller.signal
    })
  3. Add a helper function to convert HttpTransportConfig format to the format Fetch accepts (a noop in this case).

  4. At HttpTransport, based on the detected transport type, convert the common HttpTransportConfig to whatever underlying transport factory takes (in the case of Fetch that would be calling the helper function defined at (3)).

  5. Repeat (2), (3) and (4) for every transport in a way that makes sense for that particular transport. For example, MozXhr can define it's own interface to MozXhrConfig to use as a second constructor argument:

    interface MozXhrConfig {
      withCredentials?: boolean;
    }

    With a func to convert the HttpTransportConfig to MozXhrConfig, let HttpTransport do the transform of the "common" representation into the transport low level one.

    Instead of

    if (this.config.credentials === "include") {
    xhr.withCredentials = true;
    }

    we'd then have:

    if (this.config.withCredentials !== undefined) {
      xhr.withCredentials = this.config.withCredentials;
    }

The essential of my proposal is to:
a) accept that transports are internally different and have to be configured differently;
b) expose that and let user do the low level configuration if the situation requires;
c) move the logic of mapping the "shared, common" config options to low level ones from the transports themselves to the HttpTransport meta-factory (that itself utilizes per-transport helper functions) - rationale is transports do not need to even know about the existence of a HttpTransportConfig; as long as they expose the API to configure what can be configured, the necessary configuration can be done by a caller, and if the autodetecting HttpTransport factory is used that would be it's job to configure the underlying transport to deliver what user requested at the HttpTransportConfig (it be a sort of GCD of the configuration possibilities for the underlying transports);
d) eliminate intertwining of the transport implementations and the HttpTransport factory - the underlying transport implementations become loosely coupled, and do not have to limit their knobs to the "GCD" of all the transports' settings.

So, here are my two cents.

@MOZGIII
Copy link

MOZGIII commented Oct 20, 2018

Sorry, I accidentally posted the comment above mid-edit. It's now ready.

@jonnyreeves
Copy link
Contributor

Yep all makes sense. A, B and C all feel achievable inside the scope of this PR. Let's revisit D when those have been achieved.

Thanks for your perspective

Copy link
Contributor

@johanbrandhorst johanbrandhorst left a comment

Choose a reason for hiding this comment

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

I have nothing to add beyond what @MOZGIII already said :)


if (detectMozXHRSupport()) {
return mozXhrRequest;
return FetchReadableStreamTransport({ credentials: init.withCredentials ? 'include' : 'same-origin' })
Copy link
Contributor

Choose a reason for hiding this comment

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

How do you configure this to omit now?

Copy link

@MOZGIII MOZGIII Oct 21, 2018

Choose a reason for hiding this comment

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

This implementation allows explicitly using transport: FetchReadableStreamTransport({ credentials: "omit" }) in the invocation/client instead of transport: HttpTransport({ /* whatever */ }).
HttpTransport can't support omit because it's not in the "greatest common interface", as XMLHttpRequest can only allow include or same-origin - https://xhr.spec.whatwg.org/#the-send()-method see credentials mode at (6) there.

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

So far it's all good. 👍
I'd suggest extracting HttpTransport into a separate file. It's probably a good idea to restructure transports code a bit - with a single transport per file, and probably moving the base Transport class to a upper level directory - so that only transport implementations are left at the transports dir.

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

Idea of what can be exposted for each transport:

  • XMLHttpRequest:

    • headers - probably easier to expose as key-value pairs field rather than native XMLHttpRequest interface;
    • withCredentials

    I considered timeout, but it seems exposing it might distrupt normal gRPC execution. However I'd still expose it with a disclamer that it is to be used on one's own risk.

  • fetch:

    • headers - can be exposed but custom merging is required
    • mode
    • credentials
    • cache
    • redirect
    • referrer
    • referrerPolicy
    • integrity - might not make sense, but I don't see a reason why it wouldn't work technically
    • keepalive

@johanbrandhorst
Copy link
Contributor

As for the proposed transport knobs; I don't think we should expose header configuration, this is already possible with gRPC metadata.

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

It seems like metadata values map directly to headers. This kind of makes headers-metadata relationship strongly coupled. This implemnetation detail makes it so it's indeed no need to allow setting headers, because setting metadata already does that.
After consulting https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md I think it's ok, and can confirm no explicit support for settings headers is nesessary by upstream gRPC design.

@jonny-improbable
Copy link
Contributor Author

Agreed on not adding headers; I can look into adding the additional configuration options to Fetch and adding some new test coverage.

One thing worth explicitly calling out is that I have removed the 'NodeJS' transport from being automatically selected in the environment. Given issues such as #203 and #204 I would suggest that we extract the node-transport into it's own package (maybe: improbable-eng/grpc-web-node-http-transport)? Thoughts?

@johanbrandhorst
Copy link
Contributor

Agreed, it might break existing clients but I think node should be a separate concern.

@jonny-improbable
Copy link
Contributor Author

@MOZGIII referrerPolicy, keepalive and integrity all argued with the version of TypeScript we are using so I've left those out of the implementation; we can always add them later if people need them.

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

@jonny-improbable I'd rather force-add them even if TypeScript doesn't support it - it's kind of providing the full implementation for the spec. TypeScript should not be a limiting factor for implementation.
It could be added later, but that's an extra step - and that's I don't like.

@jonny-improbable
Copy link
Contributor Author

@MOZGIII thanks for the gentle encouragement, figured out I was being stupid - all good now :)

@johanbrandhorst
Copy link
Contributor

Isn't the reason we shouldn't include them because the minimum browser we aim to support don't support these settings? The risk now is that users implement things tat aren't supported in the browser's we promise support for. We must at the very least expose this compromise to users.

@jonny-improbable
Copy link
Contributor Author

@johanbrandhorst The way I read @MOZGIII's request is that developers should be able to specify and configure a concrete / very browser specific transport if the wish to; this is the case with the FetchReadableStreamTransport which will basically only work in Chrome and later versions of Edge. Using this transport, the developer assumes that their end users are only using these browsers.

For everyone else, they should use the HttpTransport which provides the Greatest Common Divisor (GCD) of XHR and FetchRedableStream based transports.

@jonny-improbable
Copy link
Contributor Author

More work on this PR - moved the code for the NodeHttpTransport over to https://github.com/improbable-eng/grpc-web-node-http-transport/tree/master

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

@johanbrandhorst in general, when it comes to building projects, it's the user of the library who's in control of browser compatibility requirement, not the library owner. Owners, therefore, should strive for providing more flexibility, than less.
This is not true, however whne it comes to "opinitionated" libraries/frameworks - there the flexibiliy is usually explicitly reduced to enforce some particular way of doing things.
gRPC implemenations are opinionated too in a way, but I guess they should only be opinionated when it comes to following the spec. Forcing programmers to use only a particular subset of options is not as good.

@MOZGIII
Copy link

MOZGIII commented Oct 21, 2018

Actually, I just noticed that the spread operator is used:

fetch(this.options.url, {
...this.init,
headers: this.metadata.toHeaders(),
method: "POST",
body: msgBytes,
signal: this.controller && this.controller.signal
}).then((res: Response) => {

This means any underlying values can be passed, which is good.

It also means that FetchTransportInit can be just:

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
export type FetchTransportInit = Omit<RequestInit, "headers" | "method" | "body" | "signal">

@@ -4,5 +4,7 @@ process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
process.env.DISABLE_CORS_TESTS = "true";
// Disable the WebSocket-based tests as they don't apply for Node environments
process.env.DISABLE_WEBSOCKET_TESTS = "true";
// Although this will be set by Travis CI; it won't be set locally.
process.env.BROWSER = "nodejs";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As the comment suggest, this is required when running the tests locally; this was not an issue before because the node-http transport was automatically detected by default; however now testRpcCombinations.ts sniffs for this environment variable and configures the NodeHttpTransport accordingly.

@@ -34,7 +34,6 @@
},
"devDependencies": {
"@types/google-protobuf": "^3.2.5",
"@types/node": "^10.7.1",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

With the removal of the node-http transport from grpc-web-client - this is no longer required.

cancel() {
this.options.debug && debug("XHR.abort");
this.xhr.abort();
}
}

export class MozChunkedArrayBufferXHR extends XHR {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed a tonne of duplicated code between the xhr and mozXhr files by using a classic Template Design Pattern.

@johanbrandhorst
Copy link
Contributor

Amazing work Jonny!

@jonny-improbable jonny-improbable added the breaking-change Will require a major version increment label Oct 25, 2018
@@ -60,7 +60,7 @@ Sending multiple messages and indicating that the client has finished sending ar

Most browser networking methods do not allow control over the sending of the body of the request, meaning that sending a single request message forces the finishing of sending, limiting these transports to unary or server-streaming methods only.

For transports that do allow control over the sending of the body (e.g. websockets - coming soon), the client can optionally indicate that it has finished sending. This is useful for client-streaming or bi-directional methods in which the server will send responses after receiving all client messages. Usage with unary methods is likely not necessary as server handlers will assume the client has finished sending after receiving the single expected message.
For transports that do allow control over the sending of the body (e.g. websockets), the client can optionally indicate that it has finished sending. This is useful for client-streaming or bi-directional methods in which the server will send responses after receiving all client messages. Usage with unary methods is likely not necessary as server handlers will assume the client has finished sending after receiving the single expected message.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unrelated change, but whilst I was here I wanted to fix this.

@jonny-improbable jonny-improbable changed the title [WIP] Allow configuration of Transports. Configurable Transports and Extraction of the Node HTTP Transport Oct 25, 2018
Copy link
Contributor

@johanbrandhorst johanbrandhorst left a comment

Choose a reason for hiding this comment

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

LGTM, small documentation question. Would appreciate @MOZGIII's opinion as well, of course.


If none are found then an exception is thrown.
will fall back to a DefaultHttpTransport which works across the vast majority of browsers with some limitations. See [Available Transports](#available-transports)
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't look right


if (detectNodeHTTPSupport()) {
return httpNodeRequest;
}

Copy link

@MOZGIII MOZGIII Oct 26, 2018

Choose a reason for hiding this comment

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

When you view this file it's easy to notice a lot of empty lines - we should probably clean them up.
Have you considered using prettier?

return FetchReadableStreamTransport(fetchInit);
}
return XhrTransport({ withCredentials: init.withCredentials });
}
Copy link

Choose a reason for hiding this comment

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

No newline at EOF - consider adding .editorconfig to this repo

@MOZGIII
Copy link

MOZGIII commented Oct 26, 2018

There are some minor code-style relating things. Other that that everything looks good, great job!

@jonny-improbable jonny-improbable merged commit 554a663 into master Oct 27, 2018
@jonny-improbable jonny-improbable deleted the feature/configurable-transports branch October 27, 2018 17:03
@jonny-improbable
Copy link
Contributor Author

Thanks @MOZGIII and @johanbrandhorst

@MOZGIII
Copy link

MOZGIII commented Oct 27, 2018

Should we expect a version bump soon?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
breaking-change Will require a major version increment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Using grpc-web-client with Angular (angular-cli)
4 participants