用 CloudFront + Lambda@Edge 记录失败请求,我们踩过的 5 个坑
在 Amazon CloudFront 前面挂 WAF 做防护是常规操作,但如果你还想记录所有失败请求的完整信息用于补数重放,事情就没那么简单了。这篇文章记录了我们在方案选型过程中踩过的坑,希望能帮后来人少走弯路。
需求是什么
我们的业务场景是这样的:一个动态站点,源站部署在非 AWS 环境(如友商云平台),前面通过 Amazon CloudFront 做全球加速,并挂载 AWS WAF 做安全防护。
业务方提了一个看似简单的需求:
记录所有失败请求的完整信息,包括全部 request headers 和 request body,用于后续异步补数重放。
这里的"失败"包括两类:
- 被 WAF 拦截的请求:触发了安全规则(如 XSS、SQL 注入),直接返回 403
- 源站响应 4xx/5xx 的请求:请求到达了源站,但处理失败(如 500 内部错误、502 网关超时)
成功请求(2xx/3xx)不记录,控制成本和存储量。
为什么需要补数?因为 WAF 规则可能误拦截正常请求,源站也可能因为临时故障返回 500。如果能记录这些失败请求的完整信息,后续就可以通过脚本从 S3 读取日志,异步重放恢复数据。
听起来不复杂对吧?但在实际落地过程中,我们踩了不少 CloudFront 和 Lambda@Edge 的坑。
📌 本文配套了 4 张带 AWS 官方架构图标的高清配图,图片文件在
blog-assets/ 目录下。
坑 1:origin-response 阶段拿不到 request body
最初的想法
最直觉的方案是:只用一个 Lambda@Edge,挂在 origin-response 事件上。当源站返回响应后,检查状态码,如果 >= 400 就把完整请求信息(headers + body)记录下来。
一个 Lambda 搞定,简单优雅。
现实的打击
我们写了代码部署上去,在 origin-response 里直接访问 request.body:
// origin-response.js — 尝试直接读取 body
const status = parseInt(response.status);
if (status >= 400) {
console.log(JSON.stringify({
bodyAvailable: !!request.body,
body: request.body ? request.body.data : null
}));
}
发送一个带 body 的 POST 请求到会返回 500 的端点,满怀期待地去看 CloudWatch 日志:
{
"bodyAvailable": false,
"body": null
}
**request.body 是 undefined。**
文档怎么说
翻了 AWS 官方文档 Work with requests and responses,找到了明确说明:
You can opt to have Lambda@Edge expose the body in a request... choose Include Body when you create a CloudFront trigger for your function that's for a viewer request or origin request event.
Include Body 选项只适用于 viewer-request 和 origin-request 两个阶段。到了 origin-response 阶段,CloudFront 传给 Lambda 的 request 对象里,method、URI、headers 都有,唯独 body 被丢弃了。
同一个文档里还有一句:
When you're working with the HTTP response, Lambda@Edge does not expose the body that is returned by the origin server to the origin-response trigger.
教训:CloudFront 各个阶段能访问的数据是不同的。不要想当然地认为某个阶段能拿到所有信息,动手之前先查文档确认 API 能力边界。

