@@ -11,2 +11,3 @@ import type { Input, InternalOptions, Options } from '../types/options.js'; | ||
| protected _options: InternalOptions; | ||
| protected _originalRequest?: Request; | ||
| constructor(input: Input, options?: Options); | ||
@@ -13,0 +14,0 @@ protected _calculateRetryDelay(error: unknown): number; |
@@ -20,3 +20,3 @@ import { HTTPError } from '../errors/HTTPError.js'; | ||
| // Before using ky.request, _fetch clones it and saves the clone for future retries to use. | ||
| // If retry is not needed, close the cloned request's ReadableStream for memory safety. | ||
| // If retry is not needed, close both the original and cloned request's ReadableStream for memory safety. | ||
| let response = await ky._fetch(); | ||
@@ -54,6 +54,11 @@ for (const hook of ky._options.hooks.afterResponse) { | ||
| .finally(async () => { | ||
| // Now that we know a retry is not needed, close the ReadableStream of the cloned request. | ||
| const originalRequest = ky._originalRequest; | ||
| const cleanupPromises = []; | ||
| if (originalRequest && !originalRequest.bodyUsed) { | ||
| cleanupPromises.push(originalRequest.body?.cancel()); | ||
| } | ||
| if (!ky.request.bodyUsed) { | ||
| await ky.request.body?.cancel(); | ||
| cleanupPromises.push(ky.request.body?.cancel()); | ||
| } | ||
| await Promise.all(cleanupPromises); | ||
| }); | ||
@@ -101,2 +106,3 @@ for (const [type, mimeType] of Object.entries(responseTypes)) { | ||
| _options; | ||
| _originalRequest; | ||
| // eslint-disable-next-line complexity | ||
@@ -257,10 +263,10 @@ constructor(input, options = {}) { | ||
| // Cloning is done here to prepare in advance for retries | ||
| const mainRequest = this.request; | ||
| this.request = mainRequest.clone(); | ||
| this._originalRequest = this.request; | ||
| this.request = this._originalRequest.clone(); | ||
| if (this._options.timeout === false) { | ||
| return this._options.fetch(mainRequest, nonRequestOptions); | ||
| return this._options.fetch(this._originalRequest, nonRequestOptions); | ||
| } | ||
| return timeout(mainRequest, nonRequestOptions, this.abortController, this._options); | ||
| return timeout(this._originalRequest, nonRequestOptions, this.abortController, this._options); | ||
| } | ||
| } | ||
| //# sourceMappingURL=Ky.js.map |
@@ -7,3 +7,3 @@ export class HTTPError extends Error { | ||
| const code = (response.status || response.status === 0) ? response.status : ''; | ||
| const title = response.statusText || ''; | ||
| const title = response.statusText ?? ''; | ||
| const status = `${code} ${title}`.trim(); | ||
@@ -10,0 +10,0 @@ const reason = status ? `status code ${status}` : 'an unknown error'; |
@@ -45,9 +45,34 @@ import { usualFormBoundarySize } from '../core/constants.js'; | ||
| }; | ||
| 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) => { | ||
| const totalBytes = Number(response.headers.get('content-length')) || 0; | ||
| let transferredBytes = 0; | ||
| if (!response.body) { | ||
| return response; | ||
| } | ||
| if (response.status === 204) { | ||
| if (onDownloadProgress) { | ||
| onDownloadProgress({ percent: 1, totalBytes, transferredBytes }, new Uint8Array()); | ||
| } | ||
| return new Response(null, { | ||
@@ -59,25 +84,4 @@ status: response.status, | ||
| } | ||
| return new Response(new ReadableStream({ | ||
| async start(controller) { | ||
| const reader = response.body.getReader(); | ||
| if (onDownloadProgress) { | ||
| onDownloadProgress({ percent: 0, transferredBytes: 0, totalBytes }, new Uint8Array()); | ||
| } | ||
| async function read() { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| controller.close(); | ||
| return; | ||
| } | ||
| if (onDownloadProgress) { | ||
| transferredBytes += value.byteLength; | ||
| const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes; | ||
| onDownloadProgress({ percent, transferredBytes, totalBytes }, value); | ||
| } | ||
| controller.enqueue(value); | ||
| await read(); | ||
| } | ||
| await read(); | ||
| }, | ||
| }), { | ||
| const totalBytes = Number(response.headers.get('content-length')) || 0; | ||
| return new Response(withProgress(response.body, totalBytes, onDownloadProgress), { | ||
| status: response.status, | ||
@@ -90,37 +94,13 @@ statusText: response.statusText, | ||
| 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); | ||
| let transferredBytes = 0; | ||
| return new Request(request, { | ||
| // @ts-expect-error - Types are outdated. | ||
| duplex: 'half', | ||
| body: new ReadableStream({ | ||
| async start(controller) { | ||
| const reader = request.body instanceof ReadableStream ? request.body.getReader() : new Response('').body.getReader(); | ||
| async function read() { | ||
| const { done, value } = await reader.read(); | ||
| if (done) { | ||
| // Ensure 100% progress is reported when the upload is complete | ||
| if (onUploadProgress) { | ||
| onUploadProgress({ percent: 1, transferredBytes, totalBytes: Math.max(totalBytes, transferredBytes) }, new Uint8Array()); | ||
| } | ||
| controller.close(); | ||
| return; | ||
| } | ||
| transferredBytes += value.byteLength; | ||
| let percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes; | ||
| if (totalBytes < transferredBytes || percent === 1) { | ||
| percent = 0.99; | ||
| } | ||
| if (onUploadProgress) { | ||
| onUploadProgress({ percent: Number(percent.toFixed(2)), transferredBytes, totalBytes }, value); | ||
| } | ||
| controller.enqueue(value); | ||
| await read(); | ||
| } | ||
| await read(); | ||
| }, | ||
| }), | ||
| body: withProgress(request.body, totalBytes, onUploadProgress), | ||
| }); | ||
| }; | ||
| //# sourceMappingURL=body.js.map |
+2
-2
| { | ||
| "name": "ky", | ||
| "version": "1.10.0", | ||
| "version": "1.11.0", | ||
| "description": "Tiny and elegant HTTP client based on the Fetch API", | ||
@@ -70,3 +70,3 @@ "license": "MIT", | ||
| "express": "^4.18.2", | ||
| "jest-leak-detector": "^29.7.0", | ||
| "jest-leak-detector": "^30.1.0", | ||
| "pify": "^6.1.0", | ||
@@ -73,0 +73,0 @@ "playwright": "^1.45.3", |
+3
-1
@@ -625,6 +625,8 @@ <div align="center"> | ||
| ```js | ||
| import { HTTPError } from "ky"; | ||
| try { | ||
| await ky('https://example.com').json(); | ||
| } catch (error) { | ||
| if (error.name === 'HTTPError') { | ||
| if (error instanceof HTTPError) { | ||
| const errorJson = await error.response.json(); | ||
@@ -631,0 +633,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
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
784
0.26%177178
-0.76%1336
-0.96%