Skip to main content
doc0blade47/honodocumentation home
Sign in

RPC Client

Last updated Oct 12, 2026100% grounded
Relevant source files

The following files were used as context for generating this wiki page:

Overview

The RPC client provides a type-safe client interface for Application Core instances via the hc client factory and recursive proxy mechanism. It bridges server route definitions with client-side invocation by translating property accesses and method calls into standard HTTP requests, URL parameters, query strings, and WebSocket handshakes WebSocket Support. By leveraging TypeScript type inference, the client maps server schemas directly into typed methods and response types Execution Context, eliminating the need for manual API contract definitions while supporting custom fetch functions, headers, and error handling.

Sources: src/client/client.ts:133-223, src/client/types.ts:67-119

Client factory and proxy architecture

Proxy creation and callback mechanism

The client factory hc initializes a recursive JavaScript Proxy object that captures arbitrary property accesses and function invocations src/client/client.ts:15-31. The createProxy helper function wraps a target function () => {} with traps for get and apply src/client/client.ts:15-29. When a property key is accessed, the get trap verifies that the key is a string and ignores built-in symbols such as then, returning a new createProxy instance with the accumulated path array extended by the accessed key src/client/client.ts:17-22. When the proxy is invoked as a function via apply, it executes the callback function passing an object containing the accumulated path segments and invocation arguments src/client/client.ts:23-28.

const createProxy = (callback: Callback, path: string[]) => {
  const proxy: unknown = new Proxy(() => {}, {
    get(_obj, key) {
      if (typeof key !== 'string' || key === 'then') {
        return undefined
      }
      return createProxy(callback, [...path, key])
    },
    apply(_1, _2, args) {
      return callback({
        path,
        args,
      })
    },
  })
  return proxy
}

Sources: src/client/client.ts:15-31

Sources: src/client/client.ts:15-31

Proxy callback routing and special methods

The hc factory function defines the proxyCallback implementation that intercepts invocations on the proxy chain src/client/client.ts:133-223. It handles special built-in inspections such as .toString() and .valueOf() to support debugging and path naming src/client/client.ts:143-159, and inspects trailing segments prefixed with a dollar sign $ to route specific utility actions or HTTP method executions src/client/client.ts:161-223.

Special Method / PropertyTrigger ConditionBehavior
toString() / valueOf()lastParts[0] is 'toString' or 'valueOf'If preceded by name, returns the target path segment name; otherwise returns function string representation or proxy callback src/client/client.ts:143-159.
urlmethod === 'url'Constructs and returns a URL object with substituted path parameters and query strings src/client/client.ts:171-184.
pathmethod === 'path'Returns the relative URL path string with path parameters and query parameters applied src/client/client.ts:171-185.
wsmethod === 'ws'Upgrades the target URL protocol to ws, appends query parameters, and establishes a WebSocket connection src/client/client.ts:187-212.
HTTP Methods ($get, $post, etc.)Method string starts with $Instantiates ClientRequestImpl and performs an HTTP fetch request src/client/client.ts:161-167, src/client/client.ts:214-221.

Sources: src/client/client.ts:133-223

Note

When accessing .name.toString() or .name.valueOf() on a proxy node, the callback extracts the parent path segment rather than evaluating the function code, enabling runtime introspection of route segment names src/client/client.ts:143-156.

Type-level inference and schema propagation

Schema mapping and route chaining

Hono translates server route definitions into strongly-typed nested client interfaces using recursive conditional types src/client/types.ts:311-318. The Client type unwraps a HonoBase instance, extracts its schema via ExtractSchema, and processes route path keys into a nested property chain via PathToChain src/client/types.ts:311-318. Each path segment in the router schema maps to an object property containing ClientRequest methods for HTTP interaction, URL generation, and WebSocket connections src/client/types.ts:67-119, src/client/types.ts:294-310.

type PathToChain<
  Prefix extends string,
  Path extends string,
  E extends Schema,
  Original extends string = Path,
> = Path extends `/${infer P}`
  ? PathToChain<Prefix, P, E, Path>
  : Path extends `${infer P}/${infer R}`
    ? { [K in P]: PathToChain<Prefix, R, E, Original> }
    : {
        [K in Path extends '' ? 'index' : Path]: ClientRequest<
          Prefix,
          Original,
          E extends Record<string, unknown> ? E[Original] : never
        >
      }
 
export type Client<T, Prefix extends string> =
  T extends HonoBase<any, infer S, any>
    ? S extends Record<infer K, Schema>
      ? K extends string
        ? PathToChain<Prefix, K, S>
        : never
      : never
    : never

