请求 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-IdX-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 的生成、透传和响应头注入。

这一节涉及到的几个层次:

  1. 基本用法requestId() 生成 UUID v4,存入 context 和响应头
  2. 透传上游 IDheaderName 选项优先使用网关或 CDN 传入的 ID
  3. 自定义生成函数:支持 nanoid、ULID 等替代格式
  4. 与日志结合:把 requestId 注入结构化日志,便于关联查询
  5. 透传给下游服务:调用其他服务时带上 X-Request-Id 头
  6. 与 OpenTelemetry 集成:requestId 作为 trace ID 或关联字段
  7. 前端错误上报:附带最近的 requestId,便于后端定位

请求 ID 是一个低成本、高收益的基础设施。没有它,排查跨服务问题会变得非常困难。

下一篇看 Secure Headers 安全头——secureHeaders() 中间件的功能和各配置项的含义。