Skip to content

Commit cd62a07

Browse files
authored
feat(hono): Support .query() handlers (#23283)
Adds support for `.query` handlers added in hono 4.13.0 - https://github.com/honojs/hono/releases/tag/v4.13.0
1 parent 0b43807 commit cd62a07

6 files changed

Lines changed: 121 additions & 17 deletions

File tree

‎dev-packages/e2e-tests/test-applications/hono-4/deno.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"@sentry/deno": "npm:@sentry/deno",
66
"@sentry/core": "npm:@sentry/core",
77
"@opentelemetry/api": "npm:@opentelemetry/api@^1.9.0",
8-
"hono": "npm:hono@^4.12.14"
8+
"hono": "npm:hono@^4.13.1"
99
},
1010
"nodeModulesDir": "manual"
1111
}

‎dev-packages/e2e-tests/test-applications/hono-4/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"@sentry/hono": "latest || *",
2020
"@sentry/node": "latest || *",
2121
"@hono/node-server": "^2.0.5",
22-
"hono": "^4.12.14"
22+
"hono": "^4.13.1"
2323
},
2424
"devDependencies": {
2525
"@playwright/test": "~1.56.0",

‎dev-packages/e2e-tests/test-applications/hono-4/src/routes.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,16 @@ export function addRoutes(app: HonoType<{ Bindings?: { E2E_TEST_DSN: string } }>
7272
},
7373
c => c.text('main inline all'),
7474
);
75+
app.query(
76+
'/test-main-inline/query',
77+
async function mainInlineQuery(_c, next) {
78+
await next();
79+
},
80+
async c => {
81+
const body = await c.req.json<{ value: string }>();
82+
return c.json({ method: c.req.method, value: body.value });
83+
},
84+
);
7585

7686
// Combined: .use() middleware + inline middleware via .get() on the same path.
7787
app.use('/test-main-inline/combined/*', middlewareA);

‎dev-packages/e2e-tests/test-applications/hono-4/tests/middleware.test.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,37 @@ const MAIN_INLINE_CASES = [
253253
] as const;
254254