Sources: src/client/types.ts:294-318

Tip

The PathToChain helper checks whether a path begins with a leading slash and recursively splits segments on slashes, transforming root paths like /api/users into nested client property accesses client.api.users.$get() src/client/types.ts:294-310.

Method expansion and input inference

HTTP methods on endpoints are governed by ExpandAllMethod, which transforms $all wildcard definitions into explicit standard HTTP methods ($get, $post, $put, $delete, $options, and $patch) pointing to the same endpoint definition src/client/types.ts:17-26. The ClientRequest type inspects the input parameters of each endpoint, requiring arguments when the input schema defines required keys or optional arguments when all input properties are optional src/client/types.ts:67-80.

type StandardMethods = `$${(typeof METHODS)[number]}`
 
/**
 * Expands '$all' into all standard HTTP methods.
 * If the schema contains '$all', it creates a type where all standard HTTP methods
 * point to the same endpoint definition as '$all', while removing '$all' itself.
 */
type ExpandAllMethod<S> = MethodNameAll extends keyof S
  ? { [M in StandardMethods]: S[MethodNameAll] } & Omit<S, MethodNameAll>
  : S

Sources: src/client/types.ts:17-26

Type HelperTarget Input / StructureResolution Behavior
ExpandAllMethod<S>Schema S containing $allExpands $all into all standard HTTP method keys while omitting the original $all key src/client/types.ts:24-26.
ClientRequest<Prefix, Path, S>Endpoint schema SMaps HTTP method keys to typed function signatures accepting inferred input parameters and options src/client/types.ts:67-80.
InferResponseType<T, U>Client fetch function TExtracts response body types filtered by optional status code U src/client/types.ts:251-254.
InferRequestType<T>Client fetch function TExtracts the expected input argument type R accepted by the client method src/client/types.ts:265-271.

Sources: src/client/types.ts:17-26, src/client/types.ts:67-80, src/client/types.ts:251-271

Schema filtering and response transformation

Global response definitions and status code filtering permit fine-tuning of client type definitions src/client/types.ts:332-392. The PickResponseByStatusCode helper evaluates an application schema and restricts all route responses to specific status codes, stripping error response types when error handling is managed centrally src/client/types.ts:388-392. Similarly, ApplyGlobalResponse injects global response definitions into an application schema prior to client generation src/client/types.ts:358-363.

export type PickResponseByStatusCode<App, U extends StatusCode> =
  App extends HonoBase<infer E, infer _ extends Schema, infer B>
    ? PickSchema<ExtractSchema<App>, U> extends infer S extends Schema
      ? Hono<E, S, B>
      : never

Sources: src/client/types.ts:388-392

Warning

When using PickResponseByStatusCode, any route responses outside the specified status code list evaluate to never, which prevents callers from handling unlisted error codes without casting src/client/types.ts:365-392.

Request dispatch and path serialization

Path formatting and parameter substitution

The RPC client handles path construction and serialization through helper utilities in src/client/utils.ts and src/client/client.ts. When a request is dispatched, mergePath cleans and joins the base URL with route segments by stripping trailing slashes from the base and leading slashes from the path segment src/client/utils.ts:11-16. URL parameters declared in route paths using colon syntax (such as /:id or optional patterns like /:id{[^/]+}?) are substituted dynamically via replaceUrlParam, which evaluates incoming key-value records and replaces matching path segments or removes them if the parameter value is undefined src/client/utils.ts:18-24. Additionally, removeIndexString scans target URLs to strip trailing /index or /index? suffixes, normalizing route destinations prior to fetch dispatch src/client/utils.ts:55-60.

export const mergePath = (base: string, path: string) => {
  base = base.replace(/\/+$/, '')
  base = base + '/'
  path = path.replace(/^\/+/, '')
  return base + path
}
 
export const replaceUrlParam = (urlString: string, params: Record<string, string | undefined>) => {
  for (const [k, v] of Object.entries(params)) {
    const reg = new RegExp('/:' + k + '(?:{[^/]+})?\\??')
    urlString = urlString.replace(reg, v ? `/${v}` : '')
  }
  return urlString
}

Sources: src/client/utils.ts:11-24

Note

The replaceUrlParam regular expression /:' + k + '(?:{[^/]+})?\\??/ matches standard named route parameters as well as custom regex constraints and optional modifiers src/client/utils.ts:18-24.

Query formatting and request bodies

Query parameters and request bodies are constructed inside the ClientRequestImpl class src/client/client.ts:33-130. Query objects are serialized into URLSearchParams using buildSearchParams, which iterates over input entries, skips undefined values, appends multi-value array entries, and sets scalar values src/client/utils.ts:26-44. Request bodies support multiple content types based on provided validation targets:

