thenavisapp
Version:
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
760 lines (648 loc) • 17.3 kB
text/typescript
import process from 'node:process';
import {EventEmitter} from 'node:events';
import stream, {PassThrough as PassThroughStream} from 'node:stream';
import {pipeline as streamPipeline} from 'node:stream/promises';
import http from 'node:http';
import net from 'node:net';
import getStream from 'get-stream';
import test from 'ava';
import delay from 'delay';
import type CacheableLookup from 'cacheable-lookup';
import type {Handler} from 'express';
import {pEvent} from 'p-event';
import got, {type RequestError, TimeoutError} from '../source/index.js';
import timedOut from '../source/core/timed-out.js';
import slowDataStream from './helpers/slow-data-stream.js';
import type {GlobalClock} from './helpers/types.js';
import withServer, {withServerAndFakeTimers, withHttpsServer} from './helpers/with-server.js';
const requestDelay = 800;
const errorMatcher = {
instanceOf: TimeoutError,
code: 'ETIMEDOUT',
};
const keepAliveAgent = new http.Agent({
keepAlive: true,
});
const defaultHandler = (clock: GlobalClock): Handler => (request, response) => {
request.resume();
request.on('end', () => {
clock.tick(requestDelay);
response.end('OK');
});
};
const downloadHandler = (clock?: GlobalClock): Handler => (_request, response) => {
response.writeHead(200, {
'transfer-encoding': 'chunked',
});
response.flushHeaders();
setImmediate(async () => {
await streamPipeline(slowDataStream(clock), response);
});
};
test.serial('timeout option', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.throwsAsync(
got({
timeout: {
request: 1,
},
retry: {
limit: 0,
},
}),
{
...errorMatcher,
message: 'Timeout awaiting \'request\' for 1ms',
},
);
});
test.serial('timeout option as object', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.throwsAsync(
got({
timeout: {request: 1},
retry: {
limit: 0,
},
}),
{
...errorMatcher,
message: 'Timeout awaiting \'request\' for 1ms',
},
);
});
test.serial('socket timeout', async t => {
await t.throwsAsync(
got('https://example.com', {
timeout: {socket: 1},
retry: {
limit: 0,
},
request() {
const stream = new PassThroughStream();
// @ts-expect-error Mocking the behaviour of a ClientRequest
stream.setTimeout = (ms, callback) => {
process.nextTick(callback);
};
// @ts-expect-error Mocking the behaviour of a ClientRequest
stream.abort = () => {};
stream.resume();
return stream as unknown as http.ClientRequest;
},
}),
{
instanceOf: TimeoutError,
code: 'ETIMEDOUT',
message: 'Timeout awaiting \'socket\' for 1ms',
},
);
});
test.serial('send timeout', withServerAndFakeTimers, async (t, server, got, clock) => {
server.post('/', defaultHandler(clock));
await t.throwsAsync(
got.post({
timeout: {send: 1},
body: new stream.PassThrough(),
retry: {
limit: 0,
},
}).on('request', request => {
request.once('socket', socket => {
socket.once('connect', () => {
clock.tick(10);
});
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'send\' for 1ms',
},
);
});
test.serial('send timeout (keepalive)', withServerAndFakeTimers, async (t, server, got, clock) => {
server.post('/', defaultHandler(clock));
server.get('/prime', (_request, response) => {
response.end('ok');
});
await got('prime', {agent: {http: keepAliveAgent}});
await t.throwsAsync(
got.post({
agent: {
http: keepAliveAgent,
},
timeout: {send: 1},
retry: {
limit: 0,
},
body: slowDataStream(clock),
}).on('request', (request: http.ClientRequest) => {
request.once('socket', socket => {
t.false(socket.connecting);
socket.once('connect', () => {
t.fail('\'connect\' event fired, invalidating test');
});
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'send\' for 1ms',
},
);
});
test.serial('response timeout', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.throwsAsync(
got({
timeout: {response: 1},
retry: {
limit: 0,
},
}),
{
...errorMatcher,
message: 'Timeout awaiting \'response\' for 1ms',
},
);
});
test.serial('response timeout unaffected by slow upload', withServerAndFakeTimers, async (t, server, got, clock) => {
server.post('/', defaultHandler(clock));
await t.notThrowsAsync(got.post({
retry: {
limit: 0,
},
body: slowDataStream(clock),
}));
});
test.serial('response timeout unaffected by slow download', withServer, async (t, server, got) => {
server.get('/', downloadHandler());
await t.notThrowsAsync(got({
timeout: {response: 200},
retry: {
limit: 0,
},
}));
await delay(100);
});
test.serial('response timeout (keepalive)', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
server.get('/prime', (_request, response) => {
response.end('ok');
});
await got('prime', {agent: {http: keepAliveAgent}});
const request = got({
agent: {
http: keepAliveAgent,
},
timeout: {response: 1},
retry: {
limit: 0,
},
}).on('request', (request: http.ClientRequest) => {
request.once('socket', socket => {
t.false(socket.connecting);
socket.once('connect', () => {
t.fail('\'connect\' event fired, invalidating test');
});
});
});
await t.throwsAsync(request, {
...errorMatcher,
message: 'Timeout awaiting \'response\' for 1ms',
});
});
test.serial('connect timeout', withServerAndFakeTimers, async (t, _server, got, clock) => {
await t.throwsAsync(
got({
createConnection(options) {
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
// @ts-expect-error We know that it is readonly, but we have to test it
socket.connecting = true;
setImmediate(() => {
socket.emit('lookup', null, '127.0.0.1', 4, 'localhost');
});
return socket;
},
timeout: {connect: 1},
retry: {
limit: 0,
},
}).on('request', (request: http.ClientRequest) => {
request.on('socket', () => {
clock.runAll();
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'connect\' for 1ms',
},
);
});
test.serial('connect timeout (ip address)', withServerAndFakeTimers, async (t, _server, _got, clock) => {
await t.throwsAsync(
got({
url: 'http://127.0.0.1',
createConnection(options) {
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
// @ts-expect-error We know that it is readonly, but we have to test it
socket.connecting = true;
return socket;
},
timeout: {connect: 1},
retry: {
limit: 0,
},
}).on('request', (request: http.ClientRequest) => {
request.on('socket', () => {
clock.runAll();
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'connect\' for 1ms',
},
);
});
test.serial('secureConnect timeout', withHttpsServer({}, true), async (t, _server, got, clock) => {
await t.throwsAsync(
got({
createConnection(options) {
const socket = new net.Socket(options as Record<string, unknown> as net.SocketConstructorOpts);
// @ts-expect-error We know that it is readonly, but we have to test it
socket.connecting = true;
setImmediate(() => {
socket.emit('lookup', null, '127.0.0.1', 4, 'localhost');
setImmediate(() => {
socket.emit('connect');
});
});
return socket;
},
timeout: {secureConnect: 0},
retry: {
limit: 0,
},
}).on('request', (request: http.ClientRequest) => {
request.on('socket', () => {
clock.runAll();
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'secureConnect\' for 0ms',
},
);
});
test('secureConnect timeout not breached', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.end('ok');
});
await t.notThrowsAsync(got({
timeout: {secureConnect: 200},
retry: {
limit: 0,
},
https: {
rejectUnauthorized: false,
},
}));
});
test.serial('lookup timeout', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.throwsAsync(
got({
dnsLookup() {},
timeout: {lookup: 1},
retry: {
limit: 0,
},
}).on('request', (request: http.ClientRequest) => {
request.on('socket', () => {
clock.runAll();
});
}),
{
...errorMatcher,
message: 'Timeout awaiting \'lookup\' for 1ms',
},
);
});
test.serial('lookup timeout no error (ip address)', withServerAndFakeTimers, async (t, server, _got, clock) => {
server.get('/', defaultHandler(clock));
await t.notThrowsAsync(got({
url: `http://127.0.0.1:${server.port}`,
timeout: {lookup: 1},
retry: {limit: 0},
}));
});
test.serial('lookup timeout no error (keepalive)', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
server.get('/prime', (_request, response) => {
response.end('ok');
});
await got('prime', {agent: {http: keepAliveAgent}});
await t.notThrowsAsync(got({
agent: {http: keepAliveAgent},
timeout: {lookup: 1},
retry: {limit: 0},
}).on('request', (request: http.ClientRequest) => {
request.once('connect', () => {
t.fail('connect event fired, invalidating test');
});
}));
keepAliveAgent.destroy();
});
test.serial('retries on timeout', withServer, async (t, server, got) => {
server.get('/', () => {});
let hasTried = false;
await t.throwsAsync(got({
timeout: {
request: 1,
},
retry: {
calculateDelay() {
if (hasTried) {
return 0;
}
hasTried = true;
return 1;
},
},
}), {
...errorMatcher,
message: 'Timeout awaiting \'request\' for 1ms',
});
t.true(hasTried);
});
test.serial('timeout with streams', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
const stream = got.stream({
timeout: {request: 0},
retry: {limit: 0},
});
await t.throwsAsync(pEvent(stream, 'response'), {code: 'ETIMEDOUT'});
});
test.serial('no error emitted when timeout is not breached (stream)', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
const stream = got.stream({
retry: {
limit: 0,
},
timeout: {
request: requestDelay * 2,
},
});
await t.notThrowsAsync(getStream(stream));
});
test.serial('no error emitted when timeout is not breached (promise)', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.notThrowsAsync(got({
retry: {
limit: 0,
},
timeout: {
request: requestDelay * 2,
},
}));
});
test.serial('no unhandled `socket hung up` errors', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
await t.throwsAsync(
got({
retry: {
limit: 0,
},
timeout: {
request: requestDelay / 2,
},
}),
{instanceOf: TimeoutError},
);
});
// TODO: use fakeTimers here
test.serial('no unhandled timeout errors', withServer, async (t, _server, got) => {
await t.throwsAsync(got({
retry: {limit: 0},
timeout: {request: 100},
request(...args) {
const result = http.request(...args);
result.once('socket', () => {
result.socket?.destroy();
});
return result;
},
}), {message: 'socket hang up'});
await delay(200);
});
// TODO: use fakeTimers here
test.serial('no unhandled timeout errors #2', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.write('Hello world!');
});
const gotPromise = got('', {
timeout: {
request: 20,
},
retry: {
calculateDelay({computedValue}) {
if (computedValue) {
return 10;
}
return 0;
},
limit: 1,
},
});
await t.throwsAsync(gotPromise, {instanceOf: TimeoutError});
await delay(100);
});
test.serial('no more timeouts after an error', withServer, async (t, _server, got) => {
const {setTimeout} = global;
const {clearTimeout} = global;
// @ts-expect-error FIXME
global.setTimeout = (callback, _ms, ...args) => {
const timeout = {
isCleared: false,
};
process.nextTick(() => {
if (timeout.isCleared) {
return;
}
callback(...args);
});
return timeout;
};
global.clearTimeout = timeout => {
if (timeout) {
// @ts-expect-error FIXME
timeout.isCleared = true;
}
};
await t.throwsAsync(got(`http://${Date.now()}.dev`, {
retry: {limit: 1},
timeout: {
lookup: 1,
connect: 1,
secureConnect: 1,
socket: 1,
response: 1,
send: 1,
request: 1,
},
}), {instanceOf: TimeoutError});
await delay(100);
global.setTimeout = setTimeout;
global.clearTimeout = clearTimeout;
});
test.serial('socket timeout is canceled on error', withServerAndFakeTimers, async (t, _server, got, clock) => {
const message = 'oh, snap!';
const promise = got({
timeout: {socket: 50},
retry: {limit: 0},
}).on('request', (request: http.ClientRequest) => {
request.destroy(new Error(message));
});
await t.throwsAsync(promise, {message});
// Wait a bit more to check if there are any unhandled errors
clock.tick(100);
});
test.serial('no memory leak when using socket timeout and keepalive agent', withServerAndFakeTimers, async (t, server, got, clock) => {
server.get('/', defaultHandler(clock));
let request: any;
await got({
agent: {http: keepAliveAgent},
timeout: {socket: requestDelay * 2},
}).on('request', _request => {
request = _request;
});
t.is(request.timeoutCb, null);
keepAliveAgent.destroy();
});
test('ensure there are no new timeouts after cancelation', t => {
const emitter = new EventEmitter();
const socket = new EventEmitter();
(socket as any).connecting = true;
timedOut(emitter as http.ClientRequest, {
connect: 1,
}, {
hostname: '127.0.0.1',
})();
emitter.emit('socket', socket);
socket.emit('lookup', null);
t.is(socket.listenerCount('connect'), 0);
});
test('double calling timedOut has no effect', t => {
const emitter = new EventEmitter();
const attach = (): () => void => timedOut(emitter as http.ClientRequest, {
connect: 1,
}, {
hostname: '127.0.0.1',
});
attach();
attach();
t.is(emitter.listenerCount('socket'), 1);
});
test.serial.failing('doesn\'t throw on early lookup', withServerAndFakeTimers, async (t, server, got) => {
server.get('/', (_request, response) => {
response.end('ok');
});
await t.notThrowsAsync(got('', {
timeout: {
lookup: 1,
},
retry: {limit: 0},
// @ts-expect-error FIXME
dnsLookup(...[_hostname, options, callback]: Parameters<CacheableLookup['lookup']>) {
if (typeof options === 'function') {
callback = options;
}
callback(null, '127.0.0.1', 4);
},
}));
});
// TODO: use fakeTimers here
test.serial('no unhandled `Premature close` error', withServer, async (t, server, got) => {
server.get('/', async (_request, response) => {
response.write('hello');
});
await t.throwsAsync(got({
timeout: {request: 10},
retry: {limit: 0},
}), {message: 'Timeout awaiting \'request\' for 10ms'});
await delay(20);
});
// TODO: use fakeTimers here
test.serial('`read` timeout - promise', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.write('o');
});
await t.throwsAsync(got({
timeout: {
read: 10,
},
retry: {
limit: 0,
},
}), {message: 'Timeout awaiting \'read\' for 10ms'});
});
// TODO: use fakeTimers here
test.serial('`read` timeout - stream', withServer, async (t, server, got) => {
t.timeout(100);
server.get('/', (_request, response) => {
response.end('ok');
});
const stream = got.stream({
timeout: {
read: 10,
},
});
await t.throwsAsync(pEvent(stream, 'end'), {message: 'Timeout awaiting \'read\' for 10ms'});
});
// TODO: use fakeTimers here
test.serial('cancelling the request removes timeouts', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.write('hello');
});
const promise = got({
timeout: {
request: 500,
},
retry: {
limit: 0,
},
}).on('downloadProgress', () => {
promise.cancel();
}).on('request', request => {
request.on('error', error => {
if (error.message === 'Timeout awaiting \'request\' for 500ms') {
t.fail(error.message);
}
});
});
await t.throwsAsync(promise, {message: 'Promise was canceled'});
await delay(1000);
});
test('timeouts are emitted ASAP', async t => {
const timeout = 500;
const marginOfError = process.env.CI ? 200 : 100;
const error = await t.throwsAsync<TimeoutError>(got('http://192.0.2.1/test', {
retry: {
limit: 0,
},
timeout: {
request: timeout,
},
}), {instanceOf: TimeoutError});
t.true(error!.timings.phases.total! < (timeout + marginOfError));
});
test('http2 timeout', async t => {
const error = await t.throwsAsync<RequestError>(got('https://123.123.123.123', {
timeout: {
request: 1,
},
http2: true,
retry: {
calculateDelay: ({computedValue}) => computedValue ? 1 : 0,
},
}));
t.true(error?.code === 'ETIMEDOUT' || error?.code === 'EUNSUPPORTED', error?.stack);
});