# ky · GPT-6.1 Sol # Ky quick start Install Ky, send a JSON request, consume its response, and handle an HTTP failure. Ky builds on the Fetch API, so you use standard Fetch inputs and options with Ky's additional options. ## Prerequisites Use Node.js 22 or later and npm for this tutorial. Ky also targets modern browsers, Bun, and Deno; no framework is required. The examples use JavaScript ES modules and absolute URLs so you can run them in Node. ## 1. Install Ky Run this command in your project: ```bash npm install ky ``` Import the default export as [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default). You do not need to construct a client before making a request. ## 2. Send JSON and consume the response Save this as `request.mjs`. It sends a POST request to HTTPBin's inspection endpoint and logs the parsed response body. ```js title="request.mjs" import ky from 'ky'; async function main() { const json = await ky.post('https://httpbin.org/anything', { json: {foo: true}, }).json(); console.log(json); } void main(); ``` Run it: ```bash node request.mjs ``` Look for your outgoing JSON in the service's response. The `json` option serializes `{foo: true}` with `JSON.stringify()` and sets `Content-Type` to `application/json` unless you override that header through `headers`. The `.json()` shortcut parses the incoming response without first awaiting a `Response`. It also sets an appropriate `Accept` header. In TypeScript, its result defaults to `unknown`; see [Send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for typed and validated responses. If you need status or headers before reading the body, await the request instead: ```js title="response.mjs" import ky from 'ky'; async function main() { const response = await ky.get('https://httpbin.org/anything'); console.log(response.status, response.headers.get('content-type')); const json = await response.json(); console.log(json); } void main(); ``` Run `node response.mjs` to inspect the response metadata and parsed JSON separately. ## 3. Handle an HTTP failure By default, a non-2xx response throws an [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror). Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to narrow the caught error before accessing its response. Save this as `failure.mjs`. It adds HTTP-failure handling to the JSON request: ```js title="failure.mjs" import ky, {isHTTPError} from 'ky'; async function main() { try { const json = await ky.post('https://httpbin.org/anything', { json: {foo: true}, }).json(); console.log(json); } catch (error) { if (isHTTPError(error)) { console.error('HTTP error status:', error.response.status); console.error('Error body:', error.data); } else { throw error; } } } void main(); ``` Run `node failure.mjs`. A successful request logs the parsed JSON. If the service rejects the request with a non-2xx response, the HTTP-error branch logs its status and any available error data. Other failures are rethrown rather than treated as HTTP responses. For error-body handling and retry implications, continue to [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors). ## What you have You now have a JSON POST request, two ways to consume a successful response, and an HTTP-failure handler using Ky's type guard. - Read [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the core ideas. - Use [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) to share request options. - See [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) to control retry behavior. # How Ky works Ky is an HTTP client built on Fetch that adds body shortcuts, error handling, retries, composable defaults, and lifecycle hooks. These five ideas explain what you pass to [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default), what you get back, and where to customize a request. ## 1. Fetch is the foundation Pass a string, `URL`, or `Request`, with standard Fetch options plus Ky's additional options. Await the call to get a response with standard properties such as `status` and `headers`. ```ts import ky from 'ky'; const url = new URL('https://api.example.com/users'); async function main() { const response = await ky(url, { credentials: 'omit', headers: {'X-Client': 'dashboard'}, }); console.log(response.status, response.headers.get('content-type')); } main().catch(console.error); ``` This sends a GET request and logs the status and content type your service returns. The samples use absolute URLs so they also work outside the browser; point them at your service. Keep using standard web APIs: pass `FormData` or a `ReadableStream` as `body`, and an `AbortController`'s signal as `signal`. See [files and streams](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/transfer-files-and-streams) and [cancellation](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) for those tasks. ## 2. Request and consume a response in one expression Call a body shortcut directly on the request promise, without awaiting the response first. Ky sets an appropriate `Accept` header for the shortcut unless you already supplied one. The `json` option handles outgoing data: it serializes the value and sets `Content-Type: application/json` unless your `headers` option specifies another content type. The `.json()` shortcut handles incoming data and returns `unknown` by default. ```ts import ky from 'ky'; async function main() { const result = await ky.post('https://api.example.com/users', { json: {name: 'Ada'}, }).json(); console.log(result); } main().catch(console.error); ``` This sends JSON and gives you the parsed response body. Inspect your service's result rather than assuming it echoes the submitted object. Use `.json()` when you have an application type named `User`. A type parameter describes the result to TypeScript; it does not validate the response. For runtime validation, pass a Standard Schema compatible validator, such as a Zod schema, to `.json(schema)`. A rejected value throws [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror). See [send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for a complete typed and validated example. Other body shortcuts include `.text()`, `.formData()`, `.arrayBuffer()`, and `.blob()`. The `.bytes()` shortcut exists only when the runtime supports `Response.prototype.bytes()`. ## 3. Errors and retries are a policy, not manual loops By default, a non-2xx HTTP response becomes an [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror). Network failures and timeouts have distinct types: [`NetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#networkerror) and [`TimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#timeouterror). Automatic retries are bounded by `retry.limit` and restricted by method and failure type. The default limit is two retries; POST is not among the default retriable methods. `retry.shouldRetry` can override the default failure checks, but only after the limit and method checks pass. Timeouts do not trigger retries by default. Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to narrow a caught error and inspect its status and pre-parsed error data. ```ts import ky, {isHTTPError} from 'ky'; async function main() { try { const result = await ky.get('https://api.example.com/users', { retry: {limit: 2}, timeout: 5000, totalTimeout: 15_000, }).json(); console.log(result); } catch (error) { if (isHTTPError(error)) { console.error(error.response.status, error.data); } else { throw error; } } } main().catch(console.error); ``` This request allows up to two retries for eligible failures; it does not guarantee a retry or success. `timeout` gives each attempt five seconds to get a response, and body shortcuts use it as a separate body-read timeout. `totalTimeout` bounds the overall operation, including retries and delays, to fifteen seconds. `beforeError` hooks are outside that overall budget. See [retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for eligibility and delay controls, and [handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for error-body handling, `throwHttpErrors`, and schema-error classification. ## 4. Compose defaults into specialized instances Use `ky.create()` for complete new defaults. Use `.extend()` to inherit and merge a parent's defaults: hooks append, headers merge, and search parameters accumulate. [`replaceOption`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#replaceoption) replaces an inherited value instead of merging it. ```ts import ky, {replaceOption} from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/', headers: {'X-Client': 'dashboard'}, searchParams: {locale: 'en'}, }); const usersApi = api.extend({ headers: {'X-Feature': 'users'}, searchParams: replaceOption({locale: 'fr'}), }); async function main() { const result = await usersApi.get('users').json(); console.log(result); } main().catch(console.error); ``` The request goes to `https://api.example.com/users?locale=fr` with both custom headers. The child inherits the base URL and client header, adds a feature header, and replaces the parent's search parameters. Creating the child does not change the parent. See [instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) for composition and [resolve request URLs](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/resolve-request-urls) for URL resolution. ## 5. Hooks belong to specific lifecycle stages Hooks customize a stage rather than acting as interchangeable interceptors. For the ordinary network request path, the stages relate as follows: ```mermaid flowchart TD A["init: change options"] --> B["Construct Request"] B --> C["beforeRequest: initial request"] C --> D["Fetch attempt"] D --> E["afterResponse: read or replace response"] D --> F["Network failure or timeout"] E --> G["Check HTTP status"] G --> H["Return response or consume body"] G --> I["Retry decision"] F --> I E -->|"ky.retry()"| I I -->|"Retry confirmed"| J["Delay and beforeRetry"] J --> D I -->|"No retry"| K["beforeError, then throw"] ``` - `init` synchronously changes options before request construction. - `beforeRequest` changes the initial outgoing request and runs once, with `retryCount: 0`. - `beforeRetry` changes a request only after a retry is confirmed. - `afterResponse` receives a response clone to read, or can return a replacement `Response`. - `beforeError` receives the error before it is thrown and must return an `Error`. ```ts import ky from 'ky'; const api = ky.extend({ hooks: { beforeRequest: [({request}) => { request.headers.set('X-Client', 'dashboard'); }], afterResponse: [({response, retryCount}) => { console.log('Response status:', response.status, 'Retries:', retryCount); }], }, }); async function main() { const result = await api.get('https://api.example.com/users').json(); console.log(result); } main().catch(console.error); ``` This adds a header to the outgoing request and logs each response that reaches `afterResponse`; it leaves the response unchanged for `.json()` to consume. Both `beforeRequest` and `beforeRetry` can return a `Request` to replace the outgoing request, or a `Response` to bypass the corresponding network attempt. An `afterResponse` hook can return `ky.retry()` to retry based on response content—even a successful HTTP status. That forced retry still respects `retry.limit`, but bypasses the method check and `shouldRetry`. See [request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) for hook return values and [authenticate requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/authenticate-requests) for authentication hooks. ## Next steps Start with the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) to make your first request, or follow [send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) to give your response data a runtime contract. # Request lifecycle The request lifecycle is the sequence in which [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) turns Fetch inputs and options into a request, handles attempts, and returns a response for you to consume. Ky builds on the Fetch API: pass a string, `URL`, or `Request`, with Fetch options plus Ky's additional options. Hooks modify specific stages of this sequence, not an interchangeable interception point. ## From options to a response Ky merges instance defaults with the options for the call, runs synchronous `init` hooks, then constructs the request. URL resolution, headers, search parameters, and an outgoing `json` body take effect before `beforeRequest` runs. ```mermaid flowchart TD A["Input and merged options"] --> B["init: modify options"] B --> C["Construct Request"] C --> D["beforeRequest: initial request only"] D --> E["Fetch attempt"] E -->|Response| F["afterResponse: read or replace a clone"] E -->|Network failure or timeout| G["Retry decision"] F -->|Unsuccessful HTTP status| G F -->|"ky.retry()"| G G -->|Retry allowed| H["Delay, then beforeRetry"] H --> E G -->|No retry| I["beforeError, then reject"] F -->|Accepted response| J["Resolve with Response"] J --> K["Caller reads body or uses a Ky shortcut"] ``` The diagram shows the normal network path; returning a `Response` from `beforeRequest` stops the remaining hooks at that stage, whereas returning a `Request` lets them continue with the replacement—see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for request replacement and network bypass. By default, a non-2xx response becomes an [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) after `afterResponse` runs. This order lets an `afterResponse` hook replace the response before Ky checks its status. ## Hooks across attempts Use `init` for mutable options, `beforeRequest` for the initial outgoing request, and `beforeRetry` for a retry Ky has already approved. Later hooks receive the request and read-only [`NormalizedOptions`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-normalizedoptions#normalizedoptions); inspect `request` for the actual request state if a hook replaces it, because `options` remains Ky's normalized snapshot. This sample runs in a browser or Node.js with Fetch support. It requests users with a search parameter and a header, and traces the stages in execution order, with `retryCount` distinguishing the initial attempt from retries. Use the trace to locate where your request stops or starts another attempt; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for each hook's role. ```ts import ky from 'ky'; const api = ky.extend({ baseUrl: 'https://api.example.com/', retry: 2, hooks: { init: [options => { options.searchParams = {active: true}; console.log('init'); }], beforeRequest: [({request, retryCount}) => { request.headers.set('x-client', 'lifecycle-example'); console.log('beforeRequest', retryCount, request.url); }], beforeRetry: [({request, error, retryCount}) => { console.log('beforeRetry', retryCount, request.url, error.message); }], afterResponse: [({response, retryCount}) => { console.log('afterResponse', retryCount, response.status); }], beforeError: [({error, retryCount}) => { console.error('beforeError', retryCount, error.message); return error; }], }, }); async function loadUsers(): Promise { try { const users = await api.get('users').json(); console.log(users); } catch (error) { console.error('Request failed', error); } } void loadUsers(); ``` Two boundaries affect this trace: an error thrown in `init` bypasses `beforeError`, and returning either a `Request` or a `Response` from `beforeRetry` skips its remaining hooks—see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the hook contracts. Read the retry loop as a decision followed by a delay and only then a confirmed-retry hook; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for ordinary and forced retry eligibility. See [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for policy configuration and [Authenticate requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/authenticate-requests) for changing credentials at these stages. ## Returning a response versus consuming its body A Ky call returns a [`ResponsePromise`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-responsepromise#responsepromise): await it to get a [`KyResponse`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#kyresponse), or call a body shortcut on it to request and consume the response in one expression. These are two separate requests. The first returns a response whose headers and body you handle yourself. The second sends JSON and returns parsed response data rather than a response object. ```ts import ky from 'ky'; async function requestUsers(): Promise { const response = await ky.get('https://api.example.com/users'); console.log(response.status, response.headers.get('content-type')); const users = await response.json(); console.log(users); const createdUser = await ky.post('https://api.example.com/users', { json: {name: 'Ada'}, }).json(); console.log(createdUser); } void requestUsers().catch(error => { console.error('Request failed', error); }); ``` Here, the first call hands body consumption to your code after the request resolves, while the second keeps it inside the shortcut operation; see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for JSON options and shortcuts, and [Send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for response validation. The distinction matters for timeouts and errors: Once the response arrives, a shortcut body-read network failure or timeout reaches `beforeError` without re-entering the retry loop, whereas a body read on the returned response is outside that lifecycle; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for timeout budgets and HTTP-error handling. For error narrowing and recovery, see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors). For how defaults compose before this lifecycle starts, see [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults). # Instances and defaults A Ky instance is a reusable client with custom defaults, so you can configure a particular API once instead of repeating options on every request. Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to compose niche-specific clients: `create()` starts with new defaults, while `extend()` inherits and merges its parent's defaults. Both return a new [`KyInstance`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#kyinstance); neither changes the parent. Requests use standard Fetch inputs and options, with Ky's additional options. ## How defaults compose An instance stores defaults. Extending it combines those defaults with another option layer; making a request combines the resulting defaults with that request's options. ```mermaid flowchart TD A["ky"] -->|"create(defaults)"| B["API client: new defaults"] B -->|"extend(options)"| C["Specialized client: merged defaults"] C -->|"get(input, options)"| D["Request: defaults merged with request options"] B -->|"create(defaults)"| E["Independent client: no inherited defaults"] ``` The merge is not a blanket replacement: | Option | What happens when you add another layer | | --- | --- | | `headers` | Names merge case-insensitively; a later value replaces the same header. | | `hooks` | Hook arrays append, preserving parent hooks before added hooks. | | `searchParams` | Parameters accumulate, including repeated names. | | `context` | Top-level properties merge; nested objects replace rather than merge. | | `signal` | Signals combine when the runtime supports `AbortSignal.any()`. | | Scalar options such as `timeout` | A later value replaces the inherited value. | Plain objects generally deep-merge and arrays append. Use [`replaceOption`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#replaceoption) when an inherited merged value must be replaced instead. ## Create and specialize a client Pass an [`Options`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-options#options) object to `create()`. This example uses `baseUrl` to resolve relative inputs and a `beforeRequest` hook to show each outgoing URL and header. Run these TypeScript examples in Node.js or a browser with Fetch support; the absolute API addresses also work as URL bases in Node.js. Point them at your service to consume its responses. ```ts import ky, {type Options} from 'ky'; const defaults: Options = { baseUrl: 'https://api.example.com/api/', timeout: 5000, headers: {'x-client': 'dashboard'}, hooks: { beforeRequest: [({request}) => { console.log(request.url, request.headers.get('x-client')); }], }, }; const api = ky.create(defaults); const usersApi = api.extend(parentOptions => ({ baseUrl: new URL('users/', parentOptions.baseUrl), })); async function main() { const user = await usersApi.get('123', { headers: {'x-client': 'user-details'}, }).json(); console.log(user); const version = await api.get('version').text(); console.log(version); const publicApi = api.create({ baseUrl: 'https://api.example.com/public/', }); const status = await publicApi.get('status').text(); console.log(status); } main().catch(console.error); ``` The specialized client requests `https://api.example.com/api/users/123` with `x-client: user-details`. The parent still requests `https://api.example.com/api/version` with `x-client: dashboard`. The function passed to `extend()` reads parent defaults and returns the options to merge with them. The `publicApi` request uses `/public/status` without inheriting the parent's `x-client` header, hook, or 5000 ms timeout. New defaults do not disable Ky's built-in behavior: without a configured `timeout`, Ky uses 10000 ms. The `.json()` call returns the parsed response body, typed as `unknown` here; `.text()` returns a string. Look at your service's response rather than expecting a particular body from these example endpoints. Keep the trailing slash in a `baseUrl` that contains a path. A page-relative input such as `users` extends that path; an origin-relative input such as `/users` resolves from the origin instead. See [Resolve request URLs](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/resolve-request-urls). ## Replace or remove inherited values Wrapping a container with `replaceOption()` replaces that entire inherited container. This example replaces both the header defaults and the hooks, so the request uses the new logging hook rather than the parent's hook. ```ts import ky, {replaceOption} from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/', headers: {'x-client': 'dashboard', 'x-private': 'internal'}, hooks: { beforeRequest: [({request}) => { console.log('Parent request:', request.url); }], }, }); const publicApi = api.extend({ headers: replaceOption({accept: 'application/json'}), hooks: replaceOption({ beforeRequest: [({request}) => { console.log('Public request:', request.url); console.log('Inherited private header:', request.headers.has('x-private')); }], }), }); async function main() { const result = await publicApi.get('public/catalog').json(); console.log(result); } main().catch(console.error); ``` The request keeps the inherited `baseUrl`, but neither `x-client` nor `x-private` remains in its header defaults. The public hook runs; the parent hook does not. Replacing headers does not prevent a retained hook from adding headers later, so also review inherited authentication hooks when creating a public client. Choose the narrowest removal you need: - `headers: {'x-private': undefined}` removes one inherited header. In a plain object, the string `'undefined'` is a header value, not a deletion marker. - `hooks: {beforeRequest: undefined}` clears only that hook stage. - `hooks: {beforeRequest: replaceOption([])}` replaces only that stage's array; wrapping the whole `hooks` object replaces all stages. - `headers: undefined`, `hooks: undefined`, or `context: undefined` clears the corresponding inherited container. An empty hook array without `replaceOption()` appends nothing; it does not clear parent hooks. Likewise, `searchParams: {tag: 'new'}` can add another `tag` rather than replace the inherited one. Wrap `searchParams` to replace inherited option parameters. Parameters already in the input URL still participate in URL merging; an object entry with an `undefined` value deletes that parameter from the URL. ## Pass context to hooks Use `context` for request metadata without putting it into the request itself. Hooks receive it through `options.context`, which is always an object and defaults to `{}`. This example follows the authentication-token pattern: the hook reads a token and explicitly adds the authorization header. ```ts import ky from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/', context: { client: 'dashboard', metadata: {source: 'navigation'}, }, hooks: { beforeRequest: [({request, options}) => { const {token} = options.context; if (typeof token === 'string') { request.headers.set('authorization', `Bearer ${token}`); } console.log(options.context); }], }, }); const usersApi = api.extend({ context: {metadata: {section: 'users'}}, }); async function main() { const result = await usersApi.get('users', { context: {token: 'example-token'}, }).json(); console.log(result); } main().catch(console.error); ``` The hook sees `client`, `metadata`, and `token`. Its `metadata` value is `{section: 'users'}`: the parent's nested `source` property is gone because context merges shallowly. Replace the example token with your credential. See [Authenticate requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/authenticate-requests) for authentication behavior. ## Inherited abort signals Passing a new `signal` to `extend()` normally combines it with inherited signals, rather than replacing them. In a runtime with `AbortSignal.any()`, aborting either controller cancels requests using the combined signal. Request-level signals use the same option merge. Use `signal: replaceOption(controller.signal)` to replace inherited signals, or `signal: undefined` to remove them. An already-aborted inherited signal otherwise affects later requests too. Without runtime support for `AbortSignal.any()`, Ky's option merge uses the last signal instead of combining signals. See [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) for cancellation procedures and [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) for the stages in which inherited hooks run. # Authenticate requests Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) hooks to attach an authentication header to the initial request and refresh credentials before a retry. The example below sends a bearer token to one trusted API origin and retries a rejected GET once with a fresh token. ## 1. Attach a token and refresh it on a 401 Install Ky in your project: ```bash npm install ky ``` Run this module in the browser or Node.js with Fetch support. Replace `` and `` with your credentials, and change the API URLs to your service. This example expects `/auth/refresh` to accept a JSON refresh token and return the new access token as plain text. Adapt that request and body reader to your service's contract. Use `beforeRequest` to set `request.headers`. It runs once before retry handling begins, not before every attempt. Use `beforeRetry` to update the header on a confirmed retry, and [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to distinguish an HTTP rejection from other failures. ```ts title="authenticated-request.ts" import ky, {isHTTPError} from 'ky'; const trustedOrigin = 'https://api.example.com'; let accessToken = ''; const refreshToken = ''; function requireTrustedOrigin(request: Request): void { if (new URL(request.url).origin !== trustedOrigin) { throw new Error('Refusing to send credentials to an untrusted origin'); } } const api = ky.extend({ credentials: 'omit', redirect: 'error', retry: { limit: 1, methods: ['get'], statusCodes: [401], }, hooks: { beforeRequest: [({request}) => { requireTrustedOrigin(request); request.headers.set('Authorization', `Bearer ${accessToken}`); }], beforeRetry: [async ({request, error}) => { requireTrustedOrigin(request); if (!isHTTPError(error) || error.response.status !== 401) { return; } accessToken = await ky.post(`${trustedOrigin}/auth/refresh`, { json: {refreshToken}, credentials: 'omit', redirect: 'error', retry: 0, }).text(); request.headers.set('Authorization', `Bearer ${accessToken}`); }], }, }); async function main(): Promise { const response = await api.get(`${trustedOrigin}/users/me`); console.log(await response.text()); } main().catch(console.error); ``` The refresh call uses `ky`, not `api`, so it does not inherit the authentication hooks. In your network inspector or server logs, look for an initial GET with the access token. If your service rejects it with 401, look for a refresh POST followed by a GET with the replacement token. The call returns the final response; the log contains your service's response body, not the refresh token. If authentication still fails, the request rejects after the single retry. 401 is not a default retry status. The explicit `statusCodes: [401]` makes it eligible here; `beforeRetry` does not itself decide whether to retry. See [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) for broader retry policies. ## 2. Keep credentials within the intended origin The example checks the actual `request.url` before adding or refreshing the header, keeps the retry at the same URL, and rejects redirects with `redirect: 'error'`. `credentials: 'omit'` disables Fetch-managed credentials; the bearer header is still set explicitly. Replacement retry requests are used as-is and are not sanitized. Changing origins can forward credentials unintentionally. Before returning a `Request` from `beforeRetry`, or passing one to `ky.retry({request})`, remove every credential you do not want forwarded. For example, delete `Authorization` and any service-specific API-key headers from a copied `Headers` object and construct the replacement with `credentials: 'omit'`. Do not copy a sensitive request body to another origin either. Returning a replacement `Request` from `beforeRetry` skips the remaining `beforeRetry` hooks. Sanitize it before returning it; do not rely on a later hook to strip credentials. Ky replaces its signal with the managed signal for timeout and abort handling, but does not strip its authentication headers. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `hooks.beforeRequest` | Array of hook functions | `[]` | Changes the initial outgoing request; `retryCount` is always `0`. | | `hooks.beforeRetry` | Array of hook functions | `[]` | Changes a confirmed retry; `retryCount` starts at `1`. | | `retry.limit` | `number` | `2` | Bounds retries. The example allows one retry. | | `retry.methods` | Readonly array of HTTP methods | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | Restricts automatic retries to eligible methods. | | `retry.statusCodes` | `readonly number[]` | `[408, 413, 429, 500, 502, 503, 504]` | Selects eligible HTTP failures. The example selects 401. | ## Related - [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains the hook stages, including response-driven retries with `afterResponse` and `ky.retry()`. - [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) explains composing reusable clients with `extend()`. - [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) explains inspecting the final rejection and reading its error data. # Retry failed requests Use a bounded retry policy when a request can recover from a temporary network failure, rate limit, or server error. Configure [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) through its `retry` option; you do not need a separate retry loop. By default, non-2xx responses become [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) instances, and eligible failures pass through the retry policy. A successful operation returns a response; an operation that exhausts its retries rejects with the final error. The examples run in a browser or Node.js with Fetch support. Install Ky in your project: ```bash npm install ky ``` The repository README describes the next version of Ky. When using the published package, consult the [current-version documentation](https://www.npmjs.com/package/ky) rather than assuming every README API is available. ## 1. Set eligibility, delays, and timeout budgets This request fetches users from your API. If it encounters an eligible failure, look for the retry count in the console and the final response status or error. The policy allows three retries after the initial attempt, unless the overall timeout ends the operation first. Replace `https://api.example.com/users` with your API endpoint. Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror) to inspect an HTTP failure and [`isTimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#istimeouterror) to identify a timeout. The example reports the status or URL from your result, rather than assuming which failure wins. ```ts import ky, {isHTTPError, isTimeoutError} from 'ky'; (async () => { try { const response = await ky.get('https://api.example.com/users', { timeout: 2000, totalTimeout: 10_000, retry: { limit: 3, methods: ['get'], statusCodes: [429, 503], afterStatusCodes: [429, 503], maxRetryAfter: 2000, delay: attemptCount => 300 * 2 ** (attemptCount - 1), backoffLimit: 1500, jitter: true, retryOnTimeout: true, }, hooks: { beforeRetry: [({retryCount}) => { console.log('Starting retry:', retryCount); }], }, }); console.log('Response status:', response.status); } catch (error) { if (isHTTPError(error)) { console.log('Final HTTP status:', error.response.status); } else if (isTimeoutError(error)) { console.log('Timeout for:', error.request.url); } else { throw error; } } })().catch(error => console.log('Request ended:', error)); ``` `limit` counts retries, not the initial attempt. Set it to a finite, non-negative integer. Numeric shorthand such as `retry: 3` changes only the limit. Automatic retries also require an eligible method; `post` and `patch` are not in the default method list. `delay` receives the retry attempt count, starting at `1`, and returns milliseconds. The default exponential delay starts at 300 ms, then 600 ms. Full jitter (`jitter: true`) randomizes the computed delay between zero and that value, and `backoffLimit` caps the result. You can instead pass a jitter function that receives the computed delay and returns milliseconds. For statuses in both `statusCodes` and `afterStatusCodes`, Ky uses `Retry-After` or a supported rate-limit timing header when available. `Retry-After` accepts seconds or an HTTP-date. If it is missing, Ky checks `RateLimit-Reset`, `X-RateLimit-Retry-After`, `X-RateLimit-Reset`, and `X-Rate-Limit-Reset`. Server timing bypasses jitter and `backoffLimit`; use `maxRetryAfter` to cap it. HTTP 413 requires a retry timing header under the default retry checks. If a retry delay consumes the remaining `totalTimeout` budget, Ky times out without starting another attempt; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for per-attempt, body-read, and overall timeout boundaries. Enable `retryOnTimeout` to retry timeouts before a response arrives. Timeouts while a body shortcut reads an already received response do not trigger retries. ## 2. Decide whether to retry a failure Use `shouldRetry` to make the retry decision. It runs only after the limit and method checks pass. Return `true` to override the default checks, `false` to stop, or `undefined` to keep the default logic. Recognized network failures are retried by default for eligible methods; unrecognized errors are not. This policy allows only two retries for HTTP 429, even though the configured limit is three. Other HTTP 4xx errors stop immediately, and other failures retain the default policy. Replace `https://api.example.com/users` with your API endpoint; a successful request returns its response without retrying. ```ts import ky, {isHTTPError} from 'ky'; (async () => { try { const response = await ky.get('https://api.example.com/users', { retry: { limit: 3, shouldRetry: ({error, retryCount}) => { if (isHTTPError(error)) { const status = error.response.status; if (status === 429) { return retryCount <= 2; } if (status >= 400 && status < 500) { return false; } } return undefined; }, }, }); console.log('Response status:', response.status); } catch (error) { console.log('Request ended:', error); } })().catch(error => console.log('Request ended:', error)); ``` Returning `true` bypasses status checks, `retryOnTimeout`, and server-timing selection; Ky uses the configured delay calculation instead. Return `undefined` when you want the normal status and timing rules to apply. ## 3. Retry based on response content Return `ky.retry()` from `afterResponse` when a successful HTTP response contains a temporary application error. The hook receives a response clone, so reading its JSON does not consume the response returned to your caller. For an API that returns `{ "error": { "code": "TEMPORARY_ERROR" } }` with HTTP 200, this example requests another attempt with a one-second delay. Other successful JSON responses pass through to the caller. Replace `https://api.example.com/jobs/42` with your API endpoint. Use [`isForceRetryError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isforceretryerror) in `beforeRetry` to identify the forced retry and read its `code`. ```ts import ky, {isForceRetryError} from 'ky'; const api = ky.extend({ retry: {limit: 2}, totalTimeout: 15_000, hooks: { afterResponse: [async ({response}) => { if (response.status === 200) { const data = await response.json<{ error?: {code?: string}; }>(); if (data.error?.code === 'TEMPORARY_ERROR') { return ky.retry({delay: 1000, code: 'TEMPORARY_ERROR'}); } } }], beforeRetry: [({error, retryCount}) => { if (isForceRetryError(error)) { console.log('Forced retry:', retryCount, error.code); } }], }, }); (async () => { try { const result = await api.get('https://api.example.com/jobs/42').json(); console.log('Final JSON:', result); } catch (error) { console.log('Request ended:', error); } })().catch(error => console.log('Request ended:', error)); ``` Forced retries still respect `retry.limit` and the overall timeout, but skip the method check and `shouldRetry`. If the response continues to request retries after the limit, the operation rejects with a [`ForceRetryError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#forceretryerror). Omit `delay` in `ky.retry()` to use the configured retry delay calculation. An explicit `delay` bypasses both jitter and `backoffLimit`. The optional `code` identifies your retry reason in the error passed to hooks. ## 4. Stop a confirmed retry early `beforeRetry` runs after Ky confirms a retry and waits for its delay. Use it to modify the retry request or throw to stop the operation. If this request encounters repeated eligible failures, it sends a retry-count header on the first retry, then throws before a second retry is sent. Replace `https://api.example.com/users` with your API endpoint. ```ts import ky from 'ky'; (async () => { try { const response = await ky.get('https://api.example.com/users', { retry: {limit: 3}, hooks: { beforeRetry: [({request, retryCount}) => { if (retryCount > 1) { throw new Error('Stopping after one retry'); } request.headers.set('X-Retry-Count', String(retryCount)); }], }, }); console.log('Response status:', response.status); } catch (error) { console.log('Request ended:', error); } })().catch(error => console.log('Request ended:', error)); ``` Throwing stops the remaining `beforeRetry` hooks and rejects the operation. Prefer throwing to returning `ky.stop`: that symbol resolves the operation with `undefined`, so response property access and body shortcuts such as `.json()` are invalid. If you deliberately use `ky.stop`, await the request without a body shortcut and check that a response exists before accessing it. Keep retry-attempt changes out of `beforeRequest`, where thrown errors are fatal rather than retried; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the retry decision points and hook stages. ## Options at a glance The `retry.*` fields belong inside the `retry` object; the timeout options sit alongside it. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `retry.limit` | `number` | `2` | Bounds the number of retries; `0` disables them. | | `retry.methods` | `readonly string[]` | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | Selects methods eligible for automatic retries. | | `retry.statusCodes` | `readonly number[]` | `[408, 413, 429, 500, 502, 503, 504]` | Selects HTTP statuses eligible under the default checks. | | `retry.afterStatusCodes` | `readonly number[]` | `[413, 429, 503]` | Selects eligible statuses that use server retry timing. | | `retry.maxRetryAfter` | `number` | `Infinity` | Caps server-provided delays in milliseconds. | | `retry.backoffLimit` | `number` | `Infinity` | Caps calculated delays in milliseconds after jitter. | | `retry.delay` | `(attemptCount: number) => number` | `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000` | Calculates delay in milliseconds. | | `retry.jitter` | `boolean \| ((delay: number) => number)` | `undefined` | Adds randomness to calculated delays. | | `retry.retryOnTimeout` | `boolean` | `false` | Allows retries for pre-response timeouts. | | `retry.shouldRetry` | `({error: Error, retryCount: number}) => boolean \| void \| Promise` | `undefined` | Overrides or defers to default retry checks. | | `timeout` | `number \| false` | `10000` | Sets the per-attempt timeout in milliseconds; `false` disables it. | | `totalTimeout` | `number \| false` | `false` | Bounds the operation in milliseconds; `false` leaves it unbounded overall. | ## Streaming uploads and related tasks Enabled retries buffer an entire streaming request body in memory so it can be replayed. For large streaming uploads that do not need retries, set `retry: {limit: 0}`. See [Transfer files and streams](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/transfer-files-and-streams). - [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) explains `throwHttpErrors` and reading HTTP error data. - [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains where each hook runs. - [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) shows how to share a retry policy across requests. - [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) covers cancellation with Fetch signals. # Resolve request URLs Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to resolve endpoint paths against an API base, prepend a path when leading slashes must append, and merge query parameters into the outgoing URL. Ky builds on Fetch: you pass a string, `URL`, or `Request` and receive a response. The samples run in a browser or Node.js with native Fetch support. They use absolute API addresses so URL construction also works on the server. Replace `https://api.example.com` with your API's origin; inspect the outgoing URLs in the console rather than relying on the service to echo them. ## 1. Resolve paths with `baseUrl` Use `baseUrl` for standard URL resolution. Give a base path a trailing slash and pass a page-relative input such as `'users'` to extend that path. Create an instance with the base as a default, then make requests through it: ```ts import ky from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/v1/', hooks: { beforeRequest: [({request}) => { console.log('Outgoing URL:', request.url); }], }, }); async function main() { await api.get('users'); await api.get('/users'); } main().catch(console.error); ``` The first call requests `/v1/users`; the second requests `/users` at the same origin. Each awaited call returns a response. The hook logs the constructed request URL before Ky sends it. The input's leading slash means origin-root, not “append to the base.” A base without a trailing slash also changes how page-relative inputs resolve: | `baseUrl` | Input | Destination | | --- | --- | --- | | `https://api.example.com/v1/` | `users` | `https://api.example.com/v1/users` | | `https://api.example.com/v1/` | `/users` | `https://api.example.com/users` | | `https://api.example.com/v1` | `users` | `https://api.example.com/users` | An absolute input bypasses `baseUrl`. For example, with no `prefix`, `'https://other.example.com/users'` stays at that address. ## 2. Append leading-slash inputs with `prefix` Prefer `baseUrl` in most cases because it follows web standards. Use `prefix` when inputs such as `/users` must append to an API path instead of discarding it. Ky joins `prefix` and the string input first, trimming trailing slashes from the prefix and leading slashes from the input at the join boundary. It then resolves the joined result against `baseUrl`, if present. ```ts import ky from 'ky'; const api = ky.create({ baseUrl: 'https://api.example.com/', prefix: 'v1', hooks: { beforeRequest: [({request}) => { console.log('Outgoing URL:', request.url); }], }, }); async function main() { await api.get('users'); await api.get('/users'); } main().catch(console.error); ``` Both calls request `https://api.example.com/v1/users`. A trailing slash on `prefix` is optional. Unlike `baseUrl`, `prefix` does string joining even when the string input contains an absolute URL; do not use it as an absolute-URL fallback. ## 3. Merge query parameters, including on a `Request` Pass `searchParams` to add query parameters without building a query string yourself. Existing input parameters remain, and new values append rather than replace existing values with the same name. Use an object value of `undefined` to delete a parameter. A `Request` input ignores `baseUrl` and `prefix`, but `searchParams` still applies: ```ts import ky from 'ky'; const request = new Request( 'https://api.example.com/users?role=reader&debug=true', ); async function main() { await ky.get(request, { baseUrl: 'https://other.example.com/', prefix: 'v1', searchParams: { page: 2, active: true, debug: undefined, }, hooks: { beforeRequest: [({request: outgoingRequest}) => { console.log('Outgoing URL:', outgoingRequest.url); }], }, }); } main().catch(console.error); ``` The outgoing URL stays at `https://api.example.com/users`. It retains `role=reader`, adds `page=2` and `active=true`, and removes `debug`. The request does not move to `other.example.com` or acquire a `/v1/` path. You can also pass a query string, a `URLSearchParams` instance, or an array of key-value pairs. For example, repeated keys represent multiple values; adding another `role` does not erase the input's `role=reader`. ## Options that matter These fields belong to [`Options`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-options#options). | Option | Type | Default | What it does | | --- | --- | --- | --- | | `baseUrl` | `string \| URL` | `undefined` | Resolves relative string inputs after prefix joining; absolute inputs bypass it. | | `prefix` | `string \| URL` | `''` | Prepends to string inputs, normalizing slashes at the join boundary. | | `searchParams` | Query string, object, key-value pairs, or `URLSearchParams` | `''` | Merges query parameters into the input URL; object values of `undefined` delete keys. | ## Server-side and input pitfalls - Server-side requests require an absolute URL. Use an absolute `baseUrl`, as above, rather than a browser-relative base such as `/api/`. In browsers, a relative base resolves against the environment's base URL, such as `document.baseURI`. - To extend a base path, keep its trailing slash and omit the input's leading slash. Use `prefix` instead when leading-slash inputs must append. - `prefix` only affects string inputs. Construct a `URL` or `Request` with its intended destination before passing it to Ky; neither receives prefix joining, and a `Request` also ignores `baseUrl`. ## Related - [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) — share URL options across requests. - [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) — inspect or modify requests with hooks. - [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) — handle failures after constructing the URL. # Handle request errors Use this approach when your application needs different handling for an HTTP error response, a failed connection, or a timeout. [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) treats non-2xx responses as errors by default and retries eligible failures before rejecting the request. The samples run in a browser or a Node.js project with Fetch support. Install Ky in your project: ```bash npm install ky ``` ## 1. Distinguish the failure and read its data An [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) gives you `response`, `request`, normalized `options`, and pre-parsed `data`. Use [`isHTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#ishttperror), [`isNetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isnetworkerror), and [`isTimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#istimeouterror) to narrow the caught value before accessing error-specific properties. - [`NetworkError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#networkerror) represents a network failure, such as DNS failure, connection refusal, or being offline. Read `request.url` and the original error in `cause`; a fetch-phase network failure has no HTTP response. - [`TimeoutError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#timeouterror) identifies an exceeded timeout and gives you the affected `request`. - [`isKyError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#iskyerror) catches other failures in Ky's HTTP lifecycle. Keep a separate fallback for errors outside that lifecycle. This request targets a nonexistent GitHub API endpoint. If the service returns a non-2xx response, the sample logs the HTTP status and parsed error body; if it returns a successful JSON response, the sample logs the response data instead. `retry: 0` disables retries so the example handles one attempt. ```ts import ky, {isHTTPError, isNetworkError, isTimeoutError, isKyError} from 'ky'; void (async () => { try { const data = await ky.get('https://api.github.com/ky-error-example-not-found', { retry: 0, timeout: 5000, }).json(); console.log('Response data:', data); } catch (error) { if (isHTTPError(error)) { console.error('HTTP status:', error.response.status); console.error('Content type:', error.response.headers.get('content-type')); console.error('Error body:', error.data); } else if (isNetworkError(error)) { console.error('Network failure:', error.request.url, error.cause); } else if (isTimeoutError(error)) { console.error('Timeout:', error.request.url); } else if (isKyError(error)) { console.error('Ky failure:', error.message); } else { console.error('Other failure:', error); } } })(); ``` Ky parses JSON error bodies based on `Content-Type`, using `parseJson` when supplied or `JSON.parse` otherwise. Other content types produce text. `data` can be `undefined` when the body is empty, unreadable, too large, fails parsing, or exceeds the error-data read/parse timeout. Check its shape before reading fields. Use the caught error's pre-parsed body for recovery decisions; see [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle#returning-a-response-versus-consuming-its-body) for body consumption and response metadata. ### Response-size failures require the next release Ky 2.1.0 does not include `maxResponseSize`, [`ResponseSizeError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#responsesizeerror), or [`isResponseSizeError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#isresponsesizeerror). These APIs require the next release; do not import them in a 2.1.0 project. With that release, `maxResponseSize` limits consumed response-stream bytes after decompression, independently of `Content-Length`. Exceeding it throws `ResponseSizeError` without automatic retries. Use `isResponseSizeError()` to distinguish it, then read `error.maxResponseSize` for the configured byte limit and `error.request` for the request. This is a body-size limit, not an HTTP status failure or a total-memory limit. ## 2. Treat expected HTTP errors as normal responses Set `throwHttpErrors: false` when checking resource availability and expecting an error response. The call returns the response, leaving you to inspect `ok` or `status` and consume its body. ```ts import ky from 'ky'; void (async () => { try { const response = await ky.get('https://api.github.com/ky-error-example-not-found', { throwHttpErrors: false, }); if (response.status === 404) { console.log('The resource is unavailable.'); } else if (!response.ok) { console.error('Unexpected HTTP status:', response.status); } else { console.log('Response body:', await response.text()); } } catch (error) { console.error('The request failed without a normal HTTP response:', error); } })(); ``` **Disabling HTTP errors also disables status-based automatic retries.** Ky considers those error responses successful. Keep `throwHttpErrors` enabled when relying on status-based retries; disable it only when error responses belong to normal application flow. Network failures and timeouts can still reject the call. For selective handling, pass a function such as `status => status !== 404`. Ky then returns 404 responses normally but throws for other non-2xx statuses. Prefer the boolean form unless you need that distinction. ## 3. Customize the error before it reaches your catch block Use `hooks.beforeError` to modify an error right before Ky throws it. The hook receives `request`, normalized `options`, `error`, and `retryCount`, and returns an `Error` or a promise of one. `retryCount` is `0` for the initial attempt and increments with each retry. The example follows the error body's `message` field, when it is a string, and adds the HTTP status. Returning the same error preserves its HTTP-specific properties for the catch block. If the body lacks that field, the original message remains unchanged. ```ts import ky, {isHTTPError} from 'ky'; const api = ky.extend({ hooks: { beforeError: [ ({error}) => { if ( isHTTPError(error) && typeof error.data === 'object' && error.data !== null && 'message' in error.data && typeof error.data.message === 'string' ) { error.message = `${error.data.message} (${error.response.status})`; } return error; }, ], }, }); void (async () => { try { await api.get('https://api.github.com/ky-error-example-not-found').json(); } catch (error) { if (isHTTPError(error)) { console.error(error.message); } else { console.error(error); } } })(); ``` `error.data` is populated before `beforeError` runs. Use the shortcut form `await ky(url).json()` to send body-read network and timeout failures through this hook as well. Errors from reading an already returned response are outside the request lifecycle. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `throwHttpErrors` | `boolean \| ((status: number) => boolean)` | `true` | Controls whether non-2xx responses throw an HTTP error. | | `hooks.beforeError` | Array of functions returning `Error \| Promise` | `[]` | Modifies or replaces the error before it is thrown. | | `timeout` | `number \| false` | `10000` | Sets the per-attempt timeout in milliseconds; shortcuts also use it as a separate body-read timeout. | | `totalTimeout` | `number \| false` | `false` | Bounds the entire operation, including retries and delays. It does not bound `beforeError` hooks. | ## Handle schema rejection separately When using `.json(schema)`, handle [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror) explicitly with `error instanceof SchemaValidationError` and read its `issues`. It does not extend [`KyError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#kyerror) and `isKyError()` does not match it: the request succeeded, but your schema rejected the data. See [Send and validate JSON](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/send-and-validate-json) for the validation sample. ## Related - [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) — configure which failures get another attempt. - [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) — choose the hook for each stage. - [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) — cancel with an abort signal. # Send and validate JSON Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to send a JSON body and consume the response in one expression. Pass a type parameter when you know the response shape; pass a Standard Schema when you need runtime validation. The examples use absolute URLs and run in the browser or Node.js. Replace `https://api.example.com` with your service's origin. Install Ky in your project: ```bash npm install ky ``` ## 1. Send JSON and read the response Send an array when your endpoint accepts a batch of records; see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for basic JSON serialization and headers. ```ts import ky from 'ky'; async function main() { const result = await ky.post('https://api.example.com/users', { json: [{name: 'Ada'}, {name: 'Grace'}], }).json(); console.log(result); } main().catch(console.error); ``` This sends two records in one JSON array and logs your service's parsed response; see [How Ky works](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/how-ky-works) for the single-record example and the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) for shortcut behavior and the default response type. ## 2. Give the response a TypeScript type Use `.json()` to get a `Promise`. The type parameter describes your application data; it does not validate the response at runtime. ```ts import ky from 'ky'; type User = { name: string; }; async function main() { const user = await ky.get('https://api.example.com/users/1').json(); console.log(user.name); } main().catch(console.error); ``` TypeScript lets you access `user.name` as a string. You can also put the type parameter on the request, as in `ky.get(url).json()`. Use a schema instead when you need to check what the server actually sends. ## 3. Validate the response with a schema Pass a Standard Schema compatible validator, such as Zod 3.24+, to `.json(schema)` to get the schema's validated output; see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for handling [`SchemaValidationError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#schemavalidationerror). Install Zod for this example: ```bash npm install zod ``` ```ts import ky, {SchemaValidationError} from 'ky'; import {z} from 'zod'; const userSchema = z.object({name: z.string()}); async function main() { try { const result = await ky.get('https://api.example.com/users/1').json(userSchema); const user = userSchema.parse(result); console.log(user.name); } catch (error) { if (error instanceof SchemaValidationError) { console.error(error.issues); } else { throw error; } } } main().catch(console.error); ``` With a valid response, `user.name` is a string inferred from the schema. With an invalid response, the catch block logs the issues instead. For handling request failures alongside schema rejection, see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors). ## 4. Customize serialization and parsing Use `stringifyJson` to change the outgoing representation with a replacer. This example omits `internalNote` from the serialized body while retaining `name`: ```ts import ky from 'ky'; async function main() { const result = await ky.post('https://api.example.com/users', { json: {name: 'Ada', internalNote: 'Local draft'}, stringifyJson: data => { const text = JSON.stringify(data, (key, value: unknown) => key === 'internalNote' ? undefined : value, ); if (text === undefined) { throw new TypeError('The request value cannot be serialized to JSON'); } return text; }, }).json(); console.log(result); } main().catch(console.error); ``` Use `parseJson` to change how Ky consumes response text. It receives the text and a context object containing `request` and `response`. Calling `.json()` without a schema on an empty response body throws because the body cannot be parsed. Provide custom empty-body handling with `parseJson`: ```ts import ky from 'ky'; async function main() { const result = await ky.get('https://api.example.com/users/1', { parseJson: (text, {request, response}) => { console.log(`Parsing JSON from ${request.url} (status: ${response.status})`); if (text.trim() === '') { return null; } const parsed: unknown = JSON.parse(text); return parsed; }, }).json(); console.log(result); } main().catch(console.error); ``` This returns `null` for an empty or whitespace-only body and parsed JSON otherwise. Inspect the URL and status in your own request's log. When you also pass a schema to `.json(schema)`, Ky runs `parseJson` before validation, so the schema must accept the value your parser returns. ## Options that matter | Option | Type | Default | What it does | | --- | --- | --- | --- | | `json` | `unknown` | Not set | Serializes an outgoing value into the request body. Use a value accepted by `JSON.stringify()`. | | `stringifyJson` | `(data: unknown) => string` | `JSON.stringify()` | Replaces outgoing JSON serialization. | | `parseJson` | `(text: string, context: {request: Request; response: Response}) => unknown` | `JSON.parse()` | Replaces incoming JSON parsing, including custom empty-body handling. | For JSON content-type headers, see the [quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start). ## Related - [Ky quick start](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/quick-start) — Make your first request. - [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) — Reuse request options across calls. - [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) — Handle HTTP, network, timeout, and schema errors. # Transfer files and streams Use [`ky`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) to send files or generated byte streams and consume downloads with progress callbacks. Ky builds on Fetch: pass standard `FormData`, `Blob`, or `ReadableStream` values through `body`, rather than serializing them with `json`. The samples run in a browser project with `ky` installed: ```bash npm install ky ``` Replace the example API URLs with endpoints that accept your uploads or serve your files. Cross-origin endpoints need the appropriate CORS configuration. Upload progress and streaming uploads also need request stream support; Chromium-based browsers require HTTP/2 for HTTPS connections. ## 1. Upload a multipart file Pass a `FormData` instance to `ky.post()`. This sample sends a small text file and a description, logs upload progress where supported, and logs the response status after the request succeeds. ```ts import ky from 'ky'; const file = new File(['Transfer example\n'], 'notes.txt', { type: 'text/plain', }); const formData = new FormData(); formData.append('file', file); formData.append('description', 'Example notes'); async function uploadFile() { const response = await ky.post('https://api.example.com/uploads', { body: formData, retry: {limit: 0}, onUploadProgress: (progress, chunk) => { console.log({ percent: progress.percent * 100, transferredBytes: progress.transferredBytes, totalBytes: progress.totalBytes, chunkBytes: chunk.byteLength, }); }, }); console.log('Upload response status:', response.status); } uploadFile().catch(console.error); ``` Leave `Content-Type` unset so Fetch generates `multipart/form-data` with the boundary matching the encoded body. An explicit `Content-Type` in `headers` takes precedence; Ky does not repair an explicitly supplied multipart header. If a `beforeRequest` hook replaces the form with a new `FormData`, delete `request.headers`'s `content-type` entry before returning `new Request(request, {body: newFormData})`. This lets the request constructor generate a boundary for the replacement body. For text-only fields that your endpoint expects as `application/x-www-form-urlencoded`, use `URLSearchParams` instead. This sends two encoded fields and returns the response without attempting to parse an upload receipt: ```ts import ky from 'ky'; const fields = new URLSearchParams(); fields.set('food', 'fries'); fields.set('drink', 'icetea'); async function submitForm() { const response = await ky.post('https://api.example.com/orders', { body: fields, }); console.log('Form response status:', response.status); } submitForm().catch(console.error); ``` ## 2. Send a streaming body without retry buffering Pass a `ReadableStream` through `body`. Ky sets `duplex: 'half'` for you in environments with request stream support. This sample sends two text chunks and logs the response status after the request succeeds: ```ts import ky from 'ky'; const encoder = new TextEncoder(); const stream = new ReadableStream({ start(controller) { controller.enqueue(encoder.encode('first line\n')); controller.enqueue(encoder.encode('second line\n')); controller.close(); }, }); async function uploadStream() { const response = await ky.post('https://api.example.com/uploads/raw', { body: stream, headers: {'content-type': 'text/plain'}, retry: {limit: 0}, }); console.log('Stream upload response status:', response.status); } uploadStream().catch(console.error); ``` Set `retry: {limit: 0}` when you do not need replay. With a positive retry limit, Ky clones the request before sending it. Cloning a streaming body uses `tee()` and buffers the body in memory for a possible retry. Disabling retries skips that clone, which matters for large uploads. ## 3. Consume a download and report progress Consume the response body to drive download progress. Calling `await ky.get(url)` alone does not demonstrate a completed download; the callback wraps the response stream. This sample reads each chunk without collecting the whole file in a `Blob`. It logs progress and a completion message for an accepted download, and cancels the reader if the body exceeds a 20 MiB application limit. The limit counts the bytes you actually read, not a server-provided size estimate. ```ts import ky from 'ky'; async function downloadFile() { const maxBytes = 20 * 1024 * 1024; const response = await ky.get('https://api.example.com/files/report.csv', { onDownloadProgress: (progress, chunk) => { console.log({ percent: progress.percent * 100, transferredBytes: progress.transferredBytes, totalBytes: progress.totalBytes, chunkBytes: chunk.byteLength, }); }, }); if (!response.body) { throw new Error('The response has no body stream'); } const reader = response.body.getReader(); let receivedBytes = 0; try { while (true) { const {done, value} = await reader.read(); if (done) { break; } receivedBytes += value.byteLength; if (receivedBytes > maxBytes) { await reader.cancel('Download exceeds the application limit'); throw new Error(`Download exceeds ${maxBytes} bytes`); } console.log('Accepted chunk bytes:', value.byteLength); } console.log('Download consumed:', receivedBytes, 'bytes'); } finally { reader.releaseLock(); } } downloadFile().catch(console.error); ``` If you need the complete file in memory instead, call `await ky.get(url, {onDownloadProgress: callback}).blob()` with your callback. The shortcut returns a `Blob` after consuming the body; it buffers the complete download rather than processing chunks individually. ## Options and progress values The callbacks receive a [`Progress`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#progress) object and a `Uint8Array` chunk. `percent` ranges from `0` to `1`; multiply it by `100` for display. `totalBytes` is an estimate, not a size limit. Download totals start from `Content-Length`; multipart upload sizes are approximate, and a streaming upload may have no known total. | Option | Type | Default | What it does | | --- | --- | --- | --- | | `body` | `BodyInit \| null` | Not set | Sends a standard Fetch body. | | `onUploadProgress` | `(progress: Progress, chunk: Uint8Array) => void` | Not set | Reports upload-stream progress where supported. | | `onDownloadProgress` | `(progress: Progress, chunk: Uint8Array) => void` | Not set | Reports progress as you consume the response stream. | | `retry.limit` | `number` | `2` | Controls retries; `0` skips request cloning for replay. | | `maxResponseSize` (next release only) | `number` | `Infinity` | Limits consumed response-body bytes. | Progress reaches `1` when the wrapped stream finishes. Do not treat upload progress reaching `1` as the server's acknowledgement: await the response separately, as the upload samples do. For an empty body stream, the completion callback receives an empty chunk; a response with no body stream has no download callback. ## Response-size limits and pitfalls - **Response-size limits:** The reader loop above enforces an application limit while processing download chunks with the released API; see [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) for next-release size enforcement and error handling. - **Upload support:** `onUploadProgress` is silently ignored without request stream support, with `keepalive: true`, or with `mode: 'no-cors'`. Ignoring the callback does not make a streaming body compatible with those environments; use a compatible non-stream body when needed. - **Download support:** `onDownloadProgress` requires response streams. Ky throws if `ReadableStream` support is missing. - **Error responses:** Ky throws [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror#httperror) for non-2xx responses by default. Its response body is consumed to populate `error.data`; use that property rather than reading `error.response` again. See [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors). ## Related - [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) explains method eligibility and retry policy. - [Cancel requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/cancel-requests) shows how to abort transfers with a signal. - [Request lifecycle](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/request-lifecycle) explains where hooks can replace requests and responses. # Cancel requests Use cancellation when a request is no longer needed, or when an extended client needs a different cancellation lifetime. [Ky](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#default) builds on Fetch: pass an `AbortController`'s signal through the `signal` option, then call `abort()` to cancel. ## 1. Pass a signal and handle cancellation This sample runs in a browser or Node.js with Fetch support. It follows the authors' cancellation example: start a request, cancel it with a timer, and distinguish cancellation from other failures. Use a slow endpoint so the request is still pending when the timer fires. ```ts import ky from 'ky'; async function main() { const controller = new AbortController(); const timer = setTimeout(() => { controller.abort(); }, 500); try { const text = await ky.get('https://httpbin.org/delay/2', { signal: controller.signal, }).text(); console.log(text); } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Fetch aborted'); } else { console.error('Fetch error:', error); } } finally { clearTimeout(timer); } } void main(); ``` With a pending request, `abort()` makes the awaited operation reject with an error named `AbortError`, and the cancellation branch prints `Fetch aborted`. If the request finishes first, `.text()` returns the response body as a string instead. Keep other failures in a separate branch rather than treating every rejection as cancellation. ## 2. Replace an inherited signal By default, `extend()` combines inherited and new signals: either controller can cancel the extended client's requests. Use [`replaceOption`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky#replaceoption) when only the new controller must control the child client. ```ts import ky, {replaceOption} from 'ky'; async function main() { const parentController = new AbortController(); const replacementController = new AbortController(); const api = ky.create({signal: parentController.signal}); const childApi = api.extend({ signal: replaceOption(replacementController.signal), }); parentController.abort(); try { const text = await childApi.get('https://httpbin.org/get').text(); console.log(text); } catch (error) { console.error('Fetch error:', error); } const pending = childApi.get('https://httpbin.org/delay/2').text(); replacementController.abort(); try { await pending; } catch (error) { if (error instanceof Error && error.name === 'AbortError') { console.log('Child request aborted'); } else { console.error('Fetch error:', error); } } } void main(); ``` Aborting the parent does not cancel the child's first request. Look for your endpoint's response text. The replacement controller cancels the subsequent child request. `extend()` returns a new client; the parent retains its original defaults. ## 3. Remove an inherited signal Pass `signal: undefined` to `extend()` to remove inherited signals without installing a replacement. The child can make requests even after the parent controller is aborted. ```ts import ky from 'ky'; async function main() { const parentController = new AbortController(); const api = ky.create({signal: parentController.signal}); const independentApi = api.extend({signal: undefined}); parentController.abort(); try { const text = await independentApi.get('https://httpbin.org/get').text(); console.log(text); } catch (error) { console.error('Fetch error:', error); } } void main(); ``` The parent signal no longer cancels this request. You can still pass a fresh signal in an individual request's options to make that request cancellable. ## Options and pitfalls The cancellation option belongs to [`Options`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-options#options). | Option | Type | Default | What it does | | --- | --- | --- | --- | | `signal` | `AbortSignal \| null \| undefined` | Not set | Supplies a Fetch cancellation signal. In `extend()`, a new signal combines with inherited signals; `replaceOption(signal)` replaces them, and explicit `undefined` removes them. | - Create a fresh controller for a new cancellation lifetime. A request using an already-aborted signal rejects rather than starting a fresh operation. - Signal combination requires `AbortSignal.any()`. Without it, Ky uses the last supplied signal instead of combining signals. - The `AbortError` check above applies to `abort()` without a custom reason. If you pass a reason to `abort(reason)`, handle that reason: Ky surfaces the user signal's abort reason. ## Related - [Instances and defaults](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/instances-and-defaults) explains how to compose client defaults. - [Handle request errors](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/handle-request-errors) covers failures other than cancellation. - [Retry failed requests](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/retry-failed-requests) covers retry policy and timeouts. # ky Tiny and elegant HTTP client based on the Fetch API ## Install ```bash npm install ky ``` ## On their own pages - [`Hooks`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-hooks) - [`HTTPError`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-httperror): Error thrown when the response has a non-2xx status code and `throwHttpErrors` is enabled. - [`KyRequest`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-kyrequest) - [`NormalizedOptions`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-normalizedoptions): Normalized options passed to the `fetch` call and hooks. - [`Options`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-options): Options are the same as `window.fetch`, except for the KyOptions - [`ResponsePromise`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-responsepromise) - [`RetryOptions`](https://bench-ky-61.atloria.app/p/bench-ky-61-6TqdzXtmKC/developer/ky-retryoptions) ## Functions ### `isForceRetryError` Type guard to check if an error is a `ForceRetryError`. ```ts function isForceRetryError(error: unknown): error is ForceRetryError ``` **Parameters** - `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}`); } } ] } }); ``` ### `isHTTPError` Type guard to check if an error is an `HTTPError`. ```ts function isHTTPError(error: unknown): error is HTTPError ``` **Parameters** - `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); } } ``` ### `isKyError` 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. ```ts function isKyError(error: unknown): error is KyError ``` **Parameters** - `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); } } ``` ### `isNetworkError` Type guard to check if an error is a `NetworkError`. ```ts function isNetworkError(error: unknown): error is NetworkError ``` **Parameters** - `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); } } ``` ### `isResponseSizeError` **Not released yet.** It is in the source, not in the latest release on npm. Type guard to check if an error is a `ResponseSizeError`. ```ts function isResponseSizeError(error: unknown): error is ResponseSizeError ``` **Parameters** - `error`: The error to check **Returns** `true` if the error is a `ResponseSizeError`, `false` otherwise **Example** ``` import ky, {isResponseSizeError} from 'ky'; try { await ky('https://example.com/data', {maxResponseSize: 1024}).json(); } catch (error) { if (isResponseSizeError(error)) { console.log(`Response exceeded ${error.maxResponseSize} bytes`); } } ``` ### `isTimeoutError` Type guard to check if an error is a `TimeoutError`. ```ts function isTimeoutError(error: unknown): error is TimeoutError ``` **Parameters** - `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); } } ``` ## Classes ### `ForceRetryError` 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. ```ts class ForceRetryError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'ForceRetryError'` | | | `customDelay` | `number \| undefined` | | | | `code` | `string \| undefined` | | | | `customRequest` | `Request \| undefined` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(options?: ForceRetryOptions)` ### `KyError` Base class for all Ky-specific errors. `HTTPError`, `NetworkError`, `TimeoutError`, `ResponseSizeError`, 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. ```ts class KyError extends Error ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'KyError'` | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `get isKyError(): true` ### `NetworkError` 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). A connection that drops while a Ky shortcut method like `.json()` is reading the response body is also wrapped in `NetworkError`, but it is not retried because the response has already been received. 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. ```ts class NetworkError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'NetworkError'` | | | `request` | `KyRequest` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request, options?: {cause?: Error | undefined})` ### `ResponseSizeError` **Not released yet.** It is in the source, not in the latest release on npm. Error thrown when the response body exceeds `maxResponseSize`. It has a `request` property with the `Request` object and a `maxResponseSize` property with the configured limit in bytes. ```ts class ResponseSizeError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'ResponseSizeError'` | | | `request` | `KyRequest` | | | | `maxResponseSize` | `number` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request, maxResponseSize: number)` **Example** ``` import ky, {isResponseSizeError} from 'ky'; try { await ky('https://example.com/data', {maxResponseSize: 1024}).json(); } catch (error) { if (isResponseSizeError(error)) { console.log(`Response exceeded ${error.maxResponseSize} bytes`); } } ``` ### `SchemaValidationError` 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()`. ```ts class SchemaValidationError extends Error ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'SchemaValidationError'` | | | `issues` | `readonly StandardSchemaV1Issue[]` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(issues: readonly StandardSchemaV1Issue[])` **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); } } ``` ### `TimeoutError` Error thrown when the request times out. It has a `request` property with the `Request` object. ```ts class TimeoutError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'TimeoutError'` | | | `request` | `KyRequest` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(request: Request)` ## Constants ### `default` The package's default export: import it under a name of your own, `import ky from 'ky'`. ```ts declare const ky: KyInstance export default ky ``` ### `replaceOption` 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. ```ts const replaceOption: (value: T) => T ``` **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] ``` ## Types ### `AfterResponseHook` ```ts type AfterResponseHook = (state: AfterResponseState) => Response | RetryMarker | void | Promise; ``` ### `AfterResponseState` ```ts type AfterResponseState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `response` | `KyResponse` | | | | `retryCount` | `number` | | The number of retries attempted. `0` for the initial request, increments with each retry. | ### `BeforeErrorHook` ```ts type BeforeErrorHook = (state: BeforeErrorState) => Error | Promise; ``` ### `BeforeErrorState` ```ts type BeforeErrorState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `error` | `Error` | | | | `retryCount` | `number` | | The number of retries attempted. `0` for the initial request, increments with each retry. | ### `BeforeRequestHook` ```ts type BeforeRequestHook = (state: BeforeRequestState) => Request | Response | void | Promise; ``` ### `BeforeRequestState` ```ts type BeforeRequestState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `retryCount` | `0` | | The number of retries attempted. Always `0`, since `beforeRequest` hooks run once before retry handling begins. | ### `BeforeRetryHook` ```ts type BeforeRetryHook = (state: BeforeRetryState) => Request | Response | typeof stop | void | Promise; ``` ### `BeforeRetryState` ```ts type BeforeRetryState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `error` | `Error` | | | | `retryCount` | `number` | | The number of retries attempted. Always `>= 1`, since this hook is only called during retries, not on the initial request. | ### `InitHook` 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. The `headers` option is always a plain object with lowercase names, where a header removed with `undefined` keeps an `undefined` value. Unlike other hooks, `init` hooks are synchronous. Any error thrown will propagate synchronously and will not be caught by `beforeError` hooks. ```ts type InitHook = (options: InitOptions) => void; ``` **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 ``` ### `Input` ```ts type Input = string | URL | Request; ``` **Members** - `toString(): string` — Returns a string representation of a string. - `valueOf(): string` — Returns the primitive value of the specified object. ### `KyInstance` ```ts type KyInstance = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `get` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'get'}`. | | `post` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'post'}`. | | `put` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'put'}`. | | `delete` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'delete'}`. | | `patch` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'patch'}`. | | `head` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'head'}`. | | `query` | `(url: Input, options?: Options) => ResponsePromise` | | Fetch the given `url` using the option `{method: 'query'}`. | | `create` | `(defaultOptions?: Options) => KyInstance` | | Create a new Ky instance with complete new defaults, without inheriting from any parent instance. | | `extend` | `(defaultOptions: Options \| ((parentOptions: Options) => Options)) => KyInstance` | | Create a new Ky instance with some defaults overridden with your own. | | `stop` | `typeof stop` | | A `Symbol` that can be returned by a `beforeRetry` hook to stop the retry. This will also short circuit the remaining `beforeRetry` hooks. | | `retry` | `typeof retry` | | Force a retry from an `afterResponse` hook. | ### `KyResponse` ```ts type KyResponse = { clone: () => KyResponse; json: () => Promise; } & Response; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `clone` | `() => KyResponse` | | | | `json` | `() => Promise` | | | | `headers` | `Headers` | | The **`headers`** read-only property of the with the response. | | `ok` | `boolean` | | The **`ok`** read-only property of the Response interface contains a Boolean stating whether the response was successful (status in the range 200-299) or not. | | `redirected` | `boolean` | | The **`redirected`** read-only property of the Response interface indicates whether or not the response is the result of a request you made which was redirected. | | `status` | `number` | | The **`status`** read-only property of the Response interface contains the HTTP status codes of the response. | | `statusText` | `string` | | The **`statusText`** read-only property of the Response interface contains the status message corresponding to the HTTP status code in Response.status. | | `type` | `ResponseType` | | The **`type`** read-only property of the Response interface contains the type of the response. | | `url` | `string` | | The **`url`** read-only property of the Response interface contains the URL of the response. | | `body` | `ReadableStream> \| null` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) | | `bodyUsed` | `boolean` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) | **Members** - `arrayBuffer(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) - `blob(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) - `bytes(): Promise>` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) - `formData(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) - `text(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) ### `Progress` ```ts type Progress = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `percent` | `number` | | A number between `0` and `1` representing the progress percentage. | | `transferredBytes` | `number` | | The number of bytes transferred so far. | | `totalBytes` | `number` | | The total number of bytes to be transferred. This is an estimate and may be `0` for an empty transfer or when the total size cannot be determined. | ### `SearchParamsOption` ```ts type SearchParamsOption = | Exclude | Record | Array> | ReadonlyArray>; ``` **Members** - `toString(): string` — Returns a string representation of a string. - `valueOf(): string` — Returns the primitive value of the specified object. ### `ShouldRetryState` ```ts type ShouldRetryState = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `error` | `Error` | | The error that caused the request to fail. | | `retryCount` | `number` | | The number of retries attempted. Starts at 1 for the first retry. | ### `StandardSchemaV1` ```ts type StandardSchemaV1 = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `'~standard'` | `{ readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, options?: StandardSchemaV1Options, ) => StandardSchemaV1Result \| Promise>; readonly types?: StandardSchemaV1Types \| undefined; }` | | | | `'~standard'` | `{ readonly version: 1; readonly vendor: string; readonly validate: ( value: unknown, options?: StandardSchemaV1Options, ) => StandardSchemaV1Result \| Promise>; readonly types?: StandardSchemaV1Types \| undefined; }` | | | ### `StandardSchemaV1InferOutput` ```ts type StandardSchemaV1InferOutput = Schema['~standard'] extends { readonly types: StandardSchemaV1Types; } ? OutputType : Extract< Awaited>, StandardSchemaV1SuccessResult > extends StandardSchemaV1SuccessResult ? OutputType : unknown; ``` ### `StandardSchemaV1Issue` ```ts type StandardSchemaV1Issue = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `message` | `string` | | | | `path?` | `ReadonlyArray \| undefined` | | | # Hooks Import it from `ky`. ```ts type Hooks = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `init?` | `readonly InitHook[] \| undefined` | `[]` | 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. The `headers` option is always a plain object with lowercase names, where a header removed with `undefined` keeps an `undefined` value. | | `beforeRequest?` | `readonly BeforeRequestHook[] \| undefined` | `[]` | 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. | | `beforeRetry?` | `readonly BeforeRetryHook[] \| undefined` | `[]` | 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. | | `beforeError?` | `readonly BeforeErrorHook[] \| undefined` | `[]` | 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. | | `afterResponse?` | `readonly AfterResponseHook[] \| undefined` | `[]` | 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). | # HTTPError Import it from `ky`. 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, unreadable, too large, parsing fails, or the error-data read/parse timeout is reached, `data` will be `undefined`. To avoid hanging or excessive buffering, `error.data` body reads and async JSON parsing are bounded by the request timeout (or 10 seconds when `timeout` is disabled), any remaining `totalTimeout` budget, and a 10 MiB response body size limit. If `maxResponseSize` is exceeded while populating `error.data`, Ky throws `ResponseSizeError` instead of `HTTPError`. If `totalTimeout` expires while populating `error.data`, Ky throws `TimeoutError` instead of `HTTPError`. 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. ```ts class HTTPError extends KyError ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `name` | | `'HTTPError'` | | | `response` | `KyResponse` | | | | `request` | `KyRequest` | | | | `options` | `Readonly` | | | | `data` | `T \| string \| undefined` | | | | `message` | `string` | | | | `stack?` | `string` | | | **Methods** - `constructor(response: Response, request: Request, options: Readonly)` # KyRequest Import it from `ky`. ```ts type KyRequest = { clone: () => KyRequest; json: () => Promise; } & Request; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `clone` | `() => KyRequest` | | | | `json` | `() => Promise` | | | | `cache` | `RequestCache` | | The **`cache`** read-only property of the Request interface contains the cache mode of the request. | | `credentials` | `RequestCredentials` | | The **`credentials`** read-only property of the Request interface reflects the value given to the Request.Request() constructor in the `credentials` option. | | `destination` | `RequestDestination` | | The **`destination`** read-only property of the **Request** interface returns a string describing the type of content being requested. | | `headers` | `Headers` | | The **`headers`** read-only property of the with the request. | | `integrity` | `string` | | The **`integrity`** read-only property of the Request interface contains the subresource integrity value of the request. | | `keepalive` | `boolean` | | The **`keepalive`** read-only property of the Request interface contains the request's `keepalive` setting (`true` or `false`), which indicates whether the browser will keep the associated request alive if the page that initiated it is unloaded before the request is complete. | | `method` | `string` | | The **`method`** read-only property of the `POST`, etc.) A String indicating the method of the request. | | `mode` | `RequestMode` | | The **`mode`** read-only property of the Request interface contains the mode of the request (e.g., `cors`, `no-cors`, `same-origin`, or `navigate`.) This is used to determine if cross-origin requests lead to valid responses, and which properties of the response are readable. | | `redirect` | `RequestRedirect` | | The **`redirect`** read-only property of the Request interface contains the mode for how redirects are handled. | | `referrer` | `string` | | The **`referrer`** read-only property of the Request. | | `referrerPolicy` | `ReferrerPolicy` | | The **`referrerPolicy`** read-only property of the referrer information, sent in the Referer header, should be included with the request. | | `signal` | `AbortSignal` | | The read-only **`signal`** property of the Request interface returns the AbortSignal associated with the request. | | `url` | `string` | | The **`url`** read-only property of the Request interface contains the URL of the request. | | `body` | `ReadableStream> \| null` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/body) | | `bodyUsed` | `boolean` | | [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bodyUsed) | **Members** - `arrayBuffer(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/arrayBuffer) - `blob(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/blob) - `bytes(): Promise>` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/bytes) - `formData(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/formData) - `text(): Promise` — [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/text) # NormalizedOptions Import it from `ky`. Normalized options passed to the `fetch` call and hooks. ```ts interface NormalizedOptions extends Readonly ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `method` | `NonNullable` | | A string to set request's method. | | `credentials?` | `NonNullable` | | A string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. Sets request's credentials. | | `headers` | `Headers` | | A Headers object, an object literal, or an array of two-item arrays to set request's headers. | | `retry` | `NormalizedRetryOptions` | | | | `baseUrl?` | `Options['baseUrl']` | | | | `prefix` | `string` | | | | `onDownloadProgress?` | `NonNullable` | | | | `onUploadProgress?` | `NonNullable` | | | | `context` | `Record` | | | | `body?` | `BodyInit \| null` | | A BodyInit object or null to set request's body. | | `cache?` | `RequestCache` | | A string indicating how the request will interact with the browser's cache to set request's cache. | | `integrity?` | `string` | | A cryptographic hash of the resource to be fetched by request. Sets request's integrity. | | `keepalive?` | `boolean` | | A boolean to set request's keepalive. | | `mode?` | `RequestMode` | | A string to indicate whether the request will use CORS, or will be restricted to same-origin URLs. Sets request's mode. | | `priority?` | `RequestPriority` | | | | `redirect?` | `RequestRedirect` | | A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. | | `referrer?` | `string` | | A string whose value is a same-origin URL, "about:client", or the empty string, to set request's referrer. | | `referrerPolicy?` | `ReferrerPolicy` | | A referrer policy to set request's referrerPolicy. | | `signal?` | `AbortSignal \| null` | | An AbortSignal to set request's signal. | | `window?` | `null` | | Can only be null. Used to disassociate request from any Window. | # Options Import it from `ky`. Options are the same as `window.fetch`, except for the KyOptions ```ts interface Options extends KyOptions, RequestOptions ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `method?` | `LiteralUnion \| undefined` | | HTTP method used to make the request. | | `headers?` | `KyHeadersInit \| undefined` | | HTTP headers used to make the request. | | `signal?` | `AbortSignal \| null \| undefined` | | An `AbortSignal` to abort the request. | | `json?` | `unknown` | | Shortcut for sending JSON. Use this instead of the `body` option. | | `parseJson?` | `((text: string, context: {request: Request; response: Response}) => unknown) \| undefined` | `JSON.parse()` | User-defined JSON-parsing function. | | `stringifyJson?` | `((data: unknown) => string) \| undefined` | `JSON.stringify()` | User-defined JSON-stringifying function. | | `searchParams?` | `SearchParamsOption` | | Search parameters to include in the request URL. Setting this will merge with any existing search parameters in the input URL. | | `baseUrl?` | `URL \| string \| undefined` | | 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. | | `prefix?` | `URL \| string \| undefined` | | 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. | | `retry?` | `RetryOptions \| number \| undefined` | | Controls retry behavior. Each field is documented in the `RetryOptions` type. | | `timeout?` | `number \| false \| undefined` | `10000` | Per-attempt timeout in milliseconds for getting a response, applied independently to each retry. Ky shortcut methods also use this value as a separate timeout for reading the response body. Cannot be greater than 2147483647. See also `totalTimeout`. | | `totalTimeout?` | `number \| false \| undefined` | `false` | Overall timeout in milliseconds for the entire operation, including retries and delays. Throws a `TimeoutError` if exceeded. Cannot be greater than 2147483647. | | `maxResponseSize?` (not released yet) | `number \| undefined` | `Infinity` | Maximum response body size in bytes. Must be a non-negative safe integer or `Infinity`. Set to `0` to allow only empty bodies. | | `hooks?` | `Hooks \| undefined` | | Hooks allow modifications during the request lifecycle. Hook functions may be async and are run serially, unless otherwise noted. | | `throwHttpErrors?` | `boolean \| ((status: number) => boolean) \| undefined` | `true` | 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'`. | | `onDownloadProgress?` | `((progress: Progress, chunk: Uint8Array) => void) \| undefined` | | Download progress event handler. | | `onUploadProgress?` | `((progress: Progress, chunk: Uint8Array) => void) \| undefined` | | Upload progress event handler. | | `fetch?` | `((input: Request, init?: RequestInit) => Promise) \| undefined` | `fetch` | 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. | | `context?` | `Record \| undefined` | `{}` | User-defined data passed to hooks. | # ResponsePromise Import it from `ky`. ```ts type ResponsePromise = { arrayBuffer: () => Promise; blob: () => Promise; formData: () => Promise; bytes: () => Promise>; json: { (schema?: undefined): Promise; (schema: Schema): Promise>; }; text: () => Promise; } & Promise>; ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `arrayBuffer` | `() => Promise` | | | | `blob` | `() => Promise` | | | | `formData` | `() => Promise` | | | | `bytes` | `() => Promise>` | | Get the response body as raw bytes. | | `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(); // or const result2 = await ky(…).json(); ``` */ (schema?: undefined): Promise; /** 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: Schema): Promise>; }` | | | | `text` | `() => Promise` | | | **Members** - `then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise` — Attaches callbacks for the resolution and/or rejection of the Promise. - `catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise` — Attaches a callback for only the rejection of the Promise. # RetryOptions Import it from `ky`. ```ts type RetryOptions = { … } ``` **Properties** | Name | Type | Default | Description | | --- | --- | --- | --- | | `limit?` | `number \| undefined` | `2` | The number of times to retry failed requests. Must be a finite, non-negative integer. | | `methods?` | `readonly HttpMethod[] \| undefined` | `['get', 'put', 'head', 'delete', 'options', 'trace', 'query']` | The HTTP methods allowed to retry. | | `statusCodes?` | `readonly number[] \| undefined` | `[408, 413, 429, 500, 502, 503, 504]` | The HTTP status codes allowed to retry. | | `afterStatusCodes?` | `readonly number[] \| undefined` | `[413, 429, 503]` | The retriable HTTP status codes that should respect retry timing headers. These status codes must also be included in `statusCodes`. | | `maxRetryAfter?` | `number \| undefined` | `Infinity` | If the retry delay from a retry timing header is greater than `maxRetryAfter`, Ky will use `maxRetryAfter`. | | `backoffLimit?` | `number \| undefined` | `Infinity` | The upper limit of the delay per retry in milliseconds. To clamp the delay, set `backoffLimit` to 1000, for example. | | `delay?` | `((attemptCount: number) => number) \| undefined` | `attemptCount => 0.3 * (2 ** (attemptCount - 1)) * 1000` | A function to calculate the delay in milliseconds between retries given `attemptCount` (starts from 1). | | `jitter?` | `boolean \| ((delay: number) => number) \| undefined` | `undefined (no jitter)` | Add random jitter to retry delays to prevent thundering herd problems. | | `retryOnTimeout?` | `boolean \| undefined` | `false` | Whether to retry when the request times out before a response is returned. Timeouts while reading a response body through Ky shortcut methods are not retried because the response has already been received. | | `shouldRetry?` | `((state: ShouldRetryState) => boolean \| void \| Promise) \| undefined` | `undefined` | A function to determine whether a retry should be attempted. |