UNPKG

got

Version:

Human-friendly and powerful HTTP request library for Node.js

88 lines (87 loc) • 3.73 kB
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); };