03-Headers、Body、Status Code

前两节建立了 HTTP 协议的基本图像:请求发出,响应返回,中间经过若干中间节点。但这个图像还缺少细节——请求和响应之间的具体信息是如何传递的?哪些字段决定内容格式,哪些字段控制缓存行为,哪些字段告诉客户端出了什么错?

本节把镜头推近,拆开 Headers、Body 和 Status Code 三个部分。

要点

  • Headers 是 HTTP 通信的元数据层,决定内容格式、缓存策略、身份认证和行为控制,本身不承载业务数据
  • Body 是实际的业务载荷,格式多样,AI 场景下 JSON 和 SSE 流最常见
  • Status Code 是服务端对这次请求结果的判定,选错状态码会让客户端行为出错
  • 在 AI 后端场景下,Headers 承担限流标识、请求追踪等运维职责,Body 涉及流式输出的 Chunk 格式设计,Status Code 需要在流已经开始后处理错误
  • Hono 通过标准 Web API 暴露这三个部分,没有引入私有抽象

1. Headers 的本质:键值对元数据

Headers 不携带业务数据,它描述的是「这次请求/响应本身」的属性——内容是什么格式、要不要缓存、客户端是谁、有没有权限。

POST /v1/chat/completions HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer sk-xxxxx
X-Request-Id: req_abc123

上面这 5 行 Headers 分别描述了:目标主机、请求体格式、身份凭证、请求追踪 ID。服务端读到这些信息后,决定如何处理这个请求。

请求头和响应头的区别在于方向:请求头从客户端到服务端,响应头从服务端到客户端。同名字段在两个方向上含义可能不同,比如 Content-Type 在请求头里描述请求体格式,在响应头里描述响应体格式。

Header 命名规则:

  • 大小写不敏感:Content-Typecontent-type 是同一个 Header
  • 连字符分隔:HTTP/2 强制小写,实际代码里按惯例用 Title-Case
  • 多值 Header:部分 Header 可以有多个值,用逗号分隔(Accept: text/html, application/json),但 Set-Cookie 是例外,必须每个值单独一行

2. 常用请求 Headers

Header作用AI 场景示例
Content-Type请求体格式application/json
Accept客户端可接受的响应格式text/event-stream(SSE 流式)
Authorization身份凭证Bearer sk-xxxxx
Content-Length请求体字节数由客户端自动计算
User-Agent客户端标识MyApp/1.0
Origin请求来源域(CORS 用)https://app.example.com
Referer上一页面地址一般不用于 API
X-Request-Id请求追踪 ID(自定义)req_abc123

几点值得展开:

Authorization:AI API 几乎都用 Bearer <token> 格式。Bearer 表示「持有此 token 即视为合法」,不需要额外签名。注意 Bearer 和 token 之间有一个空格,大小写敏感。

X-* 自定义头X- 前缀是历史约定,表示「非标准、自定义」。IETF 已在 RFC 6648 中建议停止使用 X- 前缀,但实践中仍然广泛存在,比如 X-Request-IdX-Forwarded-For。新项目建议直接用标准名如 Request-Id,但对接外部 API 时仍需按对方文档来。

Accept: text/event-stream:SSE 流式响应的关键标识。客户端通过它告知服务端希望接收流式数据,服务端据此决定是否开启流式输出。

3. 常用响应 Headers

Header作用AI 场景示例
Content-Type响应体格式application/jsontext/event-stream
Content-Length响应体字节数流式响应时不设置
Transfer-Encoding传输编码chunked(流式时常见)
Set-Cookie设置客户端 Cookie会话型 AI 应用
Cache-Control缓存策略no-store(AI 响应通常不缓存)
Access-Control-Allow-OriginCORS 允许的来源域* 或指定域
X-RateLimit-Limit限流上限100
X-RateLimit-Remaining剩余配额97
X-RateLimit-Reset限流重置时间(Unix 时间戳)1718880000
X-Request-Id请求追踪 ID(响应中回传)回传客户端传来的 ID

几个关键点:

Cache-Control: no-store:AI 生成的响应每次不同,缓存没有意义,且可能泄露其他用户数据。几乎所有 AI API 响应都应该设置 no-store

X-RateLimit-* 系列:AI API 调用成本高,限流是基本需求。把限流信息通过响应头返回,客户端可以据此做退避策略,而不是盲等到被 429 拒绝。

CORS Headers:如果 AI API 需要从浏览器直接调用(非代理),必须正确配置 CORS。Access-Control-Allow-Origin 不能设为 * 的同时携带 Authorization 头——带凭证的请求必须指定具体域名。

4. 在 Hono 中操作 Headers

读取请求头:

import { Hono } from 'hono'
 
const app = new Hono()
 
