+43
-21
@@ -27,17 +27,31 @@ import { HTTPError } from '../errors/HTTPError.js'; | ||
| const clonedResponse = ky.#decorateResponse(response.clone()); | ||
| // eslint-disable-next-line no-await-in-loop | ||
| const modifiedResponse = await hook(ky.request, ky.#getNormalizedOptions(), clonedResponse, { retryCount: ky.#retryCount }); | ||
| if (modifiedResponse instanceof globalThis.Response) { | ||
| response = modifiedResponse; | ||
| let modifiedResponse; | ||
| try { | ||
| // eslint-disable-next-line no-await-in-loop | ||
| modifiedResponse = await hook(ky.request, ky.#getNormalizedOptions(), clonedResponse, { retryCount: ky.#retryCount }); | ||
| } | ||
| catch (error) { | ||
| // Cancel both responses to prevent memory leaks when hook throws | ||
| ky.#cancelResponseBody(clonedResponse); | ||
| ky.#cancelResponseBody(response); | ||
| throw error; | ||
| } | ||
| if (modifiedResponse instanceof RetryMarker) { | ||
| // Cancel both the cloned response passed to the hook and the current response | ||
| // to prevent resource leaks (especially important in Deno/Bun) | ||
| // eslint-disable-next-line no-await-in-loop | ||
| await Promise.all([ | ||
| clonedResponse.body?.cancel(), | ||
| response.body?.cancel(), | ||
| ]); | ||
| // Cancel both the cloned response passed to the hook and the current response to prevent resource leaks (especially important in Deno/Bun). | ||
| // Do not await cancellation since hooks can clone the response, leaving extra tee branches that keep cancel promises pending per the Streams spec. | ||
| ky.#cancelResponseBody(clonedResponse); | ||
| ky.#cancelResponseBody(response); | ||
| throw new ForceRetryError(modifiedResponse.options); | ||
| } | ||
| // Determine which response to use going forward | ||
| const nextResponse = modifiedResponse instanceof globalThis.Response ? modifiedResponse : response; | ||
| // Cancel any response bodies we won't use to prevent memory leaks. | ||
| // Uses fire-and-forget since hooks may have cloned the response, creating tee branches that block cancellation. | ||
| if (clonedResponse !== nextResponse) { | ||
| ky.#cancelResponseBody(clonedResponse); | ||
| } | ||
| if (response !== nextResponse) { | ||
| ky.#cancelResponseBody(response); | ||
| } | ||
| response = nextResponse; | ||
| } | ||
@@ -63,3 +77,5 @@ ky.#decorateResponse(response); | ||
| } | ||
| return streamResponse(response.clone(), ky.#options.onDownloadProgress); | ||
| const progressResponse = response.clone(); | ||
| ky.#cancelResponseBody(response); | ||
| return streamResponse(progressResponse, ky.#options.onDownloadProgress); | ||
| } | ||
@@ -71,12 +87,7 @@ return response; | ||
| const result = ky.#retry(function_) | ||
| .finally(async () => { | ||
| .finally(() => { | ||
| const originalRequest = ky.#originalRequest; | ||
| const cleanupPromises = []; | ||
| if (originalRequest && !originalRequest.bodyUsed) { | ||
| cleanupPromises.push(originalRequest.body?.cancel()); | ||
| } | ||
| if (!ky.request.bodyUsed) { | ||
| cleanupPromises.push(ky.request.body?.cancel()); | ||
| } | ||
| await Promise.all(cleanupPromises); | ||
| // Ignore cancellation errors from already-locked or already-consumed streams. | ||
| ky.#cancelBody(originalRequest?.body ?? undefined); | ||
| ky.#cancelBody(ky.request.body ?? undefined); | ||
| }); | ||
@@ -287,2 +298,13 @@ for (const [type, mimeType] of Object.entries(responseTypes)) { | ||
| } | ||
| #cancelBody(body) { | ||
| if (!body) { | ||
| return; | ||
| } | ||
| // Ignore cancellation failures from already-locked or already-consumed streams. | ||
| void body.cancel().catch(() => undefined); | ||
| } | ||
| #cancelResponseBody(response) { | ||
| // Ignore cancellation failures from already-locked or already-consumed streams. | ||
| this.#cancelBody(response.body ?? undefined); | ||
| } | ||
| async #retry(function_) { | ||
@@ -289,0 +311,0 @@ try { |
@@ -6,3 +6,4 @@ import type { LiteralUnion, Required } from './common.js'; | ||
| export type SearchParamsOption = SearchParamsInit | Record<string, string | number | boolean | undefined> | Array<Array<string | number | boolean>>; | ||
| export type HttpMethod = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete'; | ||
| export type RequestHttpMethod = 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete'; | ||
| export type HttpMethod = LiteralUnion<RequestHttpMethod | 'options' | 'trace', string>; | ||
| export type Input = string | URL | Request; | ||
@@ -9,0 +10,0 @@ export type Progress = { |
@@ -0,1 +1,2 @@ | ||
| import type { HttpMethod } from './options.js'; | ||
| export type ShouldRetryState = { | ||
@@ -23,3 +24,3 @@ /** | ||
| */ | ||
| methods?: string[]; | ||
| methods?: HttpMethod[]; | ||
| /** | ||
@@ -26,0 +27,0 @@ The HTTP status codes allowed to retry. |
@@ -27,2 +27,3 @@ import { requestMethods } from '../core/constants.js'; | ||
| } | ||
| retry.methods &&= retry.methods.map(method => method.toLowerCase()); | ||
| if (retry.statusCodes && !Array.isArray(retry.statusCodes)) { | ||
@@ -29,0 +30,0 @@ throw new Error('retry.statusCodes must be an array'); |
@@ -5,3 +5,3 @@ import { HTTPError } from '../errors/HTTPError.js'; | ||
| /** | ||
| Type guard to check if an error is a Ky error (HTTPError or TimeoutError). | ||
| Type guard to check if an error is a Ky error. | ||
@@ -27,3 +27,3 @@ @param error - The error to check | ||
| */ | ||
| export declare function isKyError(error: unknown): error is HTTPError | TimeoutError; | ||
| export declare function isKyError(error: unknown): error is HTTPError | TimeoutError | ForceRetryError; | ||
| /** | ||
@@ -30,0 +30,0 @@ Type guard to check if an error is an HTTPError. |
@@ -5,3 +5,3 @@ import { HTTPError } from '../errors/HTTPError.js'; | ||
| /** | ||
| Type guard to check if an error is a Ky error (HTTPError or TimeoutError). | ||
| Type guard to check if an error is a Ky error. | ||
@@ -28,3 +28,3 @@ @param error - The error to check | ||
| export function isKyError(error) { | ||
| return isHTTPError(error) || isTimeoutError(error); | ||
| return isHTTPError(error) || isTimeoutError(error) || isForceRetryError(error); | ||
| } | ||
@@ -31,0 +31,0 @@ /** |
+1
-1
| { | ||
| "name": "ky", | ||
| "version": "1.14.1", | ||
| "version": "1.14.2", | ||
| "description": "Tiny and elegant HTTP client based on the Fetch API", | ||
@@ -5,0 +5,0 @@ "license": "MIT", |
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
280981
1.36%2327
1.09%