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-Type和content-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-Id、X-Forwarded-For。新项目建议直接用标准名如 Request-Id,但对接外部 API 时仍需按对方文档来。
Accept: text/event-stream:SSE 流式响应的关键标识。客户端通过它告知服务端希望接收流式数据,服务端据此决定是否开启流式输出。
3. 常用响应 Headers
| Header | 作用 | AI 场景示例 |
|---|---|---|
Content-Type | 响应体格式 | application/json 或 text/event-stream |
Content-Length | 响应体字节数 | 流式响应时不设置 |
Transfer-Encoding | 传输编码 | chunked(流式时常见) |
Set-Cookie | 设置客户端 Cookie | 会话型 AI 应用 |
Cache-Control | 缓存策略 | no-store(AI 响应通常不缓存) |
Access-Control-Allow-Origin | CORS 允许的来源域 | * 或指定域 |
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.headers 在 await next() 之后操作,是在中间件返回响应前修改响应头。如果中间件在 await next() 之前设置,会被后续中间件或 handler 覆盖。
5. Body 的数据格式
Body 是 HTTP 请求和响应中实际传输业务数据的部分。格式由 Content-Type Header 决定。
| 格式 | Content-Type | 典型场景 |
|---|---|---|
| JSON | application/json | AI API 请求/响应主体 |
| Form Data | application/x-www-form-urlencoded | 传统表单提交 |
| Multipart | multipart/form-data | 文件上传(图片、音频) |
| 纯文本 | text/plain | 简单文本输入 |
| 二进制流 | application/octet-stream | 音频流、模型权重传输 |
| SSE 流 | text/event-stream | AI 流式输出 |
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)
| 状态码 | 含义 | 使用场景 |
|---|---|---|
| 100 | Continue | 客户端继续发送请求体(大型上传前的预检) |
| 101 | Switching Protocols | 升级到 WebSocket |
日常 API 开发极少主动返回 1xx,除非实现 WebSocket 升级。
2xx 成功(Success)
| 状态码 | 含义 | AI 场景使用 |
|---|---|---|
| 200 | OK | 绝大多数正常响应 |
| 201 | Created | 资源创建成功(如创建 Fine-tuning Job) |
| 204 | No Content | 删除成功,无返回体 |
AI API 中 200 覆盖 95% 以上的场景。流式响应也是 200——只要请求合法且开始正常返回数据,状态码就是 200。
3xx 重定向(Redirection)
| 状态码 | 含义 | 区别 |
|---|---|---|
| 301 | Moved Permanently | 永久重定向,客户端应更新书签 |
| 302 | Found | 临时重定向,可能改变请求方法(POST → GET) |
| 304 | Not Modified | 缓存有效,客户端用本地缓存 |
| 307 | Temporary Redirect | 临时重定向,保持原请求方法不变 |
| 308 | Permanent Redirect | 永久重定向,保持原请求方法不变 |
AI API 中较少用到 3xx。304 可用于静态资源(模型列表、配置),减少重复传输。
4xx 客户端错误(Client Error)
| 状态码 | 含义 | AI 场景使用 |
|---|---|---|
| 400 | Bad Request | 请求格式错误(JSON 解析失败、缺少必填字段) |
| 401 | Unauthorized | 未认证(API Key 无效或过期) |
| 403 | Forbidden | 已认证但无权限(如免费用户调用 GPT-4) |
| 404 | Not Found | 资源不存在(模型 ID 错误、Fine-tuning Job ID 不存在) |
| 405 | Method Not Allowed | HTTP 方法错误(GET 请求了 POST-only 端点) |
| 409 | Conflict | 资源冲突(并发创建同名 Fine-tuning Job) |
| 422 | Unprocessable Entity | 参数校验失败(格式正确但语义错误) |
| 429 | Too Many Requests | 限流(超出 RPM/TPM 配额) |
5xx 服务端错误(Server Error)
| 状态码 | 含义 | AI 场景使用 |
|---|---|---|
| 500 | Internal Server Error | 未预期的内部错误 |
| 502 | Bad Gateway | 上游服务返回无效响应(模型网关转发失败) |
| 503 | Service Unavailable | 模型服务不可用(过载或维护中) |
| 504 | Gateway 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,中途出错时无法改变状态码。解决方式是:
- 在流中插入错误事件:
data: {"choices":[{"delta":{"content":"Hello"}}]}
data: {"error":{"message":"Model overloaded","type":"server_error"}}
- 客户端需要解析每个 SSE 事件,检查是否包含
error字段 - 服务端在发送错误事件后立即关闭流
这是 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
)
})延伸阅读
- MDN: HTTP Headers — HTTP Headers 完整参考
- MDN: HTTP Response Status Codes — 状态码完整列表
- OpenAI API: Streaming — OpenAI 流式响应协议规范
- RFC 6585: Additional HTTP Status Codes — 429 等状态码的定义
- Hono: Streaming — Hono 流式响应 Helper
总结
Headers、Body、Status Code 构成 HTTP 通信的三个层次:
- Headers 是元数据,决定「这次通信怎么进行」——格式、缓存、认证、限流
- Body 是载荷,决定「这次通信传什么」——JSON、文件、流式 chunk
- Status Code 是判定,决定「这次通信结果如何」——成功、失败、重试
在 AI 后端场景下,这三者有额外的设计考量:
- Headers 承担限流标识和请求追踪的运维职责
- Body 的 SSE 流式格式需要特殊的错误处理策略
- Status Code 的选择影响客户端行为(重试、报错、降级)
Hono 通过标准 Web API 暴露这些接口,没有私有抽象。理解了 HTTP 协议本身的设计,就能直接使用 Hono 的所有能力。
所属分组
01-Web服务基础与运行时原理