diff --git a/.changeset/inherit-default-hook-on-route.md b/.changeset/inherit-default-hook-on-route.md new file mode 100644 index 000000000..a06218063 --- /dev/null +++ b/.changeset/inherit-default-hook-on-route.md @@ -0,0 +1,5 @@ +--- +'@hono/zod-openapi': patch +--- + +fix: inherit `defaultHook` from parent app on nested routes mounted via `app.route()` diff --git a/packages/zod-openapi/src/index.test.ts b/packages/zod-openapi/src/index.test.ts index 21d7e2454..fe737d2bb 100644 --- a/packages/zod-openapi/src/index.test.ts +++ b/packages/zod-openapi/src/index.test.ts @@ -1083,6 +1083,28 @@ describe('Routers', () => { }) expect(res.status).toBe(200) }) + + it('Should apply the parent app defaultHook to nested routes mounted via app.route()', async () => { + const subApp = new OpenAPIHono().openapi(route, (c) => c.json({ id: 123 })) + + const app = new OpenAPIHono({ + defaultHook: (result, c) => { + if (!result.success) { + return c.json({ ok: false, source: 'parentDefaultHook' }, 422) + } + }, + }).route('/api', subApp) + + const res = await app.request('/api/posts', { + method: 'POST', + body: JSON.stringify({ id: 'not-a-number' }), + headers: { + 'Content-Type': 'application/json', + }, + }) + expect(res.status).toBe(422) + expect(await res.json()).toEqual({ ok: false, source: 'parentDefaultHook' }) + }) }) describe('Multi params', () => { diff --git a/packages/zod-openapi/src/index.ts b/packages/zod-openapi/src/index.ts index cfa40e54f..3fc2a1c87 100644 --- a/packages/zod-openapi/src/index.ts +++ b/packages/zod-openapi/src/index.ts @@ -576,7 +576,7 @@ export class OpenAPIHono< ? MaybePromise> | undefined : MaybePromise> | MaybePromise | undefined > - | undefined = this.defaultHook + | undefined = undefined // eslint-disable-line @typescript-eslint/no-useless-default-assignment ): OpenAPIHono< E, S & ToSchema, I, RouteConfigToTypedResponse>, @@ -586,25 +586,34 @@ export class OpenAPIHono< this.openAPIRegistry.registerPath(route) } + // Resolve the defaultHook lazily so nested apps mounted via `route()` + // can inherit the parent's defaultHook. See #1306. + /* eslint-disable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */ + const effectiveHook = + hook ?? + ((result: any, c: any) => + this.defaultHook ? (this.defaultHook as any)(result, c) : undefined) + /* eslint-enable @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call */ + const validators: MiddlewareHandler[] = [] if (route.request?.query) { - const validator = zValidator('query', route.request.query as any, hook as any) + const validator = zValidator('query', route.request.query as any, effectiveHook as any) validators.push(validator as any) } if (route.request?.params) { - const validator = zValidator('param', route.request.params as any, hook as any) + const validator = zValidator('param', route.request.params as any, effectiveHook as any) validators.push(validator as any) } if (route.request?.headers) { - const validator = zValidator('header', route.request.headers as any, hook as any) + const validator = zValidator('header', route.request.headers as any, effectiveHook as any) validators.push(validator as any) } if (route.request?.cookies) { - const validator = zValidator('cookie', route.request.cookies as any, hook as any) + const validator = zValidator('cookie', route.request.cookies as any, effectiveHook as any) validators.push(validator as any) } @@ -622,7 +631,7 @@ export class OpenAPIHono< if (isJSONContentType(mediaType)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore we can ignore the type error since Zod Validator's types are not used - const validator = zValidator('json', schema, hook) as MiddlewareHandler + const validator = zValidator('json', schema, effectiveHook as any) as MiddlewareHandler if (route.request?.body?.required) { validators.push(validator) } else { @@ -641,7 +650,7 @@ export class OpenAPIHono< if (isFormContentType(mediaType)) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore we can ignore the type error since Zod Validator's types are not used - const validator = zValidator('form', schema, hook) as MiddlewareHandler + const validator = zValidator('form', schema, effectiveHook as any) as MiddlewareHandler if (route.request?.body?.required) { validators.push(validator) } else { @@ -794,6 +803,10 @@ export class OpenAPIHono< return this as any } + if (app.defaultHook === undefined && this.defaultHook !== undefined) { + app.defaultHook = this.defaultHook + } + app.openAPIRegistry.definitions.forEach((def) => { switch (def.type) { case 'component':