Skip to content

Commit a03026e

Browse files
committed
Add http client component to runtime extension
1 parent 88bcec3 commit a03026e

File tree

14 files changed

+305
-8
lines changed

14 files changed

+305
-8
lines changed

.changeset/shy-kids-call.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@smithy/protocol-http": major
3+
"@smithy/fetch-http-handler": minor
4+
"@smithy/node-http-handler": minor
5+
"@smithy/util-test": minor
6+
---
7+
8+
Add http client component to runtime extension

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"test": "turbo run test",
1111
"test:integration": "yarn build-test-packages && turbo run test:integration",
1212
"lint": "turbo run lint",
13+
"lint-fix": "turbo run lint -- --fix",
1314
"format": "turbo run format --parallel",
1415
"stage-release": "turbo run stage-release",
1516
"extract:docs": "mkdir -p api-extractor-packages && turbo run extract:docs",

packages/fetch-http-handler/src/fetch-http-handler.spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,54 @@ describe(FetchHttpHandler.name, () => {
5555
expect(await blobToText(response.response.body)).toBe("FOO");
5656
});
5757

58+
it("put HttpClientConfig", async () => {
59+
const mockResponse = {
60+
headers: {
61+
entries: jest.fn().mockReturnValue([
62+
["foo", "bar"],
63+
["bizz", "bazz"],
64+
]),
65+
},
66+
blob: jest.fn().mockResolvedValue(new Blob(["FOO"])),
67+
};
68+
const mockFetch = jest.fn().mockResolvedValue(mockResponse);
69+
70+
(global as any).fetch = mockFetch;
71+
const fetchHttpHandler = new FetchHttpHandler();
72+
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 200);
73+
74+
await fetchHttpHandler.handle({} as any, {});
75+
76+
expect(fetchHttpHandler.httpHandlerConfigs().requestTimeout).toBe(200);
77+
});
78+
79+
it("update HttpClientConfig", async () => {
80+
const mockResponse = {
81+
headers: {
82+
entries: jest.fn().mockReturnValue([
83+
["foo", "bar"],
84+
["bizz", "bazz"],
85+
]),
86+
},
87+
blob: jest.fn().mockResolvedValue(new Blob(["FOO"])),
88+
};
89+
const mockFetch = jest.fn().mockResolvedValue(mockResponse);
90+
91+
(global as any).fetch = mockFetch;
92+
const fetchHttpHandler = new FetchHttpHandler({ requestTimeout: 200 });
93+
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 300);
94+
95+
await fetchHttpHandler.handle({} as any, {});
96+
97+
expect(fetchHttpHandler.httpHandlerConfigs().requestTimeout).toBe(300);
98+
});
99+
100+
it("httpHandlerConfigs returns empty object if handle is not called", async () => {
101+
const fetchHttpHandler = new FetchHttpHandler();
102+
fetchHttpHandler.updateHttpClientConfig("requestTimeout", 300);
103+
expect(fetchHttpHandler.httpHandlerConfigs()).toEqual({});
104+
});
105+
58106
it("defaults to response.blob for response.body = null", async () => {
59107
const mockResponse = {
60108
body: null,

packages/fetch-http-handler/src/fetch-http-handler.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ export interface FetchHttpHandlerOptions {
1919

2020
type FetchHttpHandlerConfig = FetchHttpHandlerOptions;
2121

22-
export class FetchHttpHandler implements HttpHandler {
22+
export class FetchHttpHandler implements HttpHandler<FetchHttpHandlerConfig> {
2323
private config?: FetchHttpHandlerConfig;
24-
private readonly configProvider: Promise<FetchHttpHandlerConfig>;
24+
private configProvider: Promise<FetchHttpHandlerConfig>;
2525

2626
constructor(options?: FetchHttpHandlerOptions | Provider<FetchHttpHandlerOptions | undefined>) {
2727
if (typeof options === "function") {
@@ -130,4 +130,16 @@ export class FetchHttpHandler implements HttpHandler {
130130
}
131131
return Promise.race(raceOfPromises);
132132
}
133+
134+
updateHttpClientConfig(key: keyof FetchHttpHandlerConfig, value: FetchHttpHandlerConfig[typeof key]): void {
135+
this.config = undefined;
136+
this.configProvider = this.configProvider.then((config) => {
137+
config[key] = value;
138+
return config;
139+
});
140+
}
141+
142+
httpHandlerConfigs(): FetchHttpHandlerConfig {
143+
return this.config ?? {};
144+
}
133145
}

packages/node-http-handler/src/node-http-handler.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -640,4 +640,68 @@ describe("NodeHttpHandler", () => {
640640
expect(nodeHttpHandler.destroy()).toBeUndefined();
641641
});
642642
});
643+
644+
describe("configs", () => {
645+
const mockResponse = {
646+
statusCode: 200,
647+
statusText: "OK",
648+
headers: {},
649+
body: "test",
650+
};
651+
652+
let mockHttpServer: HttpServer;
653+
let request: HttpRequest;
654+
655+
beforeAll(() => {
656+
mockHttpServer = createMockHttpServer().listen(54320);
657+
request = new HttpRequest({
658+
hostname: "localhost",
659+
method: "GET",
660+
port: (mockHttpServer.address() as AddressInfo).port,
661+
protocol: "http:",
662+
path: "/",
663+
headers: {},
664+
});
665+
});
666+
667+
afterEach(() => {
668+
mockHttpServer.removeAllListeners("request");
669+
mockHttpServer.removeAllListeners("checkContinue");
670+
});
671+
672+
afterAll(() => {
673+
mockHttpServer.close();
674+
});
675+
676+
it("put HttpClientConfig", async () => {
677+
mockHttpServer.addListener("request", createResponseFunction(mockResponse));
678+
679+
const nodeHttpHandler = new NodeHttpHandler();
680+
const requestTimeout = 200;
681+
682+
nodeHttpHandler.updateHttpClientConfig("requestTimeout", requestTimeout);
683+
684+
await nodeHttpHandler.handle(request, {});
685+
686+
expect(nodeHttpHandler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
687+
});
688+
689+
it("update existing HttpClientConfig", async () => {
690+
mockHttpServer.addListener("request", createResponseFunction(mockResponse));
691+
692+
const nodeHttpHandler = new NodeHttpHandler({ requestTimeout: 200 });
693+
const requestTimeout = 300;
694+
695+
nodeHttpHandler.updateHttpClientConfig("requestTimeout", requestTimeout);
696+
697+
await nodeHttpHandler.handle(request, {});
698+
699+
expect(nodeHttpHandler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
700+
});
701+
702+
it("httpHandlerConfigs returns empty object if handle is not called", async () => {
703+
const nodeHttpHandler = new NodeHttpHandler();
704+
expect(nodeHttpHandler.httpHandlerConfigs()).toEqual({});
705+
});
706+
});
643707
});

packages/node-http-handler/src/node-http-handler.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,9 @@ interface ResolvedNodeHttpHandlerConfig {
5050

5151
export const DEFAULT_REQUEST_TIMEOUT = 0;
5252

53-
export class NodeHttpHandler implements HttpHandler {
53+
export class NodeHttpHandler implements HttpHandler<NodeHttpHandlerOptions> {
5454
private config?: ResolvedNodeHttpHandlerConfig;
55-
private readonly configProvider: Promise<ResolvedNodeHttpHandlerConfig>;
55+
private configProvider: Promise<ResolvedNodeHttpHandlerConfig>;
5656

5757
// Node http handler is hard-coded to http/1.1: https://github.com/nodejs/node/blob/ff5664b83b89c55e4ab5d5f60068fb457f1f5872/lib/_http_server.js#L286
5858
public readonly metadata = { handlerProtocol: "http/1.1" };
@@ -192,4 +192,18 @@ export class NodeHttpHandler implements HttpHandler {
192192
writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout).catch(_reject);
193193
});
194194
}
195+
196+
updateHttpClientConfig(key: keyof NodeHttpHandlerOptions, value: NodeHttpHandlerOptions[typeof key]): void {
197+
this.config = undefined;
198+
this.configProvider = this.configProvider.then((config) => {
199+
return {
200+
...config,
201+
[key]: value,
202+
};
203+
});
204+
}
205+
206+
httpHandlerConfigs(): NodeHttpHandlerOptions {
207+
return this.config ?? {};
208+
}
195209
}

packages/node-http-handler/src/node-http2-handler.spec.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,4 +643,55 @@ describe(NodeHttp2Handler.name, () => {
643643
} as any);
644644
handler.destroy();
645645
});
646+
647+
it("put HttpClientConfig", async () => {
648+
const server = createMockHttp2Server();
649+
server.on("request", (request, response) => {
650+
expect(request.url).toBe("http://foo:bar@localhost/");
651+
response.statusCode = 200;
652+
});
653+
const handler = new NodeHttp2Handler({});
654+
655+
const requestTimeout = 200;
656+
657+
handler.updateHttpClientConfig("requestTimeout", requestTimeout);
658+
659+
await handler.handle({
660+
...getMockReqOptions(),
661+
username: "foo",
662+
password: "bar",
663+
path: "/",
664+
} as any);
665+
handler.destroy();
666+
667+
expect(handler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
668+
});
669+
670+
it("update existing HttpClientConfig", async () => {
671+
const server = createMockHttp2Server();
672+
server.on("request", (request, response) => {
673+
expect(request.url).toBe("http://foo:bar@localhost/");
674+
response.statusCode = 200;
675+
});
676+
const handler = new NodeHttp2Handler({ requestTimeout: 200 });
677+
678+
const requestTimeout = 300;
679+
680+
handler.updateHttpClientConfig("requestTimeout", requestTimeout);
681+
682+
await handler.handle({
683+
...getMockReqOptions(),
684+
username: "foo",
685+
password: "bar",
686+
path: "/",
687+
} as any);
688+
handler.destroy();
689+
690+
expect(handler.httpHandlerConfigs().requestTimeout).toEqual(requestTimeout);
691+
});
692+
693+
it("httpHandlerConfigs returns empty object if handle is not called", async () => {
694+
const nodeHttpHandler = new NodeHttp2Handler();
695+
expect(nodeHttpHandler.httpHandlerConfigs()).toEqual({});
696+
});
646697
});

