19.06-请求体大小限制

要点

  • 不限制请求体大小 = 攻击者可以发无限大的请求,耗尽你的内存和计算资源
  • Workers 有内置的请求体大小限制(默认 100MB),但你应该设置更小的业务级限制
  • Hono 的 bodyLimit 中间件可以按路由设置不同的大小限制
  • AI 场景的特殊性:LLM 的 prompt 可能很长,但也不是无限长——需要根据模型上下文窗口设置合理上限
  • 文件上传接口需要单独的大小限制——通常比普通 API 请求大得多
  • 限制请求体大小的同时也应该限制请求头大小和数量(防止 Header 注入和慢速攻击)

1. 为什么需要限制请求体大小

每个 HTTP 请求的请求体(body)都会被读进内存。如果不限制大小:

  • 内存耗尽:攻击者发一个 10GB 的 JSON 请求,Worker 试图把它全部读进内存,触发内存限制(Workers 的内存上限是 128MB)
  • CPU 浪费:解析超大 JSON 消耗 CPU 时间,Workers 的 CPU 时间有上限(免费版 10ms,付费版 30s)
  • 数据库压力:超大请求体里可能包含几百万条记录的 INSERT,全部写进 D1
  • DoS 攻击:大量超大请求占满 Worker 并发数,正常用户的请求被拒绝

Cloudflare 在边缘有 100MB 的请求体限制(免费版)。但这个限制太宽松了——大多数 API 请求只有几 KB 到几 MB。

2. Hono 的 bodyLimit 中间件

hono/body-limit 中间件在请求体被读取之前就检查 Content-Length 头,超限直接拒绝:

import { Hono } from 'hono'
import { bodyLimit } from 'hono/body-limit'
 
const app = new Hono()
 
// 全局限制:1MB
app.use('/api/*', bodyLimit({
  maxSize: 1024 * 1024,  // 1MB
  onError: (c) => {
    return c.json({ error: 'Request body too large' }, 413)
  },
}))

2.1 按路由设置不同限制

不同接口的合理请求体大小不同:

// JSON API:最大 100KB
app.use('/api/*', bodyLimit({
  maxSize: 100 * 1024,  // 100KB
}))
 
// AI 对话接口:最大 500KB(prompt 可能很长)
app.use('/api/ai/*', bodyLimit({
  maxSize: 500 * 1024,  // 500KB
}))
 
// 文件上传接口:最大 10MB
app.use('/api/upload/*', bodyLimit({
  maxSize: 10 * 1024 * 1024,  // 10MB
}))

2.2 bodyLimit 的工作原理

bodyLimit 检查的是 Content-Length 请求头。如果请求头里声明的大小超过限制,立即返回 413(Payload Too Large),不读请求体。

请求 → Content-Length: 5000000
     → bodyLimit 检查:5MB > 1MB → 返回 413,不调 handler
     → 请求体没有被读进内存

2.3 没有 Content-Length 的情况

如果请求没有 Content-Length 头(比如 chunked transfer),bodyLimit 会在读取请求体时逐块检查累计大小。超过限制立即停止读取并返回 413。

3. 各类接口的合理限制

接口类型建议限制原因
普通 JSON API100KB - 1MB大部分 CRUD 操作只需要几 KB
AI 对话(prompt)500KB - 2MB长文档分析可能需要较大 prompt
批量操作1MB - 5MB批量导入/创建
文件上传(小文件)10MB头像、文档附件
文件上传(大文件)100MB视频、大型数据集
AI 文件分析取决于模型需要根据上下文窗口评估

3.1 AI 场景的请求体限制

AI 接口的请求体主要是 prompt + 历史消息。需要考虑:

模型的上下文窗口

模型上下文窗口大约等价文本
Claude Haiku200K tokens~150K 字
Claude Sonnet200K tokens~150K 字
GPT-4o128K tokens~96K 字
Llama 3 (CF)8K tokens~6K 字

即使模型支持 200K tokens 的上下文,请求体也不需要那么大——实际 prompt 通常只有几千到几万字。