export const buildSearchParams = (query: Record<string, string | string[]>) => {
  const searchParams = new URLSearchParams()
 
  for (const [k, v] of Object.entries(query)) {
    if (v === undefined) {
      continue
    }
 
    if (Array.isArray(v)) {
      for (const v2 of v) {
        searchParams.append(k, v2)
      }
    } else {
      searchParams.set(k, v)
    }
  }
 
  return searchParams
}

Sources: src/client/utils.ts:26-44

Validation TargetSerialization MechanismResulting Content-Type / Body Behavior
args.jsonJSON.stringify(args.json)Sets Content-Type: application/json and assigns stringified JSON to rBody src/client/client.ts:81-84.
args.formFormData iterationAppends scalar and array field values into a FormData instance assigned to rBody src/client/client.ts:64-79.
args.querybuildSearchParams(args.query)Appends serialized query string parameters onto the destination URL src/client/client.ts:60-62.
args.cookieserialize(key, value, { path: '/' })Serializes cookie entries and joins them into the Cookie header src/client/client.ts:98-104.

Sources: src/client/client.ts:60-104

Header construction merges explicit request headers with optional headers provided via client options, which can be evaluated dynamically if defined as a function src/client/client.ts:93-96. If cookies are supplied in the request arguments, each cookie key-value pair is passed through cookie serialization with a default root path (/) and combined into a comma-delimited Cookie header string src/client/client.ts:98-104.

Before dispatching, the HTTP method is normalized to uppercase src/client/client.ts:91. The setBody check ensures that GET and HEAD requests omit the request body src/client/client.ts:120. Finally, fetch is invoked with the fully serialized URL, method, headers, body, and any custom options or fetch implementations supplied by the user src/client/client.ts:123-128.

  const headers = new Headers(headerValues ?? undefined)
  let url = this.url
 
  url = removeIndexString(url)
  url = replaceUrlParam(url, this.pathParams)
 
  if (this.queryParams) {
    url = url + '?' + this.queryParams.toString()
  }
  methodUpperCase = this.method.toUpperCase()
  const setBody = !(methodUpperCase === 'GET' || methodUpperCase === 'HEAD')
 
  // Pass URL string to 1st arg for testing with MSW and node-fetch
  return (opt?.fetch || fetch)(url, {
    body: setBody ? this.rBody : undefined,
    method: methodUpperCase,
    headers: headers,
    ...opt?.init,
  })
}

Sources: src/client/client.ts:110-129

Warning

Request bodies are strictly suppressed for GET and HEAD operations regardless of whether an rBody was inadvertently populated src/client/client.ts:120-124.

Response parsing and error handling

Response consumption and type detection

Response parsing and error handling are orchestrated by parseResponse, which delegates to fetchRP—a minimalized variant of fetch-result-please adapted to Hono's RPC client src/client/fetch-result-please.ts:2-4, src/client/utils.ts:84-114. When a fetch response or a promise resolving to one is received, fetchRP unwraps it and evaluates whether the response contains a body that requires parsing src/client/fetch-result-please.ts:14-27. Status codes indicating empty responses (101, 204, 205, and 304) are checked against nullBodyResponses to prevent body consumption errors src/client/fetch-result-please.ts:7, src/client/fetch-result-please.ts:25-26.

const nullBodyResponses = new Set([101, 204, 205, 304])
 
/**
 * Smartly parses and return the consumable result from a fetch `Response`.
 *
 * Throwing a structured error if the response is not `ok`. ({@link DetailedError})
 */
