Skip to content

Commit

Permalink
Add http client component to runtime extension
Browse files Browse the repository at this point in the history
  • Loading branch information
AndrewFossAWS committed Aug 28, 2023
1 parent a7598a5 commit c4dbce3
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 5 deletions.
8 changes: 8 additions & 0 deletions .changeset/three-nails-press.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@smithy/fetch-http-handler": minor
"@smithy/node-http-handler": minor
"@smithy/protocol-http": minor
"@smithy/util-test": minor
---

Add http client component to runtime extension
48 changes: 48 additions & 0 deletions packages/fetch-http-handler/src/fetch-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,54 @@ describe(FetchHttpHandler.name, () => {
expect(await blobToText(response.response.body)).toBe("FOO");
});

it("put HttpClientConfig", async () => {
const mockResponse = {
headers: {
entries: jest.fn().mockReturnValue([
["foo", "bar"],
["bizz", "bazz"],
]),
},
blob: jest.fn().mockResolvedValue(new Blob(["FOO"])),
};
const mockFetch = jest.fn().mockResolvedValue(mockResponse);

(global as any).fetch = mockFetch;
const fetchHttpHandler = new FetchHttpHandler();
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 200);

await fetchHttpHandler.handle({} as any, {});

expect(fetchHttpHandler.httpHandlerConfigs().requestTimeout).toBe(200);
});

it("update HttpClientConfig", async () => {
const mockResponse = {
headers: {
entries: jest.fn().mockReturnValue([
["foo", "bar"],
["bizz", "bazz"],
]),
},
blob: jest.fn().mockResolvedValue(new Blob(["FOO"])),
};
const mockFetch = jest.fn().mockResolvedValue(mockResponse);

(global as any).fetch = mockFetch;
const fetchHttpHandler = new FetchHttpHandler({ requestTimeout: 200 });
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 300);

await fetchHttpHandler.handle({} as any, {});

expect(fetchHttpHandler.httpHandlerConfigs().requestTimeout).toBe(300);
});

it("httpHandlerConfigs returns empty object if handle is not called", async () => {
const fetchHttpHandler = new FetchHttpHandler();
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 300);
expect(fetchHttpHandler.httpHandlerConfigs()).toEqual({});
});