app.post('/v1/chat/completions', (c) => {
  // 读取单个 Header
  const contentType = c.req.header('Content-Type')
  // → 'application/json'
 
  // 读取 Authorization
  const auth = c.req.header('Authorization')
  // → 'Bearer sk-xxxxx'
 
  // 读取自定义 Header
  const requestId = c.req.header('X-Request-Id')
 
  return c.json({ requestId })
})

设置响应头:

app.get('/v1/models', (c) => {
  // 方式一:通过 c.header()
  c.header('X-Request-Id', 'req_abc123')
  c.header('Cache-Control', 'no-store')
  c.header('X-RateLimit-Remaining', '97')
 
  return c.json({ models: [] })
})
 
// 方式二:直接操作 Response Headers
app.get('/v1/status', (c) => {
  return new Response('OK', {
    headers: {
      'X-Request-Id': 'req_abc123',
      'Content-Type': 'text/plain',
    },
  })
})

中间件里批量设置 Headers:

// 全局限流和追踪中间件
app.use('*', async (c, next) => {
  const requestId = crypto.randomUUID()
 
  // 请求头:记录客户端信息
  const clientIp = c.req.header('X-Forwarded-For') ?? 'unknown'
  const userAgent = c.req.header('User-Agent') ?? ''
 
  await next()
 
  // 响应头:回传追踪信息和限流状态
  c.res.headers.set('X-Request-Id', requestId)
  c.res.headers.set('X-RateLimit-Limit', '100')
  c.res.headers.set('X-RateLimit-Remaining', '97')
  c.res.headers.set('Cache-Control', 'no-store')
})

注意:c.res.headersawait next() 之后操作,是在中间件返回响应前修改响应头。如果中间件在 await next() 之前设置,会被后续中间件或 handler 覆盖。

5. Body 的数据格式

Body 是 HTTP 请求和响应中实际传输业务数据的部分。格式由 Content-Type Header 决定。

格式Content-Type典型场景
JSONapplication/jsonAI API 请求/响应主体
Form Dataapplication/x-www-form-urlencoded传统表单提交
Multipartmultipart/form-data文件上传(图片、音频)
纯文本text/plain简单文本输入
二进制流application/octet-stream音频流、模型权重传输
SSE 流text/event-streamAI 流式输出

JSON 是 AI API 的事实标准。OpenAI、Anthropic、Google 的 API 都用 JSON 传递模型参数和响应。

{
  "model": "gpt-4",
  "messages": [
    { "role": "user", "content": "Hello" }
  ],
  "stream": true
}

Multipart 用于文件上传场景,比如 AI 图片识别、语音转文字。一次请求可以同时携带文本字段和文件字段:

--boundary
Content-Disposition: form-data; name="prompt"

Describe this image
--boundary
Content-Disposition: form-data; name="image"; filename="photo.png"
Content-Type: image/png

[binary data]
--boundary--

SSE(Server-Sent Events) 是 AI 流式输出的标准方式。响应格式为:

data: {"choices":[{"delta":{"content":"Hello"}}]}

data: {"choices":[{"delta":{"content":" world"}}]}

data: [DONE]

每个 data: 行是一个 JSON 事件,以双换行分隔。[DONE] 是 OpenAI 约定的结束标记。

6. Body 的大小与流式处理

Body 的大小直接影响内存使用。对于 AI API,这个问题尤其突出——大文件上传(图片、音频)和大响应(长文本生成)都需要考虑。

大 Body 的内存问题:如果服务端把整个 Body 读入内存,一个 100MB 的请求会占用 100MB 服务器内存。高并发下,内存很快耗尽。

流式读取(ReadableStream):不把 Body 一次性读入内存,而是以 chunk 方式逐步处理:

app.post('/v1/audio/transcriptions', async (c) => {
  const reader = c.req.raw.body.getReader()
 
  let totalSize = 0
  while (true) {
    const { done, value } = await reader.read()
    if (done) break
 
    totalSize += value.byteLength
    // 逐块处理,不需要把整个文件放内存
  }
 
  return c.json({ totalSize })
})

流式响应的 Chunk 格式:AI 流式输出时,服务端逐 chunk 返回数据,每个 chunk 是一个完整的 SSE 事件。客户端每收到一个 chunk 就可以立即渲染,不需要等待完整响应:

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"你"}}]}

data: {"id":"chatcmpl-123","object":"chat.completion.chunk","choices":[{"delta":{"content":"好"}}]}

data: [DONE]

7. 在 Hono 中处理 Body

读取请求 Body:

app.post('/v1/chat/completions', async (c) => {
  // JSON Body → 自动解析
  const body = await c.req.json()
  // body.model === 'gpt-4'
 
  // 纯文本 Body
  const text = await c.req.text()
 
  // Form Data(含文件上传)
  const formData = await c.req.formData()
  const file = formData.get('image') as File
 
  return c.json({ received: true })
})

