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 API | 100KB - 1MB | 大部分 CRUD 操作只需要几 KB |
| AI 对话(prompt) | 500KB - 2MB | 长文档分析可能需要较大 prompt |
| 批量操作 | 1MB - 5MB | 批量导入/创建 |
| 文件上传(小文件) | 10MB | 头像、文档附件 |
| 文件上传(大文件) | 100MB | 视频、大型数据集 |
| AI 文件分析 | 取决于模型 | 需要根据上下文窗口评估 |
3.1 AI 场景的请求体限制
AI 接口的请求体主要是 prompt + 历史消息。需要考虑:
模型的上下文窗口:
| 模型 | 上下文窗口 | 大约等价文本 |
|---|---|---|
| Claude Haiku | 200K tokens | ~150K 字 |
| Claude Sonnet | 200K tokens | ~150K 字 |
| GPT-4o | 128K 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 之前就生效了:
| 限制 | 免费版 | 付费版 |
|---|---|---|
| 请求体大小 | 100MB | 500MB(可配置更大) |
| 请求头大小 | 32KB | 32KB |
| URL 长度 | 32KB | 32KB |
| 并发连接数 | 无限制 | 无限制 |
| 请求速率 | 无限制 | 无限制 |
这些是边缘层的限制。你的应用应该在 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 → 业务逻辑。
下一篇讲文件上传安全——文件上传接口的请求体限制更大,需要更多的安全校验。