it("defaults to response.blob for response.body = null", async () => {
const mockResponse = {
body: null,
Expand Down
18 changes: 16 additions & 2 deletions packages/fetch-http-handler/src/fetch-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ type FetchHttpHandlerConfig = FetchHttpHandlerOptions;

export class FetchHttpHandler implements HttpHandler {
private config?: FetchHttpHandlerConfig;
private readonly configProvider: Promise<FetchHttpHandlerConfig>;
private configProvider: Promise<FetchHttpHandlerConfig>;

constructor(options?: FetchHttpHandlerOptions | Provider<FetchHttpHandlerOptions | undefined>) {
constructor(options?: FetchHttpHandlerOptions | Provider<FetchHttpHandlerOptions | void>) {
if (typeof options === "function") {
this.configProvider = options().then((opts) => opts || {});
} else {
Expand Down Expand Up @@ -130,4 +130,18 @@ export class FetchHttpHandler implements HttpHandler {
}
return Promise.race(raceOfPromises);
}

updateHttpClientConfig(key: string, value: any): void {
this.config = undefined;
this.configProvider = this.configProvider.then((resolveConfig) => {
return {
...resolveConfig,
[key]: value,
};
});
}

httpHandlerConfigs(): Record<string, any> {
return this.config || {};
}
}
64 changes: 64 additions & 0 deletions packages/node-http-handler/src/node-http-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,4 +640,68 @@ describe("NodeHttpHandler", () => {
expect(nodeHttpHandler.destroy()).toBeUndefined();
});
});

describe("configs", () => {
const mockResponse = {
statusCode: 200,
statusText: "OK",
headers: {},
body: "test",
};

let mockHttpServer: HttpServer;
let request: HttpRequest;

beforeAll(() => {
mockHttpServer = createMockHttpServer().listen(54320);
request = new HttpRequest({
hostname: "localhost",
method: "GET",
port: (mockHttpServer.address() as AddressInfo).port,
protocol: "http:",
path: "/",
headers: {},
});
});

afterEach(() => {
mockHttpServer.removeAllListeners("request");
mockHttpServer.removeAllListeners("checkContinue");
});

afterAll(() => {
mockHttpServer.close();
});

it("put HttpClientConfig", async () => {
mockHttpServer.addListener("request", createResponseFunction(mockResponse));

const nodeHttpHandler = new NodeHttpHandler();
const requestTimeout = 200;

nodeHttpHandler.updateHttpClientConfig("requestTimeout", requestTimeout);

await nodeHttpHandler.handle(request, {});

expect(nodeHttpHandler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
});

it("update existing HttpClientConfig", async () => {
mockHttpServer.addListener("request", createResponseFunction(mockResponse));

const nodeHttpHandler = new NodeHttpHandler({ requestTimeout: 200 });
const requestTimeout = 300;

nodeHttpHandler.updateHttpClientConfig("requestTimeout", requestTimeout);

await nodeHttpHandler.handle(request, {});

expect(nodeHttpHandler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
});

it("httpHandlerConfigs returns empty object if handle is not called", async () => {
const nodeHttpHandler = new NodeHttpHandler();
expect(nodeHttpHandler.httpHandlerConfigs()).toEqual({});
});
});
});
16 changes: 15 additions & 1 deletion packages/node-http-handler/src/node-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const DEFAULT_REQUEST_TIMEOUT = 0;

export class NodeHttpHandler implements HttpHandler {
private config?: ResolvedNodeHttpHandlerConfig;
private readonly configProvider: Promise<ResolvedNodeHttpHandlerConfig>;
private configProvider: Promise<ResolvedNodeHttpHandlerConfig>;

// Node http handler is hard-coded to http/1.1: https://github.com/nodejs/node/blob/ff5664b83b89c55e4ab5d5f60068fb457f1f5872/lib/_http_server.js#L286
public readonly metadata = { handlerProtocol: "http/1.1" };
Expand Down Expand Up @@ -192,4 +192,18 @@ export class NodeHttpHandler implements HttpHandler {
writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout).catch(_reject);
});
}

updateHttpClientConfig(key: string, value: any): void {
this.config = undefined;
this.configProvider = this.configProvider.then((resolveConfig) => {
return {
...resolveConfig,
[key]: value,
};
});
}

httpHandlerConfigs(): Record<string, any> {
return this.config || {};
}
}
51 changes: 51 additions & 0 deletions packages/node-http-handler/src/node-http2-handler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,4 +643,55 @@ describe(NodeHttp2Handler.name, () => {
} as any);
handler.destroy();
});

it("put HttpClientConfig", async () => {
const server = createMockHttp2Server();
server.on("request", (request, response) => {
expect(request.url).toBe("http://foo:bar@localhost/");
response.statusCode = 200;
});
const handler = new NodeHttp2Handler({});

const requestTimeout = 200;

handler.updateHttpClientConfig("requestTimeout", requestTimeout);

await handler.handle({
...getMockReqOptions(),
username: "foo",
password: "bar",
path: "/",
} as any);
handler.destroy();

expect(handler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
});

it("update existing HttpClientConfig", async () => {
const server = createMockHttp2Server();
server.on("request", (request, response) => {
expect(request.url).toBe("http://foo:bar@localhost/");
response.statusCode = 200;
});
const handler = new NodeHttp2Handler({ requestTimeout: 200 });

const requestTimeout = 300;

handler.updateHttpClientConfig("requestTimeout", requestTimeout);

await handler.handle({
...getMockReqOptions(),
username: "foo",
password: "bar",
path: "/",
} as any);
handler.destroy();

expect(handler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
});

it("httpHandlerConfigs returns empty object if handle is not called", async () => {
const nodeHttpHandler = new NodeHttp2Handler();
expect(nodeHttpHandler.httpHandlerConfigs()).toEqual({});
});
});
16 changes: 15 additions & 1 deletion packages/node-http-handler/src/node-http2-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface NodeHttp2HandlerOptions {

export class NodeHttp2Handler implements HttpHandler {
private config?: NodeHttp2HandlerOptions;
private readonly configProvider: Promise<NodeHttp2HandlerOptions>;
private configProvider: Promise<NodeHttp2HandlerOptions>;

public readonly metadata = { handlerProtocol: "h2" };

Expand Down Expand Up @@ -202,6 +202,20 @@ export class NodeHttp2Handler implements HttpHandler {
});
}