// AI 接口的请求体限制
app.use('/api/ai/*', bodyLimit({
  maxSize: 500 * 1024,  // 500KB,足够放下 200K tokens 的 prompt
}))
 
// 在业务层再限制 prompt 长度
app.post('/api/ai/chat', async (c) => {
  const body = await c.req.json()
  const messages = body.messages
 
  // 计算 prompt 总长度(简单估算:1 token ≈ 4 字符)
  const totalChars = messages.reduce((sum, msg) => sum + msg.content.length, 0)
  const estimatedTokens = Math.ceil(totalChars / 4)
 
  if (estimatedTokens > 100000) {  // 限制 100K tokens
    return c.json({ error: 'Prompt too long' }, 400)
  }
 
  // ...
})

4. 请求体限制之外的输入限制

请求体大小只是第一道防线。即使请求体在大小限制内,也应该对内部字段做更精细的限制。

4.1 字符串字段长度限制

// 用 Zod 验证请求体结构
import { z } from 'zod'
 
const chatSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['user', 'assistant', 'system']),
    content: z.string().max(50000),  // 单条消息最多 50K 字符
  })).max(100),  // 最多 100 条消息
  model: z.string().max(100),
  temperature: z.number().min(0).max(2),
})
 
app.post('/api/ai/chat', async (c) => {
  const body = await c.req.json()
  const parsed = chatSchema.safeParse(body)
 
  if (!parsed.success) {
    return c.json({ error: parsed.error.message }, 400)
  }
 
  // 验证通过,安全使用 parsed.data
})

4.2 数组长度限制

攻击者可能发一个包含几百万个元素的数组:

// 危险:不限制数组长度
const items = await c.req.json()  // { "items": [1, 2, 3, ...1000000个] }
for (const item of items) {
  await c.env.DB.prepare('INSERT INTO items (value) VALUES (?)').bind(item).run()
}
// 100 万次数据库写入——Worker CPU 时间直接爆掉
// 正确:限制数组长度
const MAX_ITEMS = 1000
if (items.length > MAX_ITEMS) {
  return c.json({ error: `Too many items (max ${MAX_ITEMS})` }, 400)
}

4.3 嵌套深度限制

JSON 可以无限嵌套:

{"a": {"a": {"a": {"a": ...}}}}

深度嵌套的 JSON 可能导致 JSON 解析栈溢出:

// 防御:限制 JSON 嵌套深度
function parseJsonWithDepthLimit(str: string, maxDepth = 10): any {
  let depth = 0
  let inString = false
  let escape = false
 
  for (const char of str) {
    if (escape) { escape = false; continue }
    if (char === '\\') { escape = true; continue }
    if (char === '"') { inString = !inString; continue }
    if (inString) continue
 
    if (char === '{' || char === '[') {
      depth++
      if (depth > maxDepth) throw new Error('JSON too deeply nested')
    }
    if (char === '}' || char === ']') {
      depth--
    }
  }
 
  return JSON.parse(str)
}

实际上 Workers 的 JSON.parse 对深度嵌套有自己的限制,但业务层做检查更安全。

5. 请求头限制

请求体之外,请求头也可能被滥用:

5.1 请求头大小

HTTP 规范没有规定请求头的最大大小,但大多数服务器限制在 8KB-64KB。Cloudflare 边缘有自己的请求头限制(通常 32KB)。

Worker 层面不需要额外限制——Cloudflare 在边缘已经处理了。

5.2 请求头数量

恶意请求可能发送几千个请求头。Workers 运行时会自动截断过多的请求头,不需要手动处理。

5.3 请求头注入

请求头里不应该包含换行符(\r\n)。Hono 在设置响应头时会自动处理,不需要手动过滤。

6. Cloudflare 边缘的限制

Cloudflare 本身对请求有一些限制,在请求到达 Worker 之前就生效了:

限制免费版付费版
请求体大小100MB500MB(可配置更大)
请求头大小32KB32KB
URL 长度32KB32KB
并发连接数无限制无限制
请求速率无限制无限制

