Skip to content

Commit a079903

Browse files
authored
Fix endless loop (#20)
1 parent 1142f9c commit a079903

2 files changed

Lines changed: 121 additions & 55 deletions

File tree

index.js

Lines changed: 106 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,97 @@ import http2 from 'node:http2';
44
const RETRY_DELAY = 200;
55
const HTTP1_ATTEMPTS = 6; // 3 rounds × 2 IP versions
66

7-
export default function waitForLocalhost({port = 80, path = '/', useGet, statusCodes = [200], signal} = {}) {
7+
const tryHttp1 = ({
8+
ipVersion,
9+
onError,
10+
method,
11+
port,
12+
path,
13+
signal,
14+
handleResponse,
15+
}) => {
16+
const request = http.request(
17+
{
18+
method,
19+
port,
20+
path,
21+
family: ipVersion,
22+
signal,
23+
},
24+
response => {
25+
handleResponse(response.statusCode, ipVersion, onError);
26+
},
27+
);
28+
29+
request.on('error', onError);
30+
request.end();
31+
};
32+
33+
const noop = () => {};
34+
35+
const tryHttp2 = ({
36+
ipVersion,
37+
onError,
38+
method,
39+
port,
40+
path,
41+
signal,
42+
handleResponse,
43+
}) => {
44+
const hostname = ipVersion === 6 ? '[::1]' : 'localhost';
45+
const client = http2.connect(`http://${hostname}:${port}`);
46+
47+
const cleanup = () => {
48+
signal?.removeEventListener('abort', cleanup, {once: true});
49+
client.off('error', handleClientError);
50+
request.off('response', handleRequestResponse);
51+
request.off('error', handleRequestError);
52+
client.on('error', noop);
53+
request.on('error', noop);
54+
if (!client.destroyed) {
55+
client.destroy();
56+
}
57+
};
58+
59+
// Cleanup on abort
60+
signal?.addEventListener('abort', cleanup, {once: true});
61+
62+
const handleClientError = () => {
63+
cleanup();
64+
onError();
65+
};
66+
67+
client.on('error', handleClientError);
68+
69+
const request = client.request({
70+
':method': method,
71+
':path': path,
72+
});
73+
74+
const handleRequestResponse = headers => {
75+
cleanup();
76+
handleResponse(headers[':status'], ipVersion, onError);
77+
};
78+
79+
request.on('response', handleRequestResponse);
80+
81+
const handleRequestError = () => {
82+
cleanup();
83+
onError();
84+
};
85+
86+
request.on('error', handleRequestError);
87+
88+
request.end();
89+
};
90+
91+
export default function waitForLocalhost({
92+
port = 80,
93+
path = '/',
94+
useGet,
95+
statusCodes = [200],
96+
signal,
97+
} = {}) {
898
return new Promise((resolve, reject) => {
999
let attemptCount = 0;
10100
const method = useGet ? 'GET' : 'HEAD';
@@ -21,10 +111,14 @@ export default function waitForLocalhost({port = 80, path = '/', useGet, statusC
21111
}
22112
};
23113

24-
signal?.addEventListener('abort', () => {
25-
cleanup();
26-
reject(signal.reason);
27-
}, {once: true});
114+
signal?.addEventListener(
115+
'abort',
116+
() => {
117+
cleanup();
118+
reject(signal.reason);
119+
},
120+
{once: true},
121+
);
28122

29123
const handleResponse = (statusCode, ipVersion, onError) => {
30124
if (statusCodes.includes(statusCode)) {
@@ -34,61 +128,18 @@ export default function waitForLocalhost({port = 80, path = '/', useGet, statusC
34128
}
35129
};
36130

37-
const tryHttp1 = (ipVersion, onError) => {
38-
const request = http.request({
131+
const tryRequest = (ipVersion, onError) => {
132+
const useHttp2 = attemptCount > HTTP1_ATTEMPTS;
133+
const requestFunction = useHttp2 ? tryHttp2 : tryHttp1;
134+
requestFunction({
135+
ipVersion,
136+
onError,
39137
method,
40138
port,
41139
path,
42-
family: ipVersion,
43140
signal,
44-
}, response => {
45-
handleResponse(response.statusCode, ipVersion, onError);
141+
handleResponse,
46142
});
47-
48-
request.on('error', onError);
49-
request.end();
50-
};
51-
52-
const tryHttp2 = (ipVersion, onError) => {
53-
const hostname = ipVersion === 6 ? '[::1]' : 'localhost';
54-
const client = http2.connect(`http://${hostname}:${port}`);
55-
56-
const cleanupClient = () => {
57-
if (!client.destroyed) {
58-
client.destroy();
59-
}
60-
};
61-
62-
// Cleanup on abort
63-
signal?.addEventListener('abort', cleanupClient, {once: true});
64-
65-
client.on('error', () => {
66-
cleanupClient();
67-
onError();
68-
});
69-
70-
const request = client.request({
71-
':method': method,
72-
':path': path,
73-
});
74-
75-
request.on('response', headers => {
76-
cleanupClient();
77-
handleResponse(headers[':status'], ipVersion, onError);
78-
});
79-
80-
request.on('error', () => {
81-
cleanupClient();
82-
onError();
83-
});
84-
85-
request.end();
86-
};
87-
88-
const tryRequest = (ipVersion, onError) => {
89-
const useHttp2 = attemptCount > HTTP1_ATTEMPTS;
90-
const requestFunction = useHttp2 ? tryHttp2 : tryHttp1;
91-
requestFunction(ipVersion, onError);
92143
};
93144

94145
const retry = () => {

test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,3 +184,18 @@ test('should support AbortSignal.timeout()', async t => {
184184
t.is(error.name, 'TimeoutError');
185185
}
186186
});
187+
188+
test('should not loop on error', async t => {
189+
t.timeout(40_000);
190+
try {
191+
await waitForLocalhost({
192+
port: 5879,
193+
signal: AbortSignal.timeout(30_000),
194+
path: '/json/list',
195+
useGet: true,
196+
});
197+
t.fail('should have timeout out');
198+
} catch (error) {
199+
t.is(error.name, 'TimeoutError');
200+
}
201+
});

0 commit comments

Comments
 (0)