1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | exports.NodeHttp2Handler = void 0;
|
4 | const protocol_http_1 = require("@aws-sdk/protocol-http");
|
5 | const querystring_builder_1 = require("@aws-sdk/querystring-builder");
|
6 | const http2_1 = require("http2");
|
7 | const get_transformed_headers_1 = require("./get-transformed-headers");
|
8 | const write_request_body_1 = require("./write-request-body");
|
9 | class NodeHttp2Handler {
|
10 | constructor(options) {
|
11 | this.metadata = { handlerProtocol: "h2" };
|
12 | this.configProvider = new Promise((resolve, reject) => {
|
13 | if (typeof options === "function") {
|
14 | options()
|
15 | .then((opts) => {
|
16 | resolve(opts || {});
|
17 | })
|
18 | .catch(reject);
|
19 | }
|
20 | else {
|
21 | resolve(options || {});
|
22 | }
|
23 | });
|
24 | this.sessionCache = new Map();
|
25 | }
|
26 | destroy() {
|
27 | for (const sessions of this.sessionCache.values()) {
|
28 | sessions.forEach((session) => this.destroySession(session));
|
29 | }
|
30 | this.sessionCache.clear();
|
31 | }
|
32 | async handle(request, { abortSignal } = {}) {
|
33 | if (!this.config) {
|
34 | this.config = await this.configProvider;
|
35 | }
|
36 | const { requestTimeout, disableConcurrentStreams } = this.config;
|
37 | return new Promise((resolve, rejectOriginal) => {
|
38 | let fulfilled = false;
|
39 | if (abortSignal === null || abortSignal === void 0 ? void 0 : abortSignal.aborted) {
|
40 | fulfilled = true;
|
41 | const abortError = new Error("Request aborted");
|
42 | abortError.name = "AbortError";
|
43 | rejectOriginal(abortError);
|
44 | return;
|
45 | }
|
46 | const { hostname, method, port, protocol, path, query } = request;
|
47 | const authority = `${protocol}//${hostname}${port ? `:${port}` : ""}`;
|
48 | const session = this.getSession(authority, disableConcurrentStreams || false);
|
49 | const reject = (err) => {
|
50 | if (disableConcurrentStreams) {
|
51 | this.destroySession(session);
|
52 | }
|
53 | fulfilled = true;
|
54 | rejectOriginal(err);
|
55 | };
|
56 | const queryString = (0, querystring_builder_1.buildQueryString)(query || {});
|
57 | const req = session.request({
|
58 | ...request.headers,
|
59 | [http2_1.constants.HTTP2_HEADER_PATH]: queryString ? `${path}?${queryString}` : path,
|
60 | [http2_1.constants.HTTP2_HEADER_METHOD]: method,
|
61 | });
|
62 | session.ref();
|
63 | req.on("response", (headers) => {
|
64 | const httpResponse = new protocol_http_1.HttpResponse({
|
65 | statusCode: headers[":status"] || -1,
|
66 | headers: (0, get_transformed_headers_1.getTransformedHeaders)(headers),
|
67 | body: req,
|
68 | });
|
69 | fulfilled = true;
|
70 | resolve({ response: httpResponse });
|
71 | if (disableConcurrentStreams) {
|
72 | session.close();
|
73 | this.deleteSessionFromCache(authority, session);
|
74 | }
|
75 | });
|
76 | if (requestTimeout) {
|
77 | req.setTimeout(requestTimeout, () => {
|
78 | req.close();
|
79 | const timeoutError = new Error(`Stream timed out because of no activity for ${requestTimeout} ms`);
|
80 | timeoutError.name = "TimeoutError";
|
81 | reject(timeoutError);
|
82 | });
|
83 | }
|
84 | if (abortSignal) {
|
85 | abortSignal.onabort = () => {
|
86 | req.close();
|
87 | const abortError = new Error("Request aborted");
|
88 | abortError.name = "AbortError";
|
89 | reject(abortError);
|
90 | };
|
91 | }
|
92 | req.on("frameError", (type, code, id) => {
|
93 | reject(new Error(`Frame type id ${type} in stream id ${id} has failed with code ${code}.`));
|
94 | });
|
95 | req.on("error", reject);
|
96 | req.on("aborted", () => {
|
97 | reject(new Error(`HTTP/2 stream is abnormally aborted in mid-communication with result code ${req.rstCode}.`));
|
98 | });
|
99 | req.on("close", () => {
|
100 | session.unref();
|
101 | if (disableConcurrentStreams) {
|
102 | session.destroy();
|
103 | }
|
104 | if (!fulfilled) {
|
105 | reject(new Error("Unexpected error: http2 request did not get a response"));
|
106 | }
|
107 | });
|
108 | (0, write_request_body_1.writeRequestBody)(req, request);
|
109 | });
|
110 | }
|
111 | getSession(authority, disableConcurrentStreams) {
|
112 | var _a;
|
113 | const sessionCache = this.sessionCache;
|
114 | const existingSessions = sessionCache.get(authority) || [];
|
115 | if (existingSessions.length > 0 && !disableConcurrentStreams)
|
116 | return existingSessions[0];
|
117 | const newSession = (0, http2_1.connect)(authority);
|
118 | newSession.unref();
|
119 | const destroySessionCb = () => {
|
120 | this.destroySession(newSession);
|
121 | this.deleteSessionFromCache(authority, newSession);
|
122 | };
|
123 | newSession.on("goaway", destroySessionCb);
|
124 | newSession.on("error", destroySessionCb);
|
125 | newSession.on("frameError", destroySessionCb);
|
126 | newSession.on("close", () => this.deleteSessionFromCache(authority, newSession));
|
127 | if ((_a = this.config) === null || _a === void 0 ? void 0 : _a.sessionTimeout) {
|
128 | newSession.setTimeout(this.config.sessionTimeout, destroySessionCb);
|
129 | }
|
130 | existingSessions.push(newSession);
|
131 | sessionCache.set(authority, existingSessions);
|
132 | return newSession;
|
133 | }
|
134 | destroySession(session) {
|
135 | if (!session.destroyed) {
|
136 | session.destroy();
|
137 | }
|
138 | }
|
139 | deleteSessionFromCache(authority, session) {
|
140 | const existingSessions = this.sessionCache.get(authority) || [];
|
141 | if (!existingSessions.includes(session)) {
|
142 | return;
|
143 | }
|
144 | this.sessionCache.set(authority, existingSessions.filter((s) => s !== session));
|
145 | }
|
146 | }
|
147 | exports.NodeHttp2Handler = NodeHttp2Handler;
|