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 Sep 4, 2023
1 parent 697310d commit d8940f5
Show file tree
Hide file tree
Showing 14 changed files with 305 additions and 8 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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"test": "turbo run test",
"test:integration": "yarn build-test-packages && turbo run test:integration",
"lint": "turbo run lint",
"lint-fix": "turbo run lint -- --fix",
"format": "turbo run format --parallel",
"stage-release": "turbo run stage-release",
"extract:docs": "mkdir -p api-extractor-packages && turbo run extract:docs",
Expand Down
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
16 changes: 14 additions & 2 deletions packages/fetch-http-handler/src/fetch-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ export interface FetchHttpHandlerOptions {

type FetchHttpHandlerConfig = FetchHttpHandlerOptions;

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

constructor(options?: FetchHttpHandlerOptions | Provider<FetchHttpHandlerOptions | undefined>) {
if (typeof options === "function") {
Expand Down Expand Up @@ -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((config) => {
config[key] = value;
return config;
});
}

httpHandlerConfigs(): FetchHttpHandlerConfig {
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({});
});
});
});
18 changes: 16 additions & 2 deletions packages/node-http-handler/src/node-http-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ interface ResolvedNodeHttpHandlerConfig {

export const DEFAULT_REQUEST_TIMEOUT = 0;

export class NodeHttpHandler implements HttpHandler {
export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
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: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {
this.config = undefined;
this.configProvider = this.configProvider.then((config) => {
return {
...config,
[key]: value,
};
});
}

httpHandlerConfigs(): NodeHttpHandlerOptions {
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({});
});
});
18 changes: 16 additions & 2 deletions packages/node-http-handler/src/node-http2-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ export interface NodeHttp2HandlerOptions {
maxConcurrentStreams?: number;
}

export class NodeHttp2Handler implements HttpHandler {
export class NodeHttp2Handler implements HttpHandler<NodeHttp2HandlerOptions> {
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: keyof NodeHttp2HandlerOptions, value: NodeHttp2HandlerOptions[typeof key]): void {
this.config = undefined;
this.configProvider = this.configProvider.then((config) => {
return {
...config,
[key]: value,
};
});
}

httpHandlerConfigs(): NodeHttp2HandlerOptions {
return this.config ?? {};
}

/**
* Destroys a session.
* @param session The session to destroy.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { HttpHandler } from "../httpHandler";

/**
* @internal
*/
export interface HttpHandlerExtensionConfiguration<HandlerConfig extends object = Record<string, unknown>> {
setHttpHandler(handler: HttpHandler<HandlerConfig>): void;
httpHandler(): HttpHandler<HandlerConfig>;
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void;
httpHandlerConfigs(): HandlerConfig;
}

/**
* @internal
*/
export type HttpHandlerExtensionConfigType<HandlerConfig extends object = Record<string, unknown>> = Partial<{
httpHandler: HttpHandler<HandlerConfig>;
}>;

/**
* @internal
*
* Helper function to resolve default extension configuration from runtime config
*/
export const getHttpHandlerExtensionConfiguration = <HandlerConfig extends object = Record<string, unknown>>(
runtimeConfig: HttpHandlerExtensionConfigType<HandlerConfig>
) => {
let httpHandler = runtimeConfig.httpHandler!;
return {
setHttpHandler(handler: HttpHandler<HandlerConfig>): void {
httpHandler = handler;
},
httpHandler(): HttpHandler<HandlerConfig> {
return httpHandler;
},
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void {
httpHandler.updateHttpClientConfig(key, value);
},
httpHandlerConfigs(): HandlerConfig {
return httpHandler.httpHandlerConfigs();
},
};
};

/**
* @internal
*
* Helper function to resolve runtime config from default extension configuration
*/
export const resolveHttpHandlerRuntimeConfig = <HandlerConfig extends object = Record<string, unknown>>(
httpHandlerExtensionConfiguration: HttpHandlerExtensionConfiguration<HandlerConfig>
) => {
return {
requestHandler: httpHandlerExtensionConfiguration.httpHandler(),
} as HttpHandlerExtensionConfigType<HandlerConfig>;
};
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<HttpHandlerConfig> = RequestHandler<HttpRequest, HttpResponse, HttpHandlerOptions> & {
/**
* @internal
* @param key
* @param value
*/
updateHttpClientConfig(key: keyof HttpHandlerConfig, value: HttpHandlerConfig[typeof key]): void;

/**
* @internal
*/
httpHandlerConfigs(): HttpHandlerConfig;
};
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
4 changes: 4 additions & 0 deletions packages/util-stream/src/util-stream.integ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ describe("util-stream", () => {
}),
};
}
updateHttpClientConfig(key: string, value: any) {}
httpHandlerConfigs(): Record<string, any> {
return {};
}
})();

it("should allow string as payload blob and allow conversion of output payload blob to string", async () => {
Expand Down
Loading

0 comments on commit d8940f5

Please sign in to comment.