返回响应 Body:

// JSON 响应
app.get('/v1/models', (c) => {
  return c.json({
    object: 'list',
    data: [{ id: 'gpt-4', object: 'model' }],
  })
})
 
// 纯文本响应
app.get('/health', (c) => {
  return c.text('OK')
})
 
// 流式 SSE 响应
app.post('/v1/chat/completions', async (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      const encoder = new TextEncoder()
 
      // 模拟逐 token 输出
      const tokens = ['你', '好', ',', '世界', '!']
      for (const token of tokens) {
        const chunk = {
          choices: [{ delta: { content: token } }],
        }
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`)
        )
        // 模拟生成延迟
        await new Promise((r) => setTimeout(r, 100))
      }
 
      // 发送结束标记
      controller.enqueue(encoder.encode('data: [DONE]\n\n'))
      controller.close()
    },
  })
 
  return new Response(stream, {
    headers: {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-store',
      'Connection': 'keep-alive',
    },
  })
})

Hono 还提供了 streamSSE() helper,简化 SSE 流式响应的写法:

import { streamSSE } from 'hono/streaming'
 
app.post('/v1/chat/completions', async (c) => {
  return streamSSE(c, async (stream) => {
    const tokens = ['你', '好', ',', '世界', '!']
 
    for (const token of tokens) {
      const chunk = {
        choices: [{ delta: { content: token } }],
      }
      await stream.writeSSE({
        data: JSON.stringify(chunk),
        event: 'message',
      })
      await stream.sleep(100)
    }
 
    await stream.writeSSE({ data: '[DONE]' })
  })
})

8. 状态码的分类与设计哲学

Status Code 是服务端对这次请求结果的判定。它不是给开发者看的日志,而是给客户端程序看的决策依据——客户端根据状态码决定下一步行为:重试、跳转、报错、刷新。

1xx 信息性(Informational)

状态码含义使用场景
100Continue客户端继续发送请求体(大型上传前的预检)
101Switching Protocols升级到 WebSocket

日常 API 开发极少主动返回 1xx,除非实现 WebSocket 升级。

2xx 成功(Success)

状态码含义AI 场景使用
200OK绝大多数正常响应
201Created资源创建成功(如创建 Fine-tuning Job)
204No Content删除成功,无返回体

AI API 中 200 覆盖 95% 以上的场景。流式响应也是 200——只要请求合法且开始正常返回数据,状态码就是 200。

3xx 重定向(Redirection)

状态码含义区别
301Moved Permanently永久重定向,客户端应更新书签
302Found临时重定向,可能改变请求方法(POST → GET)
304Not Modified缓存有效,客户端用本地缓存
307Temporary Redirect临时重定向,保持原请求方法不变
308Permanent Redirect永久重定向,保持原请求方法不变

AI API 中较少用到 3xx。304 可用于静态资源(模型列表、配置),减少重复传输。

4xx 客户端错误(Client Error)

状态码含义AI 场景使用
400Bad Request请求格式错误(JSON 解析失败、缺少必填字段)
401Unauthorized未认证(API Key 无效或过期)
403Forbidden已认证但无权限(如免费用户调用 GPT-4)
404Not Found资源不存在(模型 ID 错误、Fine-tuning Job ID 不存在)
405Method Not AllowedHTTP 方法错误(GET 请求了 POST-only 端点)
409Conflict资源冲突(并发创建同名 Fine-tuning Job)
422Unprocessable Entity参数校验失败(格式正确但语义错误)
429Too Many Requests限流(超出 RPM/TPM 配额)

5xx 服务端错误(Server Error)

状态码含义AI 场景使用
500Internal Server Error未预期的内部错误
502Bad Gateway上游服务返回无效响应(模型网关转发失败)
503Service Unavailable模型服务不可用(过载或维护中)
504Gateway Timeout上游服务超时(模型推理时间过长)

9. AI 后端的常见状态码选择

429 Too Many Requests:AI API 调用成本高,限流是基本要求。429 响应应该携带限流信息,让客户端知道何时可以重试:

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1718880030
 
{
  "error": {
    "message": "Rate limit exceeded. Please retry after 30 seconds.",
    "type": "rate_limit_exceeded"
  }
}

Retry-After Header 告诉客户端等待秒数,客户端据此做指数退避。

401 vs 403:这是最常见的混淆。

  • 401 Unauthorized:「我不知道你是谁」——没有提供 API Key,或者 API Key 无效、过期
  • 403 Forbidden:「我知道你是谁,但你不能做这个」——已认证但无权限。比如免费用户调用付费模型、访问他人私有的 Fine-tuning Job
// 401:认证失败
if (!apiKey || !isValidApiKey(apiKey)) {
  return c.json(
    { error: { message: 'Invalid API key', type: 'authentication_error' } },
    401
  )
}
 
// 403:授权失败
if (!hasModelAccess(user, 'gpt-4')) {
  return c.json(
    { error: { message: 'No access to this model', type: 'permission_error' } },
    403
  )
}

422 Unprocessable Entity:请求格式正确(JSON 解析成功),但语义错误。比如 temperature 传了字符串、max_tokens 超出模型上下文窗口、messages 里 role 值不在允许范围:

if (body.temperature && typeof body.temperature !== 'number') {
  return c.json(
    {
      error: {
        message: 'temperature must be a number',
        type: 'invalid_request_error',
        param: 'temperature',
      },
    },
    422
  )
}

503 Service Unavailable:模型服务不可用。和 500 的区别在于:500 是「出了未预期的错」,503 是「我知道现在不行,你可以稍后重试」。503 通常配合 Retry-After Header:

if (!isModelAvailable('gpt-4')) {
  c.header('Retry-After', '60')
  return c.json(
    { error: { message: 'Model is temporarily unavailable', type: 'service_unavailable' } },
    503
  )
}

流式响应中的错误处理:这是 AI 后端独有的难题。流式响应已经开始返回 200,中途出错时无法改变状态码。解决方式是:

  1. 在流中插入错误事件:
data: {"choices":[{"delta":{"content":"Hello"}}]}

data: {"error":{"message":"Model overloaded","type":"server_error"}}
  1. 客户端需要解析每个 SSE 事件,检查是否包含 error 字段
  2. 服务端在发送错误事件后立即关闭流

这是 OpenAI 的做法,也是目前的事实标准。

10. 在 Hono 中返回状态码

基础用法——状态码作为第二个参数:

// 200(默认)
app.get('/v1/models', (c) => {
  return c.json({ data: [] })
})
 
// 201 Created
app.post('/v1/fine_tuning/jobs', async (c) => {
  const job = await createFineTuningJob(await c.req.json())
  return c.json(job, 201)
})
 
// 204 No Content
app.delete('/v1/models/:id', (c) => {
  deleteModel(c.req.param('id'))
  return c.body(null, 204)
})

错误处理的常见模式:

// 统一错误响应格式
const errorResponse = (c: Context, status: number, message: string, type: string) => {
  return c.json(
    {
      error: {
        message,
        type,
      },
    },
    status
  )
}
 
app.post('/v1/chat/completions', async (c) => {
  // 参数校验
  const body = await c.req.json().catch(() => null)
  if (!body) {
    return errorResponse(c, 400, 'Invalid JSON', 'invalid_request_error')
  }
 
  if (!body.messages || !Array.isArray(body.messages)) {
    return errorResponse(c, 422, 'messages is required and must be an array', 'invalid_request_error')
  }
 
  // 业务逻辑
  try {
    const result = await callModel(body)
    return c.json(result)
  } catch (err) {
    if (err instanceof ModelUnavailableError) {
      c.header('Retry-After', '60')
      return errorResponse(c, 503, 'Model unavailable', 'service_unavailable')
    }
    return errorResponse(c, 500, 'Internal server error', 'server_error')
  }
})

全局错误处理中间件:

app.onError((err, c) => {
  console.error('Unhandled error:', err)
 
  // 根据环境返回不同详细程度
  const message =
    process.env.NODE_ENV === 'production'
      ? 'Internal server error'
      : err.message
 
  return c.json(
    {
      error: {
        message,
        type: 'server_error',
      },
    },
    500
  )
})
 
// 404 兜底
app.notFound((c) => {
  return c.json(
    {
      error: {
        message: `Cannot ${c.req.method} ${c.req.path}`,
        type: 'not_found_error',
      },
    },
    404
  )
})

延伸阅读

总结

Headers、Body、Status Code 构成 HTTP 通信的三个层次:

  • Headers 是元数据,决定「这次通信怎么进行」——格式、缓存、认证、限流
  • Body 是载荷,决定「这次通信传什么」——JSON、文件、流式 chunk
  • Status Code 是判定,决定「这次通信结果如何」——成功、失败、重试

在 AI 后端场景下,这三者有额外的设计考量:

  1. Headers 承担限流标识和请求追踪的运维职责
  2. Body 的 SSE 流式格式需要特殊的错误处理策略
  3. Status Code 的选择影响客户端行为(重试、报错、降级)

Hono 通过标准 Web API 暴露这些接口,没有私有抽象。理解了 HTTP 协议本身的设计,就能直接使用 Hono 的所有能力。

所属分组

01-Web服务基础与运行时原理