thenavisapp
Version:
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
620 lines (513 loc) • 13.5 kB
text/typescript
import process from 'node:process';
import {EventEmitter} from 'node:events';
import {PassThrough as PassThroughStream} from 'node:stream';
import type {Socket} from 'node:net';
import http from 'node:http';
import test from 'ava';
import is from '@sindresorhus/is';
import type {Handler} from 'express';
import getStream from 'get-stream';
import {pEvent} from 'p-event';
import got, {HTTPError, TimeoutError} from '../source/index.js';
import type Request from '../source/core/index.js';
import withServer from './helpers/with-server.js';
const retryAfterOn413 = 2;
const socketTimeout = 300;
const handler413: Handler = (_request, response) => {
response.writeHead(413, {
'Retry-After': retryAfterOn413,
});
response.end();
};
const createSocketTimeoutStream = (): http.ClientRequest => {
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;
};
test('works on timeout', withServer, async (t, server, got) => {
let knocks = 0;
server.get('/', (_request, response) => {
response.end('who`s there?');
});
t.is((await got({
timeout: {
socket: socketTimeout,
},
request(...args: [
string | URL | http.RequestOptions,
(http.RequestOptions | ((response: http.IncomingMessage) => void))?,
((response: http.IncomingMessage) => void)?,
]) {
if (knocks === 1) {
// @ts-expect-error Overload error
return http.request(...args);
}
knocks++;
return createSocketTimeoutStream();
},
})).body, 'who`s there?');
});
test('retry function gets iteration count', withServer, async (t, server, got) => {
let knocks = 0;
server.get('/', (_request, response) => {
if (knocks++ === 1) {
response.end('who`s there?');
return;
}
response.statusCode = 500;
response.end();
});
await got({
retry: {
calculateDelay({attemptCount}) {
t.true(is.number(attemptCount));
return attemptCount < 2 ? 1 : 0;
},
},
});
});
test('setting to `0` disables retrying', async t => {
await t.throwsAsync(got('https://example.com', {
timeout: {socket: socketTimeout},
retry: {
calculateDelay({attemptCount}) {
t.is(attemptCount, 1);
return 0;
},
},
request: () => createSocketTimeoutStream(),
}), {
instanceOf: TimeoutError,
message: `Timeout awaiting 'socket' for ${socketTimeout}ms`,
});
});
test('custom retries', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 500;
response.end();
});
let hasTried = false;
const error = await t.throwsAsync<HTTPError>(got({
throwHttpErrors: true,
retry: {
calculateDelay({attemptCount}) {
if (attemptCount === 1) {
hasTried = true;
return 1;
}
return 0;
},
methods: [
'GET',
],
statusCodes: [
500,
],
},
}));
t.is(error?.response.statusCode, 500);
t.true(hasTried);
});
test('custom retries async', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 500;
response.end();
});
let hasTried = false;
const error = await t.throwsAsync<HTTPError>(got({
throwHttpErrors: true,
retry: {
async calculateDelay({attemptCount}) {
await new Promise(resolve => {
setTimeout(resolve, 1000);
});
if (attemptCount === 1) {
hasTried = true;
return 1;
}
return 0;
},
methods: [
'GET',
],
statusCodes: [
500,
],
},
}));
t.is(error?.response.statusCode, 500);
t.true(hasTried);
});
test('custom error codes', async t => {
const errorCode = 'OH_SNAP';
const error = await t.throwsAsync<Error & {code: typeof errorCode}>(got('https://example.com', {
request() {
const emitter = new EventEmitter() as http.ClientRequest;
emitter.abort = () => {};
// @ts-expect-error Imitating a stream
emitter.end = () => {};
// @ts-expect-error Imitating a stream
emitter.destroy = () => {};
// @ts-expect-error Imitating a stream
emitter.writable = true;
// @ts-expect-error Imitating a stream
emitter.writableEnded = false;
const error = new Error('Snap!');
(error as Error & {code: typeof errorCode}).code = errorCode;
setTimeout(() => {
emitter.emit('error', error);
});
return emitter;
},
retry: {
calculateDelay({error}) {
t.is(error.code, errorCode);
return 0;
},
methods: [
'GET',
],
errorCodes: [
errorCode,
],
},
}));
t.is(error?.code, errorCode);
});
test('respects 413 Retry-After', withServer, async (t, server, got) => {
let lastTried413access = Date.now();
server.get('/', (_request, response) => {
response.writeHead(413, {
'Retry-After': retryAfterOn413,
});
response.end((Date.now() - lastTried413access).toString());
lastTried413access = Date.now();
});
const {statusCode, body} = await got({
throwHttpErrors: false,
retry: {
limit: 1,
},
});
t.is(statusCode, 413);
t.true(Number(body) >= retryAfterOn413 * 1000);
});
test('respects 413 Retry-After with RFC-1123 timestamp', withServer, async (t, server, got) => {
let lastTried413TimestampAccess: string;
server.get('/', (_request, response) => {
const date = (new Date(Date.now() + (retryAfterOn413 * 1000))).toUTCString();
response.writeHead(413, {
'Retry-After': date,
});
response.end(lastTried413TimestampAccess);
lastTried413TimestampAccess = date;
});
const {statusCode, body} = await got({
throwHttpErrors: false,
retry: {
limit: 1,
},
});
t.is(statusCode, 413);
t.true(Date.now() >= Date.parse(body));
});
test('doesn\'t retry on 413 with empty statusCodes and methods', withServer, async (t, server, got) => {
server.get('/', handler413);
const {statusCode, retryCount} = await got({
throwHttpErrors: false,
retry: {
limit: 1,
statusCodes: [],
methods: [],
},
});
t.is(statusCode, 413);
t.is(retryCount, 0);
});
test('doesn\'t retry on 413 with empty methods', withServer, async (t, server, got) => {
server.get('/', handler413);
const {statusCode, retryCount} = await got({
throwHttpErrors: false,
retry: {
limit: 1,
statusCodes: [413],
methods: [],
},
});
t.is(statusCode, 413);
t.is(retryCount, 0);
});
test('doesn\'t retry on 413 without Retry-After header', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 413;
response.end();
});
const {retryCount} = await got({
throwHttpErrors: false,
});
t.is(retryCount, 0);
});
test('retries on 503 without Retry-After header', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 503;
response.end();
});
const {retryCount} = await got({
throwHttpErrors: false,
retry: {
limit: 1,
},
});
t.is(retryCount, 1);
});
test('doesn\'t retry on streams', withServer, async (t, server, got) => {
server.get('/', () => {});
// @ts-expect-error Error tests
const stream = got.stream({
timeout: {
request: 1,
},
retry: {
calculateDelay() {
t.fail('Retries on streams');
},
},
});
await t.throwsAsync(pEvent(stream, 'response'));
});
test('doesn\'t retry if Retry-After header is greater than maxRetryAfter', withServer, async (t, server, got) => {
server.get('/', handler413);
const {retryCount} = await got({
retry: {maxRetryAfter: 1000},
throwHttpErrors: false,
});
t.is(retryCount, 0);
});
test('doesn\'t retry when set to 0', withServer, async (t, server, got) => {
server.get('/', handler413);
const {statusCode, retryCount} = await got({
throwHttpErrors: false,
retry: {
limit: 0,
},
});
t.is(statusCode, 413);
t.is(retryCount, 0);
});
test('works when defaults.options.retry is a number', withServer, async (t, server, got) => {
server.get('/', handler413);
const instance = got.extend({
retry: {
limit: 2,
},
});
const {retryCount} = await instance({
throwHttpErrors: false,
});
t.is(retryCount, 2);
});
test('retry function can throw', withServer, async (t, server, got) => {
server.get('/', handler413);
const error = 'Simple error';
await t.throwsAsync(got({
retry: {
calculateDelay() {
throw new Error(error);
},
},
}), {message: error});
});
test('does not retry on POST', withServer, async (t, server, got) => {
server.post('/', () => {});
await t.throwsAsync(got.post({
timeout: {
request: 200,
},
hooks: {
beforeRetry: [
() => {
t.fail('Retries on POST requests');
},
],
},
}), {instanceOf: TimeoutError});
});
test('does not break on redirect', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 500;
response.end();
});
let tries = 0;
server.get('/redirect', (_request, response) => {
tries++;
response.writeHead(302, {
location: '/',
});
response.end();
});
await t.throwsAsync(got('redirect'), {message: 'Response code 500 (Internal Server Error)'});
t.is(tries, 1);
});
test('does not destroy the socket on HTTP error', withServer, async (t, server, got) => {
let returnServerError = true;
server.get('/', (_request, response) => {
if (returnServerError) {
response.statusCode = 500;
returnServerError = false;
}
response.end();
});
const sockets: Socket[] = [];
const agent = new http.Agent({
keepAlive: true,
});
await got('', {
agent: {
http: agent,
},
}).on('request', request => {
sockets.push(request.socket!);
});
t.is(sockets.length, 2);
t.is(sockets[0], sockets[1]);
agent.destroy();
});
test('can retry a Got stream', withServer, async (t, server, got) => {
let returnServerError = true;
server.get('/', (_request, response) => {
if (returnServerError) {
response.statusCode = 500;
response.end('not ok');
returnServerError = false;
return;
}
response.end('ok');
});
let globalRetryCount = 0;
const responseStreamPromise = new Promise<PassThroughStream>((resolve, reject) => {
let writeStream: PassThroughStream;
const fn = (retryStream?: Request) => {
const stream = retryStream ?? got.stream('');
globalRetryCount = stream.retryCount;
if (writeStream) {
writeStream.destroy();
}
writeStream = new PassThroughStream();
stream.pipe(writeStream);
stream.once('retry', (_retryCount, _error, createRetryStream) => {
fn(createRetryStream());
});
stream.once('error', reject);
stream.once('end', () => {
resolve(writeStream);
});
};
fn();
});
const responseStream = await responseStreamPromise;
const data = await getStream(responseStream);
t.is(data, 'ok');
t.is(globalRetryCount, 1);
});
test('throws when cannot retry a Got stream', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 500;
response.end('not ok');
});
let globalRetryCount = 0;
const streamPromise = new Promise<PassThroughStream>((resolve, reject) => {
const fn = (retryStream?: Request) => {
const stream = retryStream ?? got.stream('');
globalRetryCount = stream.retryCount;
stream.resume();
stream.once('retry', (_retryCount, _error, createRetryStream) => {
fn(createRetryStream());
});
stream.once('data', () => {
stream.destroy(new Error('data event has been emitted'));
});
stream.once('error', reject);
stream.once('end', resolve);
};
fn();
});
const error = await t.throwsAsync<HTTPError>(streamPromise, {
instanceOf: HTTPError,
});
t.is(error?.response.statusCode, 500);
t.is(error?.response.body, 'not ok');
t.is(globalRetryCount, 2);
});
test('can attach only one retry listener to a stream', withServer, async (t, _server, got) => {
const stream = got.stream('');
t.notThrows(() => {
stream.on('retry', () => {});
});
t.throws(() => {
stream.on('retry', () => {});
}, {
message: 'A retry listener has been attached already.',
});
stream.destroy();
});
test('promise does not retry when body is a stream', withServer, async (t, server, got) => {
server.post('/', (_request, response) => {
response.statusCode = 500;
response.end('not ok');
});
const body = new PassThroughStream();
body.end('hello');
const response = await got.post({
retry: {
methods: ['POST'],
},
body,
throwHttpErrors: false,
});
t.is(response.retryCount, 0);
});
test('reuses request options on retry', withServer, async (t, server, got) => {
let first = true;
server.get('/', (request, response) => {
if (first) {
first = false;
return;
}
response.end(JSON.stringify(request.headers));
});
const {body, retryCount} = await got('', {timeout: {request: 1000}, responseType: 'json'});
t.is(retryCount, 1);
t.is((body as any).accept, 'application/json');
});
test('respects backoffLimit', withServer, async (t, server, got) => {
let count = 0;
let timestamp = Date.now();
const data: number[] = [];
server.get('/', (_request, response) => {
count++;
const now = Date.now();
data.push(now - timestamp);
timestamp = now;
if (count === 3) {
response.end(JSON.stringify(data));
} else {
response.statusCode = 408;
response.end();
}
});
const body = await got('', {
retry: {
backoffLimit: 0,
},
}).json<number[]>();
t.is(body.length, 3);
t.true(body[0]! < 400);
t.true(body[1]! < 400);
t.true(body[2]! < 400);
});