hono icon indicating copy to clipboard operation
hono copied to clipboard

RPC Client types are too tightly coupled to native fetch API, making custom fetch clients impractical

Open kleinpetr opened this issue 3 weeks ago • 3 comments

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 data property 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

  1. The ClientResponse type assumes native fetch behavior
  2. Custom fetch clients have different APIs and error handling strategies
  3. TypeScript doesn't know that non-2xx responses will never arrive as normal response objects when using ofetch
  4. Users must create type workarounds like res200() for every RPC call
  5. 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 fetch option in RPC client is advertised but not truly usable with real-world fetch implementations

Possible Solutions

  1. Generic type parameter for fetch response type

    type ClientResponse<TBody, TStatus, TFormat, TFetchClient = 'native'> = ...
    
  2. Separate type for custom fetch clients

    type CustomClientResponse<TBody, TStatus> = TBody
    
  3. Configuration option to specify fetch behavior

    createClient('/', { 
      fetch: customFetch,
      fetchBehavior: 'throw-on-error' // or 'native'
    })
    
  4. 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.

kleinpetr avatar Dec 03 '25 15:12 kleinpetr

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.

NamesMT avatar Dec 06 '25 15:12 NamesMT

Btw big Nuxt/unjs ecosystem fan here too, fun fact: parseResponse uses fetchRP util, which is a modified code from ofetch's response processing.

NamesMT avatar Dec 06 '25 15:12 NamesMT

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.

Image

yusukebe avatar Dec 11 '25 08:12 yusukebe