| import { HTTPError } from '../errors/HTTPError.js'; | ||
| import { TimeoutError } from '../errors/TimeoutError.js'; | ||
| /** | ||
| * Type guard to check if an error is a Ky error (HTTPError or TimeoutError). | ||
| * | ||
| * @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 HTTPError | TimeoutError; | ||
| /** | ||
| * 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 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; |
| import { HTTPError } from '../errors/HTTPError.js'; | ||
| import { TimeoutError } from '../errors/TimeoutError.js'; | ||
| /** | ||
| * Type guard to check if an error is a Ky error (HTTPError or TimeoutError). | ||
| * | ||
| * @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 isHTTPError(error) || isTimeoutError(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 error instanceof HTTPError || (error?.name === HTTPError.name); | ||
| } | ||
| /** | ||
| * Type guard to check if an error is a TimeoutError. | ||
| * | ||
| * @param error - The error to check | ||
| * @returns `true` if the error is a TimeoutError, `false` otherwise | ||
| * | ||
| * @example | ||
| * ``` | ||
| * import ky, {isTimeoutError} from 'ky'; | ||
| * try { | ||
| * const response = await ky.get('/api/data', { timeout: 1000 }); | ||
| * } catch (error) { | ||
| * if (isTimeoutError(error)) { | ||
| * console.log('Request timed out:', error.request.url); | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| export function isTimeoutError(error) { | ||
| return error instanceof TimeoutError || (error?.name === TimeoutError.name); | ||
| } | ||
| //# sourceMappingURL=type-guards.js.map |
| {"version":3,"file":"type-guards.js","sourceRoot":"","sources":["../../source/utils/type-guards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,wBAAwB,CAAC;AACjD,OAAO,EAAC,YAAY,EAAC,MAAM,2BAA2B,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,SAAS,CAAC,KAAc;IACvC,OAAO,WAAW,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC,KAAK,CAAC,CAAC;AACpD,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,WAAW,CAAc,KAAc;IACtD,OAAO,KAAK,YAAY,SAAS,IAAI,CAAE,KAAa,EAAE,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC;AAChF,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,cAAc,CAAC,KAAc;IAC5C,OAAO,KAAK,YAAY,YAAY,IAAI,CAAE,KAAa,EAAE,IAAI,KAAK,YAAY,CAAC,IAAI,CAAC,CAAC;AACtF,CAAC","sourcesContent":["import {HTTPError} from '../errors/HTTPError.js';\nimport {TimeoutError} from '../errors/TimeoutError.js';\n\n/**\n * Type guard to check if an error is a Ky error (HTTPError or TimeoutError).\n *\n * @param error - The error to check\n * @returns `true` if the error is a Ky error, `false` otherwise\n *\n * @example\n * ```\n * import ky, {isKyError} from 'ky';\n * try {\n * const response = await ky.get('/api/data');\n * } catch (error) {\n * if (isKyError(error)) {\n * // Handle Ky-specific errors\n * console.log('Ky error occurred:', error.message);\n * } else {\n * // Handle other errors\n * console.log('Unknown error:', error);\n * }\n * }\n * ```\n */\nexport function isKyError(error: unknown): error is HTTPError | TimeoutError {\n\treturn isHTTPError(error) || isTimeoutError(error);\n}\n\n/**\n * Type 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 * ```\n * import ky, {isHTTPError} from 'ky';\n * try {\n * const response = await ky.get('/api/data');\n * } catch (error) {\n * if (isHTTPError(error)) {\n * console.log('HTTP error status:', error.response.status);\n * }\n * }\n * ```\n */\nexport function isHTTPError<T = unknown>(error: unknown): error is HTTPError<T> {\n\treturn error instanceof HTTPError || ((error as any)?.name === HTTPError.name);\n}\n\n/**\n * Type 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 * ```\n * import ky, {isTimeoutError} from 'ky';\n * try {\n * const response = await ky.get('/api/data', { timeout: 1000 });\n * } catch (error) {\n * if (isTimeoutError(error)) {\n * console.log('Request timed out:', error.request.url);\n * }\n * }\n * ```\n */\nexport function isTimeoutError(error: unknown): error is TimeoutError {\n\treturn error instanceof TimeoutError || ((error as any)?.name === TimeoutError.name);\n}\n"]} |
| import { type KyOptionsRegistry } from '../types/options.js'; | ||
| import { type RequestInitRegistry } from '../types/request.js'; | ||
| export declare const supportsRequestStreams: boolean; | ||
@@ -21,2 +20,17 @@ export declare const supportsAbortController: boolean; | ||
| export declare const kyOptionKeys: KyOptionsRegistry; | ||
| export declare const requestOptionsRegistry: RequestInitRegistry; | ||
| 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; | ||
| }; |
@@ -64,2 +64,7 @@ export const supportsRequestStreams = (() => { | ||
| }; | ||
| // 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 = { | ||
@@ -79,6 +84,4 @@ method: true, | ||
| window: true, | ||
| dispatcher: true, | ||
| duplex: true, | ||
| priority: true, | ||
| }; | ||
| //# sourceMappingURL=constants.js.map |
+25
-14
| import { HTTPError } from '../errors/HTTPError.js'; | ||
| import { TimeoutError } from '../errors/TimeoutError.js'; | ||
| import { streamRequest, streamResponse } from '../utils/body.js'; | ||
@@ -9,2 +8,3 @@ import { mergeHeaders, mergeHooks } from '../utils/merge.js'; | ||
| import { findUnknownOptions, hasSearchParameters } from '../utils/options.js'; | ||
| import { isHTTPError, isTimeoutError } from '../utils/type-guards.js'; | ||
| import { maxSafeTimeout, responseTypes, stop, supportsAbortController, supportsAbortSignal, supportsFormData, supportsResponseStreams, supportsRequestStreams, } from './constants.js'; | ||
@@ -21,7 +21,7 @@ export class Ky { | ||
| // Before using ky.request, _fetch clones it and saves the clone for future retries to use. | ||
| // If retry is not needed, close both the original and cloned request's ReadableStream for memory safety. | ||
| // If retry is not needed, close the cloned request's ReadableStream for memory safety. | ||
| let response = await ky._fetch(); | ||
| for (const hook of ky._options.hooks.afterResponse) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const modifiedResponse = await hook(ky.request, ky._options, ky._decorateResponse(response.clone())); | ||
| const modifiedResponse = await hook(ky.request, ky.#getNormalizedOptions(), ky._decorateResponse(response.clone()), { retryCount: ky._retryCount }); | ||
| if (modifiedResponse instanceof globalThis.Response) { | ||
@@ -33,6 +33,6 @@ response = modifiedResponse; | ||
| if (!response.ok && ky._options.throwHttpErrors) { | ||
| let error = new HTTPError(response, ky.request, ky._options); | ||
| let error = new HTTPError(response, ky.request, ky.#getNormalizedOptions()); | ||
| for (const hook of ky._options.hooks.beforeError) { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| error = await hook(error); | ||
| error = await hook(error, { retryCount: ky._retryCount }); | ||
| } | ||
@@ -108,2 +108,3 @@ throw error; | ||
| _originalRequest; | ||
| #cachedNormalizedOptions; | ||
| // eslint-disable-next-line complexity | ||
@@ -154,2 +155,10 @@ constructor(input, options = {}) { | ||
| } | ||
| // 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); | ||
@@ -164,7 +173,2 @@ if (hasSearchParameters(this._options.searchParams)) { | ||
| const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams); | ||
| // To provide correct form boundary, Content-Type header should be deleted each time when new Request instantiated from another one | ||
| if (((supportsFormData && this._options.body instanceof globalThis.FormData) | ||
| || this._options.body instanceof URLSearchParams) && !(this._options.headers && this._options.headers['content-type'])) { | ||
| this.request.headers.delete('content-type'); | ||
| } | ||
| // The spread of `this.request` is required as otherwise it misses the `duplex` option for some reason and throws. | ||
@@ -190,6 +194,6 @@ this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options); | ||
| this._retryCount++; | ||
| if (this._retryCount > this._options.retry.limit || error instanceof TimeoutError) { | ||
| if (this._retryCount > this._options.retry.limit || isTimeoutError(error)) { | ||
| throw error; | ||
| } | ||
| if (error instanceof HTTPError) { | ||
| if (isHTTPError(error)) { | ||
| if (!this._options.retry.statusCodes.includes(error.response.status)) { | ||
@@ -241,3 +245,3 @@ throw error; | ||
| request: this.request, | ||
| options: this._options, | ||
| options: this.#getNormalizedOptions(), | ||
| error: error, | ||
@@ -257,3 +261,3 @@ retryCount: this._retryCount, | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const result = await hook(this.request, this._options); | ||
| const result = await hook(this.request, this.#getNormalizedOptions(), { retryCount: this._retryCount }); | ||
| if (result instanceof Request) { | ||
@@ -276,3 +280,10 @@ this.request = result; | ||
| } | ||
| #getNormalizedOptions() { | ||
| if (!this.#cachedNormalizedOptions) { | ||
| const { hooks, ...normalizedOptions } = this._options; | ||
| this.#cachedNormalizedOptions = Object.freeze(normalizedOptions); | ||
| } | ||
| return this.#cachedNormalizedOptions; | ||
| } | ||
| } | ||
| //# sourceMappingURL=Ky.js.map |
@@ -7,3 +7,3 @@ /*! MIT License © Sindre Sorhus */ | ||
| export type { Input, Options, NormalizedOptions, RetryOptions, SearchParamsOption, Progress, } from './types/options.js'; | ||
| export type { Hooks, BeforeRequestHook, BeforeRetryHook, BeforeRetryState, BeforeErrorHook, AfterResponseHook, } from './types/hooks.js'; | ||
| export type { Hooks, BeforeRequestHook, BeforeRequestState, BeforeRetryHook, BeforeRetryState, BeforeErrorHook, BeforeErrorState, AfterResponseHook, AfterResponseState, } from './types/hooks.js'; | ||
| export type { ResponsePromise } from './types/ResponsePromise.js'; | ||
@@ -14,1 +14,2 @@ export type { KyRequest } from './types/request.js'; | ||
| export { TimeoutError } from './errors/TimeoutError.js'; | ||
| export { isKyError, isHTTPError, isTimeoutError } from './utils/type-guards.js'; |
@@ -26,2 +26,3 @@ /*! MIT License © Sindre Sorhus */ | ||
| export { TimeoutError } from './errors/TimeoutError.js'; | ||
| export { isKyError, isHTTPError, isTimeoutError } from './utils/type-guards.js'; | ||
| //# sourceMappingURL=index.js.map |
| import { type stop } from '../core/constants.js'; | ||
| import type { KyRequest, KyResponse, HTTPError } from '../index.js'; | ||
| import type { NormalizedOptions } from './options.js'; | ||
| export type BeforeRequestHook = (request: KyRequest, options: NormalizedOptions) => Request | Response | void | Promise<Request | Response | void>; | ||
| export type BeforeRequestState = { | ||
| /** | ||
| The number of retries attempted. `0` for the initial request, increments with each retry. | ||
| This allows you to distinguish between initial requests and retries, which is useful when you need different behavior for retries (e.g., avoiding overwriting headers set in `beforeRetry`). | ||
| */ | ||
| retryCount: number; | ||
| }; | ||
| export type BeforeRequestHook = (request: KyRequest, options: NormalizedOptions, state: BeforeRequestState) => Request | Response | void | Promise<Request | Response | void>; | ||
| export type BeforeRetryState = { | ||
@@ -9,13 +17,53 @@ request: KyRequest; | ||
| 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 = (options: BeforeRetryState) => typeof stop | void | Promise<typeof stop | void>; | ||
| export type AfterResponseHook = (request: KyRequest, options: NormalizedOptions, response: KyResponse) => Response | void | Promise<Response | void>; | ||
| export type BeforeErrorHook = (error: HTTPError) => HTTPError | Promise<HTTPError>; | ||
| export type AfterResponseState = { | ||
| /** | ||
| The number of retries attempted. `0` for the initial request, increments with each retry. | ||
| This allows you to distinguish between initial requests 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 = (request: KyRequest, options: NormalizedOptions, response: KyResponse, state: AfterResponseState) => Response | void | Promise<Response | void>; | ||
| export type BeforeErrorState = { | ||
| /** | ||
| 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 = (error: HTTPError, state: BeforeErrorState) => HTTPError | Promise<HTTPError>; | ||
| export type Hooks = { | ||
| /** | ||
| 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 normalized input and options as arguments. You could, for example, modify `options.headers` here. | ||
| 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 the normalized request, options, and a state object. You could, for example, modify `request.headers` here. | ||
| The `state.retryCount` is `0` for the initial request and increments with each retry. This allows you to distinguish between initial requests and retries, which is useful when you need different behavior for retries (e.g., avoiding overwriting headers set in `beforeRetry`). | ||
| A [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) can be returned from this hook to completely avoid making a HTTP request. This can be used to mock a request, check an internal cache, etc. An **important** consideration when returning a `Response` from this hook is that all the following hooks will be skipped, so **ensure you only return a `Response` from the last hook**. | ||
| @example | ||
| ``` | ||
| import ky from 'ky'; | ||
| const response = await ky('https://example.com', { | ||
| hooks: { | ||
| beforeRequest: [ | ||
| (request, options, {retryCount}) => { | ||
| // Only set default auth header on initial request, not on retries | ||
| // (retries may have refreshed token set by beforeRetry) | ||
| if (retryCount === 0) { | ||
| request.headers.set('Authorization', 'token initial-token'); | ||
| } | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| @default [] | ||
@@ -51,3 +99,3 @@ */ | ||
| /** | ||
| This hook enables you to read and optionally modify the response. The hook function receives normalized input, options, and a clone of the response as arguments. 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). | ||
| This hook enables you to read and optionally modify the response. The hook function receives normalized request, options, a clone of the response, and a state object. 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). | ||
@@ -63,3 +111,3 @@ @default [] | ||
| afterResponse: [ | ||
| (_input, _options, response) => { | ||
| (_request, _options, response) => { | ||
| // You could do something with the response, for example, logging. | ||
@@ -73,3 +121,3 @@ log(response); | ||
| // Or retry with a fresh token on a 403 error | ||
| async (input, options, response) => { | ||
| async (request, options, response) => { | ||
| if (response.status === 403) { | ||
@@ -82,4 +130,13 @@ // Get a fresh token | ||
| return ky(input, options); | ||
| return ky(request, options); | ||
| } | ||
| }, | ||
| // Or show a notification only on the last retry for 5xx errors | ||
| (request, options, response, {retryCount}) => { | ||
| if (response.status >= 500 && response.status <= 599) { | ||
| if (retryCount === options.retry.limit) { | ||
| showNotification('Request failed after all retries'); | ||
| } | ||
| } | ||
| } | ||
@@ -93,3 +150,3 @@ ] | ||
| /** | ||
| This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` as an argument and should return an instance of `HTTPError`. | ||
| This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` and a state object as arguments and should return an instance of `HTTPError`. | ||
@@ -105,10 +162,20 @@ @default [] | ||
| beforeError: [ | ||
| error => { | ||
| async error => { | ||
| const {response} = error; | ||
| if (response && response.body) { | ||
| if (response) { | ||
| const body = await response.json(); | ||
| error.name = 'GitHubError'; | ||
| error.message = `${response.body.message} (${response.status})`; | ||
| error.message = `${body.message} (${response.status})`; | ||
| } | ||
| return error; | ||
| }, | ||
| // Or show different message based on retry count | ||
| (error, {retryCount}) => { | ||
| if (retryCount === error.options.retry.limit) { | ||
| error.message = `${error.message} (failed after ${retryCount} retries)`; | ||
| } | ||
| return error; | ||
| } | ||
@@ -115,0 +182,0 @@ ] |
@@ -1,30 +0,3 @@ | ||
| type UndiciHeadersInit = string[][] | Record<string, string | readonly string[]> | Headers; | ||
| type UndiciBodyInit = ArrayBuffer | AsyncIterable<Uint8Array> | Blob | FormData | Iterable<Uint8Array> | ArrayBufferView | URLSearchParams | null | string; | ||
| type UndiciRequestRedirect = 'error' | 'follow' | 'manual'; | ||
| type UndiciRequestCredentials = 'omit' | 'include' | 'same-origin'; | ||
| type UndiciReferrerPolicy = '' | 'no-referrer' | 'no-referrer-when-downgrade' | 'origin' | 'origin-when-cross-origin' | 'same-origin' | 'strict-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; | ||
| type UndiciRequestMode = 'cors' | 'navigate' | 'no-cors' | 'same-origin'; | ||
| type UndiciRequestInit = { | ||
| method?: string; | ||
| keepalive?: boolean; | ||
| headers?: UndiciHeadersInit; | ||
| body?: UndiciBodyInit; | ||
| redirect?: UndiciRequestRedirect; | ||
| integrity?: string; | ||
| signal?: AbortSignal | undefined; | ||
| credentials?: UndiciRequestCredentials; | ||
| mode?: UndiciRequestMode; | ||
| referrer?: string; | ||
| referrerPolicy?: UndiciReferrerPolicy; | ||
| window?: undefined; | ||
| dispatcher?: unknown; | ||
| duplex?: unknown; | ||
| }; | ||
| type CombinedRequestInit = globalThis.RequestInit & UndiciRequestInit; | ||
| export type RequestInitRegistry = { | ||
| [K in keyof CombinedRequestInit]-?: true; | ||
| }; | ||
| export type KyRequest<T = unknown> = { | ||
| export interface KyRequest<T = unknown> extends Request { | ||
| json: <J = T>() => Promise<J>; | ||
| } & Request; | ||
| export {}; | ||
| } |
@@ -1,8 +0,2 @@ | ||
| /* | ||
| Undici types need to be here because they are not exported to globals by @types/node. | ||
| See https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/69408 | ||
| After the types are exported to globals, the Undici types can be removed from here. | ||
| */ | ||
| export {}; | ||
| //# sourceMappingURL=request.js.map |
@@ -1,3 +0,3 @@ | ||
| export type KyResponse<T = unknown> = { | ||
| export interface KyResponse<T = unknown> extends Response { | ||
| json: <J = T>() => Promise<J>; | ||
| } & Response; | ||
| } |
@@ -0,1 +1,2 @@ | ||
| import { supportsAbortSignal } from '../core/constants.js'; | ||
| import { isObject } from './is.js'; | ||
@@ -35,2 +36,38 @@ export const validateAndMerge = (...sources) => { | ||
| }); | ||
| const appendSearchParameters = (target, source) => { | ||
| const result = new URLSearchParams(); | ||
| for (const input of [target, source]) { | ||
| if (input === undefined) { | ||
| continue; | ||
| } | ||
| if (input instanceof URLSearchParams) { | ||
| for (const [key, value] of input.entries()) { | ||
| result.append(key, value); | ||
| } | ||
| } | ||
| else if (Array.isArray(input)) { | ||
| for (const pair of input) { | ||
| if (!Array.isArray(pair) || pair.length !== 2) { | ||
| throw new TypeError('Array search parameters must be provided in [[key, value], ...] format'); | ||
| } | ||
| result.append(String(pair[0]), String(pair[1])); | ||
| } | ||
| } | ||
| else if (isObject(input)) { | ||
| for (const [key, value] of Object.entries(input)) { | ||
| if (value !== undefined) { | ||
| result.append(key, String(value)); | ||
| } | ||
| } | ||
| } | ||
| else { | ||
| // String | ||
| const parameters = new URLSearchParams(input); | ||
| for (const [key, value] of parameters.entries()) { | ||
| result.append(key, value); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| // TODO: Make this strongly-typed (no `any`). | ||
@@ -41,2 +78,4 @@ export const deepMerge = (...sources) => { | ||
| let hooks = {}; | ||
| let searchParameters; | ||
| const signals = []; | ||
| for (const source of sources) { | ||
@@ -51,2 +90,20 @@ if (Array.isArray(source)) { | ||
| for (let [key, value] of Object.entries(source)) { | ||
| // Special handling for AbortSignal instances | ||
| if (key === 'signal' && value instanceof globalThis.AbortSignal) { | ||
| signals.push(value); | ||
| continue; | ||
| } | ||
| // Special handling for searchParams | ||
| if (key === 'searchParams') { | ||
| if (value === undefined || value === null) { | ||
| // Explicit undefined or null removes searchParams | ||
| searchParameters = undefined; | ||
| } | ||
| else { | ||
| // First source: keep as-is to preserve type (string/object/URLSearchParams) | ||
| // Subsequent sources: merge and convert to URLSearchParams | ||
| searchParameters = searchParameters === undefined ? value : appendSearchParameters(searchParameters, value); | ||
| } | ||
| continue; | ||
| } | ||
| if (isObject(value) && key in returnValue) { | ||
@@ -67,4 +124,21 @@ value = deepMerge(returnValue[key], value); | ||
| } | ||
| 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 |
+12
-12
| { | ||
| "name": "ky", | ||
| "version": "1.11.0", | ||
| "version": "1.12.0", | ||
| "description": "Tiny and elegant HTTP client based on the Fetch API", | ||
@@ -61,17 +61,17 @@ "license": "MIT", | ||
| "@types/busboy": "^1.5.0", | ||
| "@types/express": "^4.17.17", | ||
| "@types/node": "^20.14.12", | ||
| "@types/express": "^5.0.3", | ||
| "@types/node": "^24.7.1", | ||
| "ava": "^5.3.1", | ||
| "body-parser": "^1.20.2", | ||
| "body-parser": "^2.2.0", | ||
| "busboy": "^1.6.0", | ||
| "del-cli": "^5.1.0", | ||
| "del-cli": "^7.0.0", | ||
| "delay": "^6.0.0", | ||
| "expect-type": "^0.19.0", | ||
| "express": "^4.18.2", | ||
| "jest-leak-detector": "^30.1.0", | ||
| "expect-type": "^1.2.2", | ||
| "express": "^5.1.0", | ||
| "jest-leak-detector": "^30.2.0", | ||
| "pify": "^6.1.0", | ||
| "playwright": "^1.45.3", | ||
| "raw-body": "^2.5.2", | ||
| "tsx": "^4.16.2", | ||
| "typescript": "^5.5.4", | ||
| "playwright": "^1.56.0", | ||
| "raw-body": "^3.0.1", | ||
| "tsx": "^4.20.6", | ||
| "typescript": "^5.9.3", | ||
| "xo": "^0.58.0" | ||
@@ -78,0 +78,0 @@ }, |
+99
-12
@@ -238,2 +238,5 @@ <div align="center"> | ||
| > [!NOTE] | ||
| > Chromium-based browsers automatically retry `408 Request Timeout` responses at the network layer for keep-alive connections. This means requests may be retried by both the browser and ky. If you want to avoid duplicate retries, you can either set `keepalive: false` in your request options (though this may impact performance for multiple requests) or remove `408` from the retry status codes. | ||
| ##### timeout | ||
@@ -259,4 +262,6 @@ | ||
| 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 `request` and `options` as arguments. You could, for example, modify the `request.headers` here. | ||
| 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 the normalized request, options, and a state object. You could, for example, modify the `request.headers` here. | ||
| The `state.retryCount` is `0` for the initial request and increments with each retry. This allows you to distinguish between initial requests and retries, which is useful when you need different behavior for retries (e.g., avoiding overwriting headers set in `beforeRetry`). | ||
| The hook can return a [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) to replace the outgoing request, or return a [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) to completely avoid making an HTTP request. This can be used to mock a request, check an internal cache, etc. An **important** consideration when returning a request or response from this hook is that any remaining `beforeRequest` hooks will be skipped, so you may want to only return them from the last hook. | ||
@@ -270,4 +275,8 @@ | ||
| beforeRequest: [ | ||
| request => { | ||
| request.headers.set('X-Requested-With', 'ky'); | ||
| (request, options, {retryCount}) => { | ||
| // Only set default auth header on initial request, not on retries | ||
| // (retries may have refreshed token set by beforeRetry) | ||
| if (retryCount === 0) { | ||
| request.headers.set('Authorization', 'token initial-token'); | ||
| } | ||
| } | ||
@@ -288,2 +297,4 @@ ] | ||
| 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` and the `Response` object will be available at `error.response`. 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 not be an instance of `HTTPError`. | ||
@@ -313,4 +324,6 @@ | ||
| This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` as an argument and should return an instance of `HTTPError`. | ||
| This hook enables you to modify the `HTTPError` right before it is thrown. The hook function receives a `HTTPError` and a state object as arguments and should return an instance of `HTTPError`. | ||
| The `state.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). | ||
| ```js | ||
@@ -322,10 +335,20 @@ import ky from 'ky'; | ||
| beforeError: [ | ||
| error => { | ||
| async error => { | ||
| const {response} = error; | ||
| if (response && response.body) { | ||
| if (response) { | ||
| const body = await response.json(); | ||
| error.name = 'GitHubError'; | ||
| error.message = `${response.body.message} (${response.status})`; | ||
| error.message = `${body.message} (${response.status})`; | ||
| } | ||
| return error; | ||
| }, | ||
| // Or show different message based on retry count | ||
| (error, state) => { | ||
| if (state.retryCount === error.options.retry.limit) { | ||
| error.message = `${error.message} (failed after ${state.retryCount} retries)`; | ||
| } | ||
| return error; | ||
| } | ||
@@ -342,4 +365,6 @@ ] | ||
| This hook enables you to read and optionally modify the response. The hook function receives normalized request, options, and a clone of the response as arguments. 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). | ||
| This hook enables you to read and optionally modify the response. The hook function receives normalized request, options, a clone of the response, and a state object. 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). | ||
| The `state.retryCount` is `0` for the initial request and increments with each retry. This allows you to distinguish between initial requests and retries, which is useful when you need different behavior for retries (e.g., showing a notification only on the final retry). | ||
| ```js | ||
@@ -368,4 +393,13 @@ import ky from 'ky'; | ||
| return ky(request); | ||
| return ky(request, options); | ||
| } | ||
| }, | ||
| // Or show a notification only on the last retry for 5xx errors | ||
| (request, options, response, {retryCount}) => { | ||
| if (response.status >= 500 && response.status <= 599) { | ||
| if (retryCount === options.retry.limit) { | ||
| showNotification('Request failed after all retries'); | ||
| } | ||
| } | ||
| } | ||
@@ -630,6 +664,7 @@ ] | ||
| If you need to read the actual response when an `HTTPError` has occurred, call the respective parser method on the response object. For example: | ||
| > [!IMPORTANT] | ||
| > When catching an `HTTPError`, you must consume or cancel the `error.response` body to prevent resource leaks (especially in Deno and Bun). | ||
| ```js | ||
| import { HTTPError } from "ky"; | ||
| import {isHTTPError} from 'ky'; | ||
@@ -639,4 +674,8 @@ try { | ||
| } catch (error) { | ||
| if (error instanceof HTTPError) { | ||
| if (isHTTPError(error)) { | ||
| // Option 1: Read the error response body | ||
| const errorJson = await error.response.json(); | ||
| // Option 2: Cancel the body if you don't need it | ||
| // await error.response.body?.cancel(); | ||
| } | ||
@@ -646,2 +685,21 @@ } | ||
| You can also use the `beforeError` hook: | ||
| ```js | ||
| await ky('https://example.com', { | ||
| hooks: { | ||
| beforeError: [ | ||
| async error => { | ||
| const {response} = error; | ||
| if (response) { | ||
| error.message = `${error.message}: ${await response.text()}`; | ||
| } | ||
| return error; | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| ⌨️ **TypeScript:** Accepts an optional [type parameter](https://www.typescriptlang.org/docs/handbook/2/generics.html), which defaults to [`unknown`](https://www.typescriptlang.org/docs/handbook/2/functions.html#unknown), and is passed through to the return type of `error.response.json()`. | ||
@@ -683,2 +741,31 @@ | ||
| #### Modifying FormData in hooks | ||
| If you need to modify FormData in a `beforeRequest` hook (for example, to transform field names), delete the `Content-Type` header before creating a new `Request`: | ||
| ```js | ||
| import ky from 'ky'; | ||
| const response = await ky.post(url, { | ||
| body: formData, | ||
| hooks: { | ||
| beforeRequest: [ | ||
| request => { | ||
| const newFormData = new FormData(); | ||
| // Modify FormData as needed | ||
| for (const [key, value] of formData) { | ||
| newFormData.set(key.toLowerCase(), value); | ||
| } | ||
| // Delete `Content-Type` to let Request regenerate it with correct boundary | ||
| request.headers.delete('content-type'); | ||
| return new Request(request, {body: newFormData}); | ||
| } | ||
| ] | ||
| } | ||
| }); | ||
| ``` | ||
| ### Setting a custom `Content-Type` | ||
@@ -685,0 +772,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
200967
13.43%69
4.55%1598
19.61%871
11.1%23
15%