请求 ID 中间件
要点
- 请求 ID 是给每个请求分配的唯一标识,用于日志关联和分布式追踪
- Hono 内置
requestId()中间件自动生成并注入请求 ID - 如果上游已经传入请求 ID(例如网关或 CDN),中间件会优先使用上游的 ID
- 请求 ID 应该出现在日志和响应头里,便于前端排查和后端追踪
- 常见的 ID 格式:UUID v4、nanoid、ULID(带时间戳,可排序)
- 下游服务透传请求 ID,可以实现跨服务的链路追踪
1. 为什么需要请求 ID
一个请求可能经过多个服务:负载均衡器 → API 网关 → 业务服务 → 数据库 → 缓存 → 消息队列。每个服务都会写自己的日志。
没有请求 ID 时,排查一个问题需要在多个服务的日志里按时间范围搜索,靠时间戳和用户名猜测哪些日志属于同一个请求。这在并发量高时基本不可行。
请求 ID 解决的就是这个关联问题:每个请求进入系统时被分配一个唯一 ID,所有服务在所有日志里都带上这个 ID。排查时按 ID 搜索,就能串起整个链路。
2. 基本用法
Hono 内置的 requestId() 中间件自动生成请求 ID 并注入 context:
// src/index.ts
import { Hono } from 'hono'
import { requestId } from 'hono/request-id'
const app = new Hono()
app.use('*', requestId())
app.get('/', (c) => {
const id = c.get('requestId')
return c.json({ requestId: id })
})
export default app默认生成的 ID 是 UUID v4 格式,例如 550e8400-e29b-41d4-a716-446655440000。
requestId() 同时把 ID 写入响应头 X-Request-Id,前端可以直接读取:
// 前端代码
const res = await fetch('https://api.example.com/')
const requestId = res.headers.get('X-Request-Id')
console.log(requestId) // "550e8400-e29b-41d4-a716-446655440000"前端报问题时附上 requestId,后端就能快速定位对应的日志。
3. 透传上游的请求 ID
如果请求经过了网关或 CDN,上游可能已经生成了请求 ID 并放在某个请求头里(通常是 X-Request-Id 或 X-Correlation-Id)。requestId() 中间件会优先使用上游的 ID:
// src/index.ts
import { Hono } from 'hono'
import { requestId } from 'hono/request-id'
const app = new Hono()
app.use('*', requestId({
headerName: 'X-Request-Id', // 从该请求头读取上游 ID
}))
app.get('/', (c) => {
const id = c.get('requestId')
return c.json({ requestId: id })
})
export default app如果请求头里有 X-Request-Id,中间件直接用它;如果没有,中间件生成一个新的。这样整条链路上所有服务都使用同一个 ID。
4. 自定义 ID 生成函数
默认的 UUID v4 在大多数场景够用。如果对 ID 格式有要求,可以用 generator 选项替换生成函数:
// src/index.ts
import { Hono } from 'hono'
import { requestId } from 'hono/request-id'
const app = new Hono()
// 使用 nanoid(更短,URL 安全)
import { nanoid } from 'nanoid'
app.use('*', requestId({
generator: () => nanoid(16),
}))
app.get('/', (c) => {
const id = c.get('requestId')
return c.json({ requestId: id }) // "V1StGXR8_Z5jdHi6"
})
export default app几种常见的 ID 格式:
- UUID v4:128 位,全局唯一,不携带时间信息
- nanoid:可配置长度,URL 安全,比 UUID 更短
- ULID:带时间戳,可排序,长度 26 字符
- Snowflake ID:带时间戳和机器信息,适合分布式系统
ULID 是一个折中选择:可排序(便于日志扫描)、长度适中、不需要额外的机器配置:
import { ulid } from 'ulid'
app.use('*', requestId({
generator: () => ulid(),
}))5. 请求 ID 与日志结合
请求 ID 的主要用途是关联日志。把 requestId 注入到每条日志记录里:
// src/middleware/logger.ts
import { createMiddleware } from 'hono/factory'
export const requestLogger = createMiddleware(async (c, next) => {
const start = Date.now()
const requestId = c.get('requestId') as string
await next()
const duration = Date.now() - start
console.log(JSON.stringify({
time: new Date().toISOString(),
requestId,
method: c.req.method,
path: c.req.path,
status: c.res.status,
duration,
}))
})// src/index.ts
import { Hono } from 'hono'
import { requestId } from 'hono/request-id'
import { requestLogger } from './middleware/logger'
const app = new Hono()
// requestId 在 logger 之前,这样 logger 能读到 requestId
app.use('*', requestId())
app.use('*', requestLogger)
app.get('/', (c) => c.text('Hello'))
export default app日志输出:
{
"time": "2026-06-20T10:00:00.000Z",
"requestId": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
"method": "GET",
"path": "/",
"status": 200,
"duration": 2
}如果项目使用 pino 等结构化日志库,可以创建一个 child logger,把 requestId 绑定到每条日志:
import pino from 'pino'
const logger = pino()
export const requestLogger = createMiddleware(async (c, next) => {
const requestId = c.get('requestId') as string
const childLogger = logger.child({ requestId })
// 把 child logger 存入 context,路由里也能用
c.set('logger', childLogger)
await next()
})app.get('/users', (c) => {
const logger = c.get('logger') as pino.Logger
logger.info('fetching users from database')
// 日志自动携带 requestId
return c.json([])
})6. 透传给下游服务
业务服务调用其他服务(数据库、缓存、外部 API)时,应该把请求 ID 透传过去,让下游服务的日志也能关联到同一条链路:
// src/lib/fetch-with-trace.ts
export async function fetchWithTrace(
url: string,
requestId: string,
options: RequestInit = {},
) {
const headers = new Headers(options.headers)
headers.set('X-Request-Id', requestId)
return fetch(url, {
...options,
headers,
})
}// src/routes/users.ts
app.get('/users', async (c) => {
const requestId = c.get('requestId') as string
// 调用下游服务时带上 requestId
const res = await fetchWithTrace(
'https://user-service.internal/users',
requestId,
)
const users = await res.json()
return c.json(users)
})下游服务如果也是 Hono 应用,配置了 requestId({ headerName: 'X-Request-Id' }),就会使用传入的 ID 而不是生成新的。
7. 与 OpenTelemetry 集成
生产环境的链路追踪通常用 OpenTelemetry。请求 ID 可以作为 trace ID 的一部分:
// src/middleware/tracing.ts
import { createMiddleware } from 'hono/factory'
import { trace } from '@opentelemetry/api'
export const tracing = createMiddleware(async (c, next) => {
const tracer = trace.getTracer('hono-app')
const requestId = c.get('requestId') as string
// 用 requestId 作为 trace ID(或从 W3C Trace Context 头提取)
const span = tracer.startSpan(`${c.req.method} ${c.req.path}`, {
attributes: {
'http.method': c.req.method,
'http.url': c.req.url,
'request.id': requestId,
},
})
try {
await next()
} catch (err) {
span.setStatus({ code: 2, message: (err as Error).message })
throw err
} finally {
span.setAttribute('http.status_code', c.res.status)
span.end()
}
})requestId() 中间件生成的 ID 是字符串,OpenTelemetry 的 trace ID 要求 32 个十六进制字符。如果要用 requestId 作为 trace ID,需要选择合适的生成函数(例如 128 位的 UUID 去掉连字符)。
8. 前端错误上报
前端发生错误时,可以把最近几次请求的 requestId 一起上报,便于后端排查:
// 前端:请求拦截器
let lastRequestIds = []
const originalFetch = window.fetch
window.fetch = async (...args) => {
const res = await originalFetch(...args)
const requestId = res.headers.get('X-Request-Id')
if (requestId) {
lastRequestIds.push(requestId)
if (lastRequestIds.length > 10) {
lastRequestIds.shift()
}
}
return res
}
// 前端:错误上报
window.addEventListener('error', (event) => {
reportError({
message: event.message,
stack: event.error?.stack,
recentRequestIds: lastRequestIds, // 附带最近的 requestId
})
})后端收到错误上报后,用 recentRequestIds 搜索日志,就能看到用户出错前请求的处理链路。
总结
请求 ID 中间件是分布式追踪和日志关联的基础。Hono 内置的 requestId() 中间件处理了 ID 的生成、透传和响应头注入。
这一节涉及到的几个层次:
- 基本用法:
requestId()生成 UUID v4,存入 context 和响应头 - 透传上游 ID:
headerName选项优先使用网关或 CDN 传入的 ID - 自定义生成函数:支持 nanoid、ULID 等替代格式
- 与日志结合:把 requestId 注入结构化日志,便于关联查询
- 透传给下游服务:调用其他服务时带上 X-Request-Id 头
- 与 OpenTelemetry 集成:requestId 作为 trace ID 或关联字段
- 前端错误上报:附带最近的 requestId,便于后端定位
请求 ID 是一个低成本、高收益的基础设施。没有它,排查跨服务问题会变得非常困难。
下一篇看 Secure Headers 安全头——secureHeaders() 中间件的功能和各配置项的含义。