From fbf7c58c1bdf4e31691fcdbf44259d4f7fafceb4 Mon Sep 17 00:00:00 2001 From: fossand Date: Mon, 28 Aug 2023 15:00:00 -0700 Subject: [PATCH] Add http client component to runtime extension --- .changeset/three-nails-press.md | 8 +++ .../src/fetch-http-handler.spec.ts | 48 ++++++++++++++ .../src/fetch-http-handler.ts | 18 +++++- .../src/node-http-handler.spec.ts | 64 +++++++++++++++++++ .../src/node-http-handler.ts | 18 +++++- .../src/node-http2-handler.spec.ts | 51 +++++++++++++++ .../src/node-http2-handler.ts | 18 +++++- packages/protocol-http/src/extensions/http.ts | 42 ++++++++++++ .../extensions/httpExtensionConfiguration.ts | 42 ++++++++++++ .../protocol-http/src/extensions/index.ts | 1 + packages/protocol-http/src/httpHandler.ts | 17 ++++- packages/protocol-http/src/index.ts | 1 + .../util-stream/src/util-stream.integ.spec.ts | 4 ++ private/util-test/src/test-http-handler.ts | 10 ++- 14 files changed, 333 insertions(+), 9 deletions(-) create mode 100644 .changeset/three-nails-press.md create mode 100644 packages/protocol-http/src/extensions/http.ts create mode 100644 packages/protocol-http/src/extensions/httpExtensionConfiguration.ts create mode 100644 packages/protocol-http/src/extensions/index.ts diff --git a/.changeset/three-nails-press.md b/.changeset/three-nails-press.md new file mode 100644 index 00000000000..b67da75696d --- /dev/null +++ b/.changeset/three-nails-press.md @@ -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 diff --git a/packages/fetch-http-handler/src/fetch-http-handler.spec.ts b/packages/fetch-http-handler/src/fetch-http-handler.spec.ts index 95773743986..afb992e3717 100644 --- a/packages/fetch-http-handler/src/fetch-http-handler.spec.ts +++ b/packages/fetch-http-handler/src/fetch-http-handler.spec.ts @@ -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, diff --git a/packages/fetch-http-handler/src/fetch-http-handler.ts b/packages/fetch-http-handler/src/fetch-http-handler.ts index 6ffea61ee35..4e402d930c3 100644 --- a/packages/fetch-http-handler/src/fetch-http-handler.ts +++ b/packages/fetch-http-handler/src/fetch-http-handler.ts @@ -19,11 +19,11 @@ export interface FetchHttpHandlerOptions { type FetchHttpHandlerConfig = FetchHttpHandlerOptions; -export class FetchHttpHandler implements HttpHandler { +export class FetchHttpHandler implements HttpHandler { private config?: FetchHttpHandlerConfig; - private readonly configProvider: Promise; + private configProvider: Promise; - constructor(options?: FetchHttpHandlerOptions | Provider) { + constructor(options?: FetchHttpHandlerOptions | Provider) { if (typeof options === "function") { this.configProvider = options().then((opts) => opts || {}); } else { @@ -130,4 +130,16 @@ export class FetchHttpHandler implements HttpHandler { } return Promise.race(raceOfPromises); } + + updateHttpClientConfig(key: keyof FetchHttpHandlerConfig, value: FetchHttpHandlerConfig[typeof key]): void { + this.config = undefined; + this.configProvider = this.configProvider.then((resolveConfig) => { + resolveConfig[key] = value; + return resolveConfig; + }); + } + + httpHandlerConfigs(): FetchHttpHandlerConfig { + return this.config ?? {}; + } } diff --git a/packages/node-http-handler/src/node-http-handler.spec.ts b/packages/node-http-handler/src/node-http-handler.spec.ts index 519a71853a3..66e7f7a5a62 100644 --- a/packages/node-http-handler/src/node-http-handler.spec.ts +++ b/packages/node-http-handler/src/node-http-handler.spec.ts @@ -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({}); + }); + }); }); diff --git a/packages/node-http-handler/src/node-http-handler.ts b/packages/node-http-handler/src/node-http-handler.ts index 4f76500e7a6..5f7917813fe 100644 --- a/packages/node-http-handler/src/node-http-handler.ts +++ b/packages/node-http-handler/src/node-http-handler.ts @@ -50,9 +50,9 @@ interface ResolvedNodeHttpHandlerConfig { export const DEFAULT_REQUEST_TIMEOUT = 0; -export class NodeHttpHandler implements HttpHandler { +export class NodeHttpHandler implements HttpHandler { private config?: ResolvedNodeHttpHandlerConfig; - private readonly configProvider: Promise; + private configProvider: Promise; // 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" }; @@ -192,4 +192,18 @@ export class NodeHttpHandler implements HttpHandler { writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout).catch(_reject); }); } + + updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void { + this.config = undefined; + this.configProvider = this.configProvider.then((resolveConfig) => { + return { + ...resolveConfig, + [key]: value, + }; + }); + } + + httpHandlerConfigs(): NodeHttpHandlerOptions { + return this.config ?? {}; + } } diff --git a/packages/node-http-handler/src/node-http2-handler.spec.ts b/packages/node-http-handler/src/node-http2-handler.spec.ts index a53a8f0d5f4..1cd8c32c426 100644 --- a/packages/node-http-handler/src/node-http2-handler.spec.ts +++ b/packages/node-http-handler/src/node-http2-handler.spec.ts @@ -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({}); + }); }); diff --git a/packages/node-http-handler/src/node-http2-handler.ts b/packages/node-http-handler/src/node-http2-handler.ts index 8aaef554513..6a6a4a1d086 100644 --- a/packages/node-http-handler/src/node-http2-handler.ts +++ b/packages/node-http-handler/src/node-http2-handler.ts @@ -41,9 +41,9 @@ export interface NodeHttp2HandlerOptions { maxConcurrentStreams?: number; } -export class NodeHttp2Handler implements HttpHandler { +export class NodeHttp2Handler implements HttpHandler { private config?: NodeHttp2HandlerOptions; - private readonly configProvider: Promise; + private configProvider: Promise; public readonly metadata = { handlerProtocol: "h2" }; @@ -202,6 +202,20 @@ export class NodeHttp2Handler implements HttpHandler { }); } + updateHttpClientConfig(key: keyof NodeHttp2HandlerOptions, value: NodeHttp2HandlerOptions[typeof key]): void { + this.config = undefined; + this.configProvider = this.configProvider.then((resolveConfig) => { + return { + ...resolveConfig, + [key]: value, + }; + }); + } + + httpHandlerConfigs(): NodeHttp2HandlerOptions { + return this.config ?? {}; + } + /** * Destroys a session. * @param session The session to destroy. diff --git a/packages/protocol-http/src/extensions/http.ts b/packages/protocol-http/src/extensions/http.ts new file mode 100644 index 00000000000..9d308c24710 --- /dev/null +++ b/packages/protocol-http/src/extensions/http.ts @@ -0,0 +1,42 @@ +import { HttpHandler } from "../httpHandler"; +import { HttpHandlerExtensionConfiguration } from "./httpExtensionConfiguration"; + +/** + * @internal + */ +export const getHttpHandlerConfiguration = >( + runtimeConfig: Partial<{ httpHandler: HttpHandler }> +) => { + let httpHandler = runtimeConfig.httpHandler!; + const httpHandlerConfigs: HandlerConfig = httpHandler.httpHandlerConfigs(); + return { + setHttpHandler(handler: HttpHandler): void { + httpHandler = handler; + }, + httpHandler(): HttpHandler { + return httpHandler; + }, + updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void { + httpHandlerConfigs[key] = value; + }, + httpHandlerConfigs(): HandlerConfig { + return httpHandlerConfigs; + }, + }; +}; + +/** + * @internal + */ +export const resolveHttpHandlerRuntimeConfig = >( + httpHandlerExtensionConfiguration: HttpHandlerExtensionConfiguration +) => { + const requestHandler = httpHandlerExtensionConfiguration.httpHandler(); + const handlerConfig = httpHandlerExtensionConfiguration.httpHandlerConfigs(); + for (const key in handlerConfig) { + requestHandler.updateHttpClientConfig(key, handlerConfig[key]); + } + return { + requestHandler, + }; +}; diff --git a/packages/protocol-http/src/extensions/httpExtensionConfiguration.ts b/packages/protocol-http/src/extensions/httpExtensionConfiguration.ts new file mode 100644 index 00000000000..5a610dff480 --- /dev/null +++ b/packages/protocol-http/src/extensions/httpExtensionConfiguration.ts @@ -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: keyof HandlerConfig, value: HandlerConfig[typeof key]): void; + httpHandlerConfigs(): HandlerConfig; +} + +/** + * @internal + */ +export type HttpHandlerExtensionConfigType = Parameters[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), + }; +}; diff --git a/packages/protocol-http/src/extensions/index.ts b/packages/protocol-http/src/extensions/index.ts new file mode 100644 index 00000000000..a215a4a8114 --- /dev/null +++ b/packages/protocol-http/src/extensions/index.ts @@ -0,0 +1 @@ +export * from "./httpExtensionConfiguration"; diff --git a/packages/protocol-http/src/httpHandler.ts b/packages/protocol-http/src/httpHandler.ts index a6ebc6b7258..c4a37fd4105 100644 --- a/packages/protocol-http/src/httpHandler.ts +++ b/packages/protocol-http/src/httpHandler.ts @@ -3,4 +3,19 @@ import { HttpHandlerOptions, RequestHandler } from "@smithy/types"; import { HttpRequest } from "./httpRequest"; import { HttpResponse } from "./httpResponse"; -export type HttpHandler = RequestHandler; +/** + * @internal + */ +export type HttpHandler = RequestHandler & { + /** + * @internal + * @param key + * @param value + */ + updateHttpClientConfig(key: keyof HttpHandlerConfig, value: HttpHandlerConfig[typeof key]): void; + + /** + * @internal + */ + httpHandlerConfigs: () => HttpHandlerConfig; +}; diff --git a/packages/protocol-http/src/index.ts b/packages/protocol-http/src/index.ts index dc502ac6569..8ff7f269af8 100644 --- a/packages/protocol-http/src/index.ts +++ b/packages/protocol-http/src/index.ts @@ -1,3 +1,4 @@ +export * from "./extensions"; export * from "./Field"; export * from "./Fields"; export * from "./httpHandler"; diff --git a/packages/util-stream/src/util-stream.integ.spec.ts b/packages/util-stream/src/util-stream.integ.spec.ts index 54f2426936a..14790e22f85 100644 --- a/packages/util-stream/src/util-stream.integ.spec.ts +++ b/packages/util-stream/src/util-stream.integ.spec.ts @@ -68,6 +68,10 @@ describe("util-stream", () => { }), }; } + updateHttpClientConfig(key: string, value: any) {} + httpHandlerConfigs(): Record { + return {}; + } })(); it("should allow string as payload blob and allow conversion of output payload blob to string", async () => { diff --git a/private/util-test/src/test-http-handler.ts b/private/util-test/src/test-http-handler.ts index e96bf5f7656..54c4ff30ed6 100644 --- a/private/util-test/src/test-http-handler.ts +++ b/private/util-test/src/test-http-handler.ts @@ -35,11 +35,13 @@ const MOCK_CREDENTIALS = { secretAccessKey: "MOCK_SECRET_ACCESS_KEY_ID", }; +interface TestHttpHandlerConfig {}; + /** * Supplied to test clients to assert correct requests. * @internal */ -export class TestHttpHandler implements HttpHandler { +export class TestHttpHandler implements HttpHandler { private static WATCHER = Symbol("TestHttpHandler_WATCHER"); private originalSend?: Function; private originalRequestHandler?: RequestHandler; @@ -129,6 +131,12 @@ export class TestHttpHandler implements HttpHandler { (this.client as any).send = this.originalSend as any; } + updateHttpClientConfig(key: keyof TestHttpHandlerConfig, value: TestHttpHandlerConfig[typeof key]): void {} + + httpHandlerConfigs(): TestHttpHandlerConfig { + return {}; + } + private check(matcher?: Matcher, observed?: any) { if (matcher === undefined) { return;