export async function fetchRP(fetchRes: Response | Promise<Response>): Promise<any> {
  const _fetchRes = (await fetchRes) as unknown as Response & {
    _data: any
    /**
     * @description BodyInit property from whatwg-fetch polyfill
     *
     * @link https://github.com/JakeChampion/fetch/blob/main/fetch.js#L238
     */
    _bodyInit?: any
  }
 
  const hasBody =
    (_fetchRes.body || _fetchRes._bodyInit) && !nullBodyResponses.has(_fetchRes.status)
 
  if (hasBody) {
    const responseType = detectResponseType(_fetchRes)
    _fetchRes._data = await _fetchRes[responseType]()
  }
 

Sources: src/client/fetch-result-please.ts:7-32

When a valid body is present, detectResponseType inspects the content-type header to choose between json and text parsing src/client/fetch-result-please.ts:28-30, src/client/fetch-result-please.ts:80-95. The header value is split at the semicolon to isolate the media type before evaluating it against jsonRegex (/^application\/(?:[\w!#$%&*.^~-]*+)?json(?:;.+)?$/i) [src/client/fetch-result-please.ts:78](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/client/fetch-result-please.ts#L78), [src/client/fetch-result-please.ts:87-92](https://github.com/blade47/hono/blob/e50df01453e71b071c3e6136b161b160b9fdf916/src/client/fetch-result-please.ts#L87-L92). If no content type header exists or if the type does not match the JSON pattern, it defaults to text` src/client/fetch-result-please.ts:83-84, src/client/fetch-result-please.ts:94.

const jsonRegex = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(?:;.+)?$/i
 
function detectResponseType(response: Response): 'json' | 'text' {
  const _contentType = response.headers.get('content-type')
 
  if (!_contentType) {
    return 'text'
  }
 
  // `_contentType` might look like: `application/json; charset=utf-8`, `text/plain`, so we get the first part before `;`
  const contentType = _contentType.split(';').shift()!
 
  if (jsonRegex.test(contentType)) {
    return 'json'
  }
 
  return 'text'
}

Sources: src/client/fetch-result-please.ts:78-95

Note

The jsonRegex pattern accounts for vendor-specific and structured syntax suffixes such as +json while safely ignoring trailing parameters like character sets src/client/fetch-result-please.ts:78.

Error wrapping and DetailedError structure

If the response ok flag is false after body parsing, fetchRP throws a DetailedError containing the status code, status text, and parsed response payload src/client/fetch-result-please.ts:33-41. The DetailedError class extends the native Error object and accepts structured configuration options src/client/fetch-result-please.ts:46-75.

export class DetailedError extends Error {
  /**
   * Additional `message` that will be logged AND returned to client
   */
  public detail?: any
  /**
   * Additional `code` that will be logged AND returned to client
   */
  public code?: any
  /**
   * Additional value that will be logged AND NOT returned to client
   */
  public log?: any
  /**
   * Optionally set the status code to return, in a web server context
   */
  public statusCode?: any
 
  constructor(
    message: string,
    options: { detail?: any; code?: any; statusCode?: number; log?: any } = {}
  ) {
    super(message)
    this.name = 'DetailedError'
    this.log = options.log
    this.detail = options.detail
    this.code = options.code
    this.statusCode = options.statusCode
  }
}

Sources: src/client/fetch-result-please.ts:46-75

PropertyTypeDescription
messagestringHuman-readable error message passed to super() src/client/fetch-result-please.ts:65, src/client/fetch-result-please.ts:68.
detailanyAdditional details logged and returned to the client, including response data and status text src/client/fetch-result-please.ts:36-39, src/client/fetch-result-please.ts:50.
codeanyAdditional error code logged and returned to the client src/client/fetch-result-please.ts:52-54.
loganyAdditional value that is logged internally but not returned to the client src/client/fetch-result-please.ts:56-58, src/client/fetch-result-please.ts:70.
statusCodeanyOptional HTTP status code to return in a server context src/client/fetch-result-please.ts:60-62, src/client/fetch-result-please.ts:73.

Sources: src/client/fetch-result-please.ts:33-41, src/client/fetch-result-please.ts:46-75

Warning

The log property on DetailedError is strictly reserved for server-side logging and is intentionally excluded from payloads returned to clients src/client/fetch-result-please.ts:56-58.

WebSocket handshake and connection lifecycle

Protocol transformation and URL construction

When an RPC client proxy invokes the .ws() method, the client initiates a WebSocket connection by transforming the HTTP or HTTPS base URL scheme into a WebSocket-specific protocol (ws or wss) via replaceUrlProtocol src/client/client.ts:187-191. The proxy inspects the arguments passed to .ws() to resolve optional path parameters through replaceUrlParam and appends any query parameters supplied in the options object directly to the target URL's search parameters src/client/client.ts:188-203.

Sources: src/client/client.ts:187-203

Note

Array-valued query parameters are iterated and appended individually to preserve multiple values for the same key during the WebSocket handshake src/client/client.ts:196-202.

Client-side WebSocket establishment

The actual connection instance is created by an internal establishWebSocket helper function src/client/client.ts:204-209. This helper checks if a custom webSocket factory option was supplied in the global client request options and delegates to it; otherwise, it instantiates a native WebSocket object with the fully constructed target URL src/client/client.ts:205-211.

Sources: src/client/client.ts:204-212

Warning

Omitting the protocol transformation step would cause standard WebSocket constructors to fail when initialized against http:// or https:// base URLs src/client/client.ts:187-191.

Was this page helpful?