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.reqc.res 是你在中间件和路由中最常打交道的对象

1. 从上一篇到这里

上一篇「HTTP 协议基础」给出了一个全局视角:HTTP 是一个无状态的请求-响应协议,客户端发出 Request,服务端返回 Response。

那个视角够用,但还不够细。

当你开始在 Hono 里写一个真实的 AI 接口,你会碰到这些具体问题:

  • 请求体里的 JSON 怎么读取和校验?
  • 流式响应和普通响应在模型上有什么差别?
  • 为什么 AI 场景几乎都需要 SSE 或 ReadableStream?
  • c.reqc.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:获取资源,无 Body
  • POST:提交数据、创建资源,有 Body
  • PUT:整体替换资源
  • DELETE:删除资源

在 AI 后端中,GETPOST 几乎覆盖了绝大多数场景。聊天接口用 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重定向301302307
4xx客户端错误400 Bad Request401429
5xx服务端错误500 Internal Server Error502503

在 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 中,你不会直接操作标准的 RequestResponse 对象。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 专篇和流式响应专篇中逐一展开。

延伸阅读

总结

本篇把 Request 和 Response 拆成了各自的组成部分:方法、目标、Headers、Body、状态码。我们看到了 Fetch API 如何把这些概念封装成标准对象,以及 Hono 的 Context 如何在标准之上提供便捷操作。

在 AI 场景中,Request/Response 模型的影响主要体现在三个地方:JSON 序列化的复杂度、流式响应的必要性,以及大 Body 的内存管理。这些不是理论问题,而是每次写 AI 接口都会碰到的实际约束。

下一篇会深入 HTTP Headers,讨论哪些 Header 在 AI 后端中频繁出现,以及 Hono 如何简化 Header 的读取和设置。