got
Version:
Human-friendly and powerful HTTP request library for Node.js
240 lines (235 loc) • 9.26 kB
JavaScript
import is from '@sindresorhus/is';
import stripUrlAuth from './utils/strip-url-auth.js';
export const normalizeError = (error) => {
if (error instanceof globalThis.Error) {
return error;
}
if (is.object(error)) {
const errorLike = error;
const message = typeof errorLike.message === 'string' ? errorLike.message : 'Non-error object thrown';
const normalizedError = new globalThis.Error(message, { cause: error });
if (typeof errorLike.stack === 'string') {
normalizedError.stack = errorLike.stack;
}
if (typeof errorLike.code === 'string') {
normalizedError.code = errorLike.code;
}
if (typeof errorLike.input === 'string') {
normalizedError.input = errorLike.input;
}
return normalizedError;
}
return new globalThis.Error(String(error));
};
// A hacky check to prevent circular references.
function isRequest(x) {
return is.object(x) && '_onResponse' in x;
}
/**
An error to be thrown when a request fails.
Contains a `code` property with error class code, like `ECONNREFUSED`.
*/
export class RequestError extends Error {
name = 'RequestError';
code = 'ERR_GOT_REQUEST_ERROR';
input;
stack;
response;
request;
timings;
constructor(message, error, self, response) {
super(message, { cause: error });
// The stack is trimmed here so that the frames inside `got` do not appear in the trace the
// caller sees. `unicorn/no-useless-error-capture-stack-trace` assumes the call is redundant
// for a subclass of `Error`, which is not the case here.
// eslint-disable-next-line unicorn/no-useless-error-capture-stack-trace
Error.captureStackTrace(this, this.constructor);
// The cause is kept as thrown, so it can be any value. Only an object can carry details.
const errorLike = is.object(error) ? error : undefined;
if (errorLike?.code) {
this.code = errorLike.code;
}
this.input = errorLike?.input;
if (isRequest(self)) {
Object.defineProperties(this, {
request: {
enumerable: false,
value: self,
},
response: {
enumerable: false,
value: response ?? self.response,
},
});
this.options = self.options;
}
else {
this.options = self;
}
this.timings = this.request?.timings;
// Recover the original stacktrace
if (is.string(errorLike?.stack) && is.string(this.stack)) {
// Both stacks start with a `name: message` header, and both headers are rebuilt below
// by dropping everything before this point. A multi-line message spans several lines,
// so the whole message is the boundary. A message that is empty or absent matches at
// zero and would leave the header in the frames, so the first newline is the boundary.
// A message that is not in its stack, such as one changed after the stack was read, has no
// boundary at all.
const frameStart = (stack, message) => {
if (!message) {
return stack.indexOf('\n');
}
const messageStart = stack.indexOf(message);
return messageStart === -1 ? -1 : messageStart + message.length;
};
const headerEnd = frameStart(this.stack, this.message);
const thisStackTrace = (headerEnd === -1 ? '' : this.stack.slice(headerEnd)).split('\n').toReversed();
const causeStart = frameStart(errorLike.stack, errorLike.message);
const originalStack = is.string(errorLike.message) && causeStart !== -1
? errorLike.stack.slice(causeStart)
: `\n${errorLike.stack}`;
const errorStackTrace = originalStack.split('\n').toReversed();
// Remove duplicated traces
while (errorStackTrace.length > 0 && errorStackTrace[0] === thisStackTrace[0]) {
thisStackTrace.shift();
}
const frames = `${thisStackTrace.toReversed().join('\n')}${errorStackTrace.toReversed().join('\n')}`;
let assignedStack;
// A subclass sets `name` only after `super()` returns, so building the header here
// would label every subclass as a plain `RequestError`. Read the names on access
// instead, while still honouring an assigned stack the way `Error` does.
Object.defineProperty(this, 'stack', {
configurable: true,
enumerable: false,
get: () => assignedStack ?? `${this.name}: ${this.message}${frames}`,
set(value) {
assignedStack = is.string(value) ? value : undefined;
},
});
}
}
}
/**
An error to be thrown when the server redirects you more than ten times.
Includes a `response` property.
*/
export class MaxRedirectsError extends RequestError {
name = 'MaxRedirectsError';
code = 'ERR_TOO_MANY_REDIRECTS';
constructor(request) {
super(`Redirected ${request.options.maxRedirects} times. Aborting.`, {}, request);
}
}
/**
An error to be thrown when the server response code is not 2xx nor 3xx if `options.followRedirect` is `true`, but always except for 304.
Includes a `response` property.
*/
// eslint-disable-next-line @typescript-eslint/naming-convention
export class HTTPError extends RequestError {
name = 'HTTPError';
code = 'ERR_NON_2XX_3XX_RESPONSE';
constructor(response) {
super(`Request failed with status code ${response.statusCode} (${response.statusMessage}): ${response.request.options.method} ${stripUrlAuth(response.request.options.url)}`, {}, response.request, response);
}
}
/**
An error to be thrown when a cache method fails.
For example, if the database goes down or there's a filesystem error.
*/
export class CacheError extends RequestError {
name = 'CacheError';
code = 'ERR_CACHE_ACCESS';
constructor(error, request) {
super(error.message, error, request);
}
}
/**
An error to be thrown when the request body is a stream and an error occurs while reading from that stream.
*/
export class UploadError extends RequestError {
name = 'UploadError';
code = 'ERR_UPLOAD';
constructor(error, request) {
super(error.message, error, request);
}
}
/**
An error to be thrown when the request is aborted due to a timeout.
Includes an `event` and `timings` property.
*/
export class TimeoutError extends RequestError {
name = 'TimeoutError';
code = 'ETIMEDOUT';
timings;
event;
constructor(error, timings, request) {
super(error.message, error, request);
// `AbortSignal.timeout()` reports a `DOMException`, which names no phase.
this.event = error.event ?? 'request';
this.timings = timings;
}
}
/**
An error to be thrown when reading from response stream fails.
*/
export class ReadError extends RequestError {
name = 'ReadError';
code = 'ERR_READING_RESPONSE_STREAM';
constructor(error, request) {
super(error.message, error, request);
if (error.code === 'ECONNRESET' || error.code === 'ERR_HTTP_CONTENT_LENGTH_MISMATCH') {
this.code = error.code;
}
}
}
/**
An error which always triggers a new retry when thrown.
*/
export class RetryError extends RequestError {
name = 'RetryError';
code = 'ERR_RETRYING';
constructor(request) {
super('Retrying', {}, request);
}
}
/**
An error to be thrown when the request is aborted by AbortController.
*/
export class AbortError extends RequestError {
name = 'AbortError';
code = 'ERR_ABORTED';
constructor(request) {
super('This operation was aborted.', {}, request);
if (request.options?.signal?.aborted) {
this.cause = request.options.signal.reason;
}
}
}
/**
An error to be thrown when the response body does not match the [Standard Schema](https://standardschema.dev) validator passed to `.json(schema)`. Includes an `issues` property with the issues from the schema. Each issue has a `message` and an optional `path`.
It intentionally does not extend `RequestError`, because the request succeeded. The user-provided schema did not accept the data, so it has no `code`, `options`, `request`, `response` or `timings` property, and it is not passed to `beforeError` hooks.
@example
```
import got, {SchemaValidationError} from 'got';
import {z} from 'zod';
const userSchema = z.object({name: z.string()});
try {
const user = await got('https://example.com/user').json(userSchema);
console.log(user.name);
} catch (error) {
if (error instanceof SchemaValidationError) {
console.error(error.issues);
} else {
throw error;
}
}
```
*/
export class SchemaValidationError extends Error {
name = 'SchemaValidationError';
issues;
constructor(issues) {
super('Response schema validation failed');
this.issues = issues;
}
}