255255
test.describe('inline middleware spans (main app)', () => {
256+
test('creates middleware span for inline middleware via .query()', async ({ baseURL }) => {
257+
const fullPath = `${MAIN_INLINE_PREFIX}/query`;
258+
const transactionPromise = waitForTransaction(APP_NAME, event => {
259+
return event.contexts?.trace?.op === 'http.server' && event.transaction === `QUERY ${fullPath}`;
260+
});
261+
262+
const response = await fetch(`${baseURL}${fullPath}`, {
263+
method: 'QUERY',
264+
headers: { 'Content-Type': 'application/json' },
265+
body: JSON.stringify({ value: 'query-body' }),
266+
});
267+
expect(response.status).toBe(200);
268+
await expect(response.json()).resolves.toEqual({ method: 'QUERY', value: 'query-body' });
269+
270+
const transaction = await transactionPromise;
271+
expect(transaction.transaction).toBe(`QUERY ${fullPath}`);
272+
expect(transaction.contexts?.trace?.op).toBe('http.server');
273+
expect(transaction.transaction_info?.source).toBe('route');
274+
expect(transaction.request?.method).toBe('QUERY');
275+
276+
const middlewareSpans = (transaction.spans || []).filter(span => span.op === 'middleware');
277+
expect(middlewareSpans).toHaveLength(1);
278+
expect(middlewareSpans[0]).toEqual(
279+
expect.objectContaining({
280+
description: 'mainInlineQuery',
281+
op: 'middleware',
282+
origin: 'auto.middleware.hono',
283+
}),
284+
);
285+
});
286+
256287
MAIN_INLINE_CASES.forEach(({ name, path, method, expectedMiddlewareName }) => {
257288
test(`creates middleware span for inline middleware via ${name}`, async ({ baseURL }) => {
258289
const fullPath = `${MAIN_INLINE_PREFIX}${path}`;

‎packages/hono/src/shared/patchAppUse.ts‎

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const patchedUseInstances = new WeakSet<Hono<any>>();
88
// oxlint-disable-next-line typescript/no-explicit-any
99
const patchedMethodInstances = new WeakSet<Hono<any>>();
1010

11-
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'options', 'patch', 'all'] as const;
11+
const HTTP_METHODS = ['get', 'post', 'put', 'delete', 'options', 'patch', 'all', 'query'] as const;
1212

1313
/**
1414
* Patches `app.use` (instance own property) on a Hono instance to instrument middleware at registration time.
@@ -40,7 +40,7 @@ export function patchAppUse<E extends Env>(app: Hono<E>): void {
4040
}
4141

4242
/**
43-
* Patches HTTP method class fields (get, post, put, delete, options, patch, all) to instrument inline middleware at registration time.
43+
* Patches HTTP method class fields to instrument inline middleware at registration time.
4444
*
4545
* For `app.get('/path', mw1, mw2, handler)`, all handlers except the last are middleware and get wrapped with spans.
4646
* The final handler (the route handler) is already covered by the root http.server transaction.
@@ -54,19 +54,13 @@ export function patchHttpMethodHandlers<E extends Env>(app: Hono<E>): void {
5454
patchedMethodInstances.add(app);
5555

5656
for (const method of HTTP_METHODS) {
57-
app[method] = new Proxy(app[method], {
58-
apply(target, thisArg, args: unknown[]) {
59-
return Reflect.apply(target, thisArg, wrapInlineMiddleware(args));
60-
},
61-
});
57+
patchRegistrationMethod(app, method, wrapInlineMiddleware);
6258
}
6359

64-
app.on = new Proxy(app.on, {
65-
apply(target, thisArg, args: unknown[]) {
66-
// .on(method, path, ...handlers) — first two args are method and path
67-
const [method, path, ...handlers] = args;
68-
return Reflect.apply(target, thisArg, [method, path, ...wrapInlineMiddleware(handlers)]);
69-
},
60+
patchRegistrationMethod(app, 'on', args => {
61+
// .on(method, path, ...handlers) — first two args are method and path
62+
const [method, path, ...handlers] = args;
63+
return [method, path, ...wrapInlineMiddleware(handlers)];
7064
});
7165
}
7266

@@ -89,3 +83,17 @@ function wrapInlineMiddleware(args: unknown[]): unknown[] {
8983
}
9084
return wrapped;
9185
}
86+
87+
function patchRegistrationMethod(app: object, method: string, transformArgs: (args: unknown[]) => unknown[]): void {
88+
const registration: unknown = Reflect.get(app, method);
89+
if (typeof registration !== 'function') {
90+
return;
91+
}
92+
93+
const patchedRegistration = new Proxy(registration, {
94+
apply(target, thisArg, args: unknown[]) {
95+
return Reflect.apply(target, thisArg, transformArgs(args));
96+
},
97+
});
98+
Reflect.set(app, method, patchedRegistration);
99+
}

‎packages/hono/test/shared/patchAppUse.test.ts‎

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,7 @@ describe('patchHttpMethodHandlers (inline middleware spans on main app)', () =>
278278
patchHttpMethodHandlers(app);
279279

280280
app.on(
281-
'GET',
281+
'QUERY',
282282
'/test',
283283
async function onMw(_c: unknown, next: () => Promise<void>) {
284284
await next();
@@ -288,14 +288,69 @@ describe('patchHttpMethodHandlers (inline middleware spans on main app)', () =>
288288
},
289289
);
290290

291-
await app.fetch(new Request('http://localhost/test'));
291+
await app.fetch(new Request('http://localhost/test', { method: 'QUERY' }));
292292

293293
const spanNames = startInactiveSpanMock.mock.calls.map((c: unknown[]) => (c[0] as { name: string }).name);
294294
expect(spanNames).toHaveLength(1);
295295
expect(spanNames).toContain('onMw');
296296
expect(spanNames).not.toContain('onHandler');
297297
});
298298

299+
it('wraps app.query middleware when query is available (from 4.13.0)', async () => {
300+
const context = { value: 'context' };
301+
const result = { value: 'result' };
302+
let registeredMiddleware: ((context: unknown, next: () => Promise<void>) => Promise<void>) | undefined;
303+
let registeredHandler: ((context: unknown) => unknown) | undefined;
304+
const query = vi.fn(function (
305+
this: unknown,
306+
path: string,
307+
middleware: (context: unknown, next: () => Promise<void>) => Promise<void>,
308+
handler: (context: unknown) => unknown,
309+
) {
310+
expect(this).toBe(fakeApp);
311+
expect(path).toBe('/test');
312+
registeredMiddleware = middleware;
313+
registeredHandler = handler;
314+
return result;
315+
});
316+
const fakeApp = Object.assign(new Hono(), { query });
317+
async function queryMiddleware(receivedContext: unknown, next: () => Promise<void>) {
318+
expect(receivedContext).toBe(context);
319+
await next();
320+
}
321+
const middleware = vi.fn(queryMiddleware);
322+
const handler = vi.fn((receivedContext: unknown) => {
323+
expect(receivedContext).toBe(context);
324+
return 'handled';
325+
});
326+
327+
patchHttpMethodHandlers(fakeApp as unknown as Parameters<typeof patchHttpMethodHandlers>[0]);
328+
const registrationResult = fakeApp.query('/test', middleware, handler);
329+
330+
expect(registrationResult).toBe(result);
331+
if (!registeredMiddleware || !registeredHandler) {
332+
throw new Error('query handlers were not registered');
333+
}
334+
expect(registeredHandler).toBe(handler);
335+
336+
const next = vi.fn(async () => undefined);
337+
await registeredMiddleware(context, next);
338+
const handlerResult = registeredHandler(context);
339+
340+
expect(startInactiveSpanMock).toHaveBeenCalledTimes(1);
341+
expect(startInactiveSpanMock).toHaveBeenCalledWith(expect.objectContaining({ name: 'queryMiddleware' }));
342+
expect(middleware).toHaveBeenCalledWith(context, next);
343+
expect(next).toHaveBeenCalledTimes(1);
344+
expect(handler).toHaveBeenCalledWith(context);
345+
expect(handlerResult).toBe('handled');
346+
});
347+
348+
it('patches apps without app.query', () => {
349+
const app = new Hono();
350+
351+
expect(() => patchHttpMethodHandlers(app)).not.toThrow();
352+
});
353+
299354
it('does not wrap sole handler in app.on(method, path, handler)', async () => {
300355
const app = new Hono();
301356
patchHttpMethodHandlers(app);

0 commit comments

Comments
 (0)