Skip to content

Commit 9e58bbb

Browse files
authored
feat(node-http-handler): update timeout code and tests (#1691)
1 parent 6d02935 commit 9e58bbb

File tree

4 files changed

+114
-59
lines changed

4 files changed

+114
-59
lines changed
Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,94 @@
1-
import { ClientRequest } from "http";
2-
31
import { setConnectionTimeout } from "./set-connection-timeout";
42

53
describe("setConnectionTimeout", () => {
4+
const reject = jest.fn();
65
const clientRequest: any = {
76
on: jest.fn(),
7+
destroy: jest.fn(),
88
};
99

1010
beforeEach(() => {
11-
clientRequest.on.mockClear();
11+
jest.clearAllMocks();
1212
});
1313

1414
it("will not attach listeners if timeout is 0", () => {
15-
setConnectionTimeout(<ClientRequest>clientRequest, jest.fn(), 0);
16-
17-
expect(clientRequest.on.mock.calls.length).toBe(0);
15+
setConnectionTimeout(clientRequest, reject, 0);
16+
expect(clientRequest.on).not.toHaveBeenCalled();
1817
});
1918

2019
it("will not attach listeners if timeout is not provided", () => {
21-
setConnectionTimeout(<ClientRequest>clientRequest, jest.fn());
22-
23-
expect(clientRequest.on.mock.calls.length).toBe(0);
20+
setConnectionTimeout(clientRequest, reject);
21+
expect(clientRequest.on).not.toHaveBeenCalled();
2422
});
2523

26-
it("will attach listeners if timeout is provided", () => {
27-
setConnectionTimeout(<ClientRequest>clientRequest, jest.fn(), 100);
24+
describe("when timeout is provided", () => {
25+
const timeoutInMs = 100;
26+
const mockSocket = {
27+
connecting: true,
28+
on: jest.fn(),
29+
};
30+
31+
beforeEach(() => {
32+
jest.useFakeTimers();
33+
setConnectionTimeout(clientRequest, reject, timeoutInMs);
34+
});
35+
36+
it("attaches listener", () => {
37+
expect(clientRequest.on).toHaveBeenCalledTimes(1);
38+
expect(clientRequest.on).toHaveBeenCalledWith("socket", expect.any(Function));
39+
});
40+
41+
it("doesn't set timeout if socket is already connected", () => {
42+
clientRequest.on.mock.calls[0][1]({
43+
...mockSocket,
44+
connecting: false,
45+
});
46+
expect(mockSocket.on).not.toHaveBeenCalled();
47+
expect(setTimeout).not.toHaveBeenCalled();
48+
expect(reject).not.toHaveBeenCalled();
49+
});
50+
51+
it("rejects and aborts request if socket isn't connected by timeout", () => {
52+
clientRequest.on.mock.calls[0][1](mockSocket);
53+
expect(setTimeout).toHaveBeenCalledTimes(1);
54+
expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), timeoutInMs);
55+
expect(mockSocket.on).toHaveBeenCalledTimes(1);
56+
expect(mockSocket.on).toHaveBeenCalledWith("connect", expect.any(Function));
57+
58+
expect(clientRequest.destroy).not.toHaveBeenCalled();
59+
expect(reject).not.toHaveBeenCalled();
60+
61+
// Fast-forward until timer has been executed.
62+
jest.advanceTimersByTime(timeoutInMs);
63+
expect(clientRequest.destroy).toHaveBeenCalledTimes(1);
64+
expect(reject).toHaveBeenCalledTimes(1);
65+
expect(reject).toHaveBeenCalledWith(
66+
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
67+
name: "TimeoutError",
68+
})
69+
);
70+
});
71+
72+
it("clears timeout if socket gets connected", () => {
73+
const mockTimeoutId = 42;
74+
((setTimeout as unknown) as jest.Mock).mockReturnValueOnce(mockTimeoutId);
75+
clientRequest.on.mock.calls[0][1](mockSocket);
76+
77+
expect(clientRequest.destroy).not.toHaveBeenCalled();
78+
expect(reject).not.toHaveBeenCalled();
79+
expect(clearTimeout).not.toHaveBeenCalled();
80+
81+
// Fast-forward for half the amount of time and call connect callback to clear timer.
82+
jest.advanceTimersByTime(timeoutInMs / 2);
83+
mockSocket.on.mock.calls[0][1]();
84+
85+
expect(clearTimeout).toHaveBeenCalled();
86+
expect(clearTimeout).toHaveBeenCalledWith(mockTimeoutId);
2887

29-
expect(clientRequest.on.mock.calls.length).toBe(1);
30-
expect(clientRequest.on.mock.calls[0][0]).toBe("socket");
88+
// Fast-forward until timer has been executed.
89+
jest.runAllTimers();
90+
expect(clientRequest.destroy).not.toHaveBeenCalled();
91+
expect(reject).not.toHaveBeenCalled();
92+
});
3193
});
3294
});
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
import { ClientRequest } from "http";
22
import { Socket } from "net";
33

