RPC Client types are too tightly coupled to native fetch API, making custom fetch clients impractical
Description
The RPC client accepts a custom fetch implementation via options, but the ClientResponse types are tightly coupled to the native fetch API. This makes it impractical to use popular fetch wrappers like ofetch, ky, or axios without significant type workarounds.
Current Behavior
When using a custom fetch client, the ClientResponse types still expect native fetch methods like res.ok, res.json(), res.text(), etc. However, custom fetch clients have different APIs and behaviors:
- ofetch: Throws errors on non-2xx responses instead of returning a response object with
ok: false - ky: Similar behavior with a different API surface
- axios: Uses
dataproperty instead of methods like.json()
Example
Here's a real-world case using ofetch in a Nuxt application:
import { createClient } from '@sdk/v1'
import type { ClientResponse } from 'hono/client'
export default function useApi() {
const client = createClient('', {
fetch: useAuthFetch(), // ofetch-based custom fetcher
})
return client
}
// Workaround: Type utility to extract 200 response since ofetch throws on non-2xx
export function res200<T>(
res: T,
): Extract<T, { status: 200 }> extends ClientResponse<infer Body, 200, any>
? Body
: Extract<T, { status: 200 }> {
return res as any
}
// Usage - must use .then(res200) after every RPC call
const result = await client.v1.orders.$get({ query: q }).then(res200)
The custom fetch implementation:
export function useAuthFetch() {
const instance = $fetch.create({ // ofetch
baseURL: '/api',
onRequest({ options }) {
// Add auth headers
},
async onResponseError({ response }) {
// Handle 401, refresh tokens, etc.
// ofetch throws here for non-2xx responses
},
})
return instance
}
Problem
- The
ClientResponsetype assumes native fetch behavior - Custom fetch clients have different APIs and error handling strategies
- TypeScript doesn't know that non-2xx responses will never arrive as normal response objects when using
ofetch - Users must create type workarounds like
res200()for every RPC call - This defeats the purpose of having type-safe RPC calls
Impact
- Loss of type safety when using custom fetch clients
- Boilerplate workarounds required for every API call
- Custom fetch clients (which are essential for auth, retries, interceptors) become impractical
- The
fetchoption in RPC client is advertised but not truly usable with real-world fetch implementations
Possible Solutions
-
Generic type parameter for fetch response type
type ClientResponse<TBody, TStatus, TFormat, TFetchClient = 'native'> = ... -
Separate type for custom fetch clients
type CustomClientResponse<TBody, TStatus> = TBody -
Configuration option to specify fetch behavior
createClient('/', { fetch: customFetch, fetchBehavior: 'throw-on-error' // or 'native' }) -
Response transformer function Allow users to provide a type-safe transformer that maps their fetch client's response to a known shape
Environment
- Hono version: Latest
- Custom fetch client: ofetch (part of Nuxt/Nitro ecosystem)
- Use case: Nuxt 4 application with authentication, token refresh, and error handling
Related
This affects anyone using:
- ofetch (Nuxt/Nitro)
- ky
- axios
- Any custom fetch wrapper with authentication/retry logic
The current workaround significantly reduces the developer experience and type safety that Hono's RPC client promises to deliver.
You are able to use a custom, non-standard fetch implement with RPC type-safety by creating a simple wrapper for hc output similar to the parseResponse util from hono/client.
With that said, for the quickest solution that is already available and solves your mentioned problems, you can use parseResponse for a simple auto-parse and throw-on-error behavior, and you can combine libraries which exposes a native fetch compatible client for more features, ky, which you mentions, can be used as custom fetch client with hc for features like retries, hooks, timeouts.
Btw big Nuxt/unjs ecosystem fan here too, fun fact: parseResponse uses fetchRP util, which is a modified code from ofetch's response processing.
If making it accept these fetch wrappers, we may have to change the type of the fetch option for hc to accept them. Currently, if passing ofetch's fetch, it throws the type error.