坑 2:两套 Header 限制,差点让我们放弃最优方案
想到了 header 传递
既然 origin-response 拿不到 body,一个自然的想法是:在 origin-request 阶段把 body 写入自定义 header(如 X-Original-Body),这样 origin-response 就能从 header 里读取了。
被文档吓到了
但查阅 CloudFront Quotas 时,我们在 "Quotas on headers" 部分看到了这个:
| 限制项 | 限制值 |
|---|---|
| Custom headers: 单个 header value 最大长度 | 1,783 字符 |
| Custom headers: 所有 header 名称+值总长度 | 10,240 字符 |
我们客户的 request body 最大可以到 10KB,base64 编码后约 13.3KB,远超 1,783 字符的限制。
当时的结论是:这条路走不通。
虚惊一场
后来仔细研究才发现,文档里的 "Custom headers" 限制指的是 CloudFront 控制台里配置的静态 Origin Custom Headers——就是你在 Distribution 设置里手动添加的那种固定 header。
Lambda@Edge 在代码里动态设置的 header,受的是另一个限制,在 "General quotas" 部分:
Maximum length of a request or an origin response, including headers and query strings, but not including the body content — 20,480 bytes
也就是说,Lambda@Edge 动态注入的 header 受的是 20KB 总请求大小限制(包含所有 headers + query string)。10KB 的 body base64 编码后约 13.3KB,加上其他原始 headers(通常 2-4KB),总共约 15-17KB,在 20KB 限制内。
同一份文档里有两套不同的 header 限制,适用于完全不同的场景:
| 限制类型 | 适用场景 | 限制值 |
|---|---|---|
| Custom header value 最大长度 | CloudFront 控制台配置的静态 Origin Custom Headers | 1,783 字符 |
| Custom headers 总长度 | CloudFront 控制台配置的静态 Origin Custom Headers | 10,240 字符 |
| 请求总大小(含 headers + query string) | Lambda@Edge 动态设置的 headers、所有请求 headers | 20,480 bytes |
教训:CloudFront 的 Quotas 文档很长,不同章节的限制适用于不同场景。看到一个限制就下结论之前,先确认它是不是针对你的使用方式。
坑 3:Custom Error Pages 方案看起来完美,但丢失了关键信息
一篇让人兴奋的 Blog
我们找到了一篇 AWS 官方 Blog:Enhanced Origin Failover using Amazon CloudFront and AWS Lambda@Edge。
它介绍了一种巧妙的方案:利用 CloudFront 的 Custom Error Pages 功能,只在源站返回错误时才触发 Lambda@Edge:
- 配置 CloudFront Custom Error Page,当源站返回 4xx/5xx 时重定向到
/error-failover*路径 - 为该路径创建单独的 Cache Behavior,关联 Lambda@Edge(origin-request 触发)
- CloudFront 自动注入两个特殊 header:
cloudfront-error-uri(原始 URI)和cloudfront-error-args(原始 query string)
这样 Lambda 只在错误时执行,正常请求完全零开销。对于日均百万级请求的场景,成本几乎可以忽略。
为什么不适合我们
这个方案的设计目的是 failover——把错误请求转发到备用源站(比如 S3 静态页面),不是用来记录完整请求信息的:
- ❌ 拿不到 request body:特殊 header 只传递了 URI 和 query string
- ❌ 拿不到原始 request headers:错误重定向后是一个全新的内部请求,原始的 Authorization、Content-Type、自定义业务 header 全部丢失
- ✅ 成本确实极低
如果你的需求只是在错误时展示一个友好的错误页面或做 failover,这个方案非常好。但如果你需要记录完整的请求信息用于重放,它就不够了。
教训:一个方案在某个场景下是最优解,不代表它适合所有场景。评估方案时,先列清楚你的硬性需求,再逐一验证。
坑 4:Lambda@Edge 比你想的贵,但也没那么贵
计费方式
很多人知道 AWS Lambda 的定价,但 Lambda@Edge 的计费是不同的:
| 计费项 | Lambda@Edge | 普通 Lambda |
|---|---|---|
| 请求次数 | $0.60/百万 | $0.20/百万 |
| 计算时长 | $0.00000625125/128MB-s | $0.0000166667/GB-s |
| 免费额度 | 无 | 100万次/月 + 400,000 GB-s |
请求单价贵 3 倍,而且没有免费额度。
实际成本
以日均 100 万请求为例,每个请求触发 2 个 Lambda@Edge(origin-request + origin-response),月均 6000 万次执行:
| 项目 | 计算 | 月费用 |
|---|---|---|
| 请求费 | 6000万 × $0.60/百万 | $36.00 |
| 时长费(按 10ms 估算) | 6000万 × 0.01s × $0.00000625125 | $3.75 |
| Lambda@Edge 合计 | ~$40/月 |
加上 WAF 和日志收集的费用:
| 组件 | 月费用 |
|---|---|
| WAF(Web ACL $5 + 规则 $1 + 请求检查 3000万 × $0.60/百万) | ~$24 |
| Lambda@Edge | ~$40 |
| CloudWatch + Firehose + S3 | ~$2 |
| 总计 | ~$66/月 |
以上价格基于 AWS 官方定价页面(2026 年 3 月),实际费用可能因 region 和使用模式有所不同。建议使用 AWS Pricing Calculator 进行精确估算。
教训:Lambda@Edge 没有免费额度,请求单价是普通 Lambda 的 3 倍。但对于大多数业务场景,绝对金额并不高。不要因为"贵"就放弃,先算一下实际数字再做决定。
坑 5:Lambda@Edge 日志不在你以为的地方
这个坑不影响方案选型,但会影响你的调试效率。
Lambda@Edge 的日志不是写入函数所在 region(通常是 us-east-1)的 CloudWatch,而是写入执行节点所在 region 的 CloudWatch。
比如你的 Lambda@Edge 函数部署在 us-east-1,但用户从中国访问,请求被路由到了 us-west-2 的边缘节点,那日志就写在 us-west-2 的 CloudWatch 里。
我们部署完新版本后,在 us-east-1 的 CloudWatch 里怎么也找不到日志,最后在 us-west-2 才找到。
实际影响:
- 调试时需要在多个 region 查看日志
- 如果要通过 Subscription Filter 把日志汇聚到 Kinesis Firehose → S3,需要在每个有用户访问的 region 都设置
教训:部署 Lambda@Edge 后找不到日志?别慌,去其他 region 的 CloudWatch 看看。
我们评估过的所有方案
在确定最终方案之前,我们评估了 6 种不同的方案。这里做一个完整的对比,方便你根据自己的场景选择:
方案 A:应用层记录
在源站应用里加中间件,当响应 4xx/5xx 时记录完整请求信息。
- ✅ 零额外 AWS 成本,对正常请求零开销
- ❌ 需要改源站代码,如果源站部署在友商云平台(比如在友商云平台),改动和日志同步都不方便
- 适合:源站在 AWS 上,且有权限修改应用代码的场景
方案 B:CloudFront 实时日志 + Kinesis
CloudFront Real-time Logs 可以异步记录请求详情到 Kinesis Data Streams。
- ✅ 不需要 Lambda@Edge,异步不影响延迟
- ❌ 没有 request body,只有 headers、status code、URI 等
- 适合:不需要 body,只需要请求元数据的场景
方案 C:ALB Access Logs
如果源站前面有 ALB,可以开启 Access Logs。
- ✅ 零额外成本
- ❌ 不记录 request body 和完整 headers;源站不在 AWS 则不适用
- 适合:源站在 AWS 上,只需要基本访问日志的场景
方案 D:Custom Error Pages + Lambda@Edge
利用 CloudFront Custom Error Pages,只在错误时触发 Lambda。
- ✅ 成本极低(只在错误时执行)
- ❌ 丢失原始 request body 和 headers
- 适合:错误时做 failover 或展示友好错误页面的场景
方案 E:origin-request Lambda + 实时日志关联
origin-request 记录 body + requestId 到 CloudWatch,CloudFront 实时日志记录 status code,后续用 requestId 关联。
- ✅ 只需要一个 Lambda,成本减半
- ❌ 需要额外的 Kinesis,关联逻辑复杂,总成本并没有明显降低
- 适合:对架构复杂度不敏感,想减少 Lambda 调用次数的场景
方案 F:双 Lambda@Edge(最终方案)
origin-request 把 body 写入自定义 header,origin-response 在错误时从 header 读取并记录完整日志。
- ✅ 一条日志包含完整信息(status + headers + body),不需要关联
- ✅ 不依赖源站做任何改动
- ✅ 架构简单,易于维护
- ⚠️ 每个请求触发两次 Lambda@Edge
- ⚠️ body 通过 header 传递,受 20KB 总请求大小限制
对比总结
| 方案 | 完整 headers | 完整 body | 不改源站 | 月成本 (日100万请求) |
|---|---|---|---|---|
| A. 应用层记录 | ✅ | ✅ | ❌ | ~$0 |
| B. 实时日志 + Kinesis | ✅ | ❌ | ✅ | ~$35 |
| C. ALB Access Logs | ❌ | ❌ | ❌ | ~$0 |
| D. Error Pages + Lambda | ❌ | ❌ | ✅ | ~$25 |
| E. origin-request + 实时日志 | ✅ | ✅ | ✅ | ~$54 |
| F. 双 Lambda@Edge | ✅ | ✅ | ✅ | ~$40 |
最终方案详解
在"源站部署在友商云平台、不方便改源站代码、需要完整 headers + body"这三个硬约束下,双 Lambda@Edge 是唯一同时满足所有需求的方案。
架构图

