Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 85 additions & 1 deletion src/helper/route/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Context } from '../../context'
import { matchedRoutes, routePath, baseRoutePath, basePath } from '.'
import { matchedRoutes, routePath, baseRoutePath, basePath, handlerPath } from '.'

const defaultContextOptions = {
executionCtx: {
Expand Down Expand Up @@ -217,3 +217,87 @@ describe('basePath', () => {
expect(basePath(c)).toBe('/sub-app-path/foo')
})
})

describe('handlerPath', () => {
it('should return the path of the actual handler, ignoring middleware', () => {
const middlewareA = () => {}
const handler = () => {}
const middlewareB = () => {}
const rawRequest = new Request('http://localhost/health', { method: 'GET' })
const c = new Context(rawRequest, {
path: '/health',
matchResult: [
[
[
[middlewareA, { basePath: '/', handler: middlewareA, method: 'ALL', path: '/*' }],
{},
],
[
[handler, { basePath: '/', handler: handler, method: 'GET', path: '/health' }],
{},
],
[
[middlewareB, { basePath: '/', handler: middlewareB, method: 'ALL', path: '/*' }],
{},
],
],
],
...defaultContextOptions,
})

// Should return the GET handler path, not the middleware path
expect(handlerPath(c)).toBe('/health')
})

it('should return empty string when no handler matches', () => {
const middlewareA = () => {}
const rawRequest = new Request('http://localhost/health', { method: 'GET' })
const c = new Context(rawRequest, {
path: '/health',
matchResult: [
[
[
[middlewareA, { basePath: '/', handler: middlewareA, method: 'ALL', path: '/*' }],
{},
],
],
],
...defaultContextOptions,
})

expect(handlerPath(c)).toBe('')
})

it('should work correctly regardless of middleware registration order', () => {
const middlewareA = () => {}
const handler = () => {}
const middlewareB = () => {}
const rawRequest = new Request('http://localhost/users/123', { method: 'GET' })
const c = new Context(rawRequest, {
path: '/users/123',
matchResult: [
[
[
[middlewareA, { basePath: '/', handler: middlewareA, method: 'ALL', path: '/*' }],
{},
],
[
[
handler,
{ basePath: '/', handler: handler, method: 'GET', path: '/users/:id' },
],
{ id: '123' },
],
[
[middlewareB, { basePath: '/', handler: middlewareB, method: 'ALL', path: '/*' }],
{},
],
],
],
...defaultContextOptions,
})

// routePath(c, -1) would return '/*' (from middlewareB), but handlerPath returns the real handler
expect(handlerPath(c)).toBe('/users/:id')
})
})
36 changes: 36 additions & 0 deletions src/helper/route/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Context } from '../../context'
import { GET_MATCH_RESULT } from '../../request/constants'
import { METHOD_NAME_ALL } from '../../router'
import type { RouterRoute } from '../../types'
import { getPattern, splitRoutingPath } from '../../utils/url'

Expand Down Expand Up @@ -58,6 +59,41 @@ export const matchedRoutes = (c: Context): RouterRoute[] =>
export const routePath = (c: Context, index?: number): string =>
matchedRoutes(c).at(index ?? c.req.routeIndex)?.path ?? ''

/**
* Get the route path of the actual handler (non-middleware) that will respond to the request.
*
* This is useful in middleware when you need the path of the actual route handler,
* regardless of how many middleware are registered before or after it.
* Unlike `routePath(c, -1)`, this function is not affected by middleware registered
* after the handler.
*
* @param {Context} c - The context object
* @returns The route path of the matched handler, or `''` if not found
*
* @example
* ```ts
* import { handlerPath } from 'hono/route'
*
* app.use(async (c, next) => {
* console.log(handlerPath(c)) // '/health' — unaffected by middleware order
* await next()
* })
*
* app.get('/health', (c) => c.json({ status: 'ok' }))
*
* // Even with middleware registered after the route, handlerPath still returns '/health'
* app.use(async (c, next) => {
* await next()
* })
* ```
*/
export const handlerPath = (c: Context): string => {
const method = c.req.method
const routes = matchedRoutes(c)
const handler = routes.find((r) => r.method !== METHOD_NAME_ALL && r.method === method)
return handler?.path ?? ''
}

/**
* Get the basePath of the as-is route specified by routing.
*
Expand Down