使用Lambda与Node.js上传生成PDF至S3遇0字节/不上传问题排查
问题分析与解决方案
你的问题核心在于流处理的异步时机控制,以及Lambda执行环境的权限/资源配置问题,下面分点说明并给出修复方案:
1. 流未完成写入就触发S3上传(核心问题)
PDFKit生成PDF是通过流写入文件的,pdfDoc.end()只是标记流结束,但实际写入磁盘的操作是异步的。你在调用pdfDoc.end()后立刻用fs.statSync读取文件,此时文件还没写完,所以得到的文件大小是0,上传到S3自然就是空文件;本地环境因为磁盘IO快,刚好侥幸完成写入,但Lambda的环境IO延迟更高,就会暴露这个问题。
之前你尝试用file.on("finish")但出现卡顿,大概率是因为Promise的resolve/reject没有正确绑定到上传逻辑里,导致Lambda的执行上下文无法正确结束。
2. Lambda执行角色缺少S3权限
你提到本地用的IAM用户有S3全权限,但Lambda是使用自己的执行角色来访问S3的,你的serverless.yml里没有配置对应的权限,这会导致上传失败(虽然你说要么无法上传要么0字节,这可能是两种情况并存:权限不足时上传失败,流没写完时上传空文件)。
修复后的代码与配置
第一步:修正Handler中的流处理逻辑
修改后的handler代码,确保在文件完全写入后再执行S3上传:
const PDFDocument = require('pdfkit'); const fs = require('fs'); const { join } = require('path'); const AWS = require('aws-sdk'); const s3 = new AWS.S3(); module.exports.generatePdf = async (invoiceData, context) => { const date = new Date().toISOString(); const firstFour = (str) => str.split("-")[0]; const filename = `invoice-${firstFour(String(invoiceData.invoice_id))}-${date}.pdf`; const pdfPath = `/tmp/${filename}`; return new Promise((resolve, reject) => { const pdfDoc = new PDFDocument({ margin: 50 }); const fileStream = fs.createWriteStream(pdfPath); // 绑定流的管道,确保PDF内容正确写入文件 pdfDoc.pipe(fileStream); // 写入PDF内容(修正原代码里的变量名错误) pdfDoc .image(join(__dirname, "/assets/ds_logo.png"), 400, 35, { width: 150, height: 100, align: "right" }) .fillColor("#444444") .fontSize(20) .fontSize(10) .text(invoiceData.name, 50, 35, { align: "left" }) .text(`${invoiceData.house_number} ${invoiceData.street_address}`, 50, 45, { align: "left" }) .text(`${invoiceData.city}, ${invoiceData.state_code}, ${invoiceData.zip}`, 50, 55, { align: "left" }) .moveDown(); // 结束PDF流 pdfDoc.end(); // 监听文件写入完成事件,此时才执行S3上传 fileStream.on('finish', async () => { try { const stats = fs.statSync(pdfPath); console.log(`filesize: ${stats.size}`); console.log("----- starting s3 upload -----"); const params = { Key: filename, Body: fs.createReadStream(pdfPath), Bucket: "<bucket>", // 替换为你的实际桶名 ContentType: "application/pdf" // 移除ContentLength参数,让AWS SDK自动识别文件大小,避免提前读取的错误值 }; const uploadResult = await s3.upload(params).promise(); console.log("done"); console.log(uploadResult); resolve(uploadResult); } catch (err) { console.error(err); reject(err); } }); // 监听流错误事件,便于排查问题 fileStream.on('error', (err) => { console.error("File write error:", err); reject(err); }); pdfDoc.on('error', (err) => { console.error("PDF generation error:", err); reject(err); }); }); };
关键修改点:
- 新增
pdfDoc.pipe(fileStream)明确绑定流的管道,确保PDF内容正确写入文件 - 将所有逻辑包裹在Promise中,通过
fileStream.on('finish')触发S3上传,保证文件完全写入后再操作 - 移除
ContentLength参数,让AWS SDK自动处理文件大小,避免提前读取的错误值 - 添加流错误监听,便于快速定位PDF生成或文件写入的问题
- 修正原代码里的变量名错误(比如
datainvoiceDatahouse_number改为invoiceData.house_number)
第二步:配置Lambda执行角色的S3权限
修改serverless.yml,添加IAM权限,确保Lambda能访问目标S3桶:
service: pdfgenerator frameworkVersion: "2" provider: name: aws runtime: nodejs12.x stage: dev region: us-east-1 iamRoleStatements: - Effect: Allow Action: - s3:PutObject Resource: "arn:aws:s3:::<bucket>/*" # 替换为你的桶ARN,末尾加/*表示允许上传桶内所有对象 functions: generatePdf: handler: handler.generatePdf memorySize: 256 # 建议调高内存,128MB可能不足以处理PDF生成+上传 events: - http: path: pdf method: get
关键修改点:
- 添加
iamRoleStatements配置,赋予LambdaPutObject权限到目标S3桶 - 将
memorySize从128MB调高到256MB(如果后续切换到Puppeteer,建议调到512MB或更高)
额外注意事项
- Lambda的
/tmp目录最大容量是512MB,生成大PDF时要注意不要超过限制 - 如果后续切换到Puppeteer,需要注意Lambda的层配置(因为Puppeteer依赖Chromium,需要打包成Lambda层),并且要分配足够的内存和CPU
- 可以在Lambda控制台查看CloudWatch日志,根据错误信息进一步排查问题
内容的提问来源于stack exchange,提问作者aromanarguello
相关产品推荐
相关产品推荐

