ky
Version:
Tiny and elegant HTTP client based on the Fetch API
198 lines (186 loc) • 9.03 kB
JavaScript
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 {
// Unsupported implementations throw different errors, such as QQBrowser on iOS throwing "unsupported BodyInit type" (see issue #581).
return false;
}
}
// Bun and Deno support streaming request bodies but never read the `duplex` option, which the check relies on to tell streaming support apart from WebKit, where the `Request` constructor accepts a stream body that `fetch()` then rejects.
const isBunOrDeno = 'Bun' in globalThis || 'Deno' in globalThis;
return (duplexAccessed || isBunOrDeno) && !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', 'query'];
const validate = () => undefined;
validate();
// The media type each shortcut advertises. Deliberately narrow: `.text()` asks for text and `.formData()` for multipart, so a server can serve a different representation of the same resource when the caller wants one.
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: '*/*',
};
// Only expose `.bytes()` when the environment implements it. Checked on each call, so a polyfill added after Ky is imported still counts.
export const getSupportedResponseTypes = () => Object.keys(responseTypes).filter(type => type !== 'bytes' || typeof globalThis.Response?.prototype?.bytes === 'function');
// 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. Bun's boundary is 54 bytes, so there the estimate runs 14 bytes low for each part and once more for the closing boundary. That is negligible next to a file, but for a body of many small fields `percent` can reach its ceiling before the upload ends. The final event always reports the real total.
export const usualFormBoundarySize = 40;
/**
Symbol that can be returned by a `beforeRetry` hook to stop retrying without throwing an error.
Registered globally so that a hook written against a second copy of Ky, which happens with a duplicated dependency, still stops the retries. A plain `Symbol` is per module, so the copies would not recognise each other's symbol.
*/
export const stop = Symbol.for('ky.stop');
/**
Marker returned by `ky.retry()` to signal a forced retry from `afterResponse` hooks.
*/
export class RetryMarker {
// Branded so a marker created by a second copy of Ky, which happens with a duplicated dependency, is still recognised. A plain `instanceof` check fails across module instances, and the hook's intent would then vanish silently: no retry and no error.
get isRetryMarker() {
return true;
}
options;
constructor(options) {
this.options = options;
}
}
export const isRetryMarker = (value) => value instanceof RetryMarker || value?.isRetryMarker === true;
/**
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`.
A marker returned by a second copy of Ky, which happens with a duplicated dependency, is still recognised, the same way the Ky error guards accept branded errors.
@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);
// Ky reads these again for every attempt, so a `beforeRetry` hook can change them without rebuilding the request.
export const beforeRetryOptionKeys = ['onUploadProgress', 'onDownloadProgress', 'timeout', 'fetch', 'throwHttpErrors'];
export const kyOptionKeys = {
json: true,
parseJson: true,
stringifyJson: true,
searchParams: true,
baseUrl: true,
prefix: true,
retry: true,
timeout: true,
totalTimeout: true,
maxResponseSize: 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