核心代码
origin-request.js — 把 body 写入自定义 header,传递给下游:
'use strict';
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
// 有 body 的请求(POST/PUT/PATCH),把 body 存入自定义 header
// 因为 origin-response 阶段无法访问 request body
if (request.body && request.body.data) {
request.headers['x-original-body'] = [
{ key: 'X-Original-Body', value: request.body.data }
];
request.headers['x-original-body-encoding'] = [
{ key: 'X-Original-Body-Encoding', value: request.body.encoding || 'base64' }
];
}
return request;
};
origin-response.js — 只在错误时记录完整日志:
'use strict';
exports.handler = async (event) => {
const cf = event.Records[0].cf;
const response = cf.response;
const request = cf.request;
if (!response) return { status: '502', statusDescription: 'Bad Gateway' };
const status = parseInt(response.status);
// 只记录失败请求,成功请求直接放行(零开销)
if (status >= 400) {
// 过滤掉内部传递用的 header,只保留原始 headers
const headers = Object.fromEntries(
Object.entries(request.headers)
.filter(([k]) => !k.startsWith('x-original-body'))
.map(([k, v]) => [k, v[0].value])
);
// console.log 写入 CloudWatch Logs(异步,不阻塞响应)
console.log(JSON.stringify({
type: 'origin_error',
requestId: cf.config.requestId,
timestamp: new Date().toISOString(),
status,
method: request.method,
uri: request.uri,
querystring: request.querystring,
clientIp: request.clientIp,
headers,
body: request.headers['x-original-body']
? request.headers['x-original-body'][0].value : null,
bodyEncoding: request.headers['x-original-body-encoding']
? request.headers['x-original-body-encoding'][0].value : null
}));
}
return response;
};
日志流向

