Skip to content

fix(zod-openapi): preserve onError/notFound bindings set after basePath()#2031

Merged
yusukebe merged 2 commits into
honojs:mainfrom
tokiya-takai:fix/zod-openapi-basepath-onerror
Jul 14, 2026
Merged

fix(zod-openapi): preserve onError/notFound bindings set after basePath()#2031
yusukebe merged 2 commits into
honojs:mainfrom
tokiya-takai:fix/zod-openapi-basepath-onerror

Conversation

@tokiya-takai

@tokiya-takai tokiya-takai commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Issue: #2021

Summary

OpenAPIHono.basePath() dropped any .onError() / .notFound() registered on the returned instance
after the basePath() call. The handler silently had no effect, and the parent app's handler
ran instead. This surfaced while migrating a plain Hono app to OpenAPIHono: a route()-mounted
sub-app quietly changed which onError handled its thrown errors.

Reproduction

import { Hono } from 'hono'
import { OpenAPIHono } from '@hono/zod-openapi'

const sub = new OpenAPIHono().basePath('/v2')
sub.onError(() => new Response('from sub'))
sub.get('/boom', () => {
  throw new Error('boom')
})

const app = new Hono()
app.onError(() => new Response('from parent'))
app.route('/', sub)

const res = await app.request('/v2/boom')
console.log(await res.text()) // 'from parent' ← expected 'from sub'

Root cause

OpenAPIHono.basePath() rebuilt the instance by spreading the plain Hono clone returned by super.basePath():

return new OpenAPIHono({ ...(super.basePath(path) as any), defaultHook: this.defaultHook })

onError, notFound, request, and fetch are arrow-function instance fields, so they are own-enumerable properties permanently bound to the instance they were created on.
super.basePath() returns a throwaway clone, and spreading it copies those clone-bound methods
into the constructor options — where Object.assign(this, options) then overwrites the freshly constructed (and correctly bound) methods on the returned instance.

As a result, sub.onError(h) mutated errorHandler on the discarded clone, not on the returned
sub (sub.errorHandler stayed the built-in default). Route registration only appeared to work
because the returned instance and the clone share the same underlying router / routes. When
parent.route('/', sub) reads sub.errorHandler as a plain property, it still sees the default,
so it does not wrap the sub-app's handlers — and the parent's onError ends up handling the thrown error.

Fix

Construct a properly initialized OpenAPIHono (so its own arrow-field methods stay bound to it) and transplant only the routing state — router, routes, getPath, errorHandler, _basePath — from the clone, instead of spreading the clone into the constructor. Carrying errorHandler over preserves the "onError() before basePath()" ordering that core Hono's clone already captured.

Tests

  • Should fire onError() registered after basePath() — regression guard for this issue (fails on main, passes with the fix).
  • Should fire onError() registered before basePath() — guards the opposite ordering so the errorHandler transplant can't regress.
  • notFound() set after basePath() targets the returned instance — a same-instance contract test. Note: route() propagates errorHandler but not notFoundHandler, so this documents the binding rather than acting as a regression guard (it passes with or without the fix; commented inline).

Verified locally: full @hono/zod-openapi suite passes (81 tests, type-check clean), ESLint clean, Prettier clean.

The author should do the following, if applicable

  • Add tests
  • Run tests
  • pnpm changeset at the top of this repo and push the changeset
  • Follow the contribution guide

@changeset-bot

changeset-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: b3b6619

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@hono/zod-openapi Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@codecov

codecov Bot commented Jul 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.08%. Comparing base (f03694d) to head (b3b6619).
⚠️ Report is 13 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2031      +/-   ##
==========================================
+ Coverage   92.07%   92.08%   +0.01%     
==========================================
  Files         115      115              
  Lines        4074     4081       +7     
  Branches     1063     1063              
==========================================
+ Hits         3751     3758       +7     
  Misses        287      287              
  Partials       36       36              
Flag Coverage Δ
zod-openapi 95.12% <100.00%> (+0.21%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@yusukebe yusukebe left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@yusukebe yusukebe merged commit bf52ec2 into honojs:main Jul 14, 2026
98 checks passed
This was referenced Jul 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants