mirror of
https://github.com/honojs/hono.git
synced 2024-11-21 18:18:57 +01:00
feat(middleware): introduce Request ID middleware (#3082)
* feat(middleware): introduce Request ID middleware * fix not to accept empty string in header * rename requestID to requestId * pass the context to the generator option * add typesVersions * fix typo Co-Authored-By: Taku Amano <taku@taaas.jp> * change to generate id if validation fails Co-Authored-By: Taku Amano <taku@taaas.jp> * fix limit length test --------- Co-authored-by: Taku Amano <taku@taaas.jp>
This commit is contained in:
parent
c2698fa2e0
commit
e6d253d96f
1
jsr.json
1
jsr.json
@ -49,6 +49,7 @@
|
||||
"./method-override": "./src/middleware/method-override/index.ts",
|
||||
"./powered-by": "./src/middleware/powered-by/index.ts",
|
||||
"./pretty-json": "./src/middleware/pretty-json/index.ts",
|
||||
"./request-id": "./src/middleware/request-id/request-id.ts",
|
||||
"./secure-headers": "./src/middleware/secure-headers/secure-headers.ts",
|
||||
"./ssg": "./src/helper/ssg/index.ts",
|
||||
"./streaming": "./src/helper/streaming/index.ts",
|
||||
|
@ -223,6 +223,11 @@
|
||||
"import": "./dist/middleware/pretty-json/index.js",
|
||||
"require": "./dist/cjs/middleware/pretty-json/index.js"
|
||||
},
|
||||
"./request-id": {
|
||||
"types": "./dist/types/middleware/request-id/index.d.ts",
|
||||
"import": "./dist/middleware/request-id/index.js",
|
||||
"require": "./dist/cjs/middleware/request-id/index.js"
|
||||
},
|
||||
"./secure-headers": {
|
||||
"types": "./dist/types/middleware/secure-headers/index.d.ts",
|
||||
"import": "./dist/middleware/secure-headers/index.js",
|
||||
@ -476,6 +481,9 @@
|
||||
"pretty-json": [
|
||||
"./dist/types/middleware/pretty-json"
|
||||
],
|
||||
"request-id": [
|
||||
"./dist/types/middleware/request-id"
|
||||
],
|
||||
"streaming": [
|
||||
"./dist/types/helper/streaming"
|
||||
],
|
||||
|
155
src/middleware/request-id/index.test.ts
Normal file
155
src/middleware/request-id/index.test.ts
Normal file
@ -0,0 +1,155 @@
|
||||
import type { Context } from '../../context'
|
||||
import { Hono } from '../../hono'
|
||||
import { requestId } from '.'
|
||||
|
||||
const regexUUIDv4 = /([0-9a-f]{8})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{4})-([0-9a-f]{12})/
|
||||
|
||||
describe('Request ID Middleware', () => {
|
||||
const app = new Hono()
|
||||
app.use('*', requestId())
|
||||
app.get('/requestId', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
|
||||
it('Should return random request id', async () => {
|
||||
const res = await app.request('http://localhost/requestId')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toMatch(regexUUIDv4)
|
||||
expect(await res.text()).match(regexUUIDv4)
|
||||
})
|
||||
|
||||
it('Should return custom request id', async () => {
|
||||
const res = await app.request('http://localhost/requestId', {
|
||||
headers: {
|
||||
'X-Request-Id': 'hono-is-cool',
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBe('hono-is-cool')
|
||||
expect(await res.text()).toBe('hono-is-cool')
|
||||
})
|
||||
|
||||
it('Should return random request id without using request header', async () => {
|
||||
const res = await app.request('http://localhost/requestId', {
|
||||
headers: {
|
||||
'X-Request-Id': 'Hello!12345-@*^',
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toMatch(regexUUIDv4)
|
||||
expect(await res.text()).toMatch(regexUUIDv4)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request ID Middleware with custom generator', () => {
|
||||
function generateWord() {
|
||||
return 'HonoIsWebFramework'
|
||||
}
|
||||
function generateDoubleRequestId(c: Context) {
|
||||
const honoId = c.req.header('Hono-Request-Id')
|
||||
const ohnoId = c.req.header('Ohno-Request-Id')
|
||||
if (honoId && ohnoId) {
|
||||
return honoId + ohnoId
|
||||
}
|
||||
return crypto.randomUUID()
|
||||
}
|
||||
const app = new Hono()
|
||||
app.use('/word', requestId({ generator: generateWord }))
|
||||
app.use('/doubleRequestId', requestId({ generator: generateDoubleRequestId }))
|
||||
app.get('/word', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
app.get('/doubleRequestId', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
it('Should return custom request id', async () => {
|
||||
const res = await app.request('http://localhost/word')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBe('HonoIsWebFramework')
|
||||
expect(await res.text()).toBe('HonoIsWebFramework')
|
||||
})
|
||||
|
||||
it('Should return complex request id', async () => {
|
||||
const res = await app.request('http://localhost/doubleRequestId', {
|
||||
headers: {
|
||||
'Hono-Request-Id': 'Hello',
|
||||
'Ohno-Request-Id': 'World',
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBe('HelloWorld')
|
||||
expect(await res.text()).toBe('HelloWorld')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request ID Middleware with limit length', () => {
|
||||
const charactersOf255 = 'h'.repeat(255)
|
||||
const charactersOf256 = 'h'.repeat(256)
|
||||
|
||||
const app = new Hono()
|
||||
app.use('/requestId', requestId())
|
||||
app.use('/limit256', requestId({ limitLength: 256 }))
|
||||
app.get('/requestId', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
app.get('/limit256', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
|
||||
it('Should return custom request id', async () => {
|
||||
const res = await app.request('http://localhost/requestId', {
|
||||
headers: {
|
||||
'X-Request-Id': charactersOf255,
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBe(charactersOf255)
|
||||
expect(await res.text()).toBe(charactersOf255)
|
||||
})
|
||||
it('Should return random request id without using request header', async () => {
|
||||
const res = await app.request('http://localhost/requestId', {
|
||||
headers: {
|
||||
'X-Request-Id': charactersOf256,
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toMatch(regexUUIDv4)
|
||||
expect(await res.text()).toMatch(regexUUIDv4)
|
||||
})
|
||||
it('Should return custom request id with 256 characters', async () => {
|
||||
const res = await app.request('http://localhost/limit256', {
|
||||
headers: {
|
||||
'X-Request-Id': charactersOf256,
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBe(charactersOf256)
|
||||
expect(await res.text()).toBe(charactersOf256)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Request ID Middleware with custom header', () => {
|
||||
const app = new Hono()
|
||||
app.use('/requestId', requestId({ headerName: 'Hono-Request-Id' }))
|
||||
app.get('/emptyId', requestId({ headerName: '' }))
|
||||
app.get('/requestId', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
app.get('/emptyId', (c) => c.text(c.get('requestId') ?? 'No Request ID'))
|
||||
|
||||
it('Should return custom request id', async () => {
|
||||
const res = await app.request('http://localhost/requestId', {
|
||||
headers: {
|
||||
'Hono-Request-Id': 'hono-is-cool',
|
||||
},
|
||||
})
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('Hono-Request-Id')).toBe('hono-is-cool')
|
||||
expect(await res.text()).toBe('hono-is-cool')
|
||||
})
|
||||
|
||||
it('Should not return request id', async () => {
|
||||
const res = await app.request('http://localhost/emptyId')
|
||||
expect(res).not.toBeNull()
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('X-Request-Id')).toBeNull()
|
||||
expect(await res.text()).toMatch(regexUUIDv4)
|
||||
})
|
||||
})
|
8
src/middleware/request-id/index.ts
Normal file
8
src/middleware/request-id/index.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import type { RequestIdVariables } from './request-id'
|
||||
export type { RequestIdVariables }
|
||||
export { requestId } from './request-id'
|
||||
import type {} from '../..'
|
||||
|
||||
declare module '../..' {
|
||||
interface ContextVariableMap extends RequestIdVariables {}
|
||||
}
|
59
src/middleware/request-id/request-id.ts
Normal file
59
src/middleware/request-id/request-id.ts
Normal file
@ -0,0 +1,59 @@
|
||||
/**
|
||||
* @module
|
||||
* Request ID Middleware for Hono.
|
||||
*/
|
||||
|
||||
import type { Context } from '../../context'
|
||||
import type { MiddlewareHandler } from '../../types'
|
||||
|
||||
export type RequestIdVariables = {
|
||||
requestId: string
|
||||
}
|
||||
|
||||
export type RequestIdOptions = {
|
||||
limitLength?: number
|
||||
headerName?: string
|
||||
generator?: (c: Context) => string
|
||||
}
|
||||
|
||||
/**
|
||||
* Request ID Middleware for Hono.
|
||||
*
|
||||
* @param {object} options - Options for Request ID middleware.
|
||||
* @param {number} [options.limitLength=255] - The maximum length of request id.
|
||||
* @param {string} [options.headerName=X-Request-Id] - The header name used in request id.
|
||||
* @param {generator} [options.generator=() => crypto.randomUUID()] - The request id generation function.
|
||||
*
|
||||
* @returns {MiddlewareHandler} The middleware handler function.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* type Variables = RequestIdVariables
|
||||
* const app = new Hono<{Variables: Variables}>()
|
||||
*
|
||||
* app.use(requestId())
|
||||
* app.get('/', (c) => {
|
||||
* console.log(c.get('requestId')) // Debug
|
||||
* return c.text('Hello World!')
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export const requestId = ({
|
||||
limitLength = 255,
|
||||
headerName = 'X-Request-Id',
|
||||
generator = () => crypto.randomUUID(),
|
||||
}: RequestIdOptions = {}): MiddlewareHandler => {
|
||||
return async function requestId(c, next) {
|
||||
// If `headerName` is empty string, req.header will return the object
|
||||
let reqId = headerName ? c.req.header(headerName) : undefined
|
||||
if (!reqId || reqId.length > limitLength || /[^\w\-]/.test(reqId)) {
|
||||
reqId = generator(c)
|
||||
}
|
||||
|
||||
c.set('requestId', reqId)
|
||||
if (headerName) {
|
||||
c.header(headerName, reqId)
|
||||
}
|
||||
await next()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user