Kubb clients 6.0
@kubb/plugin-fetch, @kubb/plugin-axios, and @kubb/plugin-client 6.0 run the fetch and axios clients on one shared core. Both clients now have the same interceptors, client factory, error class, and types. A transport only sends the request, so another HTTP library such as ky takes one line:
import ky from 'ky'
import { client, toRequestInit } from './gen/.kubb/client'
client.setConfig({
transport: (request) => ky(request.url, { ...toRequestInit(request), throwHttpErrors: false, retry: 0 }),
})Code that calls the generated functions or hooks keeps working. Most projects only regenerate.
One core for fetch and axios
In v5 the two clients only looked alike. The core was already shared, but axios wrapped it in its own client, interceptors, and transport types, so an interceptor written for fetch did not run on axios. v6 drops that layer. The core builds the URL, serializes parameters, resolves auth, runs validation and interceptors, and splits success from error. Each package adds only a transport, the function that sends the resolved request.
Now one interceptor runs on both clients, because both hand it the core's ResolvedRequest, TransportResult, and ResponseError:
client.interceptors.request.use((request) => {
request.headers['X-Request-ID'] = crypto.randomUUID()
return request
})On axios, native handlers that need InternalAxiosRequestConfig or AxiosResponse go on the new axiosInstance export, the instance the default transport sends through.
Bring your own HTTP library
A transport can now return a plain web Response. The core parses it the same way the fetch client does, by responseType and Content-Type, and response interceptors still see the parsed result. toRequestInit(request) builds the RequestInit that fetch and ky take.
This works for any library that hands back an unread Response, including a polyfill such as node-fetch. Keep the library from throwing on an error status, as throwHttpErrors: false does for ky, so the client can return or throw its typed ResponseError.
Axios is the exception, since it never returns a web Response. Its transport still decodes responseType maps itself, so native axios interceptors see parsed data, as they did in v5. That is one more code path to keep in step, but it means handlers you move from v5 keep working.
Set a transport once with client.setConfig({ transport }), per client with createClient({ transport }), or per call with transport. See Use a custom transport.
Cookie parameters
An operation with an in: cookie parameter gets a <Name>Cookies type and a cookies option. The call, SDK method, query hooks, MCP tool, and Cypress request all take it, and Zod and faker get the same group:
await getSession({ cookies: { session_id: 'abc' } })cookies is always optional, because a browser sends its cookies itself. Query keys leave it out, as they do headers. A query key is written into SSR payloads, persisted caches, and devtools, and a session cookie has no business in any of them.
Fixes
- Stream calls send
Accept: text/event-stream. Before, axios asked for JSON, so a server that picks its format fromAccept, such as Wikimedia EventStreams, answered with JSON and the stream never ended. - Axios honors
credentials:include,same-origin, andomit. - On the Node http adapter, a
responseType: 'blob'call returns aBlobinstead of a corrupted string. - Both clients read bodies the same way. A body labeled JSON that is not JSON comes back as text, and a 204, 205, or 304 is
undefinedon both. - A failed blob or arraybuffer call parses a JSON or text error body, so
erroris the error object. - A failed call with an empty body reports
{ status, statusText }as itserror, soif (error)and.unwrap()see the failure. - An aborted stream stops before the next event and rejects with the signal's reason, so
lastEventIdstays at the last event you read. - On fetch, the Next.js
nextoption reachesfetch, so caching options take effect.
Breaking changes
Regenerate, run tsc --noEmit, and match each error to a row:
| Your code | Client | What to do |
|---|---|---|
| Calls generated functions, SDK methods, or hooks | all | Regenerate. Nothing else. |
Imports from templates/fetch.ts, templates/axios.ts, or templates/core.ts | fetch, axios | Import types from templates/client.ts, and the shared runtime from @kubb/plugin-client/templates/*. |
Uses FetchOptions or AxiosOptions | fetch, axios | Rename it to NativeOptions. |
Names EventStreamResult<Pet>, StreamResult, or UnwrappedStreamResult | fetch, axios | Wrap the payload: EventStreamResult<ServerSentEvent<Pet>>. |
Registers handlers on client.interceptors | axios | Move them to axiosInstance.interceptors, or rewrite them for the core objects. |
Passes an axios instance as transport, or sets defaultTransport.defaults | axios | Wrap it with createAxiosTransport(instance), and set defaults on axiosInstance. |
| Wrote your own client with stream helpers | plugin-client | Take the full event type, export ServerSentEvent, and add contentType to RequestConfig. |
Three axios changes compile, so check them by hand. client.interceptors.request handlers run in the order you register them. client.interceptors.error runs only for a ResponseError, not for a network error. A child from createClient() inherits the transport of client.
Upgrade
v6 needs Kubb 5.16.0 or later. Upgrade Kubb and your client plugin together, then regenerate:
npm install --save-dev kubb@latest @kubb/plugin-fetch@6
npx kubb generate
npx tsc --noEmitUse @kubb/plugin-axios@6 for the axios client. The query plugins (@kubb/plugin-react-query, @kubb/plugin-vue-query, @kubb/plugin-swr, and @kubb/plugin-mcp) work with both v5 and v6, so you can upgrade them separately.
Each client has a migration guide with a before and after for every change:
