Skip to content

Http Handlers #57

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

Merged
merged 22 commits into from
Nov 17, 2017
Merged

Http Handlers #57

merged 22 commits into from
Nov 17, 2017

Conversation

chrisradek
Copy link
Contributor

This is an early WIP. The stream collectors shouldn't change much, but the http handlers will still change. Currently working on http tests.

Posting this as a reference.

@chrisradek chrisradek changed the title [WIP] Http Handlers Http Handlers Oct 31, 2017
@chrisradek
Copy link
Contributor Author

This PR has now been updated. I've tested the node http handler middleware on node.js, and the fetch handler middleware in chrome/firefox.

/**
* An object that contains options to pass to the underlying http client.
*/
httpOptions?: HttpOptions;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fix: HttpOptions -> HttpType

@chrisradek
Copy link
Contributor Author

Updates:

  • CoreHandler is now a separate package, and accepts an httpHandler when instantiated.
  • HttpHandlers are now classes, instead of functions. They can be instantiated with HttpOptions, such as timeouts. The idea is that httpOptions will be shared across all commands against a client context.
  • model is now part of HandlerArguments. If a HttpHandler/CoreHandler can be set once on a client context, but used for multiple commands, then they need to have the Operation Model available after they are created. It made sense to me to put this alongside input and request, let me know what you think.

Example usage:

    const httpHandler = new NodeHttpHandler(/* HttpOptions */{
        socketTimeout: 20 //ms
    });
    const restParser = new RestParser(
        new JsonParser(
            fromBase64
        ),
        streamCollector,
        toUtf8,
        fromBase64
    );
    const coreHandler = new CoreHandler(
        httpHandler,
        restParser
    );
    /** Fake calling coreHandler at end of Middleware Stack */
    let response = await coreHandler.handle({
        request: signedRequest,
        input: {},
        model: ListFunctions
    });

@jeskew
Copy link
Contributor

jeskew commented Nov 2, 2017

I'm not crazy about including the model in the arguments passed from handler to handler, but I'm also not sure I like how complex the alternatives would need to be. Those might include:

  1. Requiring the model to be passed into the constructor. Whoever creates the context object must also use the model included there to create the core handler. This seems easy but could make it hard to support customer-supplied core handlers (which are extremely useful for tests that involve exercising complex middleware stacks).
  2. Defining a CoreHandlerConstructor interface that looks roughly like:
interface CoreHandlerCtor<Input, Output, Stream> {
    new (context: HandlerExecutionContext): Handler<Input, Output, Stream>;
}

The middleware stack's resolve method could take that instead of Handler and then instantiate it with the context. (It could also be a property on the stack to make resolve easier to call; this is what PHP and Ruby do, I believe.) You would pass in the additional constructor arguments by subclassing, subclassing anonymously, or with bind.

2 Feels a bit more sound but maybe too complicated. It would also require instantiating more core handlers, which may not be as performant as simply including more properties in the HandlerArguments object.

@chrisradek
Copy link
Contributor Author

sigh I made code changes so that the model is no longer part of the handler args. This should be ready for a final review. Unfortunately, my node-http-handler tests (relies on a mock server) are failing with a networking error, and it only happens in code build (works using the same and other versions of node.js on local machine). I've tried the suggested tips online...so I'm approaching the point where I may just rip out using an actual server.

...I'll see how it looks in the morning.

Copy link
Contributor

@jeskew jeskew left a comment

Choose a reason for hiding this comment

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

LGTM, pending the CodeBuild-only test failures getting sorted out.

I had a couple nits and questions, but none of them were blocking.

InputType extends object,
OutputType extends MetadataBearer,
StreamType = Uint8Array
> implements Handler<InputType, OutputType, StreamType> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Very nit: extra indentation

// if the request was already aborted, prevent doing extra work
if (abortSignal && abortSignal.aborted) {
const abortError = new Error('Request aborted');
abortError.name = 'AbortError';
Copy link
Contributor

Choose a reason for hiding this comment

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

I've seen this pair of lines in a few places. Might make sense to have an AbortError constructor that automatically sets the name to 'AbortError'

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, there was an error in JS implementations when subclassing an error. Basically, instanceof Error checks returned false on subclasses of errors (I think there was a work-around if you weren't using es2015 classes).

That said, I can at the very least create refactor this into a function.

// pipe automatically handles end
return;
} else if (request.body) {
req.write(request.body);
Copy link
Contributor

Choose a reason for hiding this comment

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

I think you can just use req.end(request.body) here to eliminate both the need for the separate call to .end on line 93 and the early return on line 89.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good point, that'll work!

return;
}

request.on('socket', function(this: ClientRequest, socket: Socket) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure how I feel about calling a function parameter this. On my first reading of the callback, I thought you were able to call this.abort() due to some undocumented binding on node's part.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as comment below: this isn't a function parameter, it's used by Typescript to get the function's context.

Side-note: VSCode recognizes this and uses a different color for this than other parameters 😄

import {StreamCollector} from '@aws/types';
import {Collector} from './collector';

export const streamCollector: StreamCollector<Readable> = function streamCollector(
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just export function streamCollector?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did it this way so I could define the type StreamCollector<Readable>, and enforce that the function conforms to the StreamCollector interface. I had tried writing it as just a function and use the as type at the end, but that didn't work.

If you know a way to accomplish this, I'd be all for changing this to a function.

export const streamCollector: StreamCollector<Readable> = function streamCollector(
stream
): Promise<Uint8Array> {
return new Promise((resolve, reject) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This shouldn't occur when collecting an HTTP stream, but you would want to throw if the provided stream has an encoding set.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So I looked into this some more. There isn't a public interface for getting the encoding of the source stream. (stream._readableState.encoding)

That said, I'm not sure we need to worry about it. Strings will automatically get converted to buffers as data is being piped. The user would have to set the encoding on the source stream to be different than what the actual source is, which at that point they're essentially shooting themselves in the foot.

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair point. Objection withdrawn.

});
collector.on('error', reject);
collector.on('finish', function(this: Collector) {
const bytes = new Uint8Array(Buffer.concat(this.bufferedBytes));
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm still a bit thrown off by the use of this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to record the explanation somewhere:
TS 2.0 introduced a way to specify what this should be inside of a function:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html

See Specifying the type of this for functions

* Represents the http options that can be passed to a browser http client.
*/
export interface BrowserHttpOptions extends HttpOptions {
requestTimeout?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since we have to manually timeout requests when using fetch, couldn't this property just be part of HttpOptions?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes... I didn't want to also write this functionality for Node.js unless it was specifically asked for, which is why I only put it on the BrowserHttpOptions for now.

If you feel strongly that we should have request timeouts (separate from connection/socket-read timeouts) in node.js as well, I can add that.

} else {
return response.arrayBuffer()
.then(buffer => {
httpResponse.body = buffer;
Copy link
Contributor

Choose a reason for hiding this comment

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

Could this be changed to httpResponse.body = new Uint8Array(buffer)? This won't copy the buffer and allows us to declare the StreamType of browser requests to be ReadableStream|Uint8Array, which is easier for end users to disambiguate than ReadableStream|ArrayBuffer due to the availability of ArrayBuffer.isView.

@chrisradek chrisradek merged commit b798cec into aws:master Nov 17, 2017
trivikr referenced this pull request in trivikr/aws-sdk-js-v3 Dec 10, 2018
* Adds stream collectors
* Adds node-http-handler and fetch-http-handler
* Adds core-handler
@lock
Copy link

lock bot commented Sep 26, 2019

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.

@lock lock bot locked as resolved and limited conversation to collaborators Sep 26, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants