Skip to content
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

Add http client component to runtime extension #899

Merged
merged 1 commit into from
Sep 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/shy-kids-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@smithy/protocol-http": major
"@smithy/fetch-http-handler": minor
"@smithy/node-http-handler": 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;
syall marked this conversation as resolved.
Show resolved Hide resolved
this.configProvider = this.configProvider.then((config) => {
config[key] = value;
return config;
});
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
}

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,
};
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
});
}

httpHandlerConfigs(): NodeHttpHandlerOptions {
return this.config ?? {};
}
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved
}
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>> {
syall marked this conversation as resolved.
Show resolved Hide resolved
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>
): HttpHandlerExtensionConfigType<HandlerConfig> => {
return {
httpHandler: httpHandlerExtensionConfiguration.httpHandler(),
};
};
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> & {
syall marked this conversation as resolved.
Show resolved Hide resolved
/**
* @internal
* @param key
* @param value
*/
updateHttpClientConfig(key: keyof HttpHandlerConfig, value: HttpHandlerConfig[typeof key]): void;
AndrewFossAWS marked this conversation as resolved.
Show resolved Hide resolved

/**
* @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
Loading