WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

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

@kleinpetr

Description

@kleinpetr

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions