neue Homepage auf react basis

This commit is contained in:
2026-08-07 21:39:30 +02:00
parent 164bdd19ad
commit ee89442c04
52191 changed files with 1230242 additions and 58 deletions
+8
View File
@@ -0,0 +1,8 @@
import type { Input, Options } from '../types/options.js';
import { type ResponsePromise } from '../types/ResponsePromise.js';
export declare class Ky {
#private;
static create(input: Input, options: Options): ResponsePromise;
request: Request;
constructor(input: Input, options?: Options);
}
+826
View File
@@ -0,0 +1,826 @@
import { HTTPError } from '../errors/HTTPError.js';
import { NetworkError } from '../errors/NetworkError.js';
import { NonError } from '../errors/NonError.js';
import { ForceRetryError } from '../errors/ForceRetryError.js';
import { SchemaValidationError } from '../errors/SchemaValidationError.js';
import { TimeoutError } from '../errors/TimeoutError.js';
import { streamRequest, streamResponse } from '../utils/body.js';
import { cloneShallow, mergeHeaders, mergeHooks, deletedParametersSymbol, } from '../utils/merge.js';
import { normalizeRequestMethod, normalizeRetryOptions } from '../utils/normalize.js';
import timeout from '../utils/timeout.js';
import delay from '../utils/delay.js';
import { findUnknownOptions, hasSearchParameters } from '../utils/options.js';
import isRawNetworkError from '../utils/is-network-error.js';
import { isHTTPError, isNetworkError, isTimeoutError } from '../utils/type-guards.js';
import { maxSafeTimeout, responseTypes, stop, RetryMarker, supportsAbortController, supportsAbortSignal, supportsFormData, supportsResponseStreams, supportsRequestStreams, } from './constants.js';
const maxErrorResponseBodySize = 10 * 1024 * 1024;
const prefixUrlRenamedErrorMessage = 'The `prefixUrl` option has been renamed `prefix` in v2 and enhanced to allow slashes in input. See also the new `baseUrl` option for improved flexibility with standard URL resolution: https://github.com/sindresorhus/ky#baseurl';
const timedOutResponseData = Symbol('timedOutResponseData');
const createTextDecoder = (contentType) => {
const match = /;\s*charset\s*=\s*(?:"([^"]+)"|([^;,\s]+))/i.exec(contentType);
const charset = match?.[1] ?? match?.[2];
if (charset) {
try {
return new TextDecoder(charset);
}
catch { }
}
return new TextDecoder();
};
const invalidSchemaMessage = 'The `schema` argument must follow the Standard Schema specification';
const cloneRetryOptions = (retry) => {
if (typeof retry !== 'object') {
return retry;
}
// Clone nested arrays too so init hooks can mutate retry config without leaking state across requests.
return {
...retry,
...(retry.methods && { methods: [...retry.methods] }),
...(retry.statusCodes && { statusCodes: [...retry.statusCodes] }),
...(retry.afterStatusCodes && { afterStatusCodes: [...retry.afterStatusCodes] }),
};
};
const objectToString = Object.prototype.toString;
const isRequestInstance = (value) => value instanceof globalThis.Request || objectToString.call(value) === '[object Request]';
// Accepted custom responses are treated as full Responses throughout Ky.
// If a custom fetch returns one, it must behave like a Response for cloning,
// body consumption, `json()` decoration, and any enabled stream features.
const isResponseInstance = (value) => value instanceof globalThis.Response || objectToString.call(value) === '[object Response]';
const cloneSearchParametersForInitHook = (searchParameters) => {
if (Array.isArray(searchParameters)) {
return searchParameters.map(parameter => [...parameter]);
}
return cloneShallow(searchParameters);
};
// Shallow-clone mutable option properties so init hook mutations don't leak across requests.
function cloneInitHookOptions(options) {
const clonedOptions = {
...options,
json: cloneShallow(options.json),
context: cloneShallow(options.context),
headers: cloneShallow(options.headers),
searchParams: cloneSearchParametersForInitHook(options.searchParams),
};
if (options.retry !== undefined) {
clonedOptions.retry = cloneRetryOptions(options.retry);
}
return clonedOptions;
}
const validateJsonWithSchema = async (jsonValue, schema) => {
if ((typeof schema !== 'object'
&& typeof schema !== 'function')
|| schema === null) {
throw new TypeError(invalidSchemaMessage);
}
const standardSchema = schema['~standard'];
if (typeof standardSchema !== 'object'
|| standardSchema === null
|| typeof standardSchema.validate !== 'function') {
throw new TypeError(invalidSchemaMessage);
}
const validationResult = await standardSchema.validate(jsonValue);
if (validationResult.issues) {
throw new SchemaValidationError(validationResult.issues);
}
return validationResult.value;
};
export class Ky {
static create(input, options) {
const initHooks = options.hooks?.init ?? [];
const initHookOptions = initHooks.length > 0 ? cloneInitHookOptions(options) : options;
for (const hook of initHooks) {
hook(initHookOptions);
}
const ky = new Ky(input, initHookOptions);
const function_ = async () => {
if (typeof ky.#options.timeout === 'number' && ky.#options.timeout > maxSafeTimeout) {
throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`);
}
if (typeof ky.#options.totalTimeout === 'number' && ky.#options.totalTimeout > maxSafeTimeout) {
throw new RangeError(`The \`totalTimeout\` option cannot be greater than ${maxSafeTimeout}`);
}
// Delay the fetch so that body method shortcuts can set the Accept header
await Promise.resolve();
const beforeRequestResponse = await ky.#runBeforeRequestHooks();
let response = beforeRequestResponse ?? await ky.#retry(async () => ky.#fetch());
let responseFromHook = beforeRequestResponse !== undefined
|| ky.#consumeReturnedResponseFromBeforeRetryHook();
for (;;) {
// `undefined` means a hook stopped the flow without providing a response.
// Non-native Responses still continue through Ky if they pass `isResponseInstance()`.
if (response === undefined) {
return response;
}
if (isResponseInstance(response)) {
try {
// eslint-disable-next-line no-await-in-loop
response = await ky.#runAfterResponseHooks(response);
}
catch (error) {
if (!(error instanceof ForceRetryError)) {
throw error;
}
// eslint-disable-next-line no-await-in-loop
const retriedResponse = await ky.#retryFromError(error, async () => ky.#fetch());
if (retriedResponse === undefined) {
return retriedResponse;
}
response = retriedResponse;
responseFromHook = ky.#consumeReturnedResponseFromBeforeRetryHook();
continue;
}
}
const currentResponse = response;
// Opaque responses (`response.type === 'opaque'`) from `no-cors` requests always have `status: 0` and `ok: false`, but this is not a failure - the actual status is hidden by the browser.
if (!currentResponse.ok && currentResponse.type !== 'opaque' && (typeof ky.#options.throwHttpErrors === 'function'
? ky.#options.throwHttpErrors(currentResponse.status)
: ky.#options.throwHttpErrors)) {
// `request` must reflect the request that actually failed, but `options` stays as Ky's
// normalized options snapshot. Replacement `Request` instances do not preserve the
// original `BodyInit`, so trying to make `options` mirror arbitrary requests would be lossy.
const httpError = new HTTPError(currentResponse, ky.#getResponseRequest(currentResponse), ky.#getNormalizedOptions());
const errorToThrow = httpError;
// eslint-disable-next-line no-await-in-loop
httpError.data = await ky.#getResponseData(currentResponse);
if (responseFromHook) {
throw errorToThrow;
}
// eslint-disable-next-line no-await-in-loop
const retriedResponse = await ky.#retryFromError(httpError, async () => ky.#fetch());
if (retriedResponse === undefined) {
return retriedResponse;
}
response = retriedResponse;
responseFromHook = ky.#consumeReturnedResponseFromBeforeRetryHook();
continue;
}
break;
}
if (!isResponseInstance(response)) {
return response;
}
ky.#decorateResponse(response);
// 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;
};
const result = (async () => {
try {
return await function_();
}
catch (error) {
// Non-Error throws (e.g., thrown strings) pass through unchanged
if (!(error instanceof Error)) {
throw error;
}
// Errors thrown by beforeRetry hooks must propagate unchanged.
if (ky.#beforeRetryHookErrors.has(error)) {
throw error;
}
let processedError = error;
for (const hook of ky.#options.hooks.beforeError) {
// `request` is the current failing request. `options` intentionally remains the
// stable normalized Ky options snapshot for the same reason as `HTTPError` above.
// eslint-disable-next-line no-await-in-loop
const hookResult = await hook({
request: ky.request,
options: ky.#getNormalizedOptions(),
error: processedError,
retryCount: ky.#retryCount,
});
// Only overwrite if the hook returns a valid Error instance.
if (hookResult instanceof Error) {
processedError = hookResult;
}
}
throw processedError;
}
finally {
const originalRequest = ky.#originalRequest;
// Ignore cancellation errors from already-locked or already-consumed streams.
ky.#cancelBody(originalRequest?.body ?? undefined);
// Only cancel the current request body if it's distinct from the original (i.e. it was cloned for retries).
if (ky.request !== originalRequest) {
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 (schema) => {
// 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') {
return response[type]();
}
const text = await response.text();
if (text === '') {
if (schema !== undefined) {
return validateJsonWithSchema(undefined, schema);
}
return JSON.parse(text);
}
const jsonValue = initHookOptions.parseJson
? await initHookOptions.parseJson(text, { request: ky.#getResponseRequest(response), response })
: JSON.parse(text);
return schema === undefined ? jsonValue : validateJsonWithSchema(jsonValue, schema);
};
}
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;
#beforeRetryHookErrors = new WeakSet();
#cachedNormalizedOptions;
#startTime;
#returnedResponseFromBeforeRetryHook = false;
#responseRequests = new WeakMap();
// eslint-disable-next-line complexity
constructor(input, options = {}) {
this.#input = input;
if (Object.hasOwn(options, 'prefixUrl')) {
throw new Error(prefixUrlRenamedErrorMessage);
}
this.#options = {
...options,
headers: mergeHeaders(this.#input.headers, options.headers),
hooks: mergeHooks({}, options.hooks),
method: normalizeRequestMethod(options.method ?? this.#input.method ?? 'GET'),
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
prefix: String(options.prefix || ''),
retry: normalizeRetryOptions(options.retry),
throwHttpErrors: options.throwHttpErrors ?? true,
timeout: options.timeout ?? 10_000,
totalTimeout: options.totalTimeout ?? false,
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 (typeof this.#input === 'string') {
if (this.#options.prefix) {
const normalizedPrefix = this.#options.prefix.replace(/\/+$/, '');
const normalizedInput = this.#input.replace(/^\/+/, '');
this.#input = `${normalizedPrefix}/${normalizedInput}`;
}
if (this.#options.baseUrl) {
let absoluteInput;
try {
absoluteInput = new URL(this.#input);
}
catch { }
if (!absoluteInput) {
this.#input = new URL(this.#input, (new Request(this.#options.baseUrl)).url);
}
}
}
if (supportsAbortController && supportsAbortSignal) {
this.#userProvidedAbortSignal = this.#options.signal ?? this.#input.signal;
this.#abortController = new globalThis.AbortController();
this.#options.signal = this.#createManagedSignal();
}
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)) {
const url = new URL(this.request.url);
const deleted = this.#options.searchParams?.[deletedParametersSymbol];
if (deleted) {
// Remove keys from the input URL first so later searchParams entries can intentionally re-add them.
for (const key of deleted) {
url.searchParams.delete(key);
}
}
if (typeof this.#options.searchParams === 'string') {
const stringSearchParameters = this.#options.searchParams.replace(/^\?/, '');
if (stringSearchParameters !== '') {
url.search = url.search ? `${url.search}&${stringSearchParameters}` : `?${stringSearchParameters}`;
}
}
else {
const optionsSearchParameters = new URLSearchParams(Ky.#normalizeSearchParams(this.#options.searchParams));
for (const [key, value] of optionsSearchParameters.entries()) {
url.searchParams.append(key, value);
}
}
if (this.#options.searchParams
&& typeof this.#options.searchParams === 'object'
&& !Array.isArray(this.#options.searchParams)
&& !(this.#options.searchParams instanceof URLSearchParams)) {
for (const [key, value] of Object.entries(this.#options.searchParams)) {
if (value === undefined) {
url.searchParams.delete(key);
}
}
}
// 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 (this.#options.onUploadProgress && typeof this.#options.onUploadProgress !== 'function') {
throw new TypeError('The `onUploadProgress` option must be a function');
}
// `totalTimeout` starts when the request pipeline is created, so it also includes
// Ky's internal scheduling and user hook time before the first fetch attempt.
this.#startTime = typeof this.#options.totalTimeout === 'number' ? this.#getCurrentTime() : undefined;
}
#calculateDelay() {
const retryDelay = this.#options.retry.delay(this.#retryCount + 1);
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;
}
}
return Math.min(this.#options.retry.backoffLimit, jitteredDelay);
}
async #calculateRetryDelay(error) {
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 default checks (retryOnTimeout, status codes, etc.)
if (this.#options.retry.shouldRetry !== undefined) {
const result = await this.#options.retry.shouldRetry({ error: errorObject, retryCount: this.#retryCount + 1 });
// 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)) {
if (!this.#options.retry.retryOnTimeout) {
throw error;
}
return this.#calculateDelay();
}
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();
}
if (!Number.isFinite(after)) {
return Math.min(this.#options.retry.maxRetryAfter, this.#calculateDelay());
}
after = Math.max(0, after);
// Don't apply jitter when server provides explicit retry timing
return Math.min(this.#options.retry.maxRetryAfter, after);
}
if (error.response.status === 413) {
throw error;
}
return this.#calculateDelay();
}
// Only retry known retriable error types. Unknown errors (e.g., programming bugs) are not retried.
if (!isNetworkError(error)) {
throw error;
}
return this.#calculateDelay();
}
#decorateResponse(response) {
const request = this.#getResponseRequest(response);
if (this.#options.parseJson) {
response.json = async () => {
const text = await response.text();
if (text === '') {
return JSON.parse(text);
}
return this.#options.parseJson(text, { request, response });
};
}
return response;
}
async #getResponseData(response) {
// Even with request timeouts disabled, bound error-body reads so retries and error propagation
// cannot be stalled indefinitely by never-ending response streams.
const text = await this.#readResponseText(response, this.#getErrorDataTimeout());
if (text === timedOutResponseData) {
this.#throwIfTotalTimeoutExhausted();
return undefined;
}
if (!text) {
return undefined;
}
if (!this.#isJsonContentType(response.headers.get('content-type') ?? '')) {
return text;
}
const data = await this.#parseJson(text, response, this.#getErrorDataTimeout(), this.#getResponseRequest(response));
if (data === timedOutResponseData) {
this.#throwIfTotalTimeoutExhausted();
return undefined;
}
return data;
}
#getErrorDataTimeout() {
const errorDataTimeout = this.#options.timeout === false ? 10_000 : this.#options.timeout;
const remainingTotal = this.#getRemainingTotalTimeout();
if (remainingTotal === undefined) {
return errorDataTimeout;
}
if (remainingTotal <= 0) {
throw new TimeoutError(this.request);
}
return Math.min(errorDataTimeout, remainingTotal);
}
#isJsonContentType(contentType) {
// Match JSON subtypes like `json`, `problem+json`, and `vnd.api+json`.
const mimeType = (contentType.split(';', 1)[0] ?? '').trim().toLowerCase();
return /\/(?:.*[.+-])?json$/.test(mimeType);
}
async #readResponseText(response, timeoutMs) {
const { body } = response;
if (!body) {
try {
return await response.text();
}
catch {
return undefined;
}
}
let reader;
try {
reader = body.getReader();
}
catch {
// Another consumer already locked the stream.
return undefined;
}
const decoder = createTextDecoder(response.headers.get('content-type') ?? '');
const chunks = [];
let totalBytes = 0;
const readAll = (async () => {
try {
for (;;) {
// eslint-disable-next-line no-await-in-loop
const { done, value } = await reader.read();
if (done) {
break;
}
totalBytes += value.byteLength;
if (totalBytes > maxErrorResponseBodySize) {
void reader.cancel().catch(() => undefined);
return undefined;
}
chunks.push(decoder.decode(value, { stream: true }));
}
}
catch {
return undefined;
}
chunks.push(decoder.decode());
return chunks.join('');
})();
const timeoutPromise = new Promise(resolve => {
const timeoutId = setTimeout(() => {
resolve(timedOutResponseData);
}, timeoutMs);
void readAll.finally(() => {
clearTimeout(timeoutId);
});
});
const result = await Promise.race([readAll, timeoutPromise]);
if (result === timedOutResponseData) {
void reader.cancel().catch(() => undefined);
}
return result;
}
async #parseJson(text, response, timeoutMs, request) {
let timeoutId;
try {
return await Promise.race([
Promise.resolve().then(() => this.#options.parseJson
? this.#options.parseJson(text, { request, response })
: JSON.parse(text)),
new Promise(resolve => {
timeoutId = setTimeout(() => {
resolve(timedOutResponseData);
}, timeoutMs);
}),
]);
}
catch {
return undefined;
}
finally {
clearTimeout(timeoutId);
}
}
#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);
}
#createManagedSignal() {
return this.#userProvidedAbortSignal
? AbortSignal.any([this.#userProvidedAbortSignal, this.#abortController.signal])
: this.#abortController.signal;
}
#throwIfTotalTimeoutExhausted() {
const remaining = this.#getRemainingTotalTimeout();
if (remaining !== undefined && remaining <= 0) {
throw new TimeoutError(this.request);
}
}
async #runBeforeRequestHooks() {
for (const hook of this.#options.hooks.beforeRequest) {
// eslint-disable-next-line no-await-in-loop
const result = await hook({
request: this.request,
options: this.#getNormalizedOptions(),
retryCount: 0,
});
if (isRequestInstance(result)) {
this.#assignRequest(result);
}
else if (isResponseInstance(result)) {
return result;
}
}
return undefined;
}
async #runAfterResponseHooks(response) {
const responseRequest = this.#getResponseRequest(response);
for (const hook of this.#options.hooks.afterResponse) {
const hookResponse = this.#setResponseRequest(response.clone(), responseRequest);
this.#decorateResponse(hookResponse);
let modifiedResponse;
try {
// eslint-disable-next-line no-await-in-loop
modifiedResponse = await hook({
request: this.request,
options: this.#getNormalizedOptions(),
response: hookResponse,
retryCount: this.#retryCount,
});
}
catch (error) {
// Cancel both responses to prevent memory leaks when hook throws
if (hookResponse !== response) {
this.#cancelResponseBody(hookResponse);
}
this.#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.
if (hookResponse !== response) {
this.#cancelResponseBody(hookResponse);
}
this.#cancelResponseBody(response);
throw new ForceRetryError(modifiedResponse.options);
}
const nextResponse = isResponseInstance(modifiedResponse)
? this.#setResponseRequest(modifiedResponse, responseRequest)
: 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 the hook wrapped an existing body into a new Response, both Response objects can still point at the same stream.
if (hookResponse !== response && hookResponse !== nextResponse && hookResponse.body !== nextResponse.body) {
this.#cancelResponseBody(hookResponse);
}
if (response !== nextResponse && response.body !== nextResponse.body) {
this.#cancelResponseBody(response);
}
response = nextResponse;
}
return response;
}
async #retry(function_) {
try {
return await function_();
}
catch (error) {
return this.#retryFromError(error, function_);
}
}
async #retryFromError(error, function_) {
this.#returnedResponseFromBeforeRetryHook = false;
const retryDelay = Math.min(await this.#calculateRetryDelay(error), maxSafeTimeout);
const delayOptions = { signal: this.#userProvidedAbortSignal };
const remainingTimeout = this.#getRemainingTotalTimeout();
if (remainingTimeout !== undefined) {
if (remainingTimeout <= 0) {
throw new TimeoutError(this.request);
}
// If waiting would consume all remaining budget, time out without starting another request.
if (retryDelay >= remainingTimeout) {
await delay(remainingTimeout, delayOptions);
throw new TimeoutError(this.request);
}
}
// Only use user-provided signal for delay, not our internal abortController
await delay(retryDelay, delayOptions);
this.#throwIfTotalTimeoutExhausted();
// 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 customRequest = new globalThis.Request(error.customRequest, this.#options.signal ? { signal: this.#options.signal } : undefined);
// Replacement Requests are authoritative by design. Do not rewrite headers here,
// even for cross-origin retries. Callers using `ky.retry({request})` explicitly
// opted into the exact Request they constructed.
this.#assignRequest(customRequest);
}
for (const hook of this.#options.hooks.beforeRetry) {
let hookResult;
try {
// eslint-disable-next-line no-await-in-loop
hookResult = await hook({
request: this.request,
options: this.#getNormalizedOptions(),
error: error,
retryCount: this.#retryCount + 1,
});
}
catch (hookError) {
// Preserve the original request error path (`throw error`) so beforeError hooks can still run.
if (hookError instanceof Error && hookError !== error) {
this.#beforeRetryHookErrors.add(hookError);
}
throw hookError;
}
if (isRequestInstance(hookResult)) {
// Same contract as `ky.retry({request})`: a Request returned from `beforeRetry`
// is used as-is rather than being sanitized or otherwise rewritten by Ky.
this.#assignRequest(hookResult);
break;
}
if (isResponseInstance(hookResult)) {
this.#returnedResponseFromBeforeRetryHook = true;
this.#retryCount++;
return hookResult;
}
// If `stop` is returned from the hook, the retry process is stopped
if (hookResult === stop) {
return;
}
}
this.#throwIfTotalTimeoutExhausted();
this.#retryCount++;
return this.#retry(function_);
}
#consumeReturnedResponseFromBeforeRetryHook() {
const value = this.#returnedResponseFromBeforeRetryHook;
this.#returnedResponseFromBeforeRetryHook = false;
return value;
}
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.#createManagedSignal();
// Recreate request with new signal
this.request = new globalThis.Request(this.request, { signal: this.#options.signal });
}
const nonRequestOptions = findUnknownOptions(this.#options);
const retryRequest = this.#options.retry.limit > 0 ? this.request.clone() : undefined;
const request = this.#wrapRequestWithUploadProgress(this.request, this.#options.body ?? undefined);
// Cloning is done here to prepare in advance for retries.
// Skip cloning when retries are disabled - cloning a streaming body calls ReadableStream#tee()
// which buffers the entire stream in memory, causing excessive memory usage for large uploads.
this.#originalRequest = request;
if (retryRequest) {
this.request = retryRequest;
}
try {
const remainingTotal = this.#getRemainingTotalTimeout();
if (remainingTotal !== undefined && remainingTotal <= 0) {
throw new TimeoutError(this.request);
}
const effectiveTimeout = this.#options.timeout === false
? remainingTotal
: (remainingTotal === undefined
? this.#options.timeout
: Math.min(this.#options.timeout, remainingTotal));
const response = effectiveTimeout === undefined
? await this.#options.fetch(request, nonRequestOptions)
: await timeout(request, nonRequestOptions, this.#abortController, {
timeout: effectiveTimeout,
fetch: this.#options.fetch,
});
return this.#setResponseRequest(response, request);
}
catch (error) {
if (isRawNetworkError(error)) {
throw new NetworkError(this.request, { cause: error });
}
throw error;
}
}
#getRemainingTotalTimeout() {
if (this.#startTime === undefined) {
return undefined;
}
const elapsed = this.#getCurrentTime() - this.#startTime;
return Math.max(0, this.#options.totalTimeout - elapsed);
}
#getCurrentTime() {
return globalThis.performance?.now() ?? Date.now();
}
#getNormalizedOptions() {
if (!this.#cachedNormalizedOptions) {
// Exclude Ky-specific options that are not part of `RequestInit`.
const { hooks, json, parseJson, stringifyJson, searchParams, timeout, totalTimeout, throwHttpErrors, fetch, ...normalizedOptions } = this.#options;
this.#cachedNormalizedOptions = Object.freeze(normalizedOptions);
}
return this.#cachedNormalizedOptions;
}
#assignRequest(request) {
this.#cachedNormalizedOptions = undefined;
this.request = request;
}
#getResponseRequest(response) {
return this.#responseRequests.get(response) ?? this.request;
}
#setResponseRequest(response, request) {
this.#responseRequests.set(response, request);
return response;
}
#wrapRequestWithUploadProgress(request, originalBody) {
if (!this.#options.onUploadProgress || !request.body || !supportsRequestStreams) {
return request;
}
return streamRequest(request, this.#options.onUploadProgress, originalBody ?? this.#options.body ?? undefined);
}
}
//# sourceMappingURL=Ky.js.map
File diff suppressed because one or more lines are too long
+207
View File
@@ -0,0 +1,207 @@
import { type KyOptionsRegistry } from '../types/options.js';
export declare const supportsRequestStreams: boolean;
export declare const supportsAbortController: boolean;
export declare const supportsAbortSignal: boolean;
export declare const supportsResponseStreams: boolean;
export declare const supportsFormData: boolean;
export declare const requestMethods: readonly ["get", "post", "put", "patch", "head", "delete"];
export declare const responseTypes: {
readonly json: "application/json";
readonly text: "text/*";
readonly formData: "multipart/form-data";
readonly arrayBuffer: "*/*";
readonly blob: "*/*";
readonly bytes: "*/*";
};
export declare const maxSafeTimeout = 2147483647;
export declare const usualFormBoundarySize = 40;
/**
Symbol that can be returned by a `beforeRetry` hook to stop retrying without throwing an error.
*/
export declare const stop: unique symbol;
/**
Options for forcing a retry via `ky.retry()`.
*/
export type ForceRetryOptions = {
/**
Custom delay in milliseconds before retrying.
If not provided, uses the default retry delay calculation based on `retry.delay` configuration.
**Note:** Custom delays bypass jitter and `backoffLimit`. This is intentional, as custom delays often come from server responses (e.g., `Retry-After` headers) and should be respected exactly as specified.
*/
delay?: number;
/**
Error code for the retry.
This machine-readable identifier will be included in the error message passed to `beforeRetry` hooks, allowing you to distinguish between different types of forced retries.
@example
```
return ky.retry({code: 'RATE_LIMIT'});
// Resulting error message: 'Forced retry: RATE_LIMIT'
```
*/
code?: string;
/**
Original error that caused the retry.
This allows you to preserve the error chain when forcing a retry based on caught exceptions. The error will be set as the `cause` of the `ForceRetryError`, enabling proper error chain traversal.
@example
```
try {
const data = await response.json();
validateBusinessLogic(data);
} catch (error) {
return ky.retry({
code: 'VALIDATION_FAILED',
cause: error // Preserves original error in chain
});
}
```
*/
cause?: Error;
/**
Custom request to use for the retry.
This allows you to modify or completely replace the request during a forced retry. The custom request becomes the starting point for the retry - `beforeRetry` hooks can still further modify it if needed.
**Note:** The custom request's `signal` will be replaced with Ky's managed signal to handle timeouts and user-provided abort signals correctly. If the original request body has been consumed, you must provide a new body or clone the request before consuming.
**Warning:** Custom retry requests are not sanitized. If you reuse headers across origins, remove any credentials you do not want forwarded.
@example
```
// Fallback to a different endpoint
return ky.retry({
request: new Request('https://backup-api.com/endpoint', {
method: request.method,
headers: request.headers,
}),
code: 'BACKUP_ENDPOINT'
});
// Retry with refreshed authentication token
const data = await response.json();
return ky.retry({
request: new Request(request, {
headers: {
...Object.fromEntries(request.headers),
'Authorization': `Bearer ${data.newToken}`
}
}),
code: 'TOKEN_REFRESHED'
});
```
*/
request?: Request;
};
/**
Marker returned by `ky.retry()` to signal a forced retry from `afterResponse` hooks.
*/
export declare class RetryMarker {
options: ForceRetryOptions | undefined;
constructor(options?: ForceRetryOptions);
}
/**
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, response}) => {
// Retry based on response body content
if (response.status === 200) {
const data = await response.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');
```
*/
export declare const retry: (options?: ForceRetryOptions) => RetryMarker;
export declare const kyOptionKeys: KyOptionsRegistry;
export declare const requestOptionsRegistry: {
readonly method: true;
readonly headers: true;
readonly body: true;
readonly mode: true;
readonly credentials: true;
readonly cache: true;
readonly redirect: true;
readonly referrer: true;
readonly referrerPolicy: true;
readonly integrity: true;
readonly keepalive: true;
readonly signal: true;
readonly window: true;
readonly duplex: true;
};
+184
View File
@@ -0,0 +1,184 @@
export 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;
})();
export const supportsAbortController = typeof globalThis.AbortController === 'function';
export const supportsAbortSignal = typeof globalThis.AbortSignal === 'function' && typeof globalThis.AbortSignal.any === 'function';
export const supportsResponseStreams = typeof globalThis.ReadableStream === 'function';
export const supportsFormData = typeof globalThis.FormData === 'function';
export const requestMethods = ['get', 'post', 'put', 'patch', 'head', 'delete'];
const validate = () => undefined;
validate();
export 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)
export const maxSafeTimeout = 2_147_483_647;
// Size in bytes of a typical form boundary (e.g., '------WebKitFormBoundaryaxpyiPgbbPti10Rw'), used to help estimate upload size
export const usualFormBoundarySize = 40;
/**
Symbol that can be returned by a `beforeRetry` hook to stop retrying without throwing an error.
*/
export const stop = Symbol('stop');
/**
Marker returned by `ky.retry()` to signal a forced retry from `afterResponse` hooks.
*/
export 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, response}) => {
// Retry based on response body content
if (response.status === 200) {
const data = await response.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');
```
*/
export const retry = (options) => new RetryMarker(options);
export const kyOptionKeys = {
json: true,
parseJson: true,
stringifyJson: true,
searchParams: true,
baseUrl: true,
prefix: true,
retry: true,
timeout: true,
totalTimeout: true,
hooks: true,
throwHttpErrors: true,
onDownloadProgress: true,
onUploadProgress: true,
fetch: true,
context: true,
};
// 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().
export 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,
};
//# sourceMappingURL=constants.js.map
File diff suppressed because one or more lines are too long
+14
View File
@@ -0,0 +1,14 @@
import type { ForceRetryOptions } from '../core/constants.js';
import { KyError } from './KyError.js';
/**
Error used to signal a forced retry from `afterResponse` hooks.
This is thrown when `ky.retry()` is returned from an `afterResponse` hook. It is observable in `beforeRetry` and `beforeError` hooks via the `isForceRetryError()` type guard.
*/
export declare class ForceRetryError extends KyError {
name: "ForceRetryError";
customDelay: number | undefined;
code: string | undefined;
customRequest: Request | undefined;
constructor(options?: ForceRetryOptions);
}
+25
View File
@@ -0,0 +1,25 @@
import { KyError } from './KyError.js';
import { NonError } from './NonError.js';
/**
Error used to signal a forced retry from `afterResponse` hooks.
This is thrown when `ky.retry()` is returned from an `afterResponse` hook. It is observable in `beforeRetry` and `beforeError` hooks via the `isForceRetryError()` type guard.
*/
export class ForceRetryError extends KyError {
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;
}
}
//# sourceMappingURL=ForceRetryError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ForceRetryError.js","sourceRoot":"","sources":["../../source/errors/ForceRetryError.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AACrC,OAAO,EAAC,QAAQ,EAAC,MAAM,eAAe,CAAC;AAEvC;;;;EAIE;AACF,MAAM,OAAO,eAAgB,SAAQ,OAAO;IAClC,IAAI,GAAG,iBAA0B,CAAC;IAC3C,WAAW,CAAqB;IAChC,IAAI,CAAqB;IACzB,aAAa,CAAsB;IAEnC,YAAY,OAA2B;QACtC,wDAAwD;QACxD,wEAAwE;QACxE,MAAM,KAAK,GAAG,OAAO,EAAE,KAAK;YAC3B,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YAChF,CAAC,CAAC,SAAS,CAAC;QAEb,KAAK,CACJ,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,iBAAiB,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,cAAc,EAChE,KAAK,CAAC,CAAC,CAAC,EAAC,KAAK,EAAC,CAAC,CAAC,CAAC,SAAS,CAC3B,CAAC;QAEF,IAAI,CAAC,WAAW,GAAG,OAAO,EAAE,KAAK,CAAC;QAClC,IAAI,CAAC,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC;QAC1B,IAAI,CAAC,aAAa,GAAG,OAAO,EAAE,OAAO,CAAC;IACvC,CAAC;CACD","sourcesContent":["import type {ForceRetryOptions} from '../core/constants.js';\nimport {KyError} from './KyError.js';\nimport {NonError} from './NonError.js';\n\n/**\nError used to signal a forced retry from `afterResponse` hooks.\n\nThis is thrown when `ky.retry()` is returned from an `afterResponse` hook. It is observable in `beforeRetry` and `beforeError` hooks via the `isForceRetryError()` type guard.\n*/\nexport class ForceRetryError extends KyError {\n\toverride name = 'ForceRetryError' as const;\n\tcustomDelay: number | undefined;\n\tcode: string | undefined;\n\tcustomRequest: Request | undefined;\n\n\tconstructor(options?: ForceRetryOptions) {\n\t\t// Runtime protection: wrap non-Error causes in NonError\n\t\t// TypeScript type is Error for guidance, but JS users can pass anything\n\t\tconst cause = options?.cause\n\t\t\t? (options.cause instanceof Error ? options.cause : new NonError(options.cause))\n\t\t\t: undefined;\n\n\t\tsuper(\n\t\t\toptions?.code ? `Forced retry: ${options.code}` : 'Forced retry',\n\t\t\tcause ? {cause} : undefined,\n\t\t);\n\n\t\tthis.customDelay = options?.delay;\n\t\tthis.code = options?.code;\n\t\tthis.customRequest = options?.request;\n\t}\n}\n"]}
+21
View File
@@ -0,0 +1,21 @@
import type { NormalizedOptions } from '../types/options.js';
import type { KyRequest } from '../types/request.js';
import type { KyResponse } from '../types/response.js';
import { KyError } from './KyError.js';
/**
Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled.
The error has a `response` property with the `Response` object, a `request` property with the `Request` object, an `options` property with the normalized options (either passed to `ky` when creating an instance with `ky.create()` or directly when performing the request), and a `data` property with the pre-parsed response body. For JSON responses (based on `Content-Type`), the body is parsed using the `parseJson` option if set, or `JSON.parse` by default. For other content types, it is set as plain text. If the body is empty or parsing fails, `data` will be `undefined`. To avoid hanging or excessive buffering, `error.data` population is bounded by the request timeout and a 10 MiB response body size limit. The `data` property is populated before `beforeError` hooks run, so hooks can access it.
The response body is automatically consumed when populating `error.data`, so `error.response.json()` and other body methods will not work. Use `error.data` instead. The `error.response` object is still available for headers, status, etc.
Be aware that some types of errors, such as network errors, inherently mean that a response was not received. In that case, the error will be an instance of `NetworkError` instead of `HTTPError` and will not contain a `response` property.
*/
export declare class HTTPError<T = unknown> extends KyError {
name: "HTTPError";
response: KyResponse<T>;
request: KyRequest;
options: NormalizedOptions;
data: T | string | undefined;
constructor(response: Response, request: Request, options: NormalizedOptions);
}
+28
View File
@@ -0,0 +1,28 @@
import { KyError } from './KyError.js';
/**
Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled.
The error has a `response` property with the `Response` object, a `request` property with the `Request` object, an `options` property with the normalized options (either passed to `ky` when creating an instance with `ky.create()` or directly when performing the request), and a `data` property with the pre-parsed response body. For JSON responses (based on `Content-Type`), the body is parsed using the `parseJson` option if set, or `JSON.parse` by default. For other content types, it is set as plain text. If the body is empty or parsing fails, `data` will be `undefined`. To avoid hanging or excessive buffering, `error.data` population is bounded by the request timeout and a 10 MiB response body size limit. The `data` property is populated before `beforeError` hooks run, so hooks can access it.
The response body is automatically consumed when populating `error.data`, so `error.response.json()` and other body methods will not work. Use `error.data` instead. The `error.response` object is still available for headers, status, etc.
Be aware that some types of errors, such as network errors, inherently mean that a response was not received. In that case, the error will be an instance of `NetworkError` instead of `HTTPError` and will not contain a `response` property.
*/
export class HTTPError extends KyError {
name = 'HTTPError';
response;
request;
options;
data;
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.response = response;
this.request = request;
this.options = options;
}
}
//# sourceMappingURL=HTTPError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"HTTPError.js","sourceRoot":"","sources":["../../source/errors/HTTPError.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAErC;;;;;;;;EAQE;AACF,MAAM,OAAO,SAAuB,SAAQ,OAAO;IACzC,IAAI,GAAG,WAAoB,CAAC;IACrC,QAAQ,CAAgB;IACxB,OAAO,CAAY;IACnB,OAAO,CAAoB;IAC3B,IAAI,CAAyB;IAE7B,YAAY,QAAkB,EAAE,OAAgB,EAAE,OAA0B;QAC3E,MAAM,IAAI,GAAG,CAAC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/E,MAAM,KAAK,GAAG,QAAQ,CAAC,UAAU,IAAI,EAAE,CAAC;QACxC,MAAM,MAAM,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC;QACzC,MAAM,MAAM,GAAG,MAAM,CAAC,CAAC,CAAC,eAAe,MAAM,EAAE,CAAC,CAAC,CAAC,kBAAkB,CAAC;QAErE,KAAK,CAAC,uBAAuB,MAAM,KAAK,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAEzE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD","sourcesContent":["import type {NormalizedOptions} from '../types/options.js';\nimport type {KyRequest} from '../types/request.js';\nimport type {KyResponse} from '../types/response.js';\nimport {KyError} from './KyError.js';\n\n/**\nError thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled.\n\nThe error has a `response` property with the `Response` object, a `request` property with the `Request` object, an `options` property with the normalized options (either passed to `ky` when creating an instance with `ky.create()` or directly when performing the request), and a `data` property with the pre-parsed response body. For JSON responses (based on `Content-Type`), the body is parsed using the `parseJson` option if set, or `JSON.parse` by default. For other content types, it is set as plain text. If the body is empty or parsing fails, `data` will be `undefined`. To avoid hanging or excessive buffering, `error.data` population is bounded by the request timeout and a 10 MiB response body size limit. The `data` property is populated before `beforeError` hooks run, so hooks can access it.\n\nThe response body is automatically consumed when populating `error.data`, so `error.response.json()` and other body methods will not work. Use `error.data` instead. The `error.response` object is still available for headers, status, etc.\n\nBe aware that some types of errors, such as network errors, inherently mean that a response was not received. In that case, the error will be an instance of `NetworkError` instead of `HTTPError` and will not contain a `response` property.\n*/\nexport class HTTPError<T = unknown> extends KyError {\n\toverride name = 'HTTPError' as const;\n\tresponse: KyResponse<T>;\n\trequest: KyRequest;\n\toptions: NormalizedOptions;\n\tdata: T | string | undefined;\n\n\tconstructor(response: Response, request: Request, options: NormalizedOptions) {\n\t\tconst code = (response.status || response.status === 0) ? response.status : '';\n\t\tconst title = response.statusText ?? '';\n\t\tconst status = `${code} ${title}`.trim();\n\t\tconst reason = status ? `status code ${status}` : 'an unknown error';\n\n\t\tsuper(`Request failed with ${reason}: ${request.method} ${request.url}`);\n\n\t\tthis.response = response;\n\t\tthis.request = request;\n\t\tthis.options = options;\n\t}\n}\n"]}
+11
View File
@@ -0,0 +1,11 @@
/**
Base class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, and `ForceRetryError` extend this class.
You can use `instanceof KyError` to check if an error originated from Ky, or use the `isKyError()` type guard for cross-realm compatibility and TypeScript type narrowing.
Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.
*/
export declare class KyError extends Error {
name: string;
get isKyError(): true;
}
+14
View File
@@ -0,0 +1,14 @@
/**
Base class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, and `ForceRetryError` extend this class.
You can use `instanceof KyError` to check if an error originated from Ky, or use the `isKyError()` type guard for cross-realm compatibility and TypeScript type narrowing.
Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.
*/
export class KyError extends Error {
name = 'KyError';
get isKyError() {
return true;
}
}
//# sourceMappingURL=KyError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"KyError.js","sourceRoot":"","sources":["../../source/errors/KyError.ts"],"names":[],"mappings":"AAAA;;;;;;EAME;AACF,MAAM,OAAO,OAAQ,SAAQ,KAAK;IACxB,IAAI,GAAG,SAAS,CAAC;IAE1B,IAAI,SAAS;QACZ,OAAO,IAAI,CAAC;IACb,CAAC;CACD","sourcesContent":["/**\nBase class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, and `ForceRetryError` extend this class.\n\nYou can use `instanceof KyError` to check if an error originated from Ky, or use the `isKyError()` type guard for cross-realm compatibility and TypeScript type narrowing.\n\nNote: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.\n*/\nexport class KyError extends Error {\n\toverride name = 'KyError';\n\n\tget isKyError(): true {\n\t\treturn true;\n\t}\n}\n"]}
+16
View File
@@ -0,0 +1,16 @@
import type { KyRequest } from '../types/request.js';
import { KyError } from './KyError.js';
/**
Error thrown when a network error occurs during the request (e.g., DNS failure, connection refused, offline). It has a `request` property with the `Request` object. The original error is available via the standard `cause` property.
Network errors are automatically retried (for retriable methods).
Note: Network errors are detected using runtime-specific heuristics. Unrecognized runtimes may produce errors that are not wrapped in `NetworkError`. Use the `shouldRetry` option to handle such cases.
*/
export declare class NetworkError extends KyError {
name: "NetworkError";
request: KyRequest;
constructor(request: Request, options?: {
cause?: Error;
});
}
+17
View File
@@ -0,0 +1,17 @@
import { KyError } from './KyError.js';
/**
Error thrown when a network error occurs during the request (e.g., DNS failure, connection refused, offline). It has a `request` property with the `Request` object. The original error is available via the standard `cause` property.
Network errors are automatically retried (for retriable methods).
Note: Network errors are detected using runtime-specific heuristics. Unrecognized runtimes may produce errors that are not wrapped in `NetworkError`. Use the `shouldRetry` option to handle such cases.
*/
export class NetworkError extends KyError {
name = 'NetworkError';
request;
constructor(request, options) {
super(`Request failed due to a network error: ${request.method} ${request.url}`, options);
this.request = request;
}
}
//# sourceMappingURL=NetworkError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"NetworkError.js","sourceRoot":"","sources":["../../source/errors/NetworkError.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAErC;;;;;;EAME;AACF,MAAM,OAAO,YAAa,SAAQ,OAAO;IAC/B,IAAI,GAAG,cAAuB,CAAC;IACxC,OAAO,CAAY;IAEnB,YAAY,OAAgB,EAAE,OAAyB;QACtD,KAAK,CAAC,0CAA0C,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;QAC1F,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD","sourcesContent":["import type {KyRequest} from '../types/request.js';\nimport {KyError} from './KyError.js';\n\n/**\nError thrown when a network error occurs during the request (e.g., DNS failure, connection refused, offline). It has a `request` property with the `Request` object. The original error is available via the standard `cause` property.\n\nNetwork errors are automatically retried (for retriable methods).\n\nNote: Network errors are detected using runtime-specific heuristics. Unrecognized runtimes may produce errors that are not wrapped in `NetworkError`. Use the `shouldRetry` option to handle such cases.\n*/\nexport class NetworkError extends KyError {\n\toverride name = 'NetworkError' as const;\n\trequest: KyRequest;\n\n\tconstructor(request: Request, options?: {cause?: Error}) {\n\t\tsuper(`Request failed due to a network error: ${request.method} ${request.url}`, options);\n\t\tthis.request = request;\n\t}\n}\n"]}
+10
View File
@@ -0,0 +1,10 @@
/**
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.
*/
export declare class NonError extends Error {
name: string;
readonly value: unknown;
constructor(value: unknown);
}
+27
View File
@@ -0,0 +1,27 @@
/**
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.
*/
export 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;
}
}
//# sourceMappingURL=NonError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"NonError.js","sourceRoot":"","sources":["../../source/errors/NonError.ts"],"names":[],"mappings":"AAAA;;;;EAIE;AACF,MAAM,OAAO,QAAS,SAAQ,KAAK;IACzB,IAAI,GAAG,UAAU,CAAC;IAClB,KAAK,CAAU;IAExB,YAAY,KAAc;QACzB,IAAI,OAAO,GAAG,4BAA4B,CAAC;QAE3C,4DAA4D;QAC5D,IAAI,CAAC;YACJ,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC/B,OAAO,GAAG,KAAK,CAAC;YACjB,CAAC;iBAAM,IAAI,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,SAAS,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC1G,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;YACzB,CAAC;QACF,CAAC;QAAC,MAAM,CAAC;YACR,qDAAqD;QACtD,CAAC;QAED,KAAK,CAAC,OAAO,CAAC,CAAC;QAEf,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACpB,CAAC;CACD","sourcesContent":["/**\nWrapper for non-Error values that were thrown.\n\nIn JavaScript, any value can be thrown (not just Error instances). This class wraps such values to ensure consistent error handling.\n*/\nexport class NonError extends Error {\n\toverride name = 'NonError';\n\treadonly value: unknown;\n\n\tconstructor(value: unknown) {\n\t\tlet message = 'Non-error value was thrown';\n\n\t\t// Intentionally minimal as this error is just an edge-case.\n\t\ttry {\n\t\t\tif (typeof value === 'string') {\n\t\t\t\tmessage = value;\n\t\t\t} else if (value && typeof value === 'object' && 'message' in value && typeof value.message === 'string') {\n\t\t\t\tmessage = value.message;\n\t\t\t}\n\t\t} catch {\n\t\t\t// Use default message if accessing properties throws\n\t\t}\n\n\t\tsuper(message);\n\n\t\tthis.value = value;\n\t}\n}\n"]}
+28
View File
@@ -0,0 +1,28 @@
import type { StandardSchemaV1Issue } from '../types/standard-schema.js';
/**
The error thrown when [Standard Schema](https://github.com/standard-schema/standard-schema) validation fails in `.json(schema)`. It has an `issues` property with the validation issues from the schema.
This error intentionally does not extend `KyError` because it does not represent a failure in Ky's HTTP lifecycle. The request succeeded; the user's schema rejected the data. As such, it is not matched by `isKyError()`.
@example
```
import ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';
const userSchema = z.object({name: z.string()});
try {
const user = await ky('/api/user').json(userSchema);
console.log(user.name);
} catch (error) {
if (error instanceof SchemaValidationError) {
console.error(error.issues);
}
}
```
*/
export declare class SchemaValidationError extends Error {
name: "SchemaValidationError";
readonly issues: readonly StandardSchemaV1Issue[];
constructor(issues: readonly StandardSchemaV1Issue[]);
}
+31
View File
@@ -0,0 +1,31 @@
/**
The error thrown when [Standard Schema](https://github.com/standard-schema/standard-schema) validation fails in `.json(schema)`. It has an `issues` property with the validation issues from the schema.
This error intentionally does not extend `KyError` because it does not represent a failure in Ky's HTTP lifecycle. The request succeeded; the user's schema rejected the data. As such, it is not matched by `isKyError()`.
@example
```
import ky, {SchemaValidationError} from 'ky';
import {z} from 'zod';
const userSchema = z.object({name: z.string()});
try {
const user = await ky('/api/user').json(userSchema);
console.log(user.name);
} catch (error) {
if (error instanceof SchemaValidationError) {
console.error(error.issues);
}
}
```
*/
export class SchemaValidationError extends Error {
name = 'SchemaValidationError';
issues;
constructor(issues) {
super('Response schema validation failed');
this.issues = issues;
}
}
//# sourceMappingURL=SchemaValidationError.js.map
@@ -0,0 +1 @@
{"version":3,"file":"SchemaValidationError.js","sourceRoot":"","sources":["../../source/errors/SchemaValidationError.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;EAqBE;AACF,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IACtC,IAAI,GAAG,uBAAgC,CAAC;IACxC,MAAM,CAAmC;IAElD,YAAY,MAAwC;QACnD,KAAK,CAAC,mCAAmC,CAAC,CAAC;QAC3C,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD","sourcesContent":["import type {StandardSchemaV1Issue} from '../types/standard-schema.js';\n\n/**\nThe error thrown when [Standard Schema](https://github.com/standard-schema/standard-schema) validation fails in `.json(schema)`. It has an `issues` property with the validation issues from the schema.\n\nThis error intentionally does not extend `KyError` because it does not represent a failure in Ky's HTTP lifecycle. The request succeeded; the user's schema rejected the data. As such, it is not matched by `isKyError()`.\n\n@example\n```\nimport ky, {SchemaValidationError} from 'ky';\nimport {z} from 'zod';\n\nconst userSchema = z.object({name: z.string()});\n\ntry {\n\tconst user = await ky('/api/user').json(userSchema);\n\tconsole.log(user.name);\n} catch (error) {\n\tif (error instanceof SchemaValidationError) {\n\t\tconsole.error(error.issues);\n\t}\n}\n```\n*/\nexport class SchemaValidationError extends Error {\n\toverride name = 'SchemaValidationError' as const;\n\treadonly issues: readonly StandardSchemaV1Issue[];\n\n\tconstructor(issues: readonly StandardSchemaV1Issue[]) {\n\t\tsuper('Response schema validation failed');\n\t\tthis.issues = issues;\n\t}\n}\n"]}
+10
View File
@@ -0,0 +1,10 @@
import type { KyRequest } from '../types/request.js';
import { KyError } from './KyError.js';
/**
Error thrown when the request times out. It has a `request` property with the `Request` object.
*/
export declare class TimeoutError extends KyError {
name: "TimeoutError";
request: KyRequest;
constructor(request: Request);
}
+13
View File
@@ -0,0 +1,13 @@
import { KyError } from './KyError.js';
/**
Error thrown when the request times out. It has a `request` property with the `Request` object.
*/
export class TimeoutError extends KyError {
name = 'TimeoutError';
request;
constructor(request) {
super(`Request timed out: ${request.method} ${request.url}`);
this.request = request;
}
}
//# sourceMappingURL=TimeoutError.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"TimeoutError.js","sourceRoot":"","sources":["../../source/errors/TimeoutError.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,OAAO,EAAC,MAAM,cAAc,CAAC;AAErC;;EAEE;AACF,MAAM,OAAO,YAAa,SAAQ,OAAO;IAC/B,IAAI,GAAG,cAAuB,CAAC;IACxC,OAAO,CAAY;IAEnB,YAAY,OAAgB;QAC3B,KAAK,CAAC,sBAAsB,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QAC7D,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IACxB,CAAC;CACD","sourcesContent":["import type {KyRequest} from '../types/request.js';\nimport {KyError} from './KyError.js';\n\n/**\nError thrown when the request times out. It has a `request` property with the `Request` object.\n*/\nexport class TimeoutError extends KyError {\n\toverride name = 'TimeoutError' as const;\n\trequest: KyRequest;\n\n\tconstructor(request: Request) {\n\t\tsuper(`Request timed out: ${request.method} ${request.url}`);\n\t\tthis.request = request;\n\t}\n}\n"]}
+19
View File
@@ -0,0 +1,19 @@
/*! MIT License © Sindre Sorhus */
import type { KyInstance } from './types/ky.js';
declare const ky: KyInstance;
export default ky;
export type { KyInstance } from './types/ky.js';
export type { Input, Options, NormalizedOptions, RetryOptions, ShouldRetryState, SearchParamsOption, Progress, } from './types/options.js';
export type { Hooks, InitHook, BeforeRequestHook, BeforeRequestState, BeforeRetryHook, BeforeRetryState, BeforeErrorHook, BeforeErrorState, AfterResponseHook, AfterResponseState, } from './types/hooks.js';
export type { ResponsePromise } from './types/ResponsePromise.js';
export type { StandardSchemaV1, StandardSchemaV1InferOutput, StandardSchemaV1Issue, } from './types/standard-schema.js';
export type { KyRequest } from './types/request.js';
export type { KyResponse } from './types/response.js';
export { KyError } from './errors/KyError.js';
export { HTTPError } from './errors/HTTPError.js';
export { SchemaValidationError } from './errors/SchemaValidationError.js';
export { NetworkError } from './errors/NetworkError.js';
export { TimeoutError } from './errors/TimeoutError.js';
export { ForceRetryError } from './errors/ForceRetryError.js';
export { isKyError, isHTTPError, isNetworkError, isTimeoutError, isForceRetryError, } from './utils/type-guards.js';
export { replaceOption } from './utils/merge.js';
+35
View File
@@ -0,0 +1,35 @@
/*! MIT License © Sindre Sorhus */
import { Ky } from './core/Ky.js';
import { requestMethods, stop, retry } from './core/constants.js';
import { validateAndMerge } from './utils/merge.js';
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 = createInstance();
export default ky;
export { KyError } from './errors/KyError.js';
export { HTTPError } from './errors/HTTPError.js';
export { SchemaValidationError } from './errors/SchemaValidationError.js';
export { NetworkError } from './errors/NetworkError.js';
export { TimeoutError } from './errors/TimeoutError.js';
export { ForceRetryError } from './errors/ForceRetryError.js';
export { isKyError, isHTTPError, isNetworkError, isTimeoutError, isForceRetryError, } from './utils/type-guards.js';
export { replaceOption } from './utils/merge.js';
// 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';
//# sourceMappingURL=index.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../source/index.ts"],"names":[],"mappings":"AAAA,kCAAkC;AAElC,OAAO,EAAC,EAAE,EAAC,MAAM,cAAc,CAAC;AAChC,OAAO,EAAC,cAAc,EAAE,IAAI,EAAE,KAAK,EAAC,MAAM,qBAAqB,CAAC;AAGhE,OAAO,EAAC,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAGlD,MAAM,cAAc,GAAG,CAAC,QAA2B,EAAc,EAAE;IAClE,qEAAqE;IACrE,MAAM,EAAE,GAAiC,CAAC,KAAY,EAAE,OAAiB,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,gBAAgB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,CAAC;IAEpI,KAAK,MAAM,MAAM,IAAI,cAAc,EAAE,CAAC;QACrC,qEAAqE;QACrE,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,KAAY,EAAE,OAAiB,EAAE,EAAE,CAAC,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAC,MAAM,EAAC,CAAC,CAAC,CAAC;IACnH,CAAC;IAED,EAAE,CAAC,MAAM,GAAG,CAAC,WAA8B,EAAE,EAAE,CAAC,cAAc,CAAC,gBAAgB,CAAC,WAAW,CAAC,CAAC,CAAC;IAC9F,EAAE,CAAC,MAAM,GAAG,CAAC,WAAyF,EAAE,EAAE;QACzG,IAAI,OAAO,WAAW,KAAK,UAAU,EAAE,CAAC;YACvC,WAAW,GAAG,WAAW,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC;QAC3C,CAAC;QAED,OAAO,cAAc,CAAC,gBAAgB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC;IAChE,CAAC,CAAC;IAEF,EAAE,CAAC,IAAI,GAAG,IAAI,CAAC;IACf,EAAE,CAAC,KAAK,GAAG,KAAK,CAAC;IAEjB,OAAO,EAAgB,CAAC;AACzB,CAAC,CAAC;AAEF,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;AAE5B,eAAe,EAAE,CAAC;AAmClB,OAAO,EAAC,OAAO,EAAC,MAAM,qBAAqB,CAAC;AAC5C,OAAO,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAChD,OAAO,EAAC,qBAAqB,EAAC,MAAM,mCAAmC,CAAC;AACxE,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAC,YAAY,EAAC,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAC,eAAe,EAAC,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EACN,SAAS,EACT,WAAW,EACX,cAAc,EACd,cAAc,EACd,iBAAiB,GACjB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAC,aAAa,EAAC,MAAM,kBAAkB,CAAC;AAE/C,4IAA4I;AAC5I,iDAAiD","sourcesContent":["/*! MIT License © Sindre Sorhus */\n\nimport {Ky} from './core/Ky.js';\nimport {requestMethods, stop, retry} from './core/constants.js';\nimport type {KyInstance} from './types/ky.js';\nimport type {Input, Options} from './types/options.js';\nimport {validateAndMerge} from './utils/merge.js';\nimport {type Mutable} from './utils/types.js';\n\nconst createInstance = (defaults?: Partial<Options>): KyInstance => {\n\t// eslint-disable-next-line @typescript-eslint/promise-function-async\n\tconst ky: Partial<Mutable<KyInstance>> = (input: Input, options?: Options) => Ky.create(input, validateAndMerge(defaults, options));\n\n\tfor (const method of requestMethods) {\n\t\t// eslint-disable-next-line @typescript-eslint/promise-function-async\n\t\tky[method] = (input: Input, options?: Options) => Ky.create(input, validateAndMerge(defaults, options, {method}));\n\t}\n\n\tky.create = (newDefaults?: Partial<Options>) => createInstance(validateAndMerge(newDefaults));\n\tky.extend = (newDefaults?: Partial<Options> | ((parentDefaults: Partial<Options>) => Partial<Options>)) => {\n\t\tif (typeof newDefaults === 'function') {\n\t\t\tnewDefaults = newDefaults(defaults ?? {});\n\t\t}\n\n\t\treturn createInstance(validateAndMerge(defaults, newDefaults));\n\t};\n\n\tky.stop = stop;\n\tky.retry = retry;\n\n\treturn ky as KyInstance;\n};\n\nconst ky = createInstance();\n\nexport default ky;\n\nexport type {KyInstance} from './types/ky.js';\n\nexport type {\n\tInput,\n\tOptions,\n\tNormalizedOptions,\n\tRetryOptions,\n\tShouldRetryState,\n\tSearchParamsOption,\n\tProgress,\n} from './types/options.js';\n\nexport type {\n\tHooks,\n\tInitHook,\n\tBeforeRequestHook,\n\tBeforeRequestState,\n\tBeforeRetryHook,\n\tBeforeRetryState,\n\tBeforeErrorHook,\n\tBeforeErrorState,\n\tAfterResponseHook,\n\tAfterResponseState,\n} from './types/hooks.js';\n\nexport type {ResponsePromise} from './types/ResponsePromise.js';\nexport type {\n\tStandardSchemaV1,\n\tStandardSchemaV1InferOutput,\n\tStandardSchemaV1Issue,\n} from './types/standard-schema.js';\nexport type {KyRequest} from './types/request.js';\nexport type {KyResponse} from './types/response.js';\nexport {KyError} from './errors/KyError.js';\nexport {HTTPError} from './errors/HTTPError.js';\nexport {SchemaValidationError} from './errors/SchemaValidationError.js';\nexport {NetworkError} from './errors/NetworkError.js';\nexport {TimeoutError} from './errors/TimeoutError.js';\nexport {ForceRetryError} from './errors/ForceRetryError.js';\nexport {\n\tisKyError,\n\tisHTTPError,\n\tisNetworkError,\n\tisTimeoutError,\n\tisForceRetryError,\n} from './utils/type-guards.js';\nexport {replaceOption} from './utils/merge.js';\n\n// 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.\n// export {NonError} from './errors/NonError.js';\n"]}
+60
View File
@@ -0,0 +1,60 @@
/**
Returns a `Response` object with `Body` methods added for convenience. So you can, for example, call `ky.get(input).json()` directly without having to await the `Response` first. When called like that, an appropriate `Accept` header will be set depending on the body method used. Unlike the `Body` methods of `window.fetch`, these will throw an `HTTPError` if the response status is not in the range of `200...299`. Also, `.json()` throws if the body is empty or the response status is `204`.
*/
import { type KyResponse } from './response.js';
import type { StandardSchemaV1, StandardSchemaV1InferOutput } from './standard-schema.js';
export type ResponsePromise<T = unknown> = {
arrayBuffer: () => Promise<ArrayBuffer>;
blob: () => Promise<Blob>;
formData: () => Promise<FormData>;
/**
Get the response body as raw bytes.
Note: This shortcut is only available when the runtime supports `Response.prototype.bytes()`.
*/
bytes: () => Promise<Uint8Array>;
json: {
/**
Get the response body as JSON.
@example
```
import ky from 'ky';
const json = await ky(…).json();
```
@example
```
import ky from 'ky';
interface Result {
value: number;
}
const result1 = await ky(…).json<Result>();
// or
const result2 = await ky<Result>(…).json();
```
*/
<JsonType = T>(): Promise<JsonType>;
/**
Get the response body as JSON and validate it with a Standard Schema.
Use a Standard Schema compatible validator (for example, Zod 3.24+).
Throws a `SchemaValidationError` when validation fails.
@example
```
import ky from 'ky';
import {z} from 'zod';
const userSchema = z.object({name: z.string()});
const user = await ky('/api/user').json(userSchema);
```
*/
<Schema extends StandardSchemaV1>(schema: Schema): Promise<StandardSchemaV1InferOutput<Schema>>;
};
text: () => Promise<string>;
} & Promise<KyResponse<T>>;
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ResponsePromise.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"ResponsePromise.js","sourceRoot":"","sources":["../../source/types/ResponsePromise.ts"],"names":[],"mappings":"","sourcesContent":["/**\nReturns a `Response` object with `Body` methods added for convenience. So you can, for example, call `ky.get(input).json()` directly without having to await the `Response` first. When called like that, an appropriate `Accept` header will be set depending on the body method used. Unlike the `Body` methods of `window.fetch`, these will throw an `HTTPError` if the response status is not in the range of `200...299`. Also, `.json()` throws if the body is empty or the response status is `204`.\n*/\nimport {type KyResponse} from './response.js';\nimport type {StandardSchemaV1, StandardSchemaV1InferOutput} from './standard-schema.js';\n\nexport type ResponsePromise<T = unknown> = {\n\tarrayBuffer: () => Promise<ArrayBuffer>;\n\n\tblob: () => Promise<Blob>;\n\n\tformData: () => Promise<FormData>;\n\n\t/**\n\tGet the response body as raw bytes.\n\n\tNote: This shortcut is only available when the runtime supports `Response.prototype.bytes()`.\n\t*/\n\tbytes: () => Promise<Uint8Array>;\n\n\t// TODO: Use `json<T extends JSONValue>(): Promise<T>;` when it's fixed in TS.\n\t// See https://github.com/microsoft/TypeScript/issues/15300 and https://github.com/sindresorhus/ky/pull/80\n\tjson: {\n\t\t/**\n\t\tGet the response body as JSON.\n\n\t\t@example\n\t\t```\n\t\timport ky from 'ky';\n\n\t\tconst json = await ky(…).json();\n\t\t```\n\n\t\t@example\n\t\t```\n\t\timport ky from 'ky';\n\n\t\tinterface Result {\n\t\t\tvalue: number;\n\t\t}\n\n\t\tconst result1 = await ky(…).json<Result>();\n\t\t// or\n\t\tconst result2 = await ky<Result>(…).json();\n\t\t```\n\t\t*/\n\t\t<JsonType = T>(): Promise<JsonType>;\n\n\t\t/**\n\t\tGet the response body as JSON and validate it with a Standard Schema.\n\t\tUse a Standard Schema compatible validator (for example, Zod 3.24+).\n\n\t\tThrows a `SchemaValidationError` when validation fails.\n\n\t\t@example\n\t\t```\n\t\timport ky from 'ky';\n\t\timport {z} from 'zod';\n\n\t\tconst userSchema = z.object({name: z.string()});\n\n\t\tconst user = await ky('/api/user').json(userSchema);\n\t\t```\n\t\t*/\n\t\t<Schema extends StandardSchemaV1>(schema: Schema): Promise<StandardSchemaV1InferOutput<Schema>>;\n\t};\n\n\ttext: () => Promise<string>;\n} & Promise<KyResponse<T>>;\n"]}
+7
View File
@@ -0,0 +1,7 @@
export type Primitive = null | undefined | string | number | boolean | symbol | bigint;
export type Required<T, K extends keyof T = keyof T> = T & {
[P in K]-?: T[P];
};
export type LiteralUnion<LiteralType extends BaseType, BaseType extends Primitive> = LiteralType | (BaseType & {
_?: never;
});
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=common.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"common.js","sourceRoot":"","sources":["../../source/types/common.ts"],"names":[],"mappings":"","sourcesContent":["// eslint-disable-next-line @typescript-eslint/no-restricted-types\nexport type Primitive = null | undefined | string | number | boolean | symbol | bigint;\n\nexport type Required<T, K extends keyof T = keyof T> = T & {[P in K]-?: T[P]};\n\nexport type LiteralUnion<LiteralType extends BaseType, BaseType extends Primitive> =\n\t| LiteralType\n\t| (BaseType & {_?: never});\n"]}
+344
View File
@@ -0,0 +1,344 @@
import { type stop, type RetryMarker } from '../core/constants.js';
import type { KyRequest, KyResponse } from '../index.js';
import type { NormalizedOptions, Options } from './options.js';
/**
This hook enables you to modify the options before they are used to construct the request. The hook function receives the mutable options object and can modify it in place. You could, for example, modify `searchParams`, `headers`, or `json` here.
Unlike other hooks, `init` hooks are synchronous. Any error thrown will propagate synchronously and will not be caught by `beforeError` hooks.
@example
```
import ky from 'ky';
const api = ky.extend({
hooks: {
init: [
options => {
options.searchParams = {apiKey: getApiKey()};
},
],
},
});
const response = await api.get('https://example.com/api/users');
// URL: https://example.com/api/users?apiKey=123
```
*/
export type InitHook = (options: Options) => void;
export type BeforeRequestState = {
request: KyRequest;
options: NormalizedOptions;
/**
The number of retries attempted. Always `0`, since `beforeRequest` hooks run once before retry handling begins.
*/
retryCount: 0;
};
export type BeforeRequestHook = (state: BeforeRequestState) => Request | Response | void | Promise<Request | Response | void>;
export type BeforeRetryState = {
request: KyRequest;
options: NormalizedOptions;
error: Error;
/**
The number of retries attempted. Always `>= 1`, since this hook is only called during retries, not on the initial request.
*/
retryCount: number;
};
export type BeforeRetryHook = (state: BeforeRetryState) => Request | Response | typeof stop | void | Promise<Request | Response | typeof stop | void>;
export type BeforeErrorState = {
request: KyRequest;
options: NormalizedOptions;
error: Error;
/**
The number of retries attempted. `0` for the initial request, increments with each retry.
This allows you to distinguish between the initial request and retries, which is useful when you need different error handling based on retry attempts (e.g., showing different error messages on the final attempt).
*/
retryCount: number;
};
export type BeforeErrorHook = (state: BeforeErrorState) => Error | Promise<Error>;
export type AfterResponseState = {
request: KyRequest;
options: NormalizedOptions;
response: KyResponse;
/**
The number of retries attempted. `0` for the initial request, increments with each retry.
This allows you to distinguish between the initial request and retries, which is useful when you need different behavior for retries (e.g., showing a notification only on the final retry).
*/
retryCount: number;
};
export type AfterResponseHook = (state: AfterResponseState) => Response | RetryMarker | void | Promise<Response | RetryMarker | void>;
export type Hooks = {
/**
This hook enables you to modify the options before they are used to construct the request. The hook function receives the mutable options object and can modify it in place. You could, for example, modify `searchParams`, `headers`, or `json` here.
Unlike other hooks, `init` hooks are synchronous. Any error thrown will propagate synchronously and will not be caught by `beforeError` hooks.
A common use case is to add a search parameter to every request:
@example
```
import ky from 'ky';
const api = ky.extend({
hooks: {
init: [
options => {
options.searchParams = {apiKey: getApiKey()};
},
],
},
});
const response = await api.get('https://example.com/api/users');
// URL: https://example.com/api/users?apiKey=123
```
@default []
*/
init?: InitHook[];
/**
This hook enables you to modify the request right before it is sent. Ky will make no further changes to the request after this. The hook function receives a state object with the normalized request, options, and retry count. You could, for example, modify `request.headers` here.
The `retryCount` is always `0`, since `beforeRequest` hooks run once before retry handling begins.
The hook can return a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to replace the outgoing request (remaining hooks will still run with the updated request). It can also return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to completely avoid making an HTTP request, in which case remaining `beforeRequest` hooks are skipped. This can be used to mock a request, check an internal cache, etc.
Any error thrown by `beforeRequest` hooks is treated as fatal and will not trigger Ky's retry logic.
@example
```
import ky from 'ky';
const api = ky.extend({
hooks: {
beforeRequest: [
({request}) => {
request.headers.set('Authorization', 'token initial-token');
}
]
}
});
const response = await api.get('https://example.com/api/users');
```
**Modifying the request URL:**
@example
```
import ky from 'ky';
const api = ky.extend({
hooks: {
beforeRequest: [
({request}) => {
const url = new URL(request.url);
url.searchParams.set('token', 'secret-token');
return new Request(url, request);
}
]
}
});
const response = await api.get('https://example.com/api/users');
```
@default []
*/
beforeRequest?: BeforeRequestHook[];
/**
This hook enables you to modify the request right before retry. Ky will make no further changes to the request after this. The hook function receives a state object with the normalized request, options, an error instance, and retry count. You could, for example, modify `request.headers` here.
The hook can return a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to replace the outgoing retry request, or return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to skip the retry and use that response instead. **Note:** Returning a request or response skips remaining `beforeRetry` hooks.
**Warning:** Returned `Request` objects are used as-is. If you point one at another origin, remove any credentials you do not want forwarded.
The `retryCount` is always `>= 1`, since this hook is only called during retries, not on the initial request.
If the request received a response, the error will be of type `HTTPError`. The `Response` object will be available at `error.response`, and the pre-parsed response body will be available at `error.data`. Be aware that some types of errors, such as network errors, inherently mean that a response was not received. In that case, the error will be an instance of `NetworkError` instead of `HTTPError`.
You can prevent Ky from retrying the request by throwing an error. Ky will not handle it in any way and the error will be propagated to the request initiator. The rest of the `beforeRetry` hooks will not be called in this case. Alternatively, you can return the [`ky.stop`](#kystop) symbol to do the same thing but without propagating an error (this has some limitations, see `ky.stop` docs for details).
**Modifying headers:**
@example
```
import ky from 'ky';
const response = await ky('https://example.com', {
hooks: {
beforeRetry: [
async ({request, options, error, retryCount}) => {
const token = await ky('https://example.com/refresh-token');
request.headers.set('Authorization', `token ${token}`);
}
]
}
});
```
**Modifying the request URL:**
@example
```
import ky, {isHTTPError} from 'ky';
const response = await ky('https://example.com/api', {
hooks: {
beforeRetry: [
({request, error}) => {
// Add query parameters based on error response
if (
isHTTPError(error)
&& typeof error.data === 'object'
&& error.data !== null
&& 'processId' in error.data
) {
const url = new URL(request.url);
url.searchParams.set('processId', String(error.data.processId));
return new Request(url, request);
}
}
]
}
});
```
**Returning a cached response:**
@example
```
import ky from 'ky';
const response = await ky('https://example.com/api', {
hooks: {
beforeRetry: [
({error, retryCount}) => {
// Use cached response instead of retrying
if (retryCount > 1 && cachedResponse) {
return cachedResponse;
}
}
]
}
});
```
@default []
*/
beforeRetry?: BeforeRetryHook[];
/**
This hook enables you to modify any error right before it is thrown. The hook function receives a state object with the current request, the normalized Ky options, the error, and retry count, and should return an `Error` instance.
This hook is called for all error types, including `HTTPError`, `NetworkError`, `TimeoutError`, and `ForceRetryError` (when retry limit is exceeded via `ky.retry()`). Use type guards like `isHTTPError()`, `isNetworkError()`, or `isTimeoutError()` to handle specific error types.
The `retryCount` is `0` for the initial request and increments with each retry. This allows you to distinguish between the initial request and retries, which is useful when you need different error handling based on retry attempts (e.g., showing different error messages on the final attempt).
If a `beforeRequest` or `beforeRetry` hook returns a new `Request`, inspect `request` for the final request state. `options` remains Ky's normalized options and may not mirror every property of a replacement `Request`.
@default []
@example
```
import ky, {isHTTPError} from 'ky';
await ky('https://example.com', {
hooks: {
beforeError: [
({request, options, error}) => {
if (isHTTPError(error)) {
if (
typeof error.data === 'object'
&& error.data !== null
&& 'message' in error.data
) {
error.name = 'GitHubError';
error.message = `${String(error.data.message)} (${error.response.status})`;
}
}
// `request` and `options` are always available
console.log(`Request to ${request.url} failed`, options.context);
return error;
}
]
}
});
```
*/
beforeError?: BeforeErrorHook[];
/**
This hook enables you to read and optionally modify the response. The hook function receives a state object with the normalized request, options, a clone of the response, and retry count. The return value of the hook function will be used by Ky as the response object if it's an instance of [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response).
You can also force a retry by returning [`ky.retry(options)`](#kyretryoptions). This is useful when you need to retry based on the response body content, even if the response has a successful status code. The retry will respect the `retry.limit` option and be observable in `beforeRetry` hooks.
**Warning:** `ky.retry({request})` uses the replacement request as-is. If it targets another origin, remove any credentials you do not want forwarded.
Any non-`ky.retry()` error thrown by `afterResponse` hooks is treated as fatal and will not trigger Ky's retry logic.
The `retryCount` is `0` for the initial request and increments with each retry. This allows you to distinguish between the initial request and retries, which is useful when you need different behavior for retries (e.g., showing a notification only on the final retry).
@default []
@example
```
import ky from 'ky';
const response = await ky('https://example.com', {
hooks: {
afterResponse: [
({response}) => {
// You could do something with the response, for example, logging.
log(response);
// Or return a `Response` instance to overwrite the response.
return new Response('A different response', {status: 200});
},
// Or retry with a fresh token on a 401 error
async ({request, response, retryCount}) => {
if (response.status === 401 && retryCount === 0) {
// Only refresh on first 401, not on subsequent retries
const {token} = await ky.post('https://example.com/auth/refresh').json();
const headers = new Headers(request.headers);
headers.set('Authorization', `Bearer ${token}`);
return ky.retry({
request: new Request(request, {headers}),
code: 'TOKEN_REFRESHED'
});
}
},
// Or force retry based on response body content
async ({response}) => {
if (response.status === 200) {
const data = await response.json();
if (data.error?.code === 'RATE_LIMIT') {
// Retry with custom delay from API response
return ky.retry({
delay: data.error.retryAfter * 1000,
code: 'RATE_LIMIT'
});
}
}
},
// Or show a notification only on the last retry for 5xx errors
({options, response, retryCount}) => {
if (response.status >= 500 && response.status <= 599) {
if (retryCount === options.retry.limit) {
showNotification('Request failed after all retries');
}
}
}
]
}
});
```
*/
afterResponse?: AfterResponseHook[];
};
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=hooks.js.map
File diff suppressed because one or more lines are too long
+183
View File
@@ -0,0 +1,183 @@
import { type stop, type retry } from '../core/constants.js';
import type { Input, Options } from './options.js';
import type { ResponsePromise } from './ResponsePromise.js';
export type KyInstance = {
/**
Fetch the given `url`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` method added.
@example
```
import ky from 'ky';
const json = await ky('https://example.com', {json: {foo: true}}).json();
console.log(json);
//=> `{data: '🦄'}`
```
*/
<T>(url: Input, options?: Options): ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'get'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
get: <T>(url: Input, options?: Options) => ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'post'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
post: <T>(url: Input, options?: Options) => ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'put'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
put: <T>(url: Input, options?: Options) => ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'delete'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
delete: <T>(url: Input, options?: Options) => ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'patch'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
patch: <T>(url: Input, options?: Options) => ResponsePromise<T>;
/**
Fetch the given `url` using the option `{method: 'head'}`.
@param url - `Request` object, `URL` object, or URL string.
@returns A promise with `Body` methods added.
*/
head: (url: Input, options?: Options) => ResponsePromise;
/**
Create a new Ky instance with complete new defaults, without inheriting from any parent instance.
@returns A new Ky instance.
*/
create: (defaultOptions?: Options) => KyInstance;
/**
Create a new Ky instance with some defaults overridden with your own.
In contrast to `ky.create()`, `ky.extend()` inherits defaults from its parent.
You can pass headers as a `Headers` instance or a plain object.
You can remove a header with `.extend()` by passing the header with an `undefined` value. Passing `undefined` as a string removes the header only if it comes from a `Headers` instance.
Similarly, you can remove existing `hooks` entries by extending the hook with an explicit `undefined`.
By default, `.extend()` deep-merges options: hooks are appended, headers are merged, and search parameters are accumulated. Use `replaceOption` when you want to fully replace a merged property instead.
You can also refer to parent defaults by providing a function to `.extend()`.
@example
```
import ky from 'ky';
const api = ky.create({prefix: 'https://example.com/api'});
const usersApi = api.extend((options) => ({prefix: `${options.prefix}/users`}));
const response = await usersApi.get('123');
//=> 'https://example.com/api/users/123'
const response = await api.get('version');
//=> 'https://example.com/api/version'
```
@returns A new Ky instance.
*/
extend: (defaultOptions: Options | ((parentOptions: Options) => Options)) => KyInstance;
/**
A `Symbol` that can be returned by a `beforeRetry` hook to stop the retry. This will also short circuit the remaining `beforeRetry` hooks.
Note: Returning this symbol makes Ky abort and return with an `undefined` response. Be sure to check for a response before accessing any properties on it or use [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining). It is also incompatible with body methods, such as `.json()` or `.text()`, because there is no response to parse. In general, we recommend throwing an error instead of returning this symbol, as that will cause Ky to abort and then throw, which avoids these limitations.
A valid use-case for `ky.stop` is to prevent retries when making requests for side effects, where the returned data is not important. For example, logging client activity to the server.
@example
```
import ky from 'ky';
const options = {
hooks: {
beforeRetry: [
async ({request, options, error, retryCount}) => {
const shouldStopRetry = await ky('https://example.com/api');
if (shouldStopRetry) {
return ky.stop;
}
}
]
}
};
// Note that response will be `undefined` in case `ky.stop` is returned.
const response = await ky.post('https://example.com', options);
// Using `.text()` or other body methods is not supported.
const text = await ky('https://example.com', options).text();
```
*/
readonly stop: typeof stop;
/**
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`.
@example
```
import ky, {isForceRetryError} from 'ky';
const api = ky.extend({
hooks: {
afterResponse: [
async ({response}) => {
// Retry based on response body content
if (response.status === 200) {
const data = await response.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'
});
}
}
}
],
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');
```
*/
readonly retry: typeof retry;
};
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=ky.js.map
File diff suppressed because one or more lines are too long
+439
View File
@@ -0,0 +1,439 @@
import type { LiteralUnion, Required } from './common.js';
import type { Hooks } from './hooks.js';
import type { RetryOptions } from './retry.js';
export type SearchParamsInit = string | string[][] | Record<string, string> | URLSearchParams | undefined;
export type SearchParamsOption = SearchParamsInit | Record<string, string | number | boolean | undefined> | Array<Array<string | number | boolean>>;
export type RequestHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete';
export type HttpMethod = LiteralUnion<RequestHttpMethod | 'options' | 'trace', string>;
export type Input = string | URL | Request;
export type Progress = {
/**
A number between `0` and `1` representing the progress percentage.
*/
percent: number;
/**
The number of bytes transferred so far.
*/
transferredBytes: number;
/**
The total number of bytes to be transferred. This is an estimate and may be `0` if the total size cannot be determined.
*/
totalBytes: number;
};
export type KyHeadersInit = NonNullable<RequestInit['headers']> | Record<string, string | undefined>;
/**
Custom Ky options
*/
export type KyOptions = {
/**
Shortcut for sending JSON. Use this instead of the `body` option.
Accepts any plain object or value, which will be stringified using `JSON.stringify()` and sent in the body with the correct header set.
*/
json?: unknown;
/**
User-defined JSON-parsing function.
The function receives the response text as the first argument and a context object as the second argument containing the `request` and `response`.
Use-cases:
1. Parse JSON via the [`bourne` package](https://github.com/hapijs/bourne) to protect from prototype pollution.
2. Parse JSON with [`reviver` option of `JSON.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse).
3. Log or handle JSON parse errors with request context.
@default JSON.parse()
@example
```
import ky from 'ky';
import bourne from '@hapijs/bourne';
const json = await ky('https://example.com', {
parseJson: text => bourne(text)
}).json();
```
@example
```
import ky from 'ky';
const json = await ky('https://example.com', {
parseJson: (text, {request, response}) => {
console.log(`Parsing JSON from ${request.url} (status: ${response.status})`);
return JSON.parse(text);
}
}).json();
```
*/
parseJson?: (text: string, context: {
request: Request;
response: Response;
}) => unknown;
/**
User-defined JSON-stringifying function.
Use-cases:
1. Stringify JSON with a custom `replacer` function.
@default JSON.stringify()
@example
```
import ky from 'ky';
import {DateTime} from 'luxon';
const json = await ky('https://example.com', {
stringifyJson: data => JSON.stringify(data, (key, value) => {
if (key.endsWith('_at')) {
return DateTime.fromISO(value).toSeconds();
}
return value;
})
}).json();
```
*/
stringifyJson?: (data: unknown) => string;
/**
Search parameters to include in the request URL. Setting this will merge with any existing search parameters in the input URL.
Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams).
When passing an object, setting a value to `undefined` deletes the parameter, while `null` values are preserved and converted to the string `'null'`.
*/
searchParams?: SearchParamsOption;
/**
A base URL to [resolve](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references) the `input` against. When the `input` (after applying the `prefix` option) is only a relative URL, such as `'users'`, `'/users'`, or `'//my-site.com'`, it will be resolved against the `baseUrl` to determine the destination of the request. Otherwise, the `input` is absolute, such as `'https://my-site.com'`, and it will bypass the `baseUrl`.
Useful when used with [`ky.extend()`](#kyextenddefaultoptions) to create niche-specific Ky instances.
If the `baseUrl` itself is relative, it will be resolved against the environment's base URL, such as [`document.baseURI`](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) in browsers or `location.href` in Deno (see the `--location` flag).
**Tip:** When setting a `baseUrl` that has a path, we recommend that it include a trailing slash `/`, as in `'/api/'` rather than `/api`. This ensures more intuitive behavior for page-relative `input` URLs, such as `'users'` or `'./users'`, where they will _extend_ from the full path of the `baseUrl` rather than _replacing_ its last path segment.
@example
```
import ky from 'ky';
// On https://example.com
const response = await ky('users', {baseUrl: '/api/'});
//=> 'https://example.com/api/users'
const response = await ky('/users', {baseUrl: '/api/'});
//=> 'https://example.com/users'
```
*/
baseUrl?: URL | string;
/**
A prefix to prepend to the `input` before making the request (and before it is resolved against the `baseUrl`). It can be any valid path or URL, either relative or absolute. A trailing slash `/` is optional and will be added automatically, if needed, when it is joined with `input`. Only takes effect when `input` is a string.
Useful when used with [`ky.extend()`](#kyextenddefaultoptions) to create niche-specific Ky instances.
*In most cases, you should use the `baseUrl` option instead, as it is more consistent with web standards. However, `prefix` is useful if you want origin-relative `input` URLs, such as `/users`, to be treated as if they were page-relative. In other words, the leading slash of the `input` will essentially be ignored, because the `prefix` will become part of the `input` before URL resolution happens.*
Notes:
- The `prefix` and `input` are joined with a slash `/`, and slashes are normalized at the join boundary by trimming trailing slashes from `prefix` and leading slashes from `input`.
- After `prefix` and `input` are joined, the result is resolved against the `baseUrl` option, if present.
@example
```
import ky from 'ky';
// On https://example.com
const response = await ky('users', {prefix: '/api/'});
//=> 'https://example.com/api/users'
const response = await ky('/users', {prefix: '/api/'});
//=> 'https://example.com/api/users'
```
*/
prefix?: URL | string;
/**
Controls retry behavior. Each field is documented in the `RetryOptions` type.
If `retry` is a number, it will be used as `limit` and other defaults will remain in place.
Network errors (e.g., DNS failures, connection refused, offline) are automatically retried for retriable methods. Only errors recognized as network errors are retried; other errors (e.g., programming bugs) are thrown immediately. Use `shouldRetry` to customize this behavior.
If the response provides an HTTP status contained in `afterStatusCodes`, Ky will wait until the date, timeout, or timestamp given in the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header has passed to retry the request. If `Retry-After` is missing, the non-standard [`RateLimit-Reset`](https://www.ietf.org/archive/id/draft-polli-ratelimit-headers-05.html#section-3.3) header is used in its place as a fallback. If the provided status code is not in the list, the [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header will be ignored.
If [`Retry-After`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) header is greater than `maxRetryAfter`, it will use `maxRetryAfter`.
@example
```
import ky from 'ky';
const json = await ky('https://example.com', {
retry: {
limit: 10,
methods: ['get'],
statusCodes: [413]
}
}).json();
```
*/
retry?: RetryOptions | number;
/**
Per-attempt timeout in milliseconds for getting a response, applied independently to each retry. Cannot be greater than 2147483647. See also `totalTimeout`.
If set to `false`, there will be no per-attempt timeout.
@default 10000
*/
timeout?: number | false;
/**
Overall timeout in milliseconds for the entire operation, including retries and delays. Throws a `TimeoutError` if exceeded. Cannot be greater than 2147483647.
If set to `false` or not specified, there is no overall timeout.
@default false
@example
```
import ky from 'ky';
// Each attempt gets 5s, but the whole operation must complete within 30s
const json = await ky('https://example.com', {
timeout: 5000,
totalTimeout: 30_000,
retry: {
limit: 3,
retryOnTimeout: true,
}
}).json();
```
*/
totalTimeout?: number | false;
/**
Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially, unless otherwise noted.
*/
hooks?: Hooks;
/**
Throw an `HTTPError` when, after following redirects, the response has a non-2xx status code. To also throw for redirects instead of following them, set the [`redirect`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters) option to `'manual'`.
Setting this to `false` may be useful if you are checking for resource availability and are expecting error responses.
You can also pass a function that accepts the HTTP status code and returns a boolean for selective error handling. Note that this can violate the principle of least surprise, so it's recommended to use the boolean form unless you have a specific use case like treating 404 responses differently.
Note: If `false`, error responses are considered successful and the request will not be retried.
Note: [Opaque responses](https://developer.mozilla.org/en-US/docs/Web/API/Response/type) from `no-cors` requests are returned as-is (without throwing `HTTPError`), since the actual status is hidden by the browser.
@default true
*/
throwHttpErrors?: boolean | ((status: number) => boolean);
/**
Download progress event handler.
@param progress - Object containing download progress information.
@param chunk - Data that was received. Note: It's empty for the first call.
@example
```
import ky from 'ky';
const response = await ky('https://example.com', {
onDownloadProgress: (progress, chunk) => {
// Example output:
// `0% - 0 of 1271 bytes`
// `100% - 1271 of 1271 bytes`
console.log(`${progress.percent * 100}% - ${progress.transferredBytes} of ${progress.totalBytes} bytes`);
}
});
```
*/
onDownloadProgress?: (progress: Progress, chunk: Uint8Array) => void;
/**
Upload progress event handler.
Note: Requires [request stream support](https://caniuse.com/wf-fetch-request-streams) and HTTP/2 for HTTPS connections (in Chromium-based browsers). In unsupported environments, this handler is silently ignored.
@param progress - Object containing upload progress information.
@param chunk - Data that was sent. Note: It's empty for the last call.
@example
```
import ky from 'ky';
const response = await ky.post('https://example.com/upload', {
body: largeFile,
onUploadProgress: (progress, chunk) => {
// Example output:
// `0% - 0 of 1271 bytes`
// `100% - 1271 of 1271 bytes`
console.log(`${progress.percent * 100}% - ${progress.transferredBytes} of ${progress.totalBytes} bytes`);
}
});
```
*/
onUploadProgress?: (progress: Progress, chunk: Uint8Array) => void;
/**
User-defined `fetch` function.
Has to be fully compatible with the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) standard.
Use-cases:
1. Use the `fetch` wrapper function provided by some frameworks that use server-side rendering (SSR).
2. Add custom instrumentation or logging to all requests.
@default fetch
@example
```
import ky from 'ky';
const api = ky.create({
fetch: async (request, init) => {
const start = performance.now();
const response = await fetch(request, init);
const duration = performance.now() - start;
console.log(`${request.method} ${request.url} - ${response.status} (${Math.round(duration)}ms)`);
return response;
}
});
const json = await api('https://example.com').json();
```
*/
fetch?: (input: Input, init?: RequestInit) => Promise<Response>;
/**
User-defined data passed to hooks.
This option allows you to pass arbitrary contextual data to hooks without polluting the request itself. The context is available in all hooks and is **guaranteed to always be an object** (never `undefined`), so you can safely access properties without optional chaining.
Use cases:
- Pass authentication tokens or API keys to hooks
- Attach request metadata for logging or debugging
- Implement conditional logic in hooks based on the request context
- Pass serverless environment bindings (e.g., Cloudflare Workers)
**Note:** Context is shallow merged. Top-level properties are merged, but nested objects are replaced. Only enumerable properties are copied.
@example
```
import ky from 'ky';
// Pass data to hooks
const api = ky.create({
hooks: {
beforeRequest: [
({request, options}) => {
const {token} = options.context;
if (token) {
request.headers.set('Authorization', `Bearer ${token}`);
}
}
]
}
});
await api('https://example.com', {
context: {
token: 'secret123'
}
}).json();
// Shallow merge: only top-level properties are merged
const instance = ky.create({
context: {
a: 1,
b: {
nested: true
}
}
});
const extended = instance.extend({
context: {
b: {
updated: true
},
c: 3
}
});
// Result: {a: 1, b: {updated: true}, c: 3}
// Note: The original `b.nested` is gone (shallow merge)
```
@default {}
*/
context?: Record<string, unknown>;
};
/**
Each key from KyOptions is present and set to `true`.
This type is used for identifying and working with the known keys in KyOptions.
*/
export type KyOptionsRegistry = {
[K in keyof KyOptions]-?: true;
};
/**
Options are the same as `window.fetch`, except for the KyOptions
*/
export interface Options extends KyOptions, Omit<RequestInit, 'headers'> {
/**
HTTP method used to make the request.
Internally, the standard methods (`GET`, `POST`, `PUT`, `PATCH`, `HEAD` and `DELETE`) are uppercased in order to avoid server errors due to case sensitivity.
*/
method?: LiteralUnion<HttpMethod, string>;
/**
HTTP headers used to make the request.
You can pass a `Headers` instance or a plain object.
You can remove a header with `.extend()` by passing the header with an `undefined` value. Passing `undefined` as a string removes the header only if it comes from a `Headers` instance.
@example
```
import ky from 'ky';
const url = 'https://sindresorhus.com';
const original = ky.create({
headers: {
rainbow: 'rainbow',
unicorn: 'unicorn'
}
});
const extended = original.extend({
headers: {
rainbow: undefined
}
});
const response = await extended(url).json();
console.log('rainbow' in response);
//=> false
console.log('unicorn' in response);
//=> true
```
*/
headers?: KyHeadersInit;
}
export type InternalOptions = Required<Omit<Options, 'hooks' | 'retry' | 'context' | 'throwHttpErrors'>, 'fetch' | 'prefix' | 'timeout' | 'totalTimeout'> & {
headers: Required<Headers>;
hooks: Required<Hooks>;
retry: Required<Omit<RetryOptions, 'shouldRetry'>> & Pick<RetryOptions, 'shouldRetry'>;
prefix: string;
context: Record<string, unknown>;
throwHttpErrors: boolean | ((status: number) => boolean);
};
/**
Normalized options passed to the `fetch` call and hooks.
*/
export interface NormalizedOptions extends RequestInit {
method: NonNullable<RequestInit['method']>;
credentials?: NonNullable<RequestInit['credentials']>;
retry: RetryOptions;
baseUrl?: Options['baseUrl'];
prefix: string;
onDownloadProgress: Options['onDownloadProgress'];
onUploadProgress: Options['onUploadProgress'];
context: Record<string, unknown>;
}
export type { RetryOptions, ShouldRetryState } from './retry.js';
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=options.js.map
File diff suppressed because one or more lines are too long
+3
View File
@@ -0,0 +1,3 @@
export type KyRequest<T = unknown> = {
json: <J = T>() => Promise<J>;
} & Request;
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=request.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"request.js","sourceRoot":"","sources":["../../source/types/request.ts"],"names":[],"mappings":"","sourcesContent":["export type KyRequest<T = unknown> = {\n\tjson: <J = T>() => Promise<J>;\n} & Request;\n"]}
+3
View File
@@ -0,0 +1,3 @@
export type KyResponse<T = unknown> = {
json: <J = T>() => Promise<J>;
} & Response;
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=response.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"response.js","sourceRoot":"","sources":["../../source/types/response.ts"],"names":[],"mappings":"","sourcesContent":["export type KyResponse<T = unknown> = {\n\tjson: <J = T>() => Promise<J>;\n} & Response;\n"]}
+163
View File
@@ -0,0 +1,163 @@
import type { HttpMethod } from './options.js';
export type ShouldRetryState = {
/**
The error that caused the request to fail.
*/
error: Error;
/**
The number of retries attempted. Starts at 1 for the first retry.
*/
retryCount: number;
};
export type RetryOptions = {
/**
The number of times to retry failed requests.
@default 2
*/
limit?: number;
/**
The HTTP methods allowed to retry.
@default ['get', 'put', 'head', 'delete', 'options', 'trace']
*/
methods?: HttpMethod[];
/**
The HTTP status codes allowed to retry.
@default [408, 413, 429, 500, 502, 503, 504]
*/
statusCodes?: number[];
/**
The HTTP status codes allowed to retry with a `Retry-After` header.
@default [413, 429, 503]
*/
afterStatusCodes?: number[];
/**
If the `Retry-After` header is greater than `maxRetryAfter`, it will use `maxRetryAfter`.
@default Infinity
*/
maxRetryAfter?: number;
/**
The upper limit of the delay per retry in milliseconds.
To clamp the delay, set `backoffLimit` to 1000, for example.
By default, the delay is calculated in the following way:
```
0.3 * (2 ** (attemptCount - 1)) * 1000
```
The delay increases exponentially.
@default Infinity
*/
backoffLimit?: number;
/**
A function to calculate the delay in milliseconds between retries given `attemptCount` (starts from 1).
@default attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000
*/
delay?: (attemptCount: number) => number;
/**
Add random jitter to retry delays to prevent thundering herd problems.
When many clients retry simultaneously (e.g., after hitting a rate limit), they can overwhelm the server again. Jitter adds randomness to break this synchronization.
Set to `true` to use full jitter, which randomizes the delay between 0 and the computed delay.
Alternatively, pass a function to implement custom jitter strategies.
Note: Jitter is not applied when the server provides a `Retry-After` header, as the server's explicit timing should be respected.
@default undefined (no jitter)
@example
```
import ky from 'ky';
const json = await ky('https://example.com', {
retry: {
limit: 5,
// Full jitter (randomizes delay between 0 and computed value)
jitter: true
// Percentage jitter (80-120% of delay)
// jitter: delay => delay * (0.8 + Math.random() * 0.4)
// Absolute jitter (±100ms)
// jitter: delay => delay + (Math.random() * 200 - 100)
}
}).json();
```
*/
jitter?: boolean | ((delay: number) => number) | undefined;
/**
Whether to retry when the request times out.
@default false
@example
```
import ky from 'ky';
const json = await ky('https://example.com', {
retry: {
limit: 3,
retryOnTimeout: true
}
}).json();
```
*/
retryOnTimeout?: boolean;
/**
A function to determine whether a retry should be attempted.
This function takes precedence over the default retry checks (`retryOnTimeout`, status code checks, etc.) for retriable methods. It is only called after the retry limit and method checks pass.
**Note:** This is different from the `beforeRetry` hook:
- `shouldRetry`: Controls WHETHER to retry (called before the retry decision is made)
- `beforeRetry`: Called AFTER retry is confirmed, allowing you to modify the request
Should return:
- `true` to force a retry (bypasses `retryOnTimeout`, status code checks, and other validations)
- `false` to prevent a retry (no retry will occur)
- `undefined` to use the default retry logic (`retryOnTimeout`, status codes, network errors). Unrecognized error types are not retried.
@default undefined
@example
```
import ky, {HTTPError} from 'ky';
const json = await ky('https://example.com', {
retry: {
limit: 3,
shouldRetry: ({error, retryCount}) => {
// Retry on specific business logic errors from API
if (error instanceof HTTPError) {
const status = error.response.status;
// Retry on 429 (rate limit) but only for first 2 attempts
if (status === 429 && retryCount <= 2) {
return true;
}
// Don't retry on 4xx errors except rate limits
if (status >= 400 && status < 500) {
return false;
}
}
// Use default retry logic for other errors
return undefined;
}
}
}).json();
```
*/
shouldRetry?: (state: ShouldRetryState) => boolean | undefined | Promise<boolean | undefined>;
};
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=retry.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"retry.js","sourceRoot":"","sources":["../../source/types/retry.ts"],"names":[],"mappings":"","sourcesContent":["import type {HttpMethod} from './options.js';\n\nexport type ShouldRetryState = {\n\t/**\n\tThe error that caused the request to fail.\n\t*/\n\terror: Error;\n\n\t/**\n\tThe number of retries attempted. Starts at 1 for the first retry.\n\t*/\n\tretryCount: number;\n};\n\nexport type RetryOptions = {\n\t/**\n\tThe number of times to retry failed requests.\n\n\t@default 2\n\t*/\n\tlimit?: number;\n\n\t/**\n\tThe HTTP methods allowed to retry.\n\n\t@default ['get', 'put', 'head', 'delete', 'options', 'trace']\n\t*/\n\tmethods?: HttpMethod[];\n\n\t/**\n\tThe HTTP status codes allowed to retry.\n\n\t@default [408, 413, 429, 500, 502, 503, 504]\n\t*/\n\tstatusCodes?: number[];\n\n\t/**\n\tThe HTTP status codes allowed to retry with a `Retry-After` header.\n\n\t@default [413, 429, 503]\n\t*/\n\tafterStatusCodes?: number[];\n\n\t/**\n\tIf the `Retry-After` header is greater than `maxRetryAfter`, it will use `maxRetryAfter`.\n\n\t@default Infinity\n\t*/\n\tmaxRetryAfter?: number;\n\n\t/**\n\tThe upper limit of the delay per retry in milliseconds.\n\tTo clamp the delay, set `backoffLimit` to 1000, for example.\n\n\tBy default, the delay is calculated in the following way:\n\n\t```\n\t0.3 * (2 ** (attemptCount - 1)) * 1000\n\t```\n\n\tThe delay increases exponentially.\n\n\t@default Infinity\n\t*/\n\tbackoffLimit?: number;\n\n\t/**\n\tA function to calculate the delay in milliseconds between retries given `attemptCount` (starts from 1).\n\n\t@default attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000\n\t*/\n\tdelay?: (attemptCount: number) => number;\n\n\t/**\n\tAdd random jitter to retry delays to prevent thundering herd problems.\n\n\tWhen many clients retry simultaneously (e.g., after hitting a rate limit), they can overwhelm the server again. Jitter adds randomness to break this synchronization.\n\n\tSet to `true` to use full jitter, which randomizes the delay between 0 and the computed delay.\n\n\tAlternatively, pass a function to implement custom jitter strategies.\n\n\tNote: Jitter is not applied when the server provides a `Retry-After` header, as the server's explicit timing should be respected.\n\n\t@default undefined (no jitter)\n\n\t@example\n\t```\n\timport ky from 'ky';\n\n\tconst json = await ky('https://example.com', {\n\t\tretry: {\n\t\t\tlimit: 5,\n\n\t\t\t// Full jitter (randomizes delay between 0 and computed value)\n\t\t\tjitter: true\n\n\t\t\t// Percentage jitter (80-120% of delay)\n\t\t\t// jitter: delay => delay * (0.8 + Math.random() * 0.4)\n\n\t\t\t// Absolute jitter (±100ms)\n\t\t\t// jitter: delay => delay + (Math.random() * 200 - 100)\n\t\t}\n\t}).json();\n\t```\n\t*/\n\tjitter?: boolean | ((delay: number) => number) | undefined;\n\n\t/**\n\tWhether to retry when the request times out.\n\n\t@default false\n\n\t@example\n\t```\n\timport ky from 'ky';\n\n\tconst json = await ky('https://example.com', {\n\t\tretry: {\n\t\t\tlimit: 3,\n\t\t\tretryOnTimeout: true\n\t\t}\n\t}).json();\n\t```\n\t*/\n\tretryOnTimeout?: boolean;\n\n\t/**\n\tA function to determine whether a retry should be attempted.\n\n\tThis function takes precedence over the default retry checks (`retryOnTimeout`, status code checks, etc.) for retriable methods. It is only called after the retry limit and method checks pass.\n\n\t**Note:** This is different from the `beforeRetry` hook:\n\t- `shouldRetry`: Controls WHETHER to retry (called before the retry decision is made)\n\t- `beforeRetry`: Called AFTER retry is confirmed, allowing you to modify the request\n\n\tShould return:\n\t- `true` to force a retry (bypasses `retryOnTimeout`, status code checks, and other validations)\n\t- `false` to prevent a retry (no retry will occur)\n\t- `undefined` to use the default retry logic (`retryOnTimeout`, status codes, network errors). Unrecognized error types are not retried.\n\n\t@default undefined\n\n\t@example\n\t```\n\timport ky, {HTTPError} from 'ky';\n\n\tconst json = await ky('https://example.com', {\n\t\tretry: {\n\t\t\tlimit: 3,\n\t\t\tshouldRetry: ({error, retryCount}) => {\n\t\t\t\t// Retry on specific business logic errors from API\n\t\t\t\tif (error instanceof HTTPError) {\n\t\t\t\t\tconst status = error.response.status;\n\n\t\t\t\t\t// Retry on 429 (rate limit) but only for first 2 attempts\n\t\t\t\t\tif (status === 429 && retryCount <= 2) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Don't retry on 4xx errors except rate limits\n\t\t\t\t\tif (status >= 400 && status < 500) {\n\t\t\t\t\t\treturn false;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Use default retry logic for other errors\n\t\t\t\treturn undefined;\n\t\t\t}\n\t\t}\n\t}).json();\n\t```\n\t*/\n\tshouldRetry?: (state: ShouldRetryState) => boolean | undefined | Promise<boolean | undefined>;\n};\n"]}
+33
View File
@@ -0,0 +1,33 @@
export type StandardSchemaV1Issue = {
readonly message: string;
readonly path?: ReadonlyArray<PropertyKey | {
readonly key: PropertyKey;
}> | undefined;
};
export type StandardSchemaV1SuccessResult<OutputType> = {
readonly value: OutputType;
readonly issues?: undefined;
};
export type StandardSchemaV1FailureResult = {
readonly issues: readonly StandardSchemaV1Issue[];
readonly value?: undefined;
};
export type StandardSchemaV1Result<OutputType> = StandardSchemaV1SuccessResult<OutputType> | StandardSchemaV1FailureResult;
export type StandardSchemaV1Types<InputType, OutputType> = {
readonly input: InputType;
readonly output: OutputType;
};
export type StandardSchemaV1Options = {
readonly libraryOptions?: Readonly<Record<string, unknown>> | undefined;
};
export type StandardSchemaV1<InputType = unknown, OutputType = InputType> = {
readonly '~standard': {
readonly version: 1;
readonly vendor: string;
readonly validate: (value: unknown, options?: StandardSchemaV1Options) => StandardSchemaV1Result<OutputType> | Promise<StandardSchemaV1Result<OutputType>>;
readonly types?: StandardSchemaV1Types<InputType, OutputType> | undefined;
};
};
export type StandardSchemaV1InferOutput<Schema extends StandardSchemaV1> = Schema['~standard'] extends {
readonly types: StandardSchemaV1Types<unknown, infer OutputType>;
} ? OutputType : Extract<Awaited<ReturnType<Schema['~standard']['validate']>>, StandardSchemaV1SuccessResult<unknown>> extends StandardSchemaV1SuccessResult<infer OutputType> ? OutputType : unknown;
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=standard-schema.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"standard-schema.js","sourceRoot":"","sources":["../../source/types/standard-schema.ts"],"names":[],"mappings":"","sourcesContent":["export type StandardSchemaV1Issue = {\n\treadonly message: string;\n\treadonly path?: ReadonlyArray<PropertyKey | {readonly key: PropertyKey}> | undefined;\n};\n\nexport type StandardSchemaV1SuccessResult<OutputType> = {\n\treadonly value: OutputType;\n\treadonly issues?: undefined;\n};\n\nexport type StandardSchemaV1FailureResult = {\n\treadonly issues: readonly StandardSchemaV1Issue[];\n\treadonly value?: undefined;\n};\n\nexport type StandardSchemaV1Result<OutputType> = StandardSchemaV1SuccessResult<OutputType> | StandardSchemaV1FailureResult;\n\nexport type StandardSchemaV1Types<InputType, OutputType> = {\n\treadonly input: InputType;\n\treadonly output: OutputType;\n};\n\nexport type StandardSchemaV1Options = {\n\treadonly libraryOptions?: Readonly<Record<string, unknown>> | undefined;\n};\n\nexport type StandardSchemaV1<InputType = unknown, OutputType = InputType> = {\n\treadonly '~standard': {\n\t\treadonly version: 1;\n\t\treadonly vendor: string;\n\t\treadonly validate: (\n\t\t\tvalue: unknown,\n\t\t\toptions?: StandardSchemaV1Options,\n\t\t) => StandardSchemaV1Result<OutputType> | Promise<StandardSchemaV1Result<OutputType>>;\n\t\treadonly types?: StandardSchemaV1Types<InputType, OutputType> | undefined;\n\t};\n};\n\nexport type StandardSchemaV1InferOutput<Schema extends StandardSchemaV1> = Schema['~standard'] extends {\n\treadonly types: StandardSchemaV1Types<unknown, infer OutputType>;\n}\n\t? OutputType\n\t: Extract<\n\t\tAwaited<ReturnType<Schema['~standard']['validate']>>,\n\t\tStandardSchemaV1SuccessResult<unknown>\n\t> extends StandardSchemaV1SuccessResult<infer OutputType>\n\t\t? OutputType\n\t\t: unknown;\n"]}
+4
View File
@@ -0,0 +1,4 @@
import type { Options } from '../types/options.js';
export declare const getBodySize: (body?: BodyInit | null) => number;
export declare const streamResponse: (response: Response, onDownloadProgress: Options["onDownloadProgress"]) => Response;
export declare const streamRequest: (request: Request, onUploadProgress: Options["onUploadProgress"], originalBody?: BodyInit | null) => Request;
+89
View File
@@ -0,0 +1,89 @@
import { usualFormBoundarySize } from '../core/constants.js';
const encoder = new TextEncoder();
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export 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 += encoder.encode(`Content-Disposition: form-data; name="${key}"`).byteLength;
size += typeof value === 'string'
? encoder.encode(value).byteLength
: value.size;
}
return size;
}
if (body instanceof Blob) {
return body.size;
}
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) {
return body.byteLength;
}
if (typeof body === 'string') {
return encoder.encode(body).byteLength;
}
if (body instanceof URLSearchParams) {
return encoder.encode(body.toString()).byteLength;
}
return 0;
};
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);
}
},
}));
};
export const streamResponse = (response, onDownloadProgress) => {
if (!response.body) {
return response;
}
const responseInit = {
status: response.status,
statusText: response.statusText,
headers: response.headers,
};
if (response.status === 204) {
return new Response(null, responseInit);
}
const totalBytes = Math.max(0, Number(response.headers.get('content-length')) || 0);
return new Response(withProgress(response.body, totalBytes, onDownloadProgress), responseInit);
};
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export 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),
});
};
//# sourceMappingURL=body.js.map
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
import { type InternalOptions } from '../types/options.js';
export type DelayOptions = {
signal?: InternalOptions['signal'];
};
export default function delay(ms: number, { signal }: DelayOptions): Promise<void>;
+18
View File
@@ -0,0 +1,18 @@
// https://github.com/sindresorhus/delay/tree/ab98ae8dfcb38e1593286c94d934e70d14a4e111
export default 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);
});
}
//# sourceMappingURL=delay.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"delay.js","sourceRoot":"","sources":["../../source/utils/delay.ts"],"names":[],"mappings":"AAAA,sFAAsF;AAQtF,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,KAAK,CAClC,EAAU,EACV,EAAC,MAAM,EAAe;IAEtB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,IAAI,MAAM,EAAE,CAAC;YACZ,MAAM,CAAC,cAAc,EAAE,CAAC;YACxB,MAAM,CAAC,gBAAgB,CAAC,OAAO,EAAE,YAAY,EAAE,EAAC,IAAI,EAAE,IAAI,EAAC,CAAC,CAAC;QAC9D,CAAC;QAED,SAAS,YAAY;YACpB,YAAY,CAAC,SAAS,CAAC,CAAC;YACxB,MAAM,CAAC,MAAO,CAAC,MAAe,CAAC,CAAC;QACjC,CAAC;QAED,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YACjC,MAAM,EAAE,mBAAmB,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;YACnD,OAAO,EAAE,CAAC;QACX,CAAC,EAAE,EAAE,CAAC,CAAC;IACR,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["// https://github.com/sindresorhus/delay/tree/ab98ae8dfcb38e1593286c94d934e70d14a4e111\n\nimport {type InternalOptions} from '../types/options.js';\n\nexport type DelayOptions = {\n\tsignal?: InternalOptions['signal'];\n};\n\nexport default async function delay(\n\tms: number,\n\t{signal}: DelayOptions,\n): Promise<void> {\n\treturn new Promise((resolve, reject) => {\n\t\tif (signal) {\n\t\t\tsignal.throwIfAborted();\n\t\t\tsignal.addEventListener('abort', abortHandler, {once: true});\n\t\t}\n\n\t\tfunction abortHandler() {\n\t\t\tclearTimeout(timeoutId);\n\t\t\treject(signal!.reason as Error);\n\t\t}\n\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tsignal?.removeEventListener('abort', abortHandler);\n\t\t\tresolve();\n\t\t}, ms);\n\t});\n}\n"]}
+1
View File
@@ -0,0 +1 @@
export default function isRawNetworkError(error: unknown): error is TypeError;
+40
View File
@@ -0,0 +1,40 @@
// Inlined from https://github.com/sindresorhus/is-network-error v1.3.1
const objectToString = Object.prototype.toString;
const isError = (value) => objectToString.call(value) === '[object Error]';
const errorMessages = new Set([
'network error', // Chrome
'NetworkError when attempting to fetch resource.', // Firefox
'The Internet connection appears to be offline.', // Safari 16
'Network request failed', // `cross-fetch`
'fetch failed', // Undici (Node.js)
'terminated', // Undici (Node.js)
' A network error occurred.', // Bun (WebKit) - leading space is intentional
'Network connection lost', // Cloudflare Workers (fetch)
]);
export default function isRawNetworkError(error) {
const isValid = error
&& isError(error)
&& error.name === 'TypeError'
&& typeof error.message === 'string';
if (!isValid) {
return false;
}
const { message, stack } = error;
// Safari 17+ has generic message but no stack for network errors
if (message === 'Load failed') {
return stack === undefined
// Sentry adds its own stack trace to the fetch error, so also check for that
|| '__sentry_captured__' in error;
}
// Deno network errors start with specific text
if (message.startsWith('error sending request for url')) {
return true;
}
// Chrome: exact "Failed to fetch" or with hostname: "Failed to fetch (example.com)"
if (message === 'Failed to fetch' || (message.startsWith('Failed to fetch (') && message.endsWith(')'))) {
return true;
}
// Standard network error messages
return errorMessages.has(message);
}
//# sourceMappingURL=is-network-error.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"is-network-error.js","sourceRoot":"","sources":["../../source/utils/is-network-error.ts"],"names":[],"mappings":"AAAA,uEAAuE;AAEvE,MAAM,cAAc,GAAG,MAAM,CAAC,SAAS,CAAC,QAAQ,CAAC;AAEjD,MAAM,OAAO,GAAG,CAAC,KAAc,EAAkB,EAAE,CAAC,cAAc,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,gBAAgB,CAAC;AAEpG,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC7B,eAAe,EAAE,SAAS;IAC1B,iDAAiD,EAAE,UAAU;IAC7D,gDAAgD,EAAE,YAAY;IAC9D,wBAAwB,EAAE,gBAAgB;IAC1C,cAAc,EAAE,mBAAmB;IACnC,YAAY,EAAE,mBAAmB;IACjC,4BAA4B,EAAE,8CAA8C;IAC5E,yBAAyB,EAAE,6BAA6B;CACxD,CAAC,CAAC;AAEH,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,KAAc;IACvD,MAAM,OAAO,GAAG,KAAK;WACjB,OAAO,CAAC,KAAK,CAAC;WACd,KAAK,CAAC,IAAI,KAAK,WAAW;WAC1B,OAAO,KAAK,CAAC,OAAO,KAAK,QAAQ,CAAC;IAEtC,IAAI,CAAC,OAAO,EAAE,CAAC;QACd,OAAO,KAAK,CAAC;IACd,CAAC;IAED,MAAM,EAAC,OAAO,EAAE,KAAK,EAAC,GAAG,KAAK,CAAC;IAE/B,iEAAiE;IACjE,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;QAC/B,OAAO,KAAK,KAAK,SAAS;YACzB,6EAA6E;eAC1E,qBAAqB,IAAI,KAAK,CAAC;IACpC,CAAC;IAED,+CAA+C;IAC/C,IAAI,OAAO,CAAC,UAAU,CAAC,+BAA+B,CAAC,EAAE,CAAC;QACzD,OAAO,IAAI,CAAC;IACb,CAAC;IAED,oFAAoF;IACpF,IAAI,OAAO,KAAK,iBAAiB,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,mBAAmB,CAAC,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACzG,OAAO,IAAI,CAAC;IACb,CAAC;IAED,kCAAkC;IAClC,OAAO,aAAa,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;AACnC,CAAC","sourcesContent":["// Inlined from https://github.com/sindresorhus/is-network-error v1.3.1\n\nconst objectToString = Object.prototype.toString;\n\nconst isError = (value: unknown): value is Error => objectToString.call(value) === '[object Error]';\n\nconst errorMessages = new Set([\n\t'network error', // Chrome\n\t'NetworkError when attempting to fetch resource.', // Firefox\n\t'The Internet connection appears to be offline.', // Safari 16\n\t'Network request failed', // `cross-fetch`\n\t'fetch failed', // Undici (Node.js)\n\t'terminated', // Undici (Node.js)\n\t' A network error occurred.', // Bun (WebKit) - leading space is intentional\n\t'Network connection lost', // Cloudflare Workers (fetch)\n]);\n\nexport default function isRawNetworkError(error: unknown): error is TypeError {\n\tconst isValid = error\n\t\t&& isError(error)\n\t\t&& error.name === 'TypeError'\n\t\t&& typeof error.message === 'string';\n\n\tif (!isValid) {\n\t\treturn false;\n\t}\n\n\tconst {message, stack} = error;\n\n\t// Safari 17+ has generic message but no stack for network errors\n\tif (message === 'Load failed') {\n\t\treturn stack === undefined\n\t\t\t// Sentry adds its own stack trace to the fetch error, so also check for that\n\t\t\t|| '__sentry_captured__' in error;\n\t}\n\n\t// Deno network errors start with specific text\n\tif (message.startsWith('error sending request for url')) {\n\t\treturn true;\n\t}\n\n\t// Chrome: exact \"Failed to fetch\" or with hostname: \"Failed to fetch (example.com)\"\n\tif (message === 'Failed to fetch' || (message.startsWith('Failed to fetch (') && message.endsWith(')'))) {\n\t\treturn true;\n\t}\n\n\t// Standard network error messages\n\treturn errorMessages.has(message);\n}\n"]}
+1
View File
@@ -0,0 +1 @@
export declare const isObject: (value: unknown) => value is object;
+3
View File
@@ -0,0 +1,3 @@
// eslint-disable-next-line @typescript-eslint/no-restricted-types
export const isObject = (value) => value !== null && typeof value === 'object';
//# sourceMappingURL=is.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"is.js","sourceRoot":"","sources":["../../source/utils/is.ts"],"names":[],"mappings":"AAAA,kEAAkE;AAClE,MAAM,CAAC,MAAM,QAAQ,GAAG,CAAC,KAAc,EAAmB,EAAE,CAAC,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC","sourcesContent":["// eslint-disable-next-line @typescript-eslint/no-restricted-types\nexport const isObject = (value: unknown): value is object => value !== null && typeof value === 'object';\n"]}
+29
View File
@@ -0,0 +1,29 @@
import type { KyHeadersInit, Options } from '../types/options.js';
import type { Hooks } from '../types/hooks.js';
/**
Wraps a value so that `ky.extend()` will replace the parent value instead of merging with it. Works with hooks, headers, search parameters, context, and any other deep-merged option.
By default, `.extend()` deep-merges options with the parent instance: hooks get appended, headers get merged, and search parameters get accumulated. Use `replaceOption` when you want to fully replace a merged property instead.
@example
```
import ky, {replaceOption} from 'ky';
const base = ky.create({
hooks: {beforeRequest: [addAuth, addTracking]},
});
// Replaces instead of appending
const extended = base.extend({
hooks: replaceOption({beforeRequest: [onlyThis]}),
});
// hooks.beforeRequest is now [onlyThis], not [addAuth, addTracking, onlyThis]
```
*/
export declare const replaceOption: <T>(value: T) => T;
export declare const validateAndMerge: (...sources: Array<Partial<Options> | undefined>) => Partial<Options>;
export declare const mergeHeaders: (source1?: KyHeadersInit, source2?: KyHeadersInit) => Headers;
export declare const cloneShallow: <T>(value: T) => T;
export declare const mergeHooks: (original?: Hooks, incoming?: Hooks) => Required<Hooks>;
export declare const deletedParametersSymbol: unique symbol;
export declare const deepMerge: <T>(...sources: Array<Partial<T> | undefined>) => T;
+259
View File
@@ -0,0 +1,259 @@
import { supportsAbortSignal } from '../core/constants.js';
import { isObject } from './is.js';
const replaceSymbol = Symbol('replaceOption');
const getReplaceState = (value) => isObject(value) && value[replaceSymbol] === true
? {
isReplace: true,
value: value.value,
}
: {
isReplace: false,
value,
};
/**
Wraps a value so that `ky.extend()` will replace the parent value instead of merging with it. Works with hooks, headers, search parameters, context, and any other deep-merged option.
By default, `.extend()` deep-merges options with the parent instance: hooks get appended, headers get merged, and search parameters get accumulated. Use `replaceOption` when you want to fully replace a merged property instead.
@example
```
import ky, {replaceOption} from 'ky';
const base = ky.create({
hooks: {beforeRequest: [addAuth, addTracking]},
});
// Replaces instead of appending
const extended = base.extend({
hooks: replaceOption({beforeRequest: [onlyThis]}),
});
// hooks.beforeRequest is now [onlyThis], not [addAuth, addTracking, onlyThis]
```
*/
export const replaceOption = (value) => {
const markedValue = { [replaceSymbol]: true, value };
return markedValue;
};
export const validateAndMerge = (...sources) => {
for (const source of sources) {
if ((!isObject(source) || Array.isArray(source)) && source !== undefined) {
throw new TypeError('The `options` argument must be an object');
}
}
return deepMerge({}, ...sources);
};
export 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;
};
const isPlainObject = (value) => {
if (!isObject(value) || Array.isArray(value)) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype || prototype === null;
};
export const cloneShallow = (value) => {
if (value instanceof URLSearchParams) {
const copy = new URLSearchParams(value);
const deleted = value[deletedParametersSymbol];
if (deleted) {
// Preserve internal deletion markers so init-hook cloning does not resurrect params removed during option merging.
copy[deletedParametersSymbol] = new Set(deleted);
}
return copy;
}
if (value instanceof globalThis.Headers) {
return new globalThis.Headers(value);
}
if (Array.isArray(value)) {
return [...value];
}
if (isPlainObject(value)) {
const copy = { ...value };
return copy;
}
return value;
};
const normalizeHeaderObject = (headers) => Object.fromEntries(Object.entries(headers).filter((entry) => entry[1] !== undefined));
const mergeHeaderContainers = (source1, source2) => {
if (isPlainObject(source1) && isPlainObject(source2)) {
return normalizeHeaderObject({ ...source1, ...source2 });
}
return mergeHeaders(source1, source2);
};
function newHookValue(original, incoming, property) {
return (Object.hasOwn(incoming, property) && incoming[property] === undefined)
? []
: deepMerge(original[property] ?? [], incoming[property] ?? []);
}
export const mergeHooks = (original = {}, incoming = {}) => ({
init: newHookValue(original, incoming, 'init'),
beforeRequest: newHookValue(original, incoming, 'beforeRequest'),
beforeRetry: newHookValue(original, incoming, 'beforeRetry'),
beforeError: newHookValue(original, incoming, 'beforeError'),
afterResponse: newHookValue(original, incoming, 'afterResponse'),
});
export const deletedParametersSymbol = Symbol('deletedParameters');
const appendSearchParameters = (target, source) => {
const result = new URLSearchParams();
const deleted = new Set();
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);
deleted.delete(key);
}
const inputDeleted = input[deletedParametersSymbol];
if (inputDeleted) {
for (const key of inputDeleted) {
result.delete(key);
deleted.add(key);
}
}
}
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]));
deleted.delete(String(pair[0]));
}
}
else if (isObject(input)) {
for (const [key, value] of Object.entries(input)) {
if (value === undefined) {
result.delete(key);
deleted.add(key);
}
else {
result.append(key, String(value));
deleted.delete(key);
}
}
}
else {
// String
const parameters = new URLSearchParams(input);
for (const [key, value] of parameters.entries()) {
result.append(key, value);
deleted.delete(key);
}
}
}
if (deleted.size > 0) {
result[deletedParametersSymbol] = deleted;
}
return result;
};
// TODO: Make this strongly-typed (no `any`).
export 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(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;
}
const replaceState = getReplaceState(value);
const { isReplace } = replaceState;
value = replaceState.value;
// Special handling for context - shallow merge only
if (key === 'context') {
if (value !== undefined && value !== null && (!isObject(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)
? {}
: (isReplace
? { ...value }
: { ...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 if (isReplace) {
searchParameters = value;
}
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(value) && !isReplace && key in returnValue) {
value = deepMerge(returnValue[key], value);
}
returnValue = { ...returnValue, [key]: value };
}
if (isObject(source.hooks)) {
const { value: hookValue, isReplace } = getReplaceState(source.hooks);
hooks = isReplace
? mergeHooks({}, hookValue)
: mergeHooks(hooks, hookValue);
returnValue.hooks = hooks;
}
if (isObject(source.headers)) {
const { value: headerValue, isReplace } = getReplaceState(source.headers);
headers = isReplace
? cloneShallow(headerValue)
: mergeHeaderContainers(headers, headerValue);
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);
}
}
return returnValue;
};
//# sourceMappingURL=merge.js.map
File diff suppressed because one or more lines are too long
+5
View File
@@ -0,0 +1,5 @@
import type { RetryOptions } from '../types/retry.js';
export declare const normalizeRequestMethod: (input: string) => string;
type InternalRetryOptions = Required<Omit<RetryOptions, 'shouldRetry'>> & Pick<RetryOptions, 'shouldRetry'>;
export declare const normalizeRetryOptions: (retry?: number | RetryOptions) => InternalRetryOptions;
export {};
+39
View File
@@ -0,0 +1,39 @@
import { requestMethods } from '../core/constants.js';
export 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,
};
export 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');
}
if (retry.statusCodes && !Array.isArray(retry.statusCodes)) {
throw new Error('retry.statusCodes must be an array');
}
const normalizedRetry = Object.fromEntries(Object.entries({
...retry,
methods: retry.methods?.map(method => method.toLowerCase()),
}).filter(([, value]) => value !== undefined));
return {
...defaultRetryOptions,
...normalizedRetry,
};
};
//# sourceMappingURL=normalize.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"normalize.js","sourceRoot":"","sources":["../../source/utils/normalize.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,sBAAsB,CAAC;AAIpD,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,KAAa,EAAU,EAAE,CAC/D,cAAc,CAAC,QAAQ,CAAC,KAA0B,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;AAEnF,MAAM,YAAY,GAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;AAExF,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAE7D,MAAM,qBAAqB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAI9C,MAAM,mBAAmB,GAAyB;IACjD,KAAK,EAAE,CAAC;IACR,OAAO,EAAE,YAAY;IACrB,WAAW,EAAE,gBAAgB;IAC7B,gBAAgB,EAAE,qBAAqB;IACvC,aAAa,EAAE,MAAM,CAAC,iBAAiB;IACvC,YAAY,EAAE,MAAM,CAAC,iBAAiB;IACtC,KAAK,EAAE,YAAY,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,YAAY,GAAG,CAAC,CAAC,CAAC,GAAG,IAAI;IAC7D,MAAM,EAAE,SAAS;IACjB,cAAc,EAAE,KAAK;CACrB,CAAC;AAEF,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAA+B,EAAE,EAAwB,EAAE;IAChG,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC/B,OAAO;YACN,GAAG,mBAAmB;YACtB,KAAK,EAAE,KAAK;SACZ,CAAC;IACH,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACpD,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACnD,CAAC;IAED,IAAI,KAAK,CAAC,WAAW,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC;QAC5D,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACvD,CAAC;IAED,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC;QACzD,GAAG,KAAK;QACR,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,CAAC;KAC3D,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC,CAAiB,CAAC;IAE/D,OAAO;QACN,GAAG,mBAAmB;QACtB,GAAG,eAAe;KAClB,CAAC;AACH,CAAC,CAAC","sourcesContent":["import {requestMethods} from '../core/constants.js';\nimport type {RetryOptions} from '../types/retry.js';\nimport type {HttpMethod, RequestHttpMethod} from '../types/options.js';\n\nexport const normalizeRequestMethod = (input: string): string =>\n\trequestMethods.includes(input as RequestHttpMethod) ? input.toUpperCase() : input;\n\nconst retryMethods: HttpMethod[] = ['get', 'put', 'head', 'delete', 'options', 'trace'];\n\nconst retryStatusCodes = [408, 413, 429, 500, 502, 503, 504];\n\nconst retryAfterStatusCodes = [413, 429, 503];\n\ntype InternalRetryOptions = Required<Omit<RetryOptions, 'shouldRetry'>> & Pick<RetryOptions, 'shouldRetry'>;\n\nconst defaultRetryOptions: InternalRetryOptions = {\n\tlimit: 2,\n\tmethods: retryMethods,\n\tstatusCodes: retryStatusCodes,\n\tafterStatusCodes: retryAfterStatusCodes,\n\tmaxRetryAfter: Number.POSITIVE_INFINITY,\n\tbackoffLimit: Number.POSITIVE_INFINITY,\n\tdelay: attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000,\n\tjitter: undefined,\n\tretryOnTimeout: false,\n};\n\nexport const normalizeRetryOptions = (retry: number | RetryOptions = {}): InternalRetryOptions => {\n\tif (typeof retry === 'number') {\n\t\treturn {\n\t\t\t...defaultRetryOptions,\n\t\t\tlimit: retry,\n\t\t};\n\t}\n\n\tif (retry.methods && !Array.isArray(retry.methods)) {\n\t\tthrow new Error('retry.methods must be an array');\n\t}\n\n\tif (retry.statusCodes && !Array.isArray(retry.statusCodes)) {\n\t\tthrow new Error('retry.statusCodes must be an array');\n\t}\n\n\tconst normalizedRetry = Object.fromEntries(Object.entries({\n\t\t...retry,\n\t\tmethods: retry.methods?.map(method => method.toLowerCase()),\n\t}).filter(([, value]) => value !== undefined)) as RetryOptions;\n\n\treturn {\n\t\t...defaultRetryOptions,\n\t\t...normalizedRetry,\n\t};\n};\n"]}
+3
View File
@@ -0,0 +1,3 @@
import type { SearchParamsOption } from '../types/options.js';
export declare const findUnknownOptions: (options: Record<string, unknown>) => Record<string, unknown>;
export declare const hasSearchParameters: (search: SearchParamsOption) => boolean;
+41
View File
@@ -0,0 +1,41 @@
import { kyOptionKeys, requestOptionsRegistry } from '../core/constants.js';
import { deletedParametersSymbol } from './merge.js';
export const findUnknownOptions = (options) => {
const unknownOptions = {};
for (const key in options) {
// Skip inherited properties
if (!Object.hasOwn(options, key)) {
continue;
}
// Forward every non-standard, non-Ky option to fetch().
// We intentionally do not check whether the key also exists on `Request`, because some runtimes
// patch `Request.prototype` with fetch-only extensions. For example, Next.js adds `next`, and the
// old `key in request` heuristic dropped it unless Ky kept a special-case allowlist.
// Passing all non-standard keys makes that allowlist unnecessary and preserves future fetch extensions too.
if (!(key in requestOptionsRegistry) && !(key in kyOptionKeys)) {
unknownOptions[key] = options[key];
}
}
return unknownOptions;
};
export 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 || Boolean(search[deletedParametersSymbol]?.size);
}
// Record
if (typeof search === 'object') {
return Object.keys(search).length > 0;
}
if (typeof search === 'string') {
return search.trim().length > 0;
}
return Boolean(search);
};
//# sourceMappingURL=options.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"options.js","sourceRoot":"","sources":["../../source/utils/options.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAE,sBAAsB,EAAC,MAAM,sBAAsB,CAAC;AAE1E,OAAO,EAAC,uBAAuB,EAAC,MAAM,YAAY,CAAC;AAEnD,MAAM,CAAC,MAAM,kBAAkB,GAAG,CACjC,OAAgC,EACN,EAAE;IAC5B,MAAM,cAAc,GAA4B,EAAE,CAAC;IAEnD,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC3B,4BAA4B;QAC5B,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,EAAE,CAAC;YAClC,SAAS;QACV,CAAC;QAED,wDAAwD;QACxD,gGAAgG;QAChG,kGAAkG;QAClG,qFAAqF;QACrF,4GAA4G;QAC5G,IAAI,CAAC,CAAC,GAAG,IAAI,sBAAsB,CAAC,IAAI,CAAC,CAAC,GAAG,IAAI,YAAY,CAAC,EAAE,CAAC;YAChE,cAAc,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;QACpC,CAAC;IACF,CAAC;IAED,OAAO,cAAc,CAAC;AACvB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,MAA0B,EAAW,EAAE;IAC1E,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QAC1B,OAAO,KAAK,CAAC;IACd,CAAC;IAED,oFAAoF;IACpF,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;QAC3B,OAAO,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC;IAC1B,CAAC;IAED,IAAI,MAAM,YAAY,eAAe,EAAE,CAAC;QACvC,OAAO,MAAM,CAAC,IAAI,GAAG,CAAC,IAAI,OAAO,CAAE,MAAc,CAAC,uBAAuB,CAAC,EAAE,IAAI,CAAC,CAAC;IACnF,CAAC;IAED,SAAS;IACT,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC;IACvC,CAAC;IAED,IAAI,OAAO,MAAM,KAAK,QAAQ,EAAE,CAAC;QAChC,OAAO,MAAM,CAAC,IAAI,EAAE,CAAC,MAAM,GAAG,CAAC,CAAC;IACjC,CAAC;IAED,OAAO,OAAO,CAAC,MAAM,CAAC,CAAC;AACxB,CAAC,CAAC","sourcesContent":["import {kyOptionKeys, requestOptionsRegistry} from '../core/constants.js';\nimport type {SearchParamsOption} from '../types/options.js';\nimport {deletedParametersSymbol} from './merge.js';\n\nexport const findUnknownOptions = (\n\toptions: Record<string, unknown>,\n): Record<string, unknown> => {\n\tconst unknownOptions: Record<string, unknown> = {};\n\n\tfor (const key in options) {\n\t\t// Skip inherited properties\n\t\tif (!Object.hasOwn(options, key)) {\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Forward every non-standard, non-Ky option to fetch().\n\t\t// We intentionally do not check whether the key also exists on `Request`, because some runtimes\n\t\t// patch `Request.prototype` with fetch-only extensions. For example, Next.js adds `next`, and the\n\t\t// old `key in request` heuristic dropped it unless Ky kept a special-case allowlist.\n\t\t// Passing all non-standard keys makes that allowlist unnecessary and preserves future fetch extensions too.\n\t\tif (!(key in requestOptionsRegistry) && !(key in kyOptionKeys)) {\n\t\t\tunknownOptions[key] = options[key];\n\t\t}\n\t}\n\n\treturn unknownOptions;\n};\n\nexport const hasSearchParameters = (search: SearchParamsOption): boolean => {\n\tif (search === undefined) {\n\t\treturn false;\n\t}\n\n\t// The `typeof array` still gives \"object\", so we need different checking for array.\n\tif (Array.isArray(search)) {\n\t\treturn search.length > 0;\n\t}\n\n\tif (search instanceof URLSearchParams) {\n\t\treturn search.size > 0 || Boolean((search as any)[deletedParametersSymbol]?.size);\n\t}\n\n\t// Record\n\tif (typeof search === 'object') {\n\t\treturn Object.keys(search).length > 0;\n\t}\n\n\tif (typeof search === 'string') {\n\t\treturn search.trim().length > 0;\n\t}\n\n\treturn Boolean(search);\n};\n"]}
+5
View File
@@ -0,0 +1,5 @@
export type TimeoutOptions = {
timeout: number;
fetch: typeof fetch;
};
export default function timeout(request: Request, init: RequestInit, abortController: AbortController | undefined, options: TimeoutOptions): Promise<Response>;
+20
View File
@@ -0,0 +1,20 @@
import { TimeoutError } from '../errors/TimeoutError.js';
// `Promise.race()` workaround (#91)
export default 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);
});
});
}
//# sourceMappingURL=timeout.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"timeout.js","sourceRoot":"","sources":["../../source/utils/timeout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAOvD,oCAAoC;AACpC,MAAM,CAAC,OAAO,CAAC,KAAK,UAAU,OAAO,CACpC,OAAgB,EAChB,IAAiB,EACjB,eAA4C,EAC5C,OAAuB;IAEvB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACtC,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE;YACjC,IAAI,eAAe,EAAE,CAAC;gBACrB,eAAe,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC;YAED,MAAM,CAAC,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;QACnC,CAAC,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAEpB,KAAK,OAAO;aACV,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC;aACpB,IAAI,CAAC,OAAO,CAAC;aACb,KAAK,CAAC,MAAM,CAAC;aACb,IAAI,CAAC,GAAG,EAAE;YACV,YAAY,CAAC,SAAS,CAAC,CAAC;QACzB,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACJ,CAAC","sourcesContent":["import {TimeoutError} from '../errors/TimeoutError.js';\n\nexport type TimeoutOptions = {\n\ttimeout: number;\n\tfetch: typeof fetch;\n};\n\n// `Promise.race()` workaround (#91)\nexport default async function timeout(\n\trequest: Request,\n\tinit: RequestInit,\n\tabortController: AbortController | undefined,\n\toptions: TimeoutOptions,\n): Promise<Response> {\n\treturn new Promise((resolve, reject) => {\n\t\tconst timeoutId = setTimeout(() => {\n\t\t\tif (abortController) {\n\t\t\t\tabortController.abort();\n\t\t\t}\n\n\t\t\treject(new TimeoutError(request));\n\t\t}, options.timeout);\n\n\t\tvoid options\n\t\t\t.fetch(request, init)\n\t\t\t.then(resolve)\n\t\t\t.catch(reject)\n\t\t\t.then(() => {\n\t\t\t\tclearTimeout(timeoutId);\n\t\t\t});\n\t});\n}\n"]}
+111
View File
@@ -0,0 +1,111 @@
import type { KyError } from '../errors/KyError.js';
import { HTTPError } from '../errors/HTTPError.js';
import { NetworkError } from '../errors/NetworkError.js';
import { TimeoutError } from '../errors/TimeoutError.js';
import { ForceRetryError } from '../errors/ForceRetryError.js';
/**
Type guard to check if an error is a `KyError`.
Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.
@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);
}
}
```
*/
export declare function isKyError(error: unknown): error is KyError;
/**
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);
}
}
```
*/
export declare function isHTTPError<T = unknown>(error: unknown): error is HTTPError<T>;
/**
Type guard to check if an error is a `NetworkError`.
@param error - The error to check
@returns `true` if the error is a `NetworkError`, `false` otherwise
@example
```
import ky, {isNetworkError} from 'ky';
try {
const response = await ky.get('/api/data');
} catch (error) {
if (isNetworkError(error)) {
console.log('Network error:', error.request.url);
}
}
```
*/
export declare function isNetworkError(error: unknown): error is NetworkError;
/**
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);
}
}
```
*/
export declare function isTimeoutError(error: unknown): error is TimeoutError;
/**
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}`);
}
}
]
}
});
```
*/
export declare function isForceRetryError(error: unknown): error is ForceRetryError;
+123
View File
@@ -0,0 +1,123 @@
import { HTTPError } from '../errors/HTTPError.js';
import { NetworkError } from '../errors/NetworkError.js';
import { TimeoutError } from '../errors/TimeoutError.js';
import { ForceRetryError } from '../errors/ForceRetryError.js';
// Handles cross-realm cases (e.g., iframes, different JS contexts) where `instanceof` fails.
const isErrorType = (error, cls) => error instanceof cls || error?.name === cls.name;
/**
Type guard to check if an error is a `KyError`.
Note: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.
@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);
}
}
```
*/
export function isKyError(error) {
return error?.isKyError === true || isHTTPError(error) || isNetworkError(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);
}
}
```
*/
export function isHTTPError(error) {
return isErrorType(error, HTTPError);
}
/**
Type guard to check if an error is a `NetworkError`.
@param error - The error to check
@returns `true` if the error is a `NetworkError`, `false` otherwise
@example
```
import ky, {isNetworkError} from 'ky';
try {
const response = await ky.get('/api/data');
} catch (error) {
if (isNetworkError(error)) {
console.log('Network error:', error.request.url);
}
}
```
*/
export function isNetworkError(error) {
return isErrorType(error, NetworkError);
}
/**
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);
}
}
```
*/
export function isTimeoutError(error) {
return isErrorType(error, TimeoutError);
}
/**
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}`);
}
}
]
}
});
```
*/
export function isForceRetryError(error) {
return isErrorType(error, ForceRetryError);
}
//# sourceMappingURL=type-guards.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"type-guards.js","sourceRoot":"","sources":["../../source/utils/type-guards.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAC,eAAe,EAAC,MAAM,8BAA8B,CAAC;AAE7D,6FAA6F;AAC7F,MAAM,WAAW,GAAG,CAAC,KAAc,EAAE,GAAmB,EAAW,EAAE,CACpE,KAAK,YAAa,GAAW,IAAK,KAAa,EAAE,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;;;EAuBE;AACF,MAAM,UAAU,SAAS,CAAC,KAAc;IACvC,OAAQ,KAAa,EAAE,SAAS,KAAK,IAAI,IAAI,WAAW,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,IAAI,iBAAiB,CAAC,KAAK,CAAC,CAAC;AAC/I,CAAC;AAED;;;;;;;;;;;;;;;;;EAiBE;AACF,MAAM,UAAU,WAAW,CAAc,KAAc;IACtD,OAAO,WAAW,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;;;;;;;;;EAiBE;AACF,MAAM,UAAU,cAAc,CAAC,KAAc;IAC5C,OAAO,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;EAiBE;AACF,MAAM,UAAU,cAAc,CAAC,KAAc;IAC5C,OAAO,WAAW,CAAC,KAAK,EAAE,YAAY,CAAC,CAAC;AACzC,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;EAsBE;AACF,MAAM,UAAU,iBAAiB,CAAC,KAAc;IAC/C,OAAO,WAAW,CAAC,KAAK,EAAE,eAAe,CAAC,CAAC;AAC5C,CAAC","sourcesContent":["import type {KyError} from '../errors/KyError.js';\nimport {HTTPError} from '../errors/HTTPError.js';\nimport {NetworkError} from '../errors/NetworkError.js';\nimport {TimeoutError} from '../errors/TimeoutError.js';\nimport {ForceRetryError} from '../errors/ForceRetryError.js';\n\n// Handles cross-realm cases (e.g., iframes, different JS contexts) where `instanceof` fails.\nconst isErrorType = (error: unknown, cls: {name: string}): boolean =>\n\terror instanceof (cls as any) || (error as any)?.name === cls.name;\n\n/**\nType guard to check if an error is a `KyError`.\n\nNote: `SchemaValidationError` is intentionally not considered a Ky error. `KyError` covers failures in Ky's HTTP lifecycle (bad status, timeout, retry), while schema validation errors originate from the user-provided schema, not from Ky itself.\n\n@param error - The error to check\n@returns `true` if the error is a Ky error, `false` otherwise\n\n@example\n```\nimport ky, {isKyError} from 'ky';\ntry {\n\tconst response = await ky.get('/api/data');\n} catch (error) {\n\tif (isKyError(error)) {\n\t\t// Handle Ky-specific errors\n\t\tconsole.log('Ky error occurred:', error.message);\n\t} else {\n\t\t// Handle other errors\n\t\tconsole.log('Unknown error:', error);\n\t}\n}\n```\n*/\nexport function isKyError(error: unknown): error is KyError {\n\treturn (error as any)?.isKyError === true || isHTTPError(error) || isNetworkError(error) || isTimeoutError(error) || isForceRetryError(error);\n}\n\n/**\nType guard to check if an error is an `HTTPError`.\n\n@param error - The error to check\n@returns `true` if the error is an `HTTPError`, `false` otherwise\n\n@example\n```\nimport ky, {isHTTPError} from 'ky';\ntry {\n\tconst response = await ky.get('/api/data');\n} catch (error) {\n\tif (isHTTPError(error)) {\n\t\tconsole.log('HTTP error status:', error.response.status);\n\t}\n}\n```\n*/\nexport function isHTTPError<T = unknown>(error: unknown): error is HTTPError<T> {\n\treturn isErrorType(error, HTTPError);\n}\n\n/**\nType guard to check if an error is a `NetworkError`.\n\n@param error - The error to check\n@returns `true` if the error is a `NetworkError`, `false` otherwise\n\n@example\n```\nimport ky, {isNetworkError} from 'ky';\ntry {\n\tconst response = await ky.get('/api/data');\n} catch (error) {\n\tif (isNetworkError(error)) {\n\t\tconsole.log('Network error:', error.request.url);\n\t}\n}\n```\n*/\nexport function isNetworkError(error: unknown): error is NetworkError {\n\treturn isErrorType(error, NetworkError);\n}\n\n/**\nType guard to check if an error is a `TimeoutError`.\n\n@param error - The error to check\n@returns `true` if the error is a `TimeoutError`, `false` otherwise\n\n@example\n```\nimport ky, {isTimeoutError} from 'ky';\ntry {\n\tconst response = await ky.get('/api/data', { timeout: 1000 });\n} catch (error) {\n\tif (isTimeoutError(error)) {\n\t\tconsole.log('Request timed out:', error.request.url);\n\t}\n}\n```\n*/\nexport function isTimeoutError(error: unknown): error is TimeoutError {\n\treturn isErrorType(error, TimeoutError);\n}\n\n/**\nType guard to check if an error is a `ForceRetryError`.\n\n@param error - The error to check\n@returns `true` if the error is a `ForceRetryError`, `false` otherwise\n\n@example\n```\nimport ky, {isForceRetryError} from 'ky';\n\nconst api = ky.extend({\n\thooks: {\n\t\tbeforeRetry: [\n\t\t\t({error, retryCount}) => {\n\t\t\t\tif (isForceRetryError(error)) {\n\t\t\t\t\tconsole.log(`Forced retry #${retryCount}: ${error.code}`);\n\t\t\t\t}\n\t\t\t}\n\t\t]\n\t}\n});\n```\n*/\nexport function isForceRetryError(error: unknown): error is ForceRetryError {\n\treturn isErrorType(error, ForceRetryError);\n}\n"]}
+6
View File
@@ -0,0 +1,6 @@
export type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
export type ObjectEntries<T> = T extends ArrayLike<infer U> ? Array<[string, U]> : Array<{
[K in keyof T]: [K, T[K]];
}[keyof T]>;
+2
View File
@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map
+1
View File
@@ -0,0 +1 @@
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../source/utils/types.ts"],"names":[],"mappings":"","sourcesContent":["export type Mutable<T> = {\n\t-readonly[P in keyof T]: T[P]\n};\n\nexport type ObjectEntries<T> = T extends ArrayLike<infer U>\n\t? Array<[string, U]>\n\t: Array<{[K in keyof T]: [K, T[K]]}[keyof T]>;\n"]}
+9
View File
@@ -0,0 +1,9 @@
MIT License
Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+99
View File
@@ -0,0 +1,99 @@
{
"name": "ky",
"version": "2.0.2",
"description": "Tiny and elegant HTTP client based on the Fetch API",
"license": "MIT",
"repository": "sindresorhus/ky",
"funding": "https://github.com/sindresorhus/ky?sponsor=1",
"author": {
"name": "Sindre Sorhus",
"email": "sindresorhus@gmail.com",
"url": "https://sindresorhus.com"
},
"type": "module",
"exports": {
"types": "./distribution/index.d.ts",
"default": "./distribution/index.js"
},
"main": "./distribution/index.js",
"types": "./distribution/index.d.ts",
"sideEffects": false,
"engines": {
"node": ">=22"
},
"scripts": {
"test": "xo && npm run build && ava",
"debug": "PWDEBUG=1 ava --timeout=2m",
"release": "np",
"build": "del-cli distribution && tsc --project tsconfig.dist.json",
"prepare": "npm run build"
},
"files": [
"distribution"
],
"keywords": [
"fetch",
"request",
"requests",
"http",
"https",
"fetching",
"get",
"url",
"curl",
"wget",
"net",
"network",
"ajax",
"api",
"rest",
"xhr",
"browser",
"got",
"axios",
"node-fetch"
],
"devDependencies": {
"@sindresorhus/tsconfig": "^8.1.0",
"@type-challenges/utils": "^0.1.1",
"@types/busboy": "^1.5.4",
"@types/express": "^5.0.6",
"@types/node": "^25.3.0",
"ava": "^6.4.1",
"busboy": "^1.6.0",
"del-cli": "^7.0.0",
"expect-type": "^1.3.0",
"express": "^5.2.1",
"jest-leak-detector": "^30.2.0",
"playwright": "^1.58.2",
"tsx": "^4.21.0",
"typescript": "^5.9.3",
"xo": "^1.2.3"
},
"xo": {
"rules": {
"unicorn/filename-case": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/naming-convention": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/parameter-properties": "off",
"promise/prefer-await-to-then": "off",
"unicorn/no-invalid-fetch-options": "off",
"n/prefer-global/buffer": "off",
"@stylistic/function-paren-newline": "off"
}
},
"ava": {
"extensions": {
"ts": "module"
},
"nodeArguments": [
"--import=tsx/esm"
],
"workerThreads": false
}
}
+1867
View File
File diff suppressed because it is too large Load Diff