0
0
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:
ryu 2024-07-13 14:02:36 +09:00 committed by GitHub
parent c2698fa2e0
commit e6d253d96f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 231 additions and 0 deletions

View File

@ -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",

View File

@ -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"
],

View 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)
})
})

View 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 {}
}

View 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()
}
}