got
Version:
Human-friendly and powerful HTTP request library for Node.js
88 lines (87 loc) • 3.73 kB
JavaScript
import { Buffer } from 'node:buffer';
import { normalizeError, RequestError } from './errors.js';
import stripUrlAuth from './utils/strip-url-auth.js';
const decodedBodyCache = new WeakMap();
const redirectDecisions = new WeakMap();
// TextDecoder strips a leading byte order mark, which Buffer#toString keeps.
const bomStrippingDecoders = new Map([
['utf8', new TextDecoder()],
['utf16le', new TextDecoder('utf-16le')],
]);
const normalizeEncoding = (encoding) => (encoding ?? 'utf8').toLowerCase().replace('-', '').replace('ucs2', 'utf16le');
export const isUtf8Encoding = (encoding) => normalizeEncoding(encoding) === 'utf8';
export const decodeUint8Array = (data, encoding) => bomStrippingDecoders.get(normalizeEncoding(encoding))?.decode(data) ?? Buffer.from(data).toString(encoding);
export const isResponseOk = (response) => {
const { statusCode } = response;
if ((statusCode >= 200 && statusCode <= 299) || statusCode === 304) {
return true;
}
if (statusCode < 300 || statusCode > 399) {
return false;
}
const { followRedirect } = response.request.options;
if (typeof followRedirect !== 'function') {
return !followRedirect;
}
let decision = redirectDecisions.get(response);
if (decision?.predicate !== followRedirect) {
decision = { predicate: followRedirect, follow: followRedirect(response) };
redirectDecisions.set(response, decision);
}
return !decision.follow;
};
/**
Shares the redirect decision when a response stream is wrapped for decompression.
*/
export const copyRedirectDecision = (source, destination) => {
const decision = redirectDecisions.get(source);
if (decision !== undefined) {
redirectDecisions.set(destination, decision);
}
};
/**
An error to be thrown when server response code is 2xx, and parsing body fails.
Includes a `response` property.
*/
export class ParseError extends RequestError {
name = 'ParseError';
code = 'ERR_BODY_PARSE_FAILURE';
constructor(error, response) {
const { options } = response.request;
// The thrown value is kept as the cause, so only its message is normalized.
super(`${normalizeError(error).message} in "${stripUrlAuth(options.url)}"`, error, response.request, response);
}
}
export const cacheDecodedBody = (response, decodedBody) => {
decodedBodyCache.set(response, decodedBody);
};
export const parseBody = (response, responseType, parseJson, encoding) => {
const { rawBody } = response;
const cachedDecodedBody = decodedBodyCache.get(response);
// Shortcuts must read the current bytes because callers can mutate `rawBody`.
decodedBodyCache.delete(response);
try {
if (responseType === 'text') {
return cachedDecodedBody ?? decodeUint8Array(rawBody, encoding);
}
if (responseType === 'json') {
// TODO: In the next major version, return `undefined` instead of `''` for an empty body, to match Ky. This affects `.json()`, `.json(schema)`, and `responseType: 'json'`. Also decide whether a custom `parseJson` should be called with `''` (Ky calls it).
if (rawBody.length === 0) {
return '';
}
const text = cachedDecodedBody ?? decodeUint8Array(rawBody, encoding);
return parseJson(text);
}
if (responseType === 'buffer') {
return rawBody;
}
}
catch (error) {
// `ParseError` normalizes the message and keeps the thrown value as the cause.
throw new ParseError(error, response);
}
throw new ParseError({
message: `Unknown body type '${responseType}'`,
name: 'Error',
}, response);
};