Skip to content

Conversation

@hiranya911
Copy link
Contributor

@hiranya911 hiranya911 commented May 24, 2018

The existing HTTP client abstractions (HttpRequestHandler and SignedApiRequestHandler) have a number of caveats:

  1. Do not support HTTP endpoints (HTTPS only)
  2. Do not expose response headers
  3. Complex and often confusing error handling semantics (e.g. even low-level network errors are reported as errors with HTTP status codes)
  4. Support for handling different payload types is broken (e.g. cannot handle anything other than application/json, text/html and text/plain)
  5. Not easy to extend or test (public API takes 5 arguments already)

I ran into pretty much all the above problems while prototyping the IAM support for token minting.

To resolve these problems I propose a new set of HTTP client abstractions (HttpClient and AuthorizedHttpClient). My original plan was to use the axios library under the hood, but its support for handling timeouts needs a bit of work. So I came up with an implementation which borrows from both axios and our already existing HttpRequestHandler class. The resulting implementation:

  1. Supports both HTTPS and HTTP
  2. Exposes response headers alongside status and payload
  3. Simplifies error handling -- HTTP errors are reported by throwing an HttpError which provides access to the response object. All other low-level errors result in a FirebaseAppError.
  4. Supports any payload type
  5. Fairly easy to test; new arguments can be added by extending the HttpRequestConfig type

This PR implements the new abstractions and provides unit tests. I will update the rest of the codebase to use them in a series of future PRs (work already underway in a separate branch).

@hiranya911
Copy link
Contributor Author

Related to #274

@bojeil-google
Copy link
Contributor

I haven't started review yet, but why do we want to support HTTP requests? I am hoping that you are not planning to use this with unencrypted connections.

@hiranya911
Copy link
Contributor Author

I want to be able to use this to access things like the local Metadata service, which is only available over HTTP: https://github.com/firebase/firebase-admin-node/blob/master/src/auth/credential.ts#L338

public readonly headers: any;
public readonly text: string;

private readonly data_: any;
Copy link
Contributor

Choose a reason for hiding this comment

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

private properties should not use trailing underscores (typescript style guide).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

public readonly text: string;

private readonly data_: any;
private readonly request_: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

private properties should not use trailing underscores (typescript style guide).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

return this.data_;
}
try {
return JSON.parse(this.text);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this try/catch needed again? You already test this in the constructor. You may as well throw an error directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

url: string;
headers?: {[key: string]: string};
data?: string | object | Buffer;
timeout?: number;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is there no port number (in case a non-default one is used)? It seems like this has to be provided in the URL. Maybe document that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

readonly data: any;
}

class DefaultHttpResponse implements HttpResponse {
Copy link
Contributor

Choose a reason for hiding this comment

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

Javadocs for these new structures would be nice to have.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

respStream.on('end', () => {
const responseData = Buffer.concat(responseBuffer).toString();
response.data = responseData;
settle(resolve, reject, response);
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you rename to something less abstract?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Renamed to finalizeRequest

}

function enhanceError(
error,
Copy link
Contributor

Choose a reason for hiding this comment

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

error: Error,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's not possible since we add new fields to the error object in the method body (triggers TS compilation error).

return 'access_token_' + _.random(999999999);
}

export function responseFrom(data: any, status: number = 200, headers: any = {}): HttpResponse {
Copy link
Contributor

Choose a reason for hiding this comment

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

Add javadoc. Also where is this used?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

Not used anywhere at the moment. But it will be used once we start mocking interactions in other unit tests. I have separate PRs coming up for that.

});
});

it('should be fulfilled for a 2xx response with a json payload', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This looks exactly like the test above it.

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

const reqData = {request: 'data'};
const respData = {success: true};
const scope = nock('https://' + mockHost, {
reqheaders: {
Copy link
Contributor

Choose a reason for hiding this comment

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

indent 2 spaces instead of 4

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@hiranya911
Copy link
Contributor Author

Made the suggested changes. Over to @bojeil-google for another look.

Copy link
Contributor

@bojeil-google bojeil-google left a comment

Choose a reason for hiding this comment

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

LGTM. Just have a bunch of javadoc nits to be consistent with the rest of the repo.

/**
* Creates a mock HTTP response from the given data and parameters.
*
* @param data Data to be included in the response body.
Copy link
Contributor

Choose a reason for hiding this comment

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

@param {*} data ...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

* Creates a mock HTTP response from the given data and parameters.
*
* @param data Data to be included in the response body.
* @param status HTTP status code (defaults to 200).
Copy link
Contributor

Choose a reason for hiding this comment

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

@param {number=} status...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

*
* @param data Data to be included in the response body.
* @param status HTTP status code (defaults to 200).
* @param headers HTTP headers to be included in the ersponse.
Copy link
Contributor

Choose a reason for hiding this comment

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

@param {*=} headers...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

this.request = `${resp.config.method} ${resp.config.url}`;
}

get data(): any {
Copy link
Contributor

Choose a reason for hiding this comment

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

Javadoc missing

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This seems to get inherited from the parent interface. At least on VSCode, hovering over the getter shows the documentation from the interface.

response?: LowLevelResponse;
}

function sendRequest(config: HttpRequestConfig): Promise<LowLevelResponse> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Javadoc missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

}

function settle(resolve, reject, response: LowLevelResponse) {
function finalizeRequest(resolve, reject, response: LowLevelResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Javadoc missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. I'm only adding a description to these un-exported helper functions.

private readonly parseError: Error;
private readonly request: string;

constructor(resp: LowLevelResponse) {
Copy link
Contributor

Choose a reason for hiding this comment

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

To be consistent, we have been adding javadocs in other files. Let's add one here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@hiranya911
Copy link
Contributor Author

Updated the documentation as recommended.

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.

3 participants