Next.js结合AWS S3上传文件至Digital Ocean Spaces配置失效问题
故障原因排查与修复方案
以下是直接导致程序运行失败的核心问题,按优先级排序修复即可:
- 你注释掉了API路由必填的
bodyParser: false配置。Next.js默认会对API路由的请求体做JSON/FormData预解析,formidable-serverless需要拿到原始未处理的请求流才能正确读取上传文件,该配置被注释后,formidable拿到的请求对象已经被篡改,解析文件环节直接报错。 - 环境变量前缀使用错误。
NEXT_PUBLIC_开头的环境变量是Next.js专门为客户端侧代码设计的,会在构建阶段被静态替换注入到前端公开JS包中:- 这类前缀的变量会把你的Digital Ocean Spaces访问密钥、私钥完全暴露给所有站点访客,任何人拿到密钥都可以随意篡改、删除存储桶内文件,存在严重安全风险
- 服务端运行的API路由读取这类带公共前缀的变量时,如果通过自定义config文件静态导出,很容易出现构建时变量值为空、替换逻辑异常的问题,直接导致S3客户端鉴权失败
- S3客户端配置存在兼容性问题。Digital Ocean Spaces的S3兼容接口要求endpoint必须带HTTPS协议头,你当前写法没有加
https://,会导致请求域名解析失败;Serverless运行环境下不需要手动实例化new aws.Endpoint(),直接传endpoint字符串即可。 - 错误处理逻辑缺失:原代码报错时仅返回500状态码,没有返回具体错误信息也没有结束响应,会导致请求挂起,无法定位具体故障点。
修复步骤
- 调整环境变量配置:删除自定义config文件中存储密钥的逻辑,直接在项目根目录新建
.env.local文件(该文件不要提交到Git仓库)存储服务端专用变量,不需要加NEXT_PUBLIC_前缀:
# .env.local DO_SPACES_ACCESS_KEY=你的访问密钥 DO_SPACES_SECRET_KEY=你的私钥 DO_SPACES_BUCKET=你的存储桶名称 DO_SPACES_ENDPOINT=sfo3.digitaloceanspaces.com DO_SPACES_REGION=sfo3
注意:改完环境变量必须重启Next.js开发服务,环境变量在服务启动时加载,热更新不会读取新值。如果之前把带
NEXT_PUBLIC_前缀的密钥提交过代码仓库,立刻去Digital Ocean控制台轮换密钥,避免泄露造成资源损失。
- 替换原有API路由代码为修复后的版本:
import aws from 'aws-sdk'; import formidable from 'formidable-serverless'; import fs from 'fs'; // 必须开启该配置,关闭Next.js默认的请求体解析 export const config = { api: { bodyParser: false } }; export default async (req, res) => { if (req.method !== 'POST') { return res.status(405).json({ error: '仅支持POST请求' }); } const s3 = new aws.S3({ endpoint: `https://${process.env.DO_SPACES_ENDPOINT}`, accessKeyId: process.env.DO_SPACES_ACCESS_KEY, secretAccessKey: process.env.DO_SPACES_SECRET_KEY, region: process.env.DO_SPACES_REGION, s3ForcePathStyle: false }); const form = new formidable.IncomingForm(); // 配置单文件最大限制,示例为10MB form.maxFileSize = 10 * 1024 * 1024; form.parse(req, async (err, fields, files) => { if (err) { console.error('表单解析失败:', err); return res.status(400).json({ error: '文件解析失败' }); } if (!files.file) { return res.status(400).json({ error: '未接收到上传文件' }); } try { const fileContent = fs.readFileSync(files.file.path); // 自动识别文件类型,不需要硬编码image/jpeg const contentType = files.file.type || 'application/octet-stream'; // 生成唯一文件名,避免重名覆盖原有文件 const fileKey = `uploads/${Date.now()}-${files.file.name.replace(/\s/g, '-')}`; const uploadResult = await s3.upload({ Bucket: process.env.DO_SPACES_BUCKET, ACL: 'public-read', Key: fileKey, Body: fileContent, ContentType: contentType }).promise(); // 上传完成后删除临时文件,避免占用Serverless环境磁盘空间 fs.unlinkSync(files.file.path); return res.status(200).json({ url: uploadResult.Location }); } catch (uploadErr) { console.error('上传到Spaces失败:', uploadErr); return res.status(500).json({ error: '文件上传失败' }); } }); };
内容的提问来源于stack exchange,提问作者Priscila Scapin
相关产品推荐
相关产品推荐