updateHttpClientConfig(key: string, value: any): void {
this.config = undefined;
this.configProvider = this.configProvider.then((resolveConfig) => {
return {
...resolveConfig,
[key]: value,
};
});
}

httpHandlerConfigs(): Record<string, any> {
return this.config || {};
}

/**
* Destroys a session.
* @param session The session to destroy.
Expand Down
39 changes: 39 additions & 0 deletions packages/protocol-http/src/extensions/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { HttpHandler } from "../httpHandler";
import { HttpHandlerExtensionConfiguration } from "./httpExtensionConfiguration";

/**
* @internal
*/
export const getHttpHandlerConfiguration = (runtimeConfig: Partial<{ httpHandler: HttpHandler }>) => {
const httpHandlerConfigs: Record<string, any> = {};
return {
_httpHandler: runtimeConfig.httpHandler!,
setHttpHandler(handler: HttpHandler): void {
this._httpHandler = handler;
},
httpHandler(): HttpHandler {
return this._httpHandler;
},
updateHttpClientConfig(key: string, value: any): void {
httpHandlerConfigs[key] = value;
},
httpHandlerConfigs(): Record<string, any> {
return httpHandlerConfigs;
},
};
};

/**
* @internal
*/
export const resolveHttpHandlerRuntimeConfig = (
httpHandlerExtensionConfiguration: HttpHandlerExtensionConfiguration
) => {
const runtimeConfig: Partial<Record<string, HttpHandler>> = {};
runtimeConfig.requestHandler = httpHandlerExtensionConfiguration.httpHandler();
const record: Record<string, any> = httpHandlerExtensionConfiguration.httpHandlerConfigs();
for (const key in record) {
runtimeConfig.requestHandler.updateHttpClientConfig(key, record[key]);
}
return runtimeConfig;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { HttpHandler } from "../httpHandler";
import {
getHttpHandlerConfiguration,
resolveHttpHandlerRuntimeConfig as _resolveHttpHandlerRuntimeConfig,
} from "./http";

/**
* @internal
*/
export interface HttpHandlerExtensionConfiguration {
setHttpHandler(handler: HttpHandler): void;
httpHandler(): HttpHandler;
updateHttpClientConfig(key: string, value: any): void;
httpHandlerConfigs(): Record<string, any>;
}

/**
* @internal
*/
export type HttpHandlerExtensionConfigType = Parameters<typeof getHttpHandlerConfiguration>[0];

/**
* @internal
*
* Helper function to resolve default extension configuration from runtime config
*/
export const getHttpHandlerExtensionConfiguration = (runtimeConfig: HttpHandlerExtensionConfigType) => {
return {
...getHttpHandlerConfiguration(runtimeConfig),
};
};

/**
* @internal
*
* Helper function to resolve runtime config from default extension configuration
*/
export const resolveHttpHandlerRuntimeConfig = (config: HttpHandlerExtensionConfiguration) => {
return {
..._resolveHttpHandlerRuntimeConfig(config),
};
};
1 change: 1 addition & 0 deletions packages/protocol-http/src/extensions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./httpExtensionConfiguration";
17 changes: 16 additions & 1 deletion packages/protocol-http/src/httpHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,19 @@ import { HttpHandlerOptions, RequestHandler } from "@smithy/types";
import { HttpRequest } from "./httpRequest";
import { HttpResponse } from "./httpResponse";

export type HttpHandler = RequestHandler<HttpRequest, HttpResponse, HttpHandlerOptions>;
/**
* @internal
*/
export type HttpHandler = RequestHandler<HttpRequest, HttpResponse, HttpHandlerOptions> & {
/**
* @internal
* @param key
* @param value
*/
updateHttpClientConfig: (key: string, value: any) => void;

/**
* @internal
*/
httpHandlerConfigs: () => Record<string, any> | undefined;
};
1 change: 1 addition & 0 deletions packages/protocol-http/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./extensions";
export * from "./Field";
export * from "./Fields";
export * from "./httpHandler";
Expand Down
Loading

0 comments on commit c4dbce3

Please sign in to comment.