02-Request与Response模型
要点
- HTTP 协议定义了客户端和服务端之间的通信契约,Request 和 Response 是这份契约的两端
- Request 由方法、目标、Headers、Body 四部分组成,Fetch API 的
Request对象把这些概念封装成了一个可直接操作的接口 - Response 由状态码、Headers、Body 组成,Fetch API 的
Response对象和 Hono 的c.res都基于同一套标准 - 一个请求从客户端发出到收到响应,经过 DNS 解析、TCP 连接、TLS 握手、发送、处理、返回等多个阶段
- 在 AI 场景中,JSON 序列化和流式响应是最常遇到的两种 Body 处理方式
- Hono 的 Context 封装了标准 Request/Response,
c.req和c.res是你在中间件和路由中最常打交道的对象
1. 从上一篇到这里
上一篇「HTTP 协议基础」给出了一个全局视角:HTTP 是一个无状态的请求-响应协议,客户端发出 Request,服务端返回 Response。
那个视角够用,但还不够细。
当你开始在 Hono 里写一个真实的 AI 接口,你会碰到这些具体问题:
- 请求体里的 JSON 怎么读取和校验?
- 流式响应和普通响应在模型上有什么差别?
- 为什么 AI 场景几乎都需要 SSE 或 ReadableStream?
c.req和c.res到底封装了哪些东西?
这些问题都需要把 Request 和 Response 拆开来看。
2. Request 模型的完整结构
一个 HTTP Request 由四部分组成:
POST /api/chat HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer sk-xxxx
{"model": "gpt-4", "messages": [{"role": "user", "content": "你好"}]}拆开来看:
| 组成部分 | 示例 | 作用 |
|---|---|---|
| 请求方法 | POST | 声明对资源执行什么操作 |
| 请求目标 | /api/chat | 声明请求指向哪个资源 |
| 请求头 | Content-Type: application/json | 携带元信息:内容类型、鉴权等 |
| 请求体 | {"model": "gpt-4", ...} | 携带需要传给服务端的实际数据 |
2.1 请求方法
HTTP/1.1 定义了九个标准方法,日常开发中最常用的是这四个:
GET:获取资源,无 BodyPOST:提交数据、创建资源,有 BodyPUT:整体替换资源DELETE:删除资源
在 AI 后端中,GET 和 POST 几乎覆盖了绝大多数场景。聊天接口用 POST 发送消息列表,查询对话历史用 GET。
2.2 请求目标(Request Target)
RFC 9112 定义了四种请求目标形式:
origin-form: /api/chat?stream=true
absolute-form: https://api.example.com/api/chat
authority-form: api.example.com
asterisk-form: *在 Web 开发中,最常接触的是 origin-form,即路径加查询参数。authority-form 用于 CONNECT 方法建立隧道,asterisk-form 用于 OPTIONS 探测服务器能力。
2.3 Fetch API 中的 Request 对象
浏览器和现代运行时都提供了标准的 Request 构造函数:
const request = new Request('https://api.example.com/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer sk-xxxx'
},
body: JSON.stringify({
model: 'gpt-4',
messages: [{ role: 'user', content: '你好' }]
})
})
// 读取各部分
request.method // "POST"
request.url // "https://api.example.com/api/chat"
request.headers.get('Content-Type') // "application/json"
await request.json() // { model: 'gpt-4', messages: [...] }Request 对象把方法、URL、Headers、Body 封装成一个整体。Body 是 ReadableStream,可以通过 .json()、.text()、.formData() 等方法消费。
这里有一个容易忽略的细节:Body 只能读取一次。调用 await request.json() 之后,stream 就被消费了,再次读取会得到空结果。在 Hono 中间件链中,如果你在一个中间件里读了 Body,后续中间件就读不到了。Hono 内部做了缓存处理来解决这个问题,但理解底层行为有助于排查异常。
3. Response 模型的完整结构
服务端返回的 HTTP Response 结构如下:
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_abc123
{"id": "chatcmpl-xxx", "choices": [...]}| 组成部分 | 示例 | 作用 |
|---|---|---|
| 状态行 | HTTP/1.1 200 OK | 协议版本、状态码、状态描述 |
| 响应头 | Content-Type: application/json | 携带元信息:内容类型、缓存策略等 |
| 响应体 | {"id": "chatcmpl-xxx", ...} | 携带返回给客户端的实际数据 |
3.1 状态码
状态码是五位数字,分五类:
| 范围 | 含义 | AI 接口常见示例 |
|---|---|---|
1xx | 信息性 | 101 Switching Protocols(WebSocket) |
2xx | 成功 | 200 OK |
3xx | 重定向 | 301、302、307 |
4xx | 客户端错误 | 400 Bad Request、401、429 |
5xx | 服务端错误 | 500 Internal Server Error、502、503 |
在 AI 后端中,429 Too Many Requests 尤其重要——模型 API 的限流会触发这个状态码,你的后端需要正确处理重试逻辑。
3.2 Fetch API 中的 Response 对象
const response = new Response(
JSON.stringify({ id: 'chatcmpl-xxx', content: '你好' }),
{
status: 200,
headers: {
'Content-Type': 'application/json',
'X-Request-Id': 'req_abc123'
}
}
)
response.status // 200
response.ok // true
response.headers.get('Content-Type') // "application/json"
await response.json() // { id: 'chatcmpl-xxx', content: '你好' }和 Request 一样,Response 的 Body 也是 ReadableStream,只能消费一次。
4. 请求的生命周期
一个 HTTP 请求从发出到收到响应,经历了这些阶段:
客户端构建 Request
↓
DNS 解析:域名 → IP 地址
↓
TCP 连接:三次握手
↓
TLS 握手(HTTPS):协商加密参数
↓
发送请求:请求行 + Headers + Body
↓
服务端接收并处理
↓
返回 Response:状态行 + Headers + Body
↓
客户端接收并处理 Response这个流程在上一篇 HTTP 基础中已经概览过。这里补充几个和 Request/Response 模型直接相关的点。
4.1 DNS 解析
客户端需要把 api.example.com 解析成 IP 地址。这个过程通常由操作系统和 ISP 的 DNS 服务器完成,耗时在毫秒级。对于边缘运行时(如 Cloudflare Workers),DNS 解析通常发生在平台基础设施层,开发者不需要直接处理。
4.2 TCP 连接与 TLS 握手
TCP 三次握手建立可靠连接。如果是 HTTPS,还需要 TLS 握手协商加密参数。这两步的延迟加起来通常在 1-2 个 RTT(Round-Trip Time)。
HTTP/2 和 HTTP/3 通过连接复用和多路复用来减少连接建立的开销。对 AI 后端来说,这意味着同一个客户端连续发送多个请求时,后续请求的传输层延迟会更低。
4.3 服务端处理
请求到达服务端后,Hono 会按路由匹配和中间件链依次处理:
请求进入 → 中间件链(日志、鉴权、CORS…)→ 路由处理 → 构建 Response → 返回每一层中间件都可以读取和修改 Request,也可以提前返回 Response 终止后续处理。这个模型在后续「中间件」专篇中会详细展开。
5. 序列化与反序列化
HTTP 协议的 Body 本质上是字节流。应用层需要自行决定如何编码数据。
5.1 JSON 请求体
AI 接口最常见的数据格式是 JSON。客户端把对象序列化成 JSON 字符串发送,服务端读取后反序列化回对象:
// 客户端:序列化
const body = JSON.stringify({
model: 'gpt-4',
messages: [
{ role: 'system', content: '你是一位有帮助的助手。' },
{ role: 'user', content: '解释一下 HTTP 协议' }
],
temperature: 0.7
})
// 服务端:反序列化
const data = await c.req.json()
// data.model === 'gpt-4'
// data.messages.length === 2这段代码看起来很简单,但它隐藏了一个实际问题:如果 Body 很大怎么办?
5.2 大 Body 的内存考量
AI 场景中有几种情况会产生大 Body:
- 长对话历史:几十轮对话的 messages 数组
- RAG 检索结果:多个文档片段拼成的上下文
- 文件上传:用户发送的图片、PDF、代码文件
await request.json() 会把整个 Body 读入内存。对于 1MB 的 JSON,这不是问题。对于 100MB 的 JSON,它会占用相当一部分内存,在高并发下可能成为瓶颈。
处理方式有几种:
- 设置 Body 大小限制(Hono 的
app.use()或运行时层面的 body size limit) - 对于流式场景,使用 ReadableStream 逐步消费,而不是等全部到达再处理
- 对文件类数据,使用 multipart form 并存储到对象存储,而不是整个读进内存
5.3 AI 场景中的序列化模式
AI API 的数据结构比普通 CRUD 更复杂。以下是几种常见的序列化场景:
消息列表序列化:
// 典型的聊天请求体
{
"model": "gpt-4",
"messages": [
{ "role": "system", "content": "..." },
{ "role": "user", "content": "..." },
{ "role": "assistant", "content": "..." },
{ "role": "user", "content": "..." }
],
"stream": true
}消息列表是一个有序数组,顺序直接影响模型的上下文理解。序列化时必须保持顺序,反序列化时不能重排。
工具调用结果序列化:
// 工具调用响应结构
{
"choices": [{
"message": {
"tool_calls": [{
"id": "call_abc",
"type": "function",
"function": {
"name": "get_weather",
"arguments": "{\"location\": \"北京\"}"
}
}]
}
}]
}注意 arguments 字段是一个 JSON 字符串嵌套在外层 JSON 中。这意味着你可能需要做两次反序列化,或者在定义类型时就注意这个嵌套结构。
6. 流式请求与响应
普通 HTTP 响应是「等全部处理完再一次性返回」。AI 场景中,模型生成可能需要几秒到几十秒,用户盯着空白页面等待的体验很差。
流式响应让服务端可以边生成边返回,用户在几百毫秒内就能看到第一个字。
6.1 SSE(Server-Sent Events)
SSE 是 AI 流式响应最常用的传输方式。它的 Content-Type 是 text/event-stream,数据格式是一系列 data: 行:
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: {"content": "你"}
data: {"content": "好"}
data: {"content": ","}
data: {"content": "我是"}
data: [DONE]客户端通过 EventSource API 或 fetch + ReadableStream 来消费这些数据。
6.2 流式响应在 Response 模型中的位置
从 HTTP 协议角度看,流式响应和普通响应的区别在于 Body 的传输方式:
- 普通响应:服务端生成完整 Body,设置
Content-Length,一次性发送 - 流式响应:Body 是逐步生成的,使用
Transfer-Encoding: chunked,分块发送
从 Fetch API 角度看,区别在于 Response 的 Body 类型:
// 普通响应:Body 是完整字符串
new Response(JSON.stringify({ content: '完整回复' }), {
headers: { 'Content-Type': 'application/json' }
})
// 流式响应:Body 是 ReadableStream
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder()
for await (const chunk of generateContent()) {
controller.enqueue(
encoder.encode(`data: ${JSON.stringify({ content: chunk })}\n\n`)
)
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'))
controller.close()
}
})
new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
}
})两种响应的 Headers 结构一样,区别在 Body 是完整数据还是流。
6.3 Hono 的 Streaming Helper
直接在路由里写 ReadableStream 比较繁琐。Hono 提供了 Streaming Helper 来简化这个过程:
import { Hono } from 'hono'
import { stream, streamSSE } from 'hono/streaming'
const app = new Hono()
// 文本流
app.get('/api/stream', (c) => {
return stream(c, async (stream) => {
for await (const chunk of generateContent()) {
await stream.write(chunk)
}
})
})
// SSE 流
app.get('/api/sse', (c) => {
return streamSSE(c, async (stream) => {
for await (const chunk of generateContent()) {
await stream.writeSSE({
data: JSON.stringify({ content: chunk }),
event: 'message'
})
}
})
})stream() 和 streamSSE() 帮你处理了 Response 的构建、Headers 的设置和 ReadableStream 的管理。你只需要关注数据源本身。
在 AI 场景中,数据源通常是模型 API 返回的异步迭代器。Hono 的 Streaming Helper 让这两者可以自然对接。
7. Request 和 Response 在 Hono 中的具体体现
在 Hono 中,你不会直接操作标准的 Request 和 Response 对象。Hono 的 Context 对它们做了一层封装。
7.1 c.req:封装后的 Request
app.post('/api/chat', async (c) => {
// 读取请求方法
c.req.method // "POST"
// 读取路径
c.req.path // "/api/chat"
// 读取查询参数
c.req.query('stream') // "true" | undefined
// 读取请求头
c.req.header('Authorization') // "Bearer sk-xxxx"
// 读取并解析 JSON Body
const body = await c.req.json()
// 路由参数
const id = c.req.param('id') // 来自 /api/chat/:id
})c.req 底层就是标准的 Request 对象,但它增加了路径解析、查询参数提取、Header 便捷方法等能力。在 Cloudflare Workers 环境中,c.req 还负责处理 Workers 特有的请求适配。
7.2 c.res:封装后的 Response
Hono 中更常见的写法是使用 c.json()、c.text() 等快捷方法直接返回 Response:
app.get('/api/health', (c) => {
return c.json({ status: 'ok', timestamp: Date.now() })
})
app.get('/api/text', (c) => {
return c.text('Hello, World!')
})
// 自定义状态码和 Headers
app.get('/api/resource/:id', (c) => {
const id = c.req.param('id')
return c.json(
{ id, name: 'example' },
200,
{ 'X-Request-Id': crypto.randomUUID() }
)
})当你需要更精细的控制时,可以直接操作 c.res:
// 设置响应头
c.res.headers.set('X-Custom-Header', 'value')
// 返回标准 Response 对象
return new Response('custom', {
status: 201,
headers: { 'Content-Type': 'text/plain' }
})7.3 Context 的桥梁角色
Hono 的 Context 对象(c)是中间件和路由的核心。它同时持有 Request 和 Response,并在整个处理链中传递:
// 中间件:读取 Request,可以修改或提前返回 Response
app.use('/api/*', async (c, next) => {
const token = c.req.header('Authorization')
if (!token) {
return c.json({ error: 'Unauthorized' }, 401)
}
await next()
// next() 之后可以继续操作 c.res
})
// 路由处理:读取 Request,构建 Response
app.post('/api/chat', async (c) => {
const body = await c.req.json()
// 处理业务逻辑...
return c.json({ result: 'ok' })
})这个模型和标准 Fetch API 的 Handler 一致,只是 Context 提供了更丰富的操作接口。
8. 小结:模型如何影响 AI 接口设计
回到本文的核心问题:Request 和 Response 的模型如何影响 AI 后端的接口设计?
几个具体的影响:
- Body 的格式选择:AI 接口的输入通常是复杂的嵌套 JSON(消息列表、工具定义、模型参数),你的 Request 解析和校验逻辑需要能处理这种复杂度。
- 流式是默认需求:模型生成耗时,用户需要尽快看到输出。接口设计时就要决定是普通 JSON 响应还是 SSE 流。
- 状态码要准确:
429(限流)、503(模型不可用)这些状态码不只是协议要求,它们直接影响客户端的重试策略。 - Body 大小需要提前考虑:长对话、RAG 上下文、文件上传都会产生大 Body,需要在接口层面设置限制和处理策略。
- 类型安全贯穿始终:AI 数据结构复杂,从 Request 解析到 Response 构建,类型定义和校验不能省。
这些点会在后续的 Headers 专篇、Body 专篇和流式响应专篇中逐一展开。
延伸阅读
- Fetch API - Request:标准 Request 对象的完整 API 文档
- Fetch API - Response:标准 Response 对象的完整 API 文档
- RFC 9112 - HTTP Message:HTTP 消息格式规范,定义了请求目标形式、Headers 和 Body 的传输规则
- Hono Request:Hono 对标准 Request 的封装,提供路径解析、查询参数、Header 等便捷方法
- Hono Context:Hono Context 的完整 API,包含 Request/Response 封装和快捷返回方法
- Hono Streaming Helper:Hono 的流式响应能力,
stream()和streamSSE()的使用方式 - MDN - Server-Sent Events:SSE 协议的详细说明和客户端使用方法
- ReadableStream:流式 API 的核心接口,理解它有助于掌握流式响应的底层机制
总结
本篇把 Request 和 Response 拆成了各自的组成部分:方法、目标、Headers、Body、状态码。我们看到了 Fetch API 如何把这些概念封装成标准对象,以及 Hono 的 Context 如何在标准之上提供便捷操作。
在 AI 场景中,Request/Response 模型的影响主要体现在三个地方:JSON 序列化的复杂度、流式响应的必要性,以及大 Body 的内存管理。这些不是理论问题,而是每次写 AI 接口都会碰到的实际约束。
下一篇会深入 HTTP Headers,讨论哪些 Header 在 AI 后端中频繁出现,以及 Hono 如何简化 Header 的读取和设置。