这些是边缘层的限制。你的应用应该在 Worker 层设置更严格的业务级限制。

7. 慢速请求防护

除了单次请求体大小,慢速攻击也是资源耗尽的一种方式:

7.1 Slowloris 攻击

攻击者用极慢的速率发送请求体——每个字节间隔几秒,占用连接但不触发超时。

Workers 的无服务器模型天然不怕 Slowloris——每个请求在独立的 isolate 里处理,慢速请求不会阻塞其他请求。但 CPU 时间仍然有限制。

7.2 请求体读取超时

Workers 没有请求体读取超时的配置——Cloudflare 在边缘处理了连接级的超时。Worker 层面能做的是限制读取请求体的总时间:

app.post('/api/upload', async (c) => {
  // 设置读取请求体的超时
  const timeout = new Promise((_, reject) =>
    setTimeout(() => reject(new Error('Timeout')), 30000)
  )
 
  try {
    const body = await Promise.race([
      c.req.parseBody(),
      timeout,
    ])
    // ...
  } catch (e) {
    if (e.message === 'Timeout') {
      return c.json({ error: 'Request timeout' }, 408)
    }
    throw e
  }
})

8. 配置建议

8.1 分层限制策略

第 1 层:Cloudflare 边缘(100MB / 500MB)

第 2 层:Hono bodyLimit 中间件(按路由设置)

第 3 层:Zod 验证(字段级限制)

第 4 层:业务逻辑(数组长度、prompt token 数)

每一层处理不同粒度的限制。边缘层挡住极端情况,Worker 层做业务级限制,业务代码做字段级验证。

8.2 完整的限制配置示例

import { Hono } from 'hono'
import { bodyLimit } from 'hono/body-limit'
import { z } from 'zod'
 
const app = new Hono()
 
// 第 2 层:bodyLimit
app.use('/api/*', bodyLimit({ maxSize: 1024 * 1024 }))  // 1MB
app.use('/api/ai/*', bodyLimit({ maxSize: 500 * 1024 }))  // 500KB
app.use('/api/upload/*', bodyLimit({ maxSize: 10 * 1024 * 1024 }))  // 10MB
 
// 第 3 层 + 第 4 层:Zod 验证 + 业务限制
const chatSchema = z.object({
  messages: z.array(z.object({
    role: z.enum(['user', 'assistant', 'system']),
    content: z.string().max(50000),
  })).max(100),
  model: z.string().max(100),
})
 
app.post('/api/ai/chat', async (c) => {
  const body = await c.req.json()
  const parsed = chatSchema.safeParse(body)
 
  if (!parsed.success) {
    return c.json({ error: 'Invalid request' }, 400)
  }
 
  // 第 4 层:token 数估算
  const totalChars = parsed.data.messages.reduce(
    (sum, msg) => sum + msg.content.length, 0
  )
  if (totalChars > 400000) {  // ~100K tokens
    return c.json({ error: 'Prompt too long' }, 400)
  }
 
  // 业务逻辑...
  return c.json({ ok: true })
})

9. 小结

请求体大小限制是防御资源耗尽攻击的基本措施。不限制大小 = 攻击者可以发无限大的请求耗尽内存和 CPU。

Hono 的 bodyLimit 中间件按路由设置不同限制——普通 API 100KB、AI 接口 500KB、文件上传 10MB。

请求体大小只是第一道防线。内部字段的长度、数组的大小、JSON 嵌套深度也需要限制——用 Zod 验证是最方便的方式。

AI 场景的特殊性:prompt 可能很长但不应该超过模型的上下文窗口。在 bodyLimit 之外,业务层还要估算 token 数。

Cloudflare 边缘有自己的限制(免费版 100MB),Worker 层应该设置更严格的业务级限制。分层策略:边缘 → bodyLimit → Zod → 业务逻辑。

下一篇讲文件上传安全——文件上传接口的请求体限制更大,需要更多的安全校验。