4-
export function setConnectionTimeout(request: ClientRequest, reject: (err: Error) => void, timeoutInMs = 0) {
4+
export const setConnectionTimeout = (request: ClientRequest, reject: (err: Error) => void, timeoutInMs = 0) => {
55
if (!timeoutInMs) {
66
return;
77
}
88

9-
request.on("socket", function (this: ClientRequest, socket: Socket) {
9+
request.on("socket", (socket: Socket) => {
1010
if (socket.connecting) {
11-
// Throw a connecting timeout error unless a connection is made within x time
11+
// Throw a connecting timeout error unless a connection is made within x time.
1212
const timeoutId = setTimeout(() => {
13-
// abort the request to destroy it
14-
this.abort();
15-
16-
const timeoutError = new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`);
17-
timeoutError.name = "TimeoutError";
18-
reject(timeoutError);
13+
// destroy the request.
14+
request.destroy();
15+
reject(
16+
Object.assign(new Error(`Socket timed out without establishing a connection within ${timeoutInMs} ms`), {
17+
name: "TimeoutError",
18+
})
19+
);
1920
}, timeoutInMs);
2021

21-
// if the connection was established, cancel the timeout
22+
// if the connection was established, cancel the timeout.
2223
socket.on("connect", () => {
2324
clearTimeout(timeoutId);
2425
});
2526
}
2627
});
27-
}
28+
};
Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,50 @@
1-
import { ClientRequest } from "http";
2-
31
import { setSocketTimeout } from "./set-socket-timeout";
42

53
describe("setSocketTimeout", () => {
64
const clientRequest: any = {
7-
abort: jest.fn(),
5+
destroy: jest.fn(),
86
setTimeout: jest.fn(),
97
};
108

119
beforeEach(() => {
12-
clientRequest.abort.mockClear();
13-
clientRequest.setTimeout.mockClear();
10+
jest.clearAllMocks();
1411
});
1512

1613
it(`sets the request's timeout if provided`, () => {
17-
setSocketTimeout(<ClientRequest>clientRequest, jest.fn(), 100);
14+
setSocketTimeout(clientRequest, jest.fn(), 100);
1815

19-
expect(clientRequest.setTimeout.mock.calls.length).toBe(1);
20-
expect(clientRequest.setTimeout.mock.calls[0][0]).toBe(100);
16+
expect(clientRequest.setTimeout).toHaveBeenCalledTimes(1);
17+
expect(clientRequest.setTimeout).toHaveBeenLastCalledWith(100, expect.any(Function));
2118
});
2219

2320
it(`sets the request's timeout to 0 if not provided`, () => {
24-
setSocketTimeout(<ClientRequest>clientRequest, jest.fn());
21+
setSocketTimeout(clientRequest, jest.fn());
2522

26-
expect(clientRequest.setTimeout.mock.calls.length).toBe(1);
27-
expect(clientRequest.setTimeout.mock.calls[0][0]).toBe(0);
23+
expect(clientRequest.setTimeout).toHaveBeenCalledTimes(1);
24+
expect(clientRequest.setTimeout).toHaveBeenLastCalledWith(0, expect.any(Function));
2825
});
2926

30-
it(`aborts the request on timeout`, () => {
31-
setSocketTimeout(<ClientRequest>clientRequest, jest.fn());
32-
33-
expect(clientRequest.setTimeout.mock.calls.length).toBe(1);
34-
expect(clientRequest.setTimeout.mock.calls[0][0]).toBe(0);
27+
it(`destroys the request on timeout`, () => {
28+
setSocketTimeout(clientRequest, jest.fn());
29+
expect(clientRequest.destroy).not.toHaveBeenCalled();
3530

3631
// call setTimeout callback
37-
const cb = clientRequest.setTimeout.mock.calls[0][1];
38-
cb.call(clientRequest);
39-
expect(clientRequest.abort.mock.calls.length).toBe(1);
32+
clientRequest.setTimeout.mock.calls[0][1]();
33+
expect(clientRequest.destroy).toHaveBeenCalledTimes(1);
4034
});
4135

4236
it(`rejects on timeout with a TimeoutError`, () => {
4337
const reject = jest.fn();
38+
const timeoutInMs = 100;
4439

45-
setSocketTimeout(<ClientRequest>clientRequest, reject);
46-
47-
expect(clientRequest.setTimeout.mock.calls.length).toBe(1);
48-
expect(clientRequest.setTimeout.mock.calls[0][0]).toBe(0);
40+
setSocketTimeout(clientRequest, reject, timeoutInMs);
41+
expect(reject).not.toHaveBeenCalled();
4942

5043
// call setTimeout callback
51-
const cb = clientRequest.setTimeout.mock.calls[0][1];
52-
cb.call(clientRequest);
53-
expect(reject.mock.calls.length).toBe(1);
54-
expect(reject.mock.calls[0][0].name).toBe("TimeoutError");
44+
clientRequest.setTimeout.mock.calls[0][1]();
45+
expect(reject).toHaveBeenCalledTimes(1);
46+
expect(reject).toHaveBeenCalledWith(
47+
Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" })
48+
);
5549
});
5650
});
Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import { ClientRequest } from "http";
22

3-
export function setSocketTimeout(request: ClientRequest, reject: (err: Error) => void, timeoutInMs = 0) {
4-
request.setTimeout(timeoutInMs, function (this: ClientRequest) {
5-
// abort the request to destroy it
6-
this.abort();
7-
const timeoutError = new Error(`Connection timed out after ${timeoutInMs} ms`);
8-
timeoutError.name = "TimeoutError";
9-
reject(timeoutError);
3+
export const setSocketTimeout = (request: ClientRequest, reject: (err: Error) => void, timeoutInMs = 0) => {
4+
request.setTimeout(timeoutInMs, () => {
5+
// destroy the request
6+
request.destroy();
7+
reject(Object.assign(new Error(`Connection timed out after ${timeoutInMs} ms`), { name: "TimeoutError" }));
108
});
11-
}
9+
};

0 commit comments

Comments
 (0)