-
-
Notifications
You must be signed in to change notification settings - Fork 521
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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>
- Loading branch information
Showing
5 changed files
with
231 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() | ||
} | ||
} |