Skip to content

Commit f6c10e9

Browse files
committed
fix(node-http-handler): open promise handle while waiting for http continue
1 parent 129d85b commit f6c10e9

5 files changed

+77
-30
lines changed

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

+11-7
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,19 @@ describe("NodeHttpHandler", () => {
1818
let hRequestSpy: jest.SpyInstance;
1919
let hsRequestSpy: jest.SpyInstance;
2020
const randomMaxSocket = Math.round(Math.random() * 50) + 1;
21-
const mockRequestImpl = (_options, cb) => {
21+
const mockRequestImpl = (protocol: string) => (_options, cb) => {
2222
cb({
2323
statusCode: 200,
2424
body: "body",
2525
headers: {},
26+
protocol,
2627
});
27-
return new http.ClientRequest(_options);
28+
return new http.ClientRequest({ ..._options, protocol });
2829
};
2930

3031
beforeEach(() => {
31-
hRequestSpy = jest.spyOn(http, "request").mockImplementation(mockRequestImpl);
32-
hsRequestSpy = jest.spyOn(https, "request").mockImplementation(mockRequestImpl);
32+
hRequestSpy = jest.spyOn(http, "request").mockImplementation(mockRequestImpl("http:"));
33+
hsRequestSpy = jest.spyOn(https, "request").mockImplementation(mockRequestImpl("https:"));
3334
});
3435

3536
afterEach(() => {
@@ -97,8 +98,8 @@ describe("NodeHttpHandler", () => {
9798
return {
9899
connectionTimeout: 12345,
99100
socketTimeout: 12345,
100-
httpAgent: null,
101-
httpsAgent: null,
101+
httpAgent: void 0,
102+
httpsAgent: void 0,
102103
};
103104
};
104105

@@ -118,7 +119,10 @@ describe("NodeHttpHandler", () => {
118119
});
119120

120121
describe("http", () => {
121-
const mockHttpServer: HttpServer = createMockHttpServer().listen(54321);
122+
let mockHttpServer: HttpServer;
123+
beforeAll(() => {
124+
mockHttpServer = createMockHttpServer().listen(54321);
125+
});
122126

123127
afterEach(() => {
124128
mockHttpServer.removeAllListeners("request");

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import { Agent as hsAgent, request as hsRequest, RequestOptions } from "https";
77
import { NODEJS_TIMEOUT_ERROR_CODES } from "./constants";
88
import { getTransformedHeaders } from "./get-transformed-headers";
99
import { setConnectionTimeout } from "./set-connection-timeout";
10+
import { setSocketKeepAlive } from "./set-socket-keep-alive";
1011
import { setSocketTimeout } from "./set-socket-timeout";
1112
import { writeRequestBody } from "./write-request-body";
12-
import { setSocketKeepAlive } from "./set-socket-keep-alive";
1313

1414
/**
1515
* Represents the http options that can be passed to a node http client.
@@ -93,7 +93,17 @@ export class NodeHttpHandler implements HttpHandler {
9393
if (!this.config) {
9494
this.config = await this.configProvider;
9595
}
96-
return new Promise((resolve, reject) => {
96+
return new Promise((_resolve, _reject) => {
97+
let writeRequestBodyPromise = Promise.resolve();
98+
const resolve = async (arg: { response: HttpResponse }) => {
99+
await writeRequestBodyPromise;
100+
_resolve(arg);
101+
};
102+
const reject = async (arg: unknown) => {
103+
await writeRequestBodyPromise;
104+
_reject(arg);
105+
};
106+
97107
if (!this.config) {
98108
throw new Error("Node HTTP request handler config is not resolved");
99109
}
@@ -120,6 +130,7 @@ export class NodeHttpHandler implements HttpHandler {
120130

121131
// create the http request
122132
const requestFunc = isSSL ? hsRequest : hRequest;
133+
123134
const req = requestFunc(nodeHttpsOptions, (res) => {
124135
const httpResponse = new HttpResponse({
125136
statusCode: res.statusCode || -1,
@@ -163,7 +174,7 @@ export class NodeHttpHandler implements HttpHandler {
163174
});
164175
}
165176

166-
writeRequestBody(req, request);
177+
writeRequestBodyPromise = writeRequestBody(req, request, this.config.requestTimeout);
167178
});
168179
}
169180
}

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe(NodeHttp2Handler.name, () => {
1515
const protocol = "http:";
1616
const hostname = "localhost";
1717
const port = 45321;
18-
let mockH2Server = undefined;
18+
let mockH2Server: any = undefined;
1919
let mockH2Servers: Record<number, Http2Server> = {};
2020

2121
const authority = `${protocol}//${hostname}:${port}/`;
@@ -233,7 +233,7 @@ describe(NodeHttp2Handler.name, () => {
233233

234234
// ...and validate that the mocked response is received
235235
const responseBody = await new Promise((resolve) => {
236-
const buffers = [];
236+
const buffers: any[] = [];
237237
resultReader.on("data", (chunk) => buffers.push(chunk));
238238
resultReader.on("close", () => {
239239
resolve(Buffer.concat(buffers).toString("utf8"));

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

+21-11
Original file line numberDiff line numberDiff line change
@@ -76,17 +76,27 @@ export class NodeHttp2Handler implements HttpHandler {
7676
}
7777
}
7878
const { requestTimeout, disableConcurrentStreams } = this.config;
79-
return new Promise((resolve, rejectOriginal) => {
79+
return new Promise((_resolve, _reject) => {
8080
// It's redundant to track fulfilled because promises use the first resolution/rejection
8181
// but avoids generating unnecessary stack traces in the "close" event handler.
8282
let fulfilled = false;
8383

84+
let writeRequestBodyPromise = Promise.resolve();
85+
const resolve = async (arg: { response: HttpResponse }) => {
86+
await writeRequestBodyPromise;
87+
_resolve(arg);
88+
};
89+
const reject = async (arg: unknown) => {
90+
await writeRequestBodyPromise;
91+
_reject(arg);
92+
};
93+
8494
// if the request was already aborted, prevent doing extra work
8595
if (abortSignal?.aborted) {
8696
fulfilled = true;
8797
const abortError = new Error("Request aborted");
8898
abortError.name = "AbortError";
89-
rejectOriginal(abortError);
99+
reject(abortError);
90100
return;
91101
}
92102

@@ -98,12 +108,12 @@ export class NodeHttp2Handler implements HttpHandler {
98108
disableConcurrentStreams: disableConcurrentStreams || false,
99109
} as ConnectConfiguration);
100110

101-
const reject = (err: Error) => {
111+
const rejectWithDestroy = (err: Error) => {
102112
if (disableConcurrentStreams) {
103113
this.destroySession(session);
104114
}
105115
fulfilled = true;
106-
rejectOriginal(err);
116+
reject(err);
107117
};
108118

109119
const queryString = buildQueryString(query || {});
@@ -138,7 +148,7 @@ export class NodeHttp2Handler implements HttpHandler {
138148
req.close();
139149
const timeoutError = new Error(`Stream timed out because of no activity for ${requestTimeout} ms`);
140150
timeoutError.name = "TimeoutError";
141-
reject(timeoutError);
151+
rejectWithDestroy(timeoutError);
142152
});
143153
}
144154

@@ -147,17 +157,17 @@ export class NodeHttp2Handler implements HttpHandler {
147157
req.close();
148158
const abortError = new Error("Request aborted");
149159
abortError.name = "AbortError";
150-
reject(abortError);
160+
rejectWithDestroy(abortError);
151161
};
152162
}
153163

154164
// Set up handlers for errors
155165
req.on("frameError", (type: number, code: number, id: number) => {
156-
reject(new Error(`Frame type id ${type} in stream id ${id} has failed with code ${code}.`));
166+
rejectWithDestroy(new Error(`Frame type id ${type} in stream id ${id} has failed with code ${code}.`));
157167
});
158-
req.on("error", reject);
168+
req.on("error", rejectWithDestroy);
159169
req.on("aborted", () => {
160-
reject(new Error(`HTTP/2 stream is abnormally aborted in mid-communication with result code ${req.rstCode}.`));
170+
rejectWithDestroy(new Error(`HTTP/2 stream is abnormally aborted in mid-communication with result code ${req.rstCode}.`));
161171
});
162172

163173
// The HTTP/2 error code used when closing the stream can be retrieved using the
@@ -169,11 +179,11 @@ export class NodeHttp2Handler implements HttpHandler {
169179
session.destroy();
170180
}
171181
if (!fulfilled) {
172-
reject(new Error("Unexpected error: http2 request did not get a response"));
182+
rejectWithDestroy(new Error("Unexpected error: http2 request did not get a response"));
173183
}
174184
});
175185

176-
writeRequestBody(req, request);
186+
writeRequestBodyPromise = writeRequestBody(req, request, requestTimeout);
177187
});
178188
}
179189

packages/node-http-handler/src/write-request-body.ts

+29-7
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,37 @@ import { ClientRequest } from "http";
33
import { ClientHttp2Stream } from "http2";
44
import { Readable } from "stream";
55

6-
export function writeRequestBody(httpRequest: ClientRequest | ClientHttp2Stream, request: HttpRequest) {
7-
const expect = request.headers["Expect"] || request.headers["expect"];
6+
const MIN_WAIT_TIME = 1000;
7+
8+
/**
9+
* This resolves when writeBody has been called.
10+
*
11+
* @param httpRequest - opened Node.js request.
12+
* @param request - container with the request body.
13+
* @param maxContinueTimeoutMs - maximum time to wait for the continue event. Minimum of 1000ms.
14+
*/
15+
export async function writeRequestBody(
16+
httpRequest: ClientRequest | ClientHttp2Stream,
17+
request: HttpRequest,
18+
maxContinueTimeoutMs = MIN_WAIT_TIME
19+
): Promise<void> {
20+
const headers = request.headers ?? {};
21+
const expect = headers["Expect"] || headers["expect"];
22+
823
if (expect === "100-continue") {
9-
httpRequest.on("continue", () => {
10-
writeBody(httpRequest, request.body);
11-
});
12-
} else {
13-
writeBody(httpRequest, request.body);
24+
await Promise.race<void>([
25+
new Promise((resolve) => {
26+
setTimeout(resolve, Math.max(MIN_WAIT_TIME, maxContinueTimeoutMs));
27+
}),
28+
new Promise((resolve) => {
29+
httpRequest.on("continue", () => {
30+
resolve();
31+
});
32+
}),
33+
]);
1434
}
35+
36+
writeBody(httpRequest, request.body);
1537
}
1638

1739
function writeBody(

0 commit comments

Comments
 (0)