失败请求的日志通过两条路径汇聚到 S3:
- WAF 拦截的请求:WAF 自动记录日志到 CloudWatch Logs(包含 headers + body 前 8KB + 触发规则),通过 Subscription Filter → Kinesis Firehose → S3
- 源站失败的请求:origin-response Lambda 通过 console.log 写入 CloudWatch Logs(包含完整 headers + body + status code),同样通过 Subscription Filter → Kinesis Firehose → S3
两类日志最终统一存储在 S3,补数脚本从 S3 读取、解析、重放。
已知限制
| 限制 | 说明 | 影响 |
|---|---|---|
| Body 大小 | Lambda@Edge Include Body 最大 1MB,超过会被截断 | 大文件上传的 body 可能不完整 |
| Header 总大小 | 请求总大小(含 headers + query string)最大 20KB | body 超过约 15KB 时可能超限 |
| WAF body 检查 | WAF 日志只包含 body 的前 8KB | WAF 拦截的请求,body 可能不完整 |
| 日志分散 | Lambda@Edge 日志写入执行节点所在 region | 需要在多个 region 设置日志收集 |
踩坑速查表

最后,把所有踩过的坑整理成一张表,方便快速查阅:
| # | 踩坑点 | 现象 | 原因 | 解决方案 |
|---|---|---|---|---|
| 1 | origin-response 拿不到 body | request.body 为 undefined | Include Body 只支持 viewer-request 和 origin-request | 在 origin-request 阶段通过 header 传递 body |
| 2 | Custom Header 限制混淆 | 以为 header value 最大只有 1,783 字符 | 那是控制台静态配置的限制,Lambda@Edge 动态设置受 20KB 总请求大小限制 | 区分两套限制,确认适用场景 |
| 3 | Error Pages 方案丢失请求信息 | 拿不到原始 headers 和 body | 错误重定向是全新的内部请求 | 该方案只适合 failover,不适合记录请求信息 |
| 4 | Lambda@Edge 成本高于预期 | 没有免费额度,请求单价 $0.60/百万 | Lambda@Edge 定价独立于普通 Lambda | 先算实际数字,日百万级请求约 $40/月 |
| 5 | 找不到 Lambda@Edge 日志 | us-east-1 的 CloudWatch 里没有日志 | 日志写入执行节点所在 region | 在多个 region 查看,或设置跨 region 日志收集 |
| 6 | CloudFront Functions 不支持 body | 想用更便宜的 CF Functions 替代 | CF Functions 不支持 Include Body | 只能用 Lambda@Edge |
写在最后
CloudFront + Lambda@Edge 是一个强大的组合,但它的各种限制分散在不同的文档页面里,很容易踩坑。希望这篇文章能帮你在做类似方案时少走弯路。
核心建议:
- 先查文档确认能力边界,再动手写代码。CloudFront 四个阶段(viewer-request、origin-request、origin-response、viewer-response)能访问的数据是不同的
- 注意区分不同的 Quota 限制,同一份文档里的不同章节可能针对不同场景
- 先算成本再做决定,Lambda@Edge 看起来贵,但绝对金额可能完全可以接受
- 选方案时列清楚硬性需求,然后逐一验证每个候选方案是否满足
如果你也在做类似的失败请求记录方案,欢迎留言交流。
参考资料
- AWS Lambda Pricing — Lambda@Edge 定价在页面底部 "Lambda@Edge Pricing" 部分
- AWS WAF Pricing — Web ACL、规则、请求检查定价
- Work with requests and responses (Lambda@Edge) — Include Body 选项说明、origin-response 的限制
- CloudFront Quotas — Lambda@Edge 限制、Custom Header 限制、请求总大小限制
- Enhanced Origin Failover using CloudFront and Lambda@Edge — Custom Error Pages + Lambda@Edge 方案
更多推荐



所有评论(0)