Skip to content
Draft
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
1 change: 1 addition & 0 deletions jsr.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"./pretty-json": "./src/middleware/pretty-json/index.ts",
"./request-id": "./src/middleware/request-id/request-id.ts",
"./language": "./src/middleware/language/language.ts",
"./route-check": "./src/middleware/route-check/index.ts",
"./secure-headers": "./src/middleware/secure-headers/secure-headers.ts",
"./combine": "./src/middleware/combine/index.ts",
"./ssg": "./src/helper/ssg/index.ts",
Expand Down
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,11 @@
"import": "./dist/middleware/language/index.js",
"require": "./dist/cjs/middleware/language/index.js"
},
"./route-check": {
"types": "./dist/types/middleware/route-check/index.d.ts",
"import": "./dist/middleware/route-check/index.js",
"require": "./dist/cjs/middleware/route-check/index.js"
},
"./secure-headers": {
"types": "./dist/types/middleware/secure-headers/index.d.ts",
"import": "./dist/middleware/secure-headers/index.js",
Expand Down Expand Up @@ -539,6 +544,9 @@
"ssg": [
"./dist/types/helper/ssg"
],
"route-check": [
"./dist/types/middleware/route-check"
],
"secure-headers": [
"./dist/types/middleware/secure-headers"
],
Expand Down
231 changes: 231 additions & 0 deletions src/middleware/route-check/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
import { Hono } from '../../hono'
import { bearerAuth } from '../bearer-auth'
import { routeCheck } from '.'

describe('Route Check Middleware', () => {
let app: Hono

beforeEach(() => {
app = new Hono()
})

describe('Basic functionality', () => {
it('Should allow access to existing routes', async () => {
app.use('/admin/*', routeCheck())
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

const res = await app.request('/admin/dashboard')
expect(res.status).toBe(200)
expect(await res.text()).toBe('Admin Dashboard')
})

it('Should return 404 for non-existent routes', async () => {
app.use('/admin/*', routeCheck())
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

const res = await app.request('/admin/non-existent')
expect(res.status).toBe(404)
expect(await res.text()).toBe('404 Not Found')
})

it('Should work with POST routes', async () => {
app.use('/api/*', routeCheck())
app.post('/api/users', (c) => c.text('User Created'))

const res = await app.request('/api/users', { method: 'POST' })
expect(res.status).toBe(200)
expect(await res.text()).toBe('User Created')
})

it('Should return 404 for non-existent POST routes', async () => {
app.use('/api/*', routeCheck())
app.post('/api/users', (c) => c.text('User Created'))

const res = await app.request('/api/posts', { method: 'POST' })
expect(res.status).toBe(404)
})
})

describe('Integration with authentication middleware', () => {
it('Should skip authentication for non-existent routes', async () => {
let authExecuted = false

app.use('/admin/*', routeCheck())
app.use('/admin/*', async (c, next) => {
authExecuted = true
await next()
})
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

const res = await app.request('/admin/non-existent')
expect(res.status).toBe(404)
expect(authExecuted).toBe(false)
})

it('Should execute authentication for existing routes', async () => {
let authExecuted = false

app.use('/admin/*', routeCheck())
app.use('/admin/*', async (c, next) => {
authExecuted = true
await next()
})
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

const res = await app.request('/admin/dashboard')
expect(res.status).toBe(200)
expect(authExecuted).toBe(true)
})

it('Should work with bearerAuth middleware', async () => {
app.use('/admin/*', routeCheck())
app.use('/admin/*', bearerAuth({ token: 'my-secret' }))
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

// Non-existent route should return 404 without auth
const res1 = await app.request('/admin/non-existent')
expect(res1.status).toBe(404)

// Existing route without auth should return 401
const res2 = await app.request('/admin/dashboard')
expect(res2.status).toBe(401)

// Existing route with auth should return 200
const res3 = await app.request('/admin/dashboard', {
headers: { Authorization: 'Bearer my-secret' },
})
expect(res3.status).toBe(200)
expect(await res3.text()).toBe('Admin Dashboard')
})
})

describe('Support for different route definitions', () => {
it('Should work with app.all()', async () => {
app.use('/admin/*', routeCheck())
app.all('/admin/settings', (c) => c.text('Settings'))

const res1 = await app.request('/admin/settings', { method: 'GET' })
expect(res1.status).toBe(200)
expect(await res1.text()).toBe('Settings')

const res2 = await app.request('/admin/settings', { method: 'POST' })
expect(res2.status).toBe(200)
expect(await res2.text()).toBe('Settings')
})

it('Should distinguish between handlers and middleware', async () => {
app.use('/api/*', routeCheck())
app.use('/api/*', async (c, next) => {
// This is middleware, not a handler
await next()
})
app.get('/api/users', (c) => c.text('Users'))

// Should return 200 because handler exists
const res1 = await app.request('/api/users')
expect(res1.status).toBe(200)

// Should return 404 because no handler exists
const res2 = await app.request('/api/posts')
expect(res2.status).toBe(404)
})

it('Should work with multiple HTTP methods on same path', async () => {
app.use('/api/*', routeCheck())
app.get('/api/users', (c) => c.text('Get Users'))
app.post('/api/users', (c) => c.text('Create User'))

const res1 = await app.request('/api/users', { method: 'GET' })
expect(res1.status).toBe(200)
expect(await res1.text()).toBe('Get Users')

const res2 = await app.request('/api/users', { method: 'POST' })
expect(res2.status).toBe(200)
expect(await res2.text()).toBe('Create User')

// PUT should return 404 as no handler exists
const res3 = await app.request('/api/users', { method: 'PUT' })
expect(res3.status).toBe(404)
})
})

describe('Custom onNotFound handler', () => {
it('Should use custom handler when route not found', async () => {
app.use(
'/api/*',
routeCheck({
onNotFound: (c) => c.json({ error: 'API endpoint not found' }, 404),
})
)
app.get('/api/users', (c) => c.text('Users'))

const res = await app.request('/api/posts')
expect(res.status).toBe(404)
expect(await res.json()).toEqual({ error: 'API endpoint not found' })
})

it('Should use default handler when onNotFound not specified', async () => {
app.use('/api/*', routeCheck())
app.get('/api/users', (c) => c.text('Users'))

const res = await app.request('/api/posts')
expect(res.status).toBe(404)
expect(await res.text()).toBe('404 Not Found')
})

it('Should not call onNotFound when route exists', async () => {
let onNotFoundCalled = false

app.use(
'/api/*',
routeCheck({
onNotFound: (c) => {
onNotFoundCalled = true
return c.json({ error: 'Not found' }, 404)
},
})
)
app.get('/api/users', (c) => c.text('Users'))

const res = await app.request('/api/users')
expect(res.status).toBe(200)
expect(onNotFoundCalled).toBe(false)
})
})

describe('Edge cases', () => {
it('Should work without any routes defined', async () => {
app.use('/admin/*', routeCheck())

const res = await app.request('/admin/anything')
expect(res.status).toBe(404)
})

it('Should work with nested paths', async () => {
app.use('/api/v1/*', routeCheck())
app.get('/api/v1/users/:id', (c) => c.text(`User ${c.req.param('id')}`))

const res1 = await app.request('/api/v1/users/123')
expect(res1.status).toBe(200)
expect(await res1.text()).toBe('User 123')

const res2 = await app.request('/api/v1/posts/123')
expect(res2.status).toBe(404)
})

it('Should work when placed after some middleware', async () => {
let middleware1Executed = false

app.use('/admin/*', async (c, next) => {
middleware1Executed = true
await next()
})
app.use('/admin/*', routeCheck())
app.get('/admin/dashboard', (c) => c.text('Admin Dashboard'))

const res = await app.request('/admin/non-existent')
expect(res.status).toBe(404)
expect(middleware1Executed).toBe(true)
})
})
})
65 changes: 65 additions & 0 deletions src/middleware/route-check/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* @module
* Route Check Middleware for Hono.
*/

import type { Context } from '../../context'
import { matchedRoutes } from '../../helper/route'
import type { MiddlewareHandler } from '../../types'
import { findTargetHandler, isMiddleware } from '../../utils/handler'

type RouteCheckOptions = {
/**
* Custom handler to execute when no route is found.
* If not specified, returns default 404 response.
*/
onNotFound?: (c: Context) => Response | Promise<Response>
}

/**
* Route Check Middleware for Hono.
*
* Checks if a route handler exists before executing subsequent middleware.
* Returns 404 immediately for non-existent routes, skipping expensive operations
* like authentication.
*
* @param {RouteCheckOptions} options - Configuration options.
* @returns {MiddlewareHandler} The middleware handler function.
*
* @example
* ```ts
* app.use('/admin/*', routeCheck())
* app.use('/admin/*', bearerAuth({ token: 'my-secret' }))
*
* app.get('/admin/dashboard', (c) => c.text('Dashboard'))
*
* // GET /admin/non-existent returns 404 without authentication
* // GET /admin/dashboard requires authentication
* ```
*
* @example
* With custom not found handler:
* ```ts
* app.use('/api/*', routeCheck({
* onNotFound: (c) => c.json({ error: 'Not Found' }, 404)
* }))
* ```
*/
export const routeCheck = (options?: RouteCheckOptions): MiddlewareHandler => {
return async (c, next) => {
const routes = matchedRoutes(c)

// Check if there's at least one actual handler (not middleware)
const hasActualHandler = routes.some((route) => {
const targetHandler = findTargetHandler(route.handler)
return !isMiddleware(targetHandler)
})

if (!hasActualHandler) {
// No actual handler found - return 404 immediately
return options?.onNotFound ? options.onNotFound(c) : c.notFound()
}

await next()
}
}
Loading