Breaking
- Require Node.js 22 f1da0fc
- Unify hook signatures around a single state object (#827) ecdd45e
- All hooks now receive a single
{request, options, retryCount, ...}state object instead of separate arguments.
- All hooks now receive a single
- Rename
prefixUrltoprefix, and allow leading slashes in input (#606) 1f2ad7f - Make
beforeErrorhook receive all errors, not justHTTPError(#829) 101c74b - Make
.json()throw on empty bodies and204responses instead of returning an empty string (#854) 1b8e1ff - Merge
searchParamswith input URL instead of replacing (#840) 29e78fe - Strip Ky-specific properties from normalized options passed to hooks (#826) 433febd
- Treat hook errors as fatal outside retry handling (#834) 90c6d00
New
- Add
totalTimeoutoption for an overall timeout across all retries (#848) c20d7c7 - Add
baseUrloption for standard URL resolution (#606) 1f2ad7f - Add
dataproperty toHTTPErrorwith pre-parsed response body (#823) 1341f5c- The response body is automatically consumed and parsed, fixing resource leaks and making error details immediately available without awaiting (#642).
- Add
inithook (#841) 87c6740 - Add
NetworkErrorclass and tighten retry logic (#842) eaf0b80 - Add Standard Schema validation for
.json()(#830) 94741a9 - Add
replaceOptionhelper for.extend()(#846) bb8412e - Add
requestandoptionstobeforeErrorhook state (#835) 01e0b85 - Add request/response context to
parseJsonoption (#849) 3713ce8 - Don't throw
HTTPErrorfor opaque responses fromno-corsrequests (#847) 1d15eb6 - Gracefully ignore
onUploadProgresswhen request streams are unsupported (#845) 1e38ff4
Fixes
- Fix
beforeRequesthooks being skipped when aRequestis returned (#832) aec65db - Ignore non-Errors returned by
beforeErrorhooks (#833) a541fc0
Migration guide
Hook signatures
All hooks now receive a single state object instead of separate arguments.
hooks: {
- beforeRequest: [(request, options) => {
+ beforeRequest: [({request, options}) => {
request.headers.set('X-Custom', 'value');
}],
- afterResponse: [(request, options, response) => {
+ afterResponse: [({request, options, response}) => {
log(response.status);
}],
- beforeRetry: [({request, options, error, retryCount}) => {
+ beforeRetry: [({request, options, error, retryCount}) => {
// Same as before - beforeRetry already used an object
}],
- beforeError: [(error) => {
+ beforeError: [({error}) => {
return error;
}],
}prefixUrl renamed to prefix
-ky('users', {prefixUrl: 'https://example.com/api/'});
+ky('users', {prefix: 'https://example.com/api/'});Leading slashes in input are now allowed with prefix. There's also a new baseUrl option for standard URL resolution, which you may prefer over prefix.
beforeError hook receives all errors
Previously only received HTTPError. Now receives all error types. Use type guards:
+import {isHTTPError} from 'ky';
hooks: {
beforeError: [
- (error) => {
- const {response} = error;
+ ({error}) => {
+ if (isHTTPError(error)) {
+ const {response} = error;
+ }
return error;
}
]
}.json() on empty responses
.json() now throws a parse error on empty bodies and 204 responses instead of returning an empty string. This aligns with native JSON.parse behavior and surfaces the real issue; your code expected JSON but the server sent none.
Check the status before calling .json() if you expect empty responses:
const response = await ky('https://example.com/api');
if (response.status !== 204) {
const data = await response.json();
}Ky-specific options stripped from hooks
Ky-specific properties (hooks, json, parseJson, stringifyJson, searchParams, timeout, throwHttpErrors, fetch) are no longer available on the options object passed to hooks. If you need access to these values, store them in a variable outside the hook or use the context option to pass data between hooks.
searchParams merging
searchParams now merges with existing query parameters in the input URL instead of replacing them.
// v1: searchParams replaces ?existing=1
// v2: searchParams merges with ?existing=1
ky('https://example.com?existing=1', {searchParams: {added: 2}});
// => https://example.com?existing=1&added=2HTTPError response body
error.response.json() and other body methods no longer work since the body is now automatically consumed. Use error.data instead, which has the pre-parsed response body immediately available.
-const body = await error.response.json();
-console.log(body.message);
+console.log(error.data.message);This fixes resource leaks when catching HTTPError without consuming the body (#633) and makes error details synchronously available (#642). We considered cloning the response to keep both paths working, but that doubles memory usage for error bodies and adds edge cases around locked/large streams for little benefit. error.response is still available for headers and status.
Upgrading from 2.0.0-0
.json() on empty responses
The behavior changed again from the prerelease. .json() now throws instead of returning undefined for empty bodies and 204 responses. The return type is back to Promise<T> (no more | undefined).