packages/node-http-handler/src/node-http2-handler.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ export interface NodeHttp2HandlerOptions {
4141
maxConcurrentStreams?: number;
4242
}
4343

44-
export class NodeHttp2Handler implements HttpHandler {
44+
export class NodeHttp2Handler implements HttpHandler<NodeHttp2HandlerOptions> {
4545
private config?: NodeHttp2HandlerOptions;
46-
private readonly configProvider: Promise<NodeHttp2HandlerOptions>;
46+
private configProvider: Promise<NodeHttp2HandlerOptions>;
4747

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

@@ -202,6 +202,20 @@ export class NodeHttp2Handler implements HttpHandler {
202202
});
203203
}
204204

205+
updateHttpClientConfig(key: keyof NodeHttp2HandlerOptions, value: NodeHttp2HandlerOptions[typeof key]): void {
206+
this.config = undefined;
207+
this.configProvider = this.configProvider.then((config) => {
208+
return {
209+
...config,
210+
[key]: value,
211+
};
212+
});
213+
}
214+
215+
httpHandlerConfigs(): NodeHttp2HandlerOptions {
216+
return this.config ?? {};
217+
}
218+
205219
/**
206220
* Destroys a session.
207221
* @param session The session to destroy.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { HttpHandler } from "../httpHandler";
2+
3+
/**
4+
* @internal
5+
*/
6+
export interface HttpHandlerExtensionConfiguration<HandlerConfig extends object = Record<string, unknown>> {
7+
setHttpHandler(handler: HttpHandler<HandlerConfig>): void;
8+
httpHandler(): HttpHandler<HandlerConfig>;
9+
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void;
10+
httpHandlerConfigs(): HandlerConfig;
11+
}
12+
13+
/**
14+
* @internal
15+
*/
16+
export type HttpHandlerExtensionConfigType<HandlerConfig extends object = Record<string, unknown>> = Partial<{
17+
httpHandler: HttpHandler<HandlerConfig>;
18+
}>;
19+
20+
/**
21+
* @internal
22+
*
23+
* Helper function to resolve default extension configuration from runtime config
24+
*/
25+
export const getHttpHandlerExtensionConfiguration = <HandlerConfig extends object = Record<string, unknown>>(
26+
runtimeConfig: HttpHandlerExtensionConfigType<HandlerConfig>
27+
) => {
28+
let httpHandler = runtimeConfig.httpHandler!;
29+
return {
30+
setHttpHandler(handler: HttpHandler<HandlerConfig>): void {
31+
httpHandler = handler;
32+
},
33+
httpHandler(): HttpHandler<HandlerConfig> {
34+
return httpHandler;
35+
},
36+
updateHttpClientConfig(key: keyof HandlerConfig, value: HandlerConfig[typeof key]): void {
37+
httpHandler.updateHttpClientConfig(key, value);
38+
},
39+
httpHandlerConfigs(): HandlerConfig {
40+
return httpHandler.httpHandlerConfigs();
41+
},
42+
};
43+
};
44+
45+
/**
46+
* @internal
47+
*
48+
* Helper function to resolve runtime config from default extension configuration
49+
*/
50+
export const resolveHttpHandlerRuntimeConfig = <HandlerConfig extends object = Record<string, unknown>>(
51+
httpHandlerExtensionConfiguration: HttpHandlerExtensionConfiguration<HandlerConfig>
52+
): HttpHandlerExtensionConfigType<HandlerConfig> => {
53+
return {
54+
httpHandler: httpHandlerExtensionConfiguration.httpHandler(),
55+
};
56+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./httpExtensionConfiguration";

packages/protocol-http/src/httpHandler.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,19 @@ import { HttpHandlerOptions, RequestHandler } from "@smithy/types";
33
import { HttpRequest } from "./httpRequest";
44
import { HttpResponse } from "./httpResponse";
55

6-
export type HttpHandler = RequestHandler<HttpRequest, HttpResponse, HttpHandlerOptions>;
6+
/**
7+
* @internal
8+
*/
9+
export type HttpHandler<HttpHandlerConfig> = RequestHandler<HttpRequest, HttpResponse, HttpHandlerOptions> & {
10+
/**
11+
* @internal
12+
* @param key
13+
* @param value
14+
*/
15+
updateHttpClientConfig(key: keyof HttpHandlerConfig, value: HttpHandlerConfig[typeof key]): void;
16+
17+
/**
18+
* @internal
19+
*/
20+
httpHandlerConfigs(): HttpHandlerConfig;
21+
};

packages/protocol-http/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./extensions";
12
export * from "./Field";
23
export * from "./Fields";
34
export * from "./httpHandler";

packages/util-stream/src/util-stream.integ.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ describe("util-stream", () => {
6868
}),
6969
};
7070
}
71+
updateHttpClientConfig(key: string, value: any) {}
72+
httpHandlerConfigs(): Record<string, any> {
73+
return {};
74+
}
7175
})();
7276

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

0 commit comments

Comments
 (0)