UNPKG

@microlink/mql

Version:

Microlink Query Language. The official HTTP client to interact with Microlink API for Node.js, browsers & Deno.

1,274 lines (1,221 loc) 54.5 kB
function getDefaultExportFromCjs (x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, 'default') ? x['default'] : x; } function getAugmentedNamespace(n) { if (Object.prototype.hasOwnProperty.call(n, '__esModule')) return n; var f = n.default; if (typeof f == "function") { var a = function a () { var isInstance = false; try { isInstance = this instanceof a; } catch {} if (isInstance) { return Reflect.construct(f, arguments, this.constructor); } return f.apply(this, arguments); }; a.prototype = f.prototype; } else a = {}; Object.defineProperty(a, '__esModule', {value: true}); Object.keys(n).forEach(function (k) { var d = Object.getOwnPropertyDescriptor(n, k); Object.defineProperty(a, k, d.get ? d : { enumerable: true, get: function () { return n[k]; } }); }); return a; } var lightweight = {exports: {}}; var dist = {}; function iter(output, nullish, sep, val, key) { var k, pfx = key ? (key + sep) : key; if (val == null) { if (nullish) output[key] = val; } else if (typeof val != 'object') { output[key] = val; } else if (Array.isArray(val)) { for (k=0; k < val.length; k++) { iter(output, nullish, sep, val[k], pfx + k); } } else { for (k in val) { iter(output, nullish, sep, val[k], pfx + k); } } } function flattie(input, glue, toNull) { var output = {}; if (typeof input == 'object') { iter(output, !!toNull, glue || '.', input, ''); } return output; } dist.flattie = flattie; class HTTPError extends Error { response; request; options; constructor(response, request, options) { const code = (response.status || response.status === 0) ? response.status : ''; const title = response.statusText ?? ''; const status = `${code} ${title}`.trim(); const reason = status ? `status code ${status}` : 'an unknown error'; super(`Request failed with ${reason}: ${request.method} ${request.url}`); this.name = 'HTTPError'; this.response = response; this.request = request; this.options = options; } } /** Wrapper for non-Error values that were thrown. In JavaScript, any value can be thrown (not just Error instances). This class wraps such values to ensure consistent error handling. */ class NonError extends Error { name = 'NonError'; value; constructor(value) { let message = 'Non-error value was thrown'; // Intentionally minimal as this error is just an edge-case. try { if (typeof value === 'string') { message = value; } else if (value && typeof value === 'object' && 'message' in value && typeof value.message === 'string') { message = value.message; } } catch { // Use default message if accessing properties throws } super(message); this.value = value; } } /** Internal error used to signal a forced retry from afterResponse hooks. This is thrown when a user returns ky.retry() from an afterResponse hook. */ class ForceRetryError extends Error { name = 'ForceRetryError'; customDelay; code; customRequest; constructor(options) { // Runtime protection: wrap non-Error causes in NonError // TypeScript type is Error for guidance, but JS users can pass anything const cause = options?.cause ? (options.cause instanceof Error ? options.cause : new NonError(options.cause)) : undefined; super(options?.code ? `Forced retry: ${options.code}` : 'Forced retry', cause ? { cause } : undefined); this.customDelay = options?.delay; this.code = options?.code; this.customRequest = options?.request; } } const supportsRequestStreams = (() => { let duplexAccessed = false; let hasContentType = false; const supportsReadableStream = typeof globalThis.ReadableStream === 'function'; const supportsRequest = typeof globalThis.Request === 'function'; if (supportsReadableStream && supportsRequest) { try { hasContentType = new globalThis.Request('https://empty.invalid', { body: new globalThis.ReadableStream(), method: 'POST', // @ts-expect-error - Types are outdated. get duplex() { duplexAccessed = true; return 'half'; }, }).headers.has('Content-Type'); } catch (error) { // QQBrowser on iOS throws "unsupported BodyInit type" error (see issue #581) if (error instanceof Error && error.message === 'unsupported BodyInit type') { return false; } throw error; } } return duplexAccessed && !hasContentType; })(); const supportsAbortController = typeof globalThis.AbortController === 'function'; const supportsAbortSignal = typeof globalThis.AbortSignal === 'function' && typeof globalThis.AbortSignal.any === 'function'; const supportsResponseStreams = typeof globalThis.ReadableStream === 'function'; const supportsFormData = typeof globalThis.FormData === 'function'; const requestMethods = ['get', 'post', 'put', 'patch', 'head', 'delete']; const responseTypes = { json: 'application/json', text: 'text/*', formData: 'multipart/form-data', arrayBuffer: '*/*', blob: '*/*', // Supported in modern Fetch implementations (for example, browsers and recent Node.js/undici). // We still feature-check at runtime before exposing the shortcut. bytes: '*/*', }; // The maximum value of a 32bit int (see issue #117) const maxSafeTimeout = 2_147_483_647; // Size in bytes of a typical form boundary, used to help estimate upload size const usualFormBoundarySize = new TextEncoder().encode('------WebKitFormBoundaryaxpyiPgbbPti10Rw').length; const stop = Symbol('stop'); /** Marker returned by ky.retry() to signal a forced retry from afterResponse hooks. */ class RetryMarker { options; constructor(options) { this.options = options; } } /** Force a retry from an `afterResponse` hook. This allows you to retry a request based on the response content, even if the response has a successful status code. The retry will respect the `retry.limit` option and skip the `shouldRetry` check. The forced retry is observable in `beforeRetry` hooks, where the error will be a `ForceRetryError`. @param options - Optional configuration for the retry. @example ``` import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ hooks: { afterResponse: [ async (request, options, response) => { // Retry based on response body content if (response.status === 200) { const data = await response.clone().json(); // Simple retry with default delay if (data.error?.code === 'TEMPORARY_ERROR') { return ky.retry(); } // Retry with custom delay from API response if (data.error?.code === 'RATE_LIMIT') { return ky.retry({ delay: data.error.retryAfter * 1000, code: 'RATE_LIMIT' }); } // Retry with a modified request (e.g., fallback endpoint) if (data.error?.code === 'FALLBACK_TO_BACKUP') { return ky.retry({ request: new Request('https://backup-api.com/endpoint', { method: request.method, headers: request.headers, }), code: 'BACKUP_ENDPOINT' }); } // Retry with refreshed authentication if (data.error?.code === 'TOKEN_REFRESH' && data.newToken) { return ky.retry({ request: new Request(request, { headers: { ...Object.fromEntries(request.headers), 'Authorization': `Bearer ${data.newToken}` } }), code: 'TOKEN_REFRESHED' }); } // Retry with cause to preserve error chain try { validateResponse(data); } catch (error) { return ky.retry({ code: 'VALIDATION_FAILED', cause: error }); } } } ], beforeRetry: [ ({error, retryCount}) => { // Observable in beforeRetry hooks if (isForceRetryError(error)) { console.log(`Forced retry #${retryCount}: ${error.message}`); // Example output: "Forced retry #1: Forced retry: RATE_LIMIT" } } ] } }); const response = await api.get('https://example.com/api'); ``` */ const retry = (options) => new RetryMarker(options); const kyOptionKeys = { json: true, parseJson: true, stringifyJson: true, searchParams: true, prefixUrl: true, retry: true, timeout: true, hooks: true, throwHttpErrors: true, onDownloadProgress: true, onUploadProgress: true, fetch: true, context: true, }; // Vendor-specific fetch options that should always be passed to fetch() // even if they appear on the Request object due to vendor patching. // See: https://github.com/sindresorhus/ky/issues/541 const vendorSpecificOptions = { next: true, // Next.js cache revalidation (revalidate, tags) }; // Standard RequestInit options that should NOT be passed separately to fetch() // because they're already applied to the Request object. // Note: `dispatcher` and `priority` are NOT included here - they're fetch-only // options that the Request constructor doesn't accept, so they need to be passed // separately to fetch(). const requestOptionsRegistry = { method: true, headers: true, body: true, mode: true, credentials: true, cache: true, redirect: true, referrer: true, referrerPolicy: true, integrity: true, keepalive: true, signal: true, window: true, duplex: true, }; // eslint-disable-next-line @typescript-eslint/ban-types const getBodySize = (body) => { if (!body) { return 0; } if (body instanceof FormData) { // This is an approximation, as FormData size calculation is not straightforward let size = 0; for (const [key, value] of body) { size += usualFormBoundarySize; size += new TextEncoder().encode(`Content-Disposition: form-data; name="${key}"`).length; size += typeof value === 'string' ? new TextEncoder().encode(value).length : value.size; } return size; } if (body instanceof Blob) { return body.size; } if (body instanceof ArrayBuffer) { return body.byteLength; } if (typeof body === 'string') { return new TextEncoder().encode(body).length; } if (body instanceof URLSearchParams) { return new TextEncoder().encode(body.toString()).length; } if ('byteLength' in body) { return (body).byteLength; } if (typeof body === 'object' && body !== null) { try { const jsonString = JSON.stringify(body); return new TextEncoder().encode(jsonString).length; } catch { return 0; } } return 0; // Default case, unable to determine size }; const withProgress = (stream, totalBytes, onProgress) => { let previousChunk; let transferredBytes = 0; return stream.pipeThrough(new TransformStream({ transform(currentChunk, controller) { controller.enqueue(currentChunk); if (previousChunk) { transferredBytes += previousChunk.byteLength; let percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes; // Avoid reporting 100% progress before the stream is actually finished (in case totalBytes is inaccurate) if (percent >= 1) { // Epsilon is used here to get as close as possible to 100% without reaching it. // If we were to use 0.99 here, percent could potentially go backwards. percent = 1 - Number.EPSILON; } onProgress?.({ percent, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes }, previousChunk); } previousChunk = currentChunk; }, flush() { if (previousChunk) { transferredBytes += previousChunk.byteLength; onProgress?.({ percent: 1, totalBytes: Math.max(totalBytes, transferredBytes), transferredBytes }, previousChunk); } }, })); }; const streamResponse = (response, onDownloadProgress) => { if (!response.body) { return response; } if (response.status === 204) { return new Response(null, { status: response.status, statusText: response.statusText, headers: response.headers, }); } const totalBytes = Math.max(0, Number(response.headers.get('content-length')) || 0); return new Response(withProgress(response.body, totalBytes, onDownloadProgress), { status: response.status, statusText: response.statusText, headers: response.headers, }); }; // eslint-disable-next-line @typescript-eslint/ban-types const streamRequest = (request, onUploadProgress, originalBody) => { if (!request.body) { return request; } // Use original body for size calculation since request.body is already a stream const totalBytes = getBodySize(originalBody ?? request.body); return new Request(request, { // @ts-expect-error - Types are outdated. duplex: 'half', body: withProgress(request.body, totalBytes, onUploadProgress), }); }; // eslint-disable-next-line @typescript-eslint/ban-types const isObject$1 = (value) => value !== null && typeof value === 'object'; const validateAndMerge = (...sources) => { for (const source of sources) { if ((!isObject$1(source) || Array.isArray(source)) && source !== undefined) { throw new TypeError('The `options` argument must be an object'); } } return deepMerge({}, ...sources); }; const mergeHeaders = (source1 = {}, source2 = {}) => { const result = new globalThis.Headers(source1); const isHeadersInstance = source2 instanceof globalThis.Headers; const source = new globalThis.Headers(source2); for (const [key, value] of source.entries()) { if ((isHeadersInstance && value === 'undefined') || value === undefined) { result.delete(key); } else { result.set(key, value); } } return result; }; function newHookValue(original, incoming, property) { return (Object.hasOwn(incoming, property) && incoming[property] === undefined) ? [] : deepMerge(original[property] ?? [], incoming[property] ?? []); } const mergeHooks = (original = {}, incoming = {}) => ({ beforeRequest: newHookValue(original, incoming, 'beforeRequest'), beforeRetry: newHookValue(original, incoming, 'beforeRetry'), afterResponse: newHookValue(original, incoming, 'afterResponse'), beforeError: newHookValue(original, incoming, 'beforeError'), }); const appendSearchParameters = (target, source) => { const result = new URLSearchParams(); for (const input of [target, source]) { if (input === undefined) { continue; } if (input instanceof URLSearchParams) { for (const [key, value] of input.entries()) { result.append(key, value); } } else if (Array.isArray(input)) { for (const pair of input) { if (!Array.isArray(pair) || pair.length !== 2) { throw new TypeError('Array search parameters must be provided in [[key, value], ...] format'); } result.append(String(pair[0]), String(pair[1])); } } else if (isObject$1(input)) { for (const [key, value] of Object.entries(input)) { if (value !== undefined) { result.append(key, String(value)); } } } else { // String const parameters = new URLSearchParams(input); for (const [key, value] of parameters.entries()) { result.append(key, value); } } } return result; }; // TODO: Make this strongly-typed (no `any`). const deepMerge = (...sources) => { let returnValue = {}; let headers = {}; let hooks = {}; let searchParameters; const signals = []; for (const source of sources) { if (Array.isArray(source)) { if (!Array.isArray(returnValue)) { returnValue = []; } returnValue = [...returnValue, ...source]; } else if (isObject$1(source)) { for (let [key, value] of Object.entries(source)) { // Special handling for AbortSignal instances if (key === 'signal' && value instanceof globalThis.AbortSignal) { signals.push(value); continue; } // Special handling for context - shallow merge only if (key === 'context') { if (value !== undefined && value !== null && (!isObject$1(value) || Array.isArray(value))) { throw new TypeError('The `context` option must be an object'); } // Shallow merge: always create a new object to prevent mutation bugs returnValue = { ...returnValue, context: (value === undefined || value === null) ? {} : { ...returnValue.context, ...value }, }; continue; } // Special handling for searchParams if (key === 'searchParams') { if (value === undefined || value === null) { // Explicit undefined or null removes searchParams searchParameters = undefined; } else { // First source: keep as-is to preserve type (string/object/URLSearchParams) // Subsequent sources: merge and convert to URLSearchParams searchParameters = searchParameters === undefined ? value : appendSearchParameters(searchParameters, value); } continue; } if (isObject$1(value) && key in returnValue) { value = deepMerge(returnValue[key], value); } returnValue = { ...returnValue, [key]: value }; } if (isObject$1(source.hooks)) { hooks = mergeHooks(hooks, source.hooks); returnValue.hooks = hooks; } if (isObject$1(source.headers)) { headers = mergeHeaders(headers, source.headers); returnValue.headers = headers; } } } if (searchParameters !== undefined) { returnValue.searchParams = searchParameters; } if (signals.length > 0) { if (signals.length === 1) { returnValue.signal = signals[0]; } else if (supportsAbortSignal) { returnValue.signal = AbortSignal.any(signals); } else { // When AbortSignal.any is not available, use the last signal // This maintains the previous behavior before signal merging was added // This can be remove when the `supportsAbortSignal` check is removed.` returnValue.signal = signals.at(-1); } } if (returnValue.context === undefined) { returnValue.context = {}; } return returnValue; }; const normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input; const retryMethods = ['get', 'put', 'head', 'delete', 'options', 'trace']; const retryStatusCodes = [408, 413, 429, 500, 502, 503, 504]; const retryAfterStatusCodes = [413, 429, 503]; const defaultRetryOptions = { limit: 2, methods: retryMethods, statusCodes: retryStatusCodes, afterStatusCodes: retryAfterStatusCodes, maxRetryAfter: Number.POSITIVE_INFINITY, backoffLimit: Number.POSITIVE_INFINITY, delay: attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000, jitter: undefined, retryOnTimeout: false, }; const normalizeRetryOptions = (retry = {}) => { if (typeof retry === 'number') { return { ...defaultRetryOptions, limit: retry, }; } if (retry.methods && !Array.isArray(retry.methods)) { throw new Error('retry.methods must be an array'); } retry.methods &&= retry.methods.map(method => method.toLowerCase()); if (retry.statusCodes && !Array.isArray(retry.statusCodes)) { throw new Error('retry.statusCodes must be an array'); } const normalizedRetry = Object.fromEntries(Object.entries(retry).filter(([, value]) => value !== undefined)); return { ...defaultRetryOptions, ...normalizedRetry, }; }; class TimeoutError extends Error { request; constructor(request) { super(`Request timed out: ${request.method} ${request.url}`); this.name = 'TimeoutError'; this.request = request; } } // `Promise.race()` workaround (#91) async function timeout(request, init, abortController, options) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (abortController) { abortController.abort(); } reject(new TimeoutError(request)); }, options.timeout); void options .fetch(request, init) .then(resolve) .catch(reject) .then(() => { clearTimeout(timeoutId); }); }); } // https://github.com/sindresorhus/delay/tree/ab98ae8dfcb38e1593286c94d934e70d14a4e111 async function delay(ms, { signal }) { return new Promise((resolve, reject) => { if (signal) { signal.throwIfAborted(); signal.addEventListener('abort', abortHandler, { once: true }); } function abortHandler() { clearTimeout(timeoutId); reject(signal.reason); } const timeoutId = setTimeout(() => { signal?.removeEventListener('abort', abortHandler); resolve(); }, ms); }); } const findUnknownOptions = (request, options) => { const unknownOptions = {}; for (const key in options) { // Skip inherited properties if (!Object.hasOwn(options, key)) { continue; } // An option is passed to fetch() if: // 1. It's not a standard RequestInit option (not in requestOptionsRegistry) // 2. It's not a ky-specific option (not in kyOptionKeys) // 3. Either: // a. It's not on the Request object, OR // b. It's a vendor-specific option that should always be passed (in vendorSpecificOptions) if (!(key in requestOptionsRegistry) && !(key in kyOptionKeys) && (!(key in request) || key in vendorSpecificOptions)) { unknownOptions[key] = options[key]; } } return unknownOptions; }; const hasSearchParameters = (search) => { if (search === undefined) { return false; } // The `typeof array` still gives "object", so we need different checking for array. if (Array.isArray(search)) { return search.length > 0; } if (search instanceof URLSearchParams) { return search.size > 0; } // Record if (typeof search === 'object') { return Object.keys(search).length > 0; } if (typeof search === 'string') { return search.trim().length > 0; } return Boolean(search); }; /** Type guard to check if an error is a Ky error. @param error - The error to check @returns `true` if the error is a Ky error, `false` otherwise @example ``` import ky, {isKyError} from 'ky'; try { const response = await ky.get('/api/data'); } catch (error) { if (isKyError(error)) { // Handle Ky-specific errors console.log('Ky error occurred:', error.message); } else { // Handle other errors console.log('Unknown error:', error); } } ``` */ function isKyError(error) { return isHTTPError(error) || isTimeoutError(error) || isForceRetryError(error); } /** Type guard to check if an error is an HTTPError. @param error - The error to check @returns `true` if the error is an HTTPError, `false` otherwise @example ``` import ky, {isHTTPError} from 'ky'; try { const response = await ky.get('/api/data'); } catch (error) { if (isHTTPError(error)) { console.log('HTTP error status:', error.response.status); } } ``` */ function isHTTPError(error) { return error instanceof HTTPError || (error?.name === HTTPError.name); } /** Type guard to check if an error is a TimeoutError. @param error - The error to check @returns `true` if the error is a TimeoutError, `false` otherwise @example ``` import ky, {isTimeoutError} from 'ky'; try { const response = await ky.get('/api/data', { timeout: 1000 }); } catch (error) { if (isTimeoutError(error)) { console.log('Request timed out:', error.request.url); } } ``` */ function isTimeoutError(error) { return error instanceof TimeoutError || (error?.name === TimeoutError.name); } /** Type guard to check if an error is a ForceRetryError. @param error - The error to check @returns `true` if the error is a ForceRetryError, `false` otherwise @example ``` import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ hooks: { beforeRetry: [ ({error, retryCount}) => { if (isForceRetryError(error)) { console.log(`Forced retry #${retryCount}: ${error.code}`); } } ] } }); ``` */ function isForceRetryError(error) { return error instanceof ForceRetryError || (error?.name === ForceRetryError.name); } class Ky { static create(input, options) { const ky = new Ky(input, options); const function_ = async () => { if (typeof ky.#options.timeout === 'number' && ky.#options.timeout > maxSafeTimeout) { throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`); } // Delay the fetch so that body method shortcuts can set the Accept header await Promise.resolve(); // Before using ky.request, _fetch clones it and saves the clone for future retries to use. // If retry is not needed, close the cloned request's ReadableStream for memory safety. let response = await ky.#fetch(); for (const hook of ky.#options.hooks.afterResponse) { // Clone the response before passing to hook so we can cancel it if needed const clonedResponse = ky.#decorateResponse(response.clone()); let modifiedResponse; try { // eslint-disable-next-line no-await-in-loop modifiedResponse = await hook(ky.request, ky.#getNormalizedOptions(), clonedResponse, { retryCount: ky.#retryCount }); } catch (error) { // Cancel both responses to prevent memory leaks when hook throws ky.#cancelResponseBody(clonedResponse); ky.#cancelResponseBody(response); throw error; } if (modifiedResponse instanceof RetryMarker) { // Cancel both the cloned response passed to the hook and the current response to prevent resource leaks (especially important in Deno/Bun). // Do not await cancellation since hooks can clone the response, leaving extra tee branches that keep cancel promises pending per the Streams spec. ky.#cancelResponseBody(clonedResponse); ky.#cancelResponseBody(response); throw new ForceRetryError(modifiedResponse.options); } // Determine which response to use going forward const nextResponse = modifiedResponse instanceof globalThis.Response ? modifiedResponse : response; // Cancel any response bodies we won't use to prevent memory leaks. // Uses fire-and-forget since hooks may have cloned the response, creating tee branches that block cancellation. if (clonedResponse !== nextResponse) { ky.#cancelResponseBody(clonedResponse); } if (response !== nextResponse) { ky.#cancelResponseBody(response); } response = nextResponse; } ky.#decorateResponse(response); if (!response.ok && (typeof ky.#options.throwHttpErrors === 'function' ? ky.#options.throwHttpErrors(response.status) : ky.#options.throwHttpErrors)) { let error = new HTTPError(response, ky.request, ky.#getNormalizedOptions()); for (const hook of ky.#options.hooks.beforeError) { // eslint-disable-next-line no-await-in-loop error = await hook(error, { retryCount: ky.#retryCount }); } throw error; } // If `onDownloadProgress` is passed, it uses the stream API internally if (ky.#options.onDownloadProgress) { if (typeof ky.#options.onDownloadProgress !== 'function') { throw new TypeError('The `onDownloadProgress` option must be a function'); } if (!supportsResponseStreams) { throw new Error('Streams are not supported in your environment. `ReadableStream` is missing.'); } const progressResponse = response.clone(); ky.#cancelResponseBody(response); return streamResponse(progressResponse, ky.#options.onDownloadProgress); } return response; }; // Always wrap in #retry to catch forced retries from afterResponse hooks // Method retriability is checked in #calculateRetryDelay for non-forced retries const result = ky.#retry(function_) .finally(() => { const originalRequest = ky.#originalRequest; // Ignore cancellation errors from already-locked or already-consumed streams. ky.#cancelBody(originalRequest?.body ?? undefined); ky.#cancelBody(ky.request.body ?? undefined); }); for (const [type, mimeType] of Object.entries(responseTypes)) { // Only expose `.bytes()` when the environment implements it. if (type === 'bytes' && typeof globalThis.Response?.prototype?.bytes !== 'function') { continue; } result[type] = async () => { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing ky.request.headers.set('accept', ky.request.headers.get('accept') || mimeType); const response = await result; if (type === 'json') { if (response.status === 204) { return ''; } const text = await response.text(); if (text === '') { return ''; } if (options.parseJson) { return options.parseJson(text); } return JSON.parse(text); } return response[type](); }; } return result; } // eslint-disable-next-line unicorn/prevent-abbreviations static #normalizeSearchParams(searchParams) { // Filter out undefined values from plain objects if (searchParams && typeof searchParams === 'object' && !Array.isArray(searchParams) && !(searchParams instanceof URLSearchParams)) { return Object.fromEntries(Object.entries(searchParams).filter(([, value]) => value !== undefined)); } return searchParams; } request; #abortController; #retryCount = 0; // eslint-disable-next-line @typescript-eslint/prefer-readonly -- False positive: #input is reassigned on line 202 #input; #options; #originalRequest; #userProvidedAbortSignal; #cachedNormalizedOptions; // eslint-disable-next-line complexity constructor(input, options = {}) { this.#input = input; this.#options = { ...options, headers: mergeHeaders(this.#input.headers, options.headers), hooks: mergeHooks({ beforeRequest: [], beforeRetry: [], beforeError: [], afterResponse: [], }, options.hooks), method: normalizeRequestMethod(options.method ?? this.#input.method ?? 'GET'), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing prefixUrl: String(options.prefixUrl || ''), retry: normalizeRetryOptions(options.retry), throwHttpErrors: options.throwHttpErrors ?? true, timeout: options.timeout ?? 10_000, fetch: options.fetch ?? globalThis.fetch.bind(globalThis), context: options.context ?? {}, }; if (typeof this.#input !== 'string' && !(this.#input instanceof URL || this.#input instanceof globalThis.Request)) { throw new TypeError('`input` must be a string, URL, or Request'); } if (this.#options.prefixUrl && typeof this.#input === 'string') { if (this.#input.startsWith('/')) { throw new Error('`input` must not begin with a slash when using `prefixUrl`'); } if (!this.#options.prefixUrl.endsWith('/')) { this.#options.prefixUrl += '/'; } this.#input = this.#options.prefixUrl + this.#input; } if (supportsAbortController && supportsAbortSignal) { this.#userProvidedAbortSignal = this.#options.signal ?? this.#input.signal; this.#abortController = new globalThis.AbortController(); this.#options.signal = this.#userProvidedAbortSignal ? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal]) : this.#abortController.signal; } if (supportsRequestStreams) { // @ts-expect-error - Types are outdated. this.#options.duplex = 'half'; } if (this.#options.json !== undefined) { this.#options.body = this.#options.stringifyJson?.(this.#options.json) ?? JSON.stringify(this.#options.json); this.#options.headers.set('content-type', this.#options.headers.get('content-type') ?? 'application/json'); } // To provide correct form boundary, Content-Type header should be deleted when creating Request from another Request with FormData/URLSearchParams body // Only delete if user didn't explicitly provide a custom content-type const userProvidedContentType = options.headers && new globalThis.Headers(options.headers).has('content-type'); if (this.#input instanceof globalThis.Request && ((supportsFormData && this.#options.body instanceof globalThis.FormData) || this.#options.body instanceof URLSearchParams) && !userProvidedContentType) { this.#options.headers.delete('content-type'); } this.request = new globalThis.Request(this.#input, this.#options); if (hasSearchParameters(this.#options.searchParams)) { // eslint-disable-next-line unicorn/prevent-abbreviations const textSearchParams = typeof this.#options.searchParams === 'string' ? this.#options.searchParams.replace(/^\?/, '') : new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams)).toString(); // eslint-disable-next-line unicorn/prevent-abbreviations const searchParams = '?' + textSearchParams; const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams); // Recreate request with the updated URL. We already have all options in this.#options, including duplex. this.request = new globalThis.Request(url, this.#options); } // If `onUploadProgress` is passed, it uses the stream API internally if (this.#options.onUploadProgress) { if (typeof this.#options.onUploadProgress !== 'function') { throw new TypeError('The `onUploadProgress` option must be a function'); } if (!supportsRequestStreams) { throw new Error('Request streams are not supported in your environment. The `duplex` option for `Request` is not available.'); } this.request = this.#wrapRequestWithUploadProgress(this.request, this.#options.body ?? undefined); } } #calculateDelay() { const retryDelay = this.#options.retry.delay(this.#retryCount); let jitteredDelay = retryDelay; if (this.#options.retry.jitter === true) { jitteredDelay = Math.random() * retryDelay; } else if (typeof this.#options.retry.jitter === 'function') { jitteredDelay = this.#options.retry.jitter(retryDelay); if (!Number.isFinite(jitteredDelay) || jitteredDelay < 0) { jitteredDelay = retryDelay; } } // Handle undefined backoffLimit by treating it as no limit (Infinity) const backoffLimit = this.#options.retry.backoffLimit ?? Number.POSITIVE_INFINITY; return Math.min(backoffLimit, jitteredDelay); } async #calculateRetryDelay(error) { this.#retryCount++; if (this.#retryCount > this.#options.retry.limit) { throw error; } // Wrap non-Error throws to ensure consistent error handling const errorObject = error instanceof Error ? error : new NonError(error); // Handle forced retry from afterResponse hook - skip method check and shouldRetry if (errorObject instanceof ForceRetryError) { return errorObject.customDelay ?? this.#calculateDelay(); } // Check if method is retriable for non-forced retries if (!this.#options.retry.methods.includes(this.request.method.toLowerCase())) { throw error; } // User-provided shouldRetry function takes precedence over all other checks if (this.#options.retry.shouldRetry !== undefined) { const result = await this.#options.retry.shouldRetry({ error: errorObject, retryCount: this.#retryCount }); // Strict boolean checking - only exact true/false are handled specially if (result === false) { throw error; } if (result === true) { // Force retry - skip all other validation and return delay return this.#calculateDelay(); } // If undefined or any other value, fall through to default behavior } // Default timeout behavior if (isTimeoutError(error) && !this.#options.retry.retryOnTimeout) { throw error; } if (isHTTPError(error)) { if (!this.#options.retry.statusCodes.includes(error.response.status)) { throw error; } const retryAfter = error.response.headers.get('Retry-After') ?? error.response.headers.get('RateLimit-Reset') ?? error.response.headers.get('X-RateLimit-Retry-After') // Symfony-based services ?? error.response.headers.get('X-RateLimit-Reset') // GitHub ?? error.response.headers.get('X-Rate-Limit-Reset'); // Twitter if (retryAfter && this.#options.retry.afterStatusCodes.includes(error.response.status)) { let after = Number(retryAfter) * 1000; if (Number.isNaN(after)) { after = Date.parse(retryAfter) - Date.now(); } else if (after >= Date.parse('2024-01-01')) { // A large number is treated as a timestamp (fixed threshold protects against clock skew) after -= Date.now(); } const max = this.#options.retry.maxRetryAfter ?? after; // Don't apply jitter when server provides explicit retry timing return after < max ? after : max; } if (error.response.status === 413) { throw error; } } return this.#calculateDelay(); } #decorateResponse(response) { if (this.#options.parseJson) { response.json = async () => this.#options.parseJson(await response.text()); } return response; } #cancelBody(body) { if (!body) { return; } // Ignore cancellation failures from already-locked or already-consumed streams. void body.cancel().catch(() => undefined); } #cancelResponseBody(response) { // Ignore cancellation failures from already-locked or already-consumed streams. this.#cancelBody(response.body ?? undefined); } async #retry(function_) { try { return await function_(); } catch (error) { const ms = Math.min(await this.#calculateRetryDelay(error), maxSafeTimeout); if (this.#retryCount < 1) { throw error; } // Only use user-provided signal for delay, not our internal abortController await delay(ms, this.#userProvidedAbortSignal ? { signal: this.#userProvidedAbortSignal } : {}); // Apply custom request from forced retry before beforeRetry hooks // Ensure the custom request has the correct managed signal for timeouts and user aborts if (error instanceof ForceRetryError && error.customRequest) { const managedRequest = this.#options.signal ? new globalThis.Request(error.customRequest, { signal: this.#options.signal }) : new globalThis.Request(error.customRequest); this.#assignRequest(managedRequest); } for (const hook of this.#options.hooks.beforeRetry) { // eslint-disable-next-line no-await-in-loop const hookResult = await hook({ request: this.request, options: this.#getNormalizedOptions(), error: error, retryCount: this.#retryCount, }); if (hookResult instanceof globalThis.Request) { this.#assignRequest(hookResult); break; } // If a Response is returned, use it and skip the retry if (hookResult instanceof globalThis.Response) { return hookResult; } // If `stop` is returned from the hook, the retry process is stopped if (hookResult === stop) { return; } } return this.#retry(function_); } } async #fetch() { // Reset abortController if it was aborted (happens on timeout retry) if (this.#abortController?.signal.aborted) { this.#abortController = new globalThis.AbortController(); this.#options.signal = this.#userProvidedAbortSignal ? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal]) : this.#abortController.signal; // Recreate request with new signal this.request = new globalThis.Request(this.request, { signal: this.#options.signal }); } for (const hook of this.#options.hooks.beforeRequest) { // eslint-disable-next-line no-await-in-loop const result = await hook(this.request, this.#getNormalizedOptions(), { retryCount: this.#retryCount }); if (result instanceof Response) { return result; } if (result instanceof globalThis.Request) { this.#assignRequest(result); break; } } const nonRequestOptions = findUnknownOptions(this.request, this.#options); // Cloning is done here to prepare in advance for retries this.#originalRequest = this.request; this.request = this.#originalRequest.clone(); if (this.#options.timeout === false) { return this.#options.fetch(this.#originalRequest, nonRequestOptions); } return timeout(this.#originalRequest, nonRequestOptions, this.#abortController, this.#options); } #getNormalizedOptions() { if (!this.#cachedNormalizedOptions) { const { hooks, ...normalizedOptions } = this.#options; this.#cachedNormalizedOptions = Object.freeze(normalizedOptions); } return this.#cachedNormalizedOptions; } #assignRequest(request) { this.#cachedNormalizedOptions = undefined; this.request = this.#wrapRequestWithUploadProgress(request); } #wrapRequestWithUploadProgress(request, originalBody) { if (!this.#options.onUploadProgress || !request.body) { return request; } return streamRequest(request, this.#options.onUploadProgress, originalBody ?? this.#options.body ?? undefined); } } /*! MIT License © Sindre Sorhus */ const createInstance = (defaults) => { // eslint-disable-next-line @typescript-eslint/promise-function-async const ky = (input, options) => Ky.create(input, validateAndMerge(defaults, options)); for (const method of requestMethods) { // eslint-disable-next-line @typescript-eslint/promise-function-async ky[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method })); } ky.create = (newDefaults) => createInstance(validateAndMerge(newDefaults)); ky.extend = (newDefaults) => { if (typeof newDefaults === 'function') { newDefaults = newDefaults(defaults ?? {}); } return createInstance(validateAndMerge(defaults, newDefaults)); }; ky.stop = stop; ky.retry = retry; return ky; }; const ky$1 = createInstance(); // Intentionally not exporting this for now as it's just an implementation detail and we don't want to commit to a certain API yet at least. // export {NonError} from './errors/NonError.js'; var distribution = /*#__PURE__*/Object.freeze({ __proto__: null, ForceRetryError: ForceRetryError, HTTPError: HTTPError, TimeoutError: TimeoutError, default: ky$1, isForceRetryError: isForceRetryError, isHTTPError: isHTTPError, isKyError: isKyError, isTimeoutError: isTimeoutError }); var require$$1 = /*@__PURE__*/getAugmentedNamespace(distribution); const VERSION$1 = '0.14.2'; var constants = { VERSION: VERSION$1, USER_AGENT: `mql/${VERSION$1}`, /** * Based on require('got').defaults.options.retry.statusCodes * but without 429 (too many requests) */ RETRY_STATUS_CODES: [408, 413, 500, 502, 503, 504, 521, 522, 524] }; const ENDPOINT = { FREE: 'https://api.microlink.io/', PRO: 'https://pro.microlink.io/' }; const isObject = input => input !== null && typeof input === 'object'; const isBuffer = input => input != null && input.constructor != null && typeof input.constructor.isBuffer === 'function' && input.constructor.isBuffer(input); const parseBody = (input, error, url) => { try { return JSON.parse(input) } catch (_) { const message = input || error.message; return { status: 'error', data: { url: message }, more: 'https://microlink.io/efatalclient', code: 'EFATALCLIENT', message, url } } }; const isURL = url => { try { return /^https?:\/\//i.test(new URL(url).href) } catch (_) { return false } }; const factory$1 = streamResponseType => ({ VERSION, MicrolinkError, got, flatten }) => { const assertUrl = (url = '') => { if (!isURL(url)) { const message = `The \`url\` as \`${url}\` is not valid. Ensure it has protocol (http or https) and hostname.`; throw new MicrolinkError({ status: 'fail', data: { url: message }, more: 'https://microlink.io/einvalurlc