在传统的 Express 或 Node.js 项目中,Multer 一直是处理 multipart/form-data 文件上传的首选中间件,功能成熟且配置灵活。但把 Multer 直接搬到 Next.js 的 API 路由中时,经常会遇到一个奇怪的现象:上传完成后保存的文件体积比原文件小,或者文件内容从某个位置开始全部损坏,打开图片报错、解压文件失败。这本质上不是 Multer 的 bug,而是 Next.js 对请求体的处理机制与 Multer 的工作方式发生了冲突。本文将深入分析冲突的根源,并给出多种可落地的解决方案。

一、问题的根源:请求体被提前消费了
Multer 的工作原理是从 Node.js 的原生可读流(request 对象)中读取原始字节,然后按照 multipart 协议解析出每个字段和文件。也就是说,Multer 必须拿到一个未被处理过的原始请求流,才能完整地解析出文件内容。
而 Next.js 的 API 路由默认会启用内置的 body 解析器(bodyParser)。当请求进入 handler 之前,Next.js 已经尝试把请求体解析成 JSON 或表单数据,这个过程会消费掉底层流中的数据。等请求传到你的处理函数时,req 已经不再是原始的可读流,Multer 从中读到的只是残缺的数据,这就是文件不完整最常见的原因。
另一个常见触发场景是:开发者先用 req.body 或 getParsedBody 读取了一次请求体,然后再把请求交给 Multer 处理。可读流只能被消费一次,第一次读取之后再交给 Multer,它拿到的自然是空数据或不完整数据。
二、解决方案:禁用 bodyParser 并正确包装 handler
最直接的解决办法是在对应的 API 路由中导出一个配置对象,关闭内置的请求体解析,让 Multer 独占原始流。写法如下:
import multer from 'multer';
import { NextApiRequest, NextApiResponse } from 'next';
import fs from 'fs';
import path from 'path';
const upload = multer({
dest: path.join(process.cwd(), 'uploads'),
limits: { fileSize: 20 * 1024 * 1024 } // 限制单文件最大 20MB
});
// 关闭 Next.js 内置的 body 解析器,这是关键步骤
export const config = {
api: {
bodyParser: false,
},
};
export default function handler(req, res) {
upload.single('file')(req, res, (err) => {
if (err) {
return res.status(500).json({ error: err.message });
}
// Multer 解析完成后,文件信息挂载在 req.file 上
const file = req.file;
if (!file) {
return res.status(400).json({ error: '未找到上传的文件字段' });
}
return res.status(200).json({
filename: file.originalname,
size: file.size,
savedAs: file.filename
});
});
}
这段代码有三个关键点需要注意。第一,export const config 必须与 handler 一起从同一个文件导出,Next.js 会在构建期读取它,放在别的文件里不会生效。第二,Multer 的中间件要通过函数式调用的方式包裹住整个请求处理逻辑,而不是全局注册。第三,前端表单中文件字段的名字必须与 upload.single('file') 中的参数一致,否则 Multer 找不到对应字段,也会返回空的文件信息。
前端提交时务必使用 FormData,并且不要手动设置 Content-Type 请求头。浏览器会自动附带正确的 multipart 边界字符串,手动指定反而会破坏边界识别,导致 Multer 解析出残缺的文件:
async function uploadFile(file) {
const formData = new FormData();
formData.append('file', file);
// 不要手动设置 Content-Type,让浏览器自动处理边界
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
return res.json();
}
三、App Router 时代的替代方案:放弃 Multer,使用原生流式处理
如果项目使用的是 App Router(app 目录下的 route.ts),情况有所不同。App Router 的路由处理函数直接基于 Web 标准的 Request 对象,Node 风格的中间件生态无法直接套用,强行用适配器把 Request 转回 Node 流再接 Multer,代码复杂且容易出错。此时更推荐使用原生方案,直接从请求中读取 FormData:
import { writeFile } from 'fs/promises';
import path from 'path';
export async function POST(request: Request) {
const formData = await request.formData();
const file = formData.get('file') as File | null;
if (!file) {
return Response.json({ error: '未找到文件' }, { status: 400 });
}
const buffer = Buffer.from(await file.arrayBuffer());
const savePath = path.join(process.cwd(), 'uploads', file.name);
await writeFile(savePath, buffer);
return Response.json({ filename: file.name, size: buffer.length });
}
这种原生方式的优点是与框架零冲突,不存在请求体被提前消费的问题,文件内容完整可靠。缺点是文件会先完整加载进内存,对超大文件不友好。如果需要上传几百 MB 甚至更大的文件,建议改用流式写入:通过 file.stream() 获取可读流,配合 Readable.fromWeb 转成 Node 流后用管道写入磁盘,内存占用可以保持在很低的水平。
四、其他导致文件不完整的检查点
除了请求体解析冲突之外,还有几个容易被忽略的细节会导致文件截断,逐一排查可以避免反复踩坑。
- 文件大小限制:Multer 的
limits.fileSize超限时默认会静默截断文件而不是报错,建议始终显式配置并在错误回调中判断LIMIT_FILE_SIZE错误码。另外如果前面有 Nginx 反向代理,还要检查client_max_body_size是否小于上传体积。 - 磁盘写入路径权限:保存目录不存在或进程没有写入权限时,可能出现只写入了部分内容的异常状态,上传前先用
fs.mkdirSync(dir, { recursive: true })确保目录存在。 - fetch 请求被打断:客户端网络不稳定或页面提前卸载会导致请求中断,表现为服务端只收到部分数据。可以在上传时加 loading 状态防止用户重复操作,必要时用分片上传提升可靠性。
- serverless 环境的临时目录:Vercel 等平台的文件系统是只读或临时的,Multer 写入的文件在函数实例回收后会消失,这种环境应把文件直接上传到 S3、OSS 等对象存储,而不是本地磁盘。
总结来看,Next.js 中 Multer 上传文件不完整的核心原因是内置请求体解析器抢占了原始流,在 Pages Router 中通过 export const config 关闭 bodyParser 即可解决;而在 App Router 中,直接使用原生的 request.formData() 或流式处理是更符合架构设计的做法。结合大小限制、目录权限和部署环境的检查,就能构建出稳定可靠的文件上传接口。