thenavisapp
Version:
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
452 lines (375 loc) • 11.9 kB
text/typescript
import {Buffer} from 'node:buffer';
import {promisify} from 'node:util';
import net from 'node:net';
import http from 'node:http';
import stream from 'node:stream';
import {pipeline as streamPipeline} from 'node:stream/promises';
import {Agent} from 'node:https';
import test from 'ava';
import getStream from 'get-stream';
import is from '@sindresorhus/is';
import got, {RequestError, HTTPError, TimeoutError} from '../source/index.js';
import type Request from '../source/core/index.js';
import withServer from './helpers/with-server.js';
import invalidUrl from './helpers/invalid-url.js';
test('properties', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.end('not');
});
const url = new URL(server.url);
const error = (await t.throwsAsync<HTTPError<string>>(got('')))!;
t.truthy(error);
t.truthy(error.response);
t.truthy(error.options);
t.true(Object.prototype.propertyIsEnumerable.call(error, 'options'));
t.false(Object.prototype.propertyIsEnumerable.call(error, 'response'));
t.is(error.code, 'ERR_NON_2XX_3XX_RESPONSE');
t.is(error.message, 'Response code 404 (Not Found)');
t.deepEqual(error.options.url, url);
t.is(error.response.headers.connection, 'keep-alive');
// Assert is used for type checking
t.assert(error.response.body === 'not');
});
test('catches dns errors', async t => {
const error = (await t.throwsAsync<RequestError<undefined>>(got('http://doesntexist', {retry: {limit: 0}})))!;
t.truthy(error);
t.regex(error.message, /ENOTFOUND|EAI_AGAIN/);
t.is((error.options.url as URL).host, 'doesntexist');
t.is(error.options.method, 'GET');
t.true(['ENOTFOUND', 'EAI_AGAIN'].includes(error.code));
});
test('`options.body` form error message', async t => {
// @ts-expect-error Error tests
await t.throwsAsync(got.post('https://example.com', {body: Buffer.from('test'), form: ''}),
{
instanceOf: RequestError,
message: 'Expected values which are `plain object` or `undefined`. Received values of type `string`.',
},
// {message: 'The `body`, `json` and `form` options are mutually exclusive'}
);
});
test('no plain object restriction on json body', withServer, async (t, server, got) => {
server.post('/body', async (request, response) => {
await streamPipeline(request, response);
});
class CustomObject {
a = 123;
}
const body = await got.post('body', {json: new CustomObject()}).json();
t.deepEqual(body, {a: 123});
});
test('default status message', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 400;
response.end('body');
});
const error = await t.throwsAsync<HTTPError>(got(''),
{
instanceOf: HTTPError,
message: 'Response code 400 (Bad Request)',
});
t.is(error?.response.statusCode, 400);
t.is(error?.response.statusMessage, 'Bad Request');
});
test('custom status message', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 400;
response.statusMessage = 'Something Exploded';
response.end('body');
});
const error = await t.throwsAsync<HTTPError>(got(''),
{
instanceOf: HTTPError,
message: 'Response code 400 (Something Exploded)',
});
t.is(error?.response.statusCode, 400);
t.is(error?.response.statusMessage, 'Something Exploded');
});
test('custom body', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.end('not');
});
const error = await t.throwsAsync<HTTPError>(got(''),
{
instanceOf: HTTPError,
message: 'Response code 404 (Not Found)',
});
t.is(error?.response.statusCode, 404);
// Typecheck for default `any` type
t.assert(error?.response.body === 'not');
});
test('custom json body', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.header('content-type', 'application/json');
response.end(JSON.stringify({
message: 'not found',
}));
});
const error = await t.throwsAsync<HTTPError<{message: string}>>(got('', {responseType: 'json'}),
{
instanceOf: HTTPError,
message: 'Response code 404 (Not Found)',
});
t.is(error?.response.statusCode, 404);
// Assert is used for body typecheck
t.assert(error?.response.body.message === 'not found');
});
test('contains Got options', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.end();
});
const options = {
context: {
foo: 'bar',
},
} as const;
const error = await t.throwsAsync<HTTPError>(got(options),
{
instanceOf: HTTPError,
message: 'Response code 404 (Not Found)',
});
t.is(error?.response.statusCode, 404);
t.is(error?.options.context.foo, options.context.foo);
});
test.failing('empty status message is overriden by the default one', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.writeHead(400, '');
response.end('body');
});
const error = await t.throwsAsync<HTTPError>(got(''),
{
instanceOf: HTTPError,
message: 'Response code 400 (Bad Request)',
});
t.is(error?.response.statusCode, 400);
t.is(error?.response.statusMessage, http.STATUS_CODES[400]);
});
test('`http.request` error', async t => {
await t.throwsAsync(got('https://example.com', {
request() {
throw new TypeError('The header content contains invalid characters');
},
}), {
instanceOf: RequestError,
message: 'The header content contains invalid characters',
code: 'ERR_GOT_REQUEST_ERROR',
});
});
test('`http.request` pipe error', async t => {
const message = 'snap!';
await t.throwsAsync(got('https://example.com', {
// @ts-expect-error Error tests
request() {
const proxy = new stream.PassThrough();
const anyProxy = proxy as any;
anyProxy.socket = {
remoteAddress: '',
prependOnceListener() {},
};
anyProxy.headers = {};
anyProxy.abort = () => {};
proxy.resume();
proxy.read = () => {
proxy.destroy(new Error(message));
return null;
};
return proxy;
},
throwHttpErrors: false,
}), {
instanceOf: RequestError,
message,
});
});
test('`http.request` error through CacheableRequest', async t => {
await t.throwsAsync(got('https://example.com', {
request() {
throw new TypeError('The header content contains invalid characters');
},
cache: new Map(),
}), {
instanceOf: RequestError,
message: 'The header content contains invalid characters',
});
});
test('returns a stream even if normalization fails', async t => {
const stream = got('https://example.com', {
isStream: true,
// @ts-expect-error Testing purposes
hooks: false,
}) as unknown as Request;
await t.throwsAsync(getStream(stream), {
instanceOf: RequestError,
message: 'Expected value which is `Object`, received value of type `boolean`.',
});
});
test('normalization errors using convenience methods', async t => {
const url = 'undefined/https://example.com';
{
const error = await t.throwsAsync(got(url).json());
invalidUrl(t, error!, url);
}
{
const error = await t.throwsAsync(got(url).text());
invalidUrl(t, error!, url);
}
{
const error = await t.throwsAsync(got(url).buffer());
invalidUrl(t, error!, url);
}
});
test('errors can have request property', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.end();
});
const error = await t.throwsAsync<HTTPError>(got(''));
t.truthy(error?.response);
t.truthy(error?.request.downloadProgress);
});
test('promise does not hang on timeout on HTTP error', withServer, async (t, server, got) => {
server.get('/', (_request, response) => {
response.statusCode = 404;
response.write('asdf');
});
await t.throwsAsync(got({
timeout: {
request: 100,
},
}), {
instanceOf: TimeoutError,
code: 'ETIMEDOUT',
});
});
test('no uncaught parse errors', async t => {
const server = net.createServer();
const listen = promisify(server.listen.bind(server));
const close = promisify(server.close.bind(server));
await listen();
server.on('connection', socket => {
socket.resume();
socket.end([
'HTTP/1.1 404 Not Found',
'transfer-encoding: chunked',
'',
'0',
'',
'',
].join('\r\n'));
});
await t.throwsAsync(got.head(`http://localhost:${(server.address() as net.AddressInfo).port}`), {
instanceOf: RequestError,
message: /^Parse Error/,
});
await close();
});
test('no uncaught parse errors #2', async t => {
const server = net.createServer();
const listen = promisify(server.listen.bind(server));
const close = promisify(server.close.bind(server));
await listen();
server.on('connection', socket => {
socket.resume();
socket.write([
'HTTP/1.1 200 OK',
'content-length: 1',
'',
'0a',
].join('\r\n'));
});
await t.throwsAsync(got(`http://localhost:${(server.address() as net.AddressInfo).port}`), {
instanceOf: RequestError,
message: /^Parse Error/,
});
await close();
});
// Fails randomly on Node 10:
// Blocked by https://github.com/istanbuljs/nyc/issues/619
// eslint-disable-next-line ava/no-skip-test
test.skip('the old stacktrace is recovered', async t => {
const error = await t.throwsAsync(got('https://example.com', {
request() {
throw new Error('foobar');
},
}));
t.true(error?.stack!.includes('at Object.request'));
// The first `at get` points to where the error was wrapped,
// the second `at get` points to the real cause.
t.not(error?.stack!.indexOf('at get'), error?.stack!.lastIndexOf('at get'));
});
test('should wrap got cause', async t => {
const error = await t.throwsAsync<RequestError>(got('https://github.com', {retry: {limit: 0}, timeout: {request: 1}}));
const cause = error?.cause as TimeoutError;
t.is(error?.code, cause.code);
t.is(error?.message, cause.message);
});
test('should wrap non-got cause', async t => {
class SocksProxyAgent extends Agent {
createConnection() {
throw new SocksClientError('oh no');
}
}
class SocksClientError extends Error {}
const error = await t.throwsAsync<RequestError>(got('https://github.com', {retry: {limit: 0}, timeout: {read: 1}, agent: {https: new SocksProxyAgent()}}));
const cause = error?.cause as Error;
t.is(error?.code, 'ERR_GOT_REQUEST_ERROR');
t.is(error?.message, cause.message);
t.is(error?.message, 'oh no');
t.assert(cause instanceof SocksClientError);
});
test.serial('custom stack trace', withServer, async (t, _server, got) => {
// eslint-disable-next-line @typescript-eslint/naming-convention
const ErrorCaptureStackTrace = Error.captureStackTrace;
const enable = () => {
Error.captureStackTrace = (target: {stack: any}) => {
target.stack = [
'line 1',
'line 2',
];
};
};
const disable = () => {
Error.captureStackTrace = ErrorCaptureStackTrace;
};
// Node.js default behavior
{
const stream = got.stream('');
stream.destroy(new Error('oh no'));
const caught = await t.throwsAsync(getStream(stream));
t.is(is(caught?.stack), 'string');
}
// Passing a custom error
{
enable();
const error = new Error('oh no');
disable();
const stream = got.stream('');
stream.destroy(error);
const caught = await t.throwsAsync(getStream(stream));
t.is(is(caught?.stack), 'string');
}
// Custom global behavior
{
enable();
const error = new Error('oh no');
const stream = got.stream('');
stream.destroy(error);
const caught = await t.throwsAsync(getStream(stream));
t.is(is(caught?.stack), 'Array');
disable();
}
// Passing a default error that needs some processing
{
const error = new Error('oh no');
enable();
const stream = got.stream('');
stream.destroy(error);
const caught = await t.throwsAsync(getStream(stream));
t.is(is(caught?.stack), 'Array');
disable();
}
});