如何在Node.js/NestJS中不加载至内存传递Multipart文件
在Node.js/NestJS中流式转发Multipart文件(避免内存占用飙升)
核心结论
完全可以通过**流式管道(Pipe)**处理multipart数据,不需要将整个文件加载到内存。核心思路是直接把客户端的请求流转发给目标服务,数据以chunk(块)的形式传输,从源服务直接流向目标服务,中间不缓存完整文件。
为什么Multer不适合?
Multer的工作机制是先将整个multipart请求解析完毕(文件全部加载到内存或写入磁盘),再触发后续业务逻辑。这种"先接收再转发"的模式会导致大文件场景下内存占用飙升,CPU因文件读写/解析持续高负载。
NestJS中的具体实现方案
方案1:跳过解析,直接转发原生请求流(Express适配器)
利用NestJS底层的Express请求对象,直接将请求流管道化到目标服务,完全跳过Multer的解析步骤。
import { Controller, Post, Req, Res } from '@nestjs/common'; import { Request, Response } from 'express'; import * as http from 'http'; @Controller('file-proxy') export class FileProxyController { @Post('upload') async proxyUpload(@Req() req: Request, @Res() res: Response) { return new Promise<void>((resolve, reject) => { // 配置目标服务请求参数 const targetRequest = http.request({ hostname: 'your-target-service.com', port: 80, path: '/api/upload', method: 'POST', headers: { // 复制原请求的headers,保留multipart的boundary信息 ...req.headers, // 替换Host头为目标服务地址 host: 'your-target-service.com', }, }); // 1. 将客户端请求流转发到目标服务 req.pipe(targetRequest); // 2. 将目标服务的响应流回发给客户端 targetRequest.on('response', (targetRes) => { res.writeHead(targetRes.statusCode, targetRes.headers); targetRes.pipe(res); targetRes.on('end', resolve); targetRes.on('error', reject); }); // 处理流错误 req.on('error', reject); targetRequest.on('error', reject); }); } }
关键注意事项:
- 禁用对应路由的全局body解析器:如果NestJS全局启用了body解析器(默认),需要在该路由上禁用,避免框架提前将请求体读入内存。可以通过自定义拦截器或在main.ts中配置排除规则:
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { json } from 'express'; async function bootstrap() { const app = await NestFactory.create(AppModule); // 对该路由跳过body解析,保留其他路由的json解析能力 app.use((req, res, next) => { if (req.path === '/file-proxy/upload' && req.headers['content-type']?.startsWith('multipart/form-data')) { next(); } else { json()(req, res, next); } }); await app.listen(3000); } bootstrap(); - 不要手动设置
Content-Length:流式传输采用chunked编码,框架会自动处理长度计算,手动设置反而可能导致请求异常。
方案2:使用Fastify适配器+流式Multipart解析
如果你的NestJS项目使用Fastify适配器,推荐用@fastify/multipart插件实现逐块解析和转发,比Express更高效。
import { Controller, Post, Req, Res } from '@nestjs/common'; import { FastifyRequest, FastifyReply } from 'fastify'; import * as http from 'http'; @Controller('file-proxy') export class FileProxyController { @Post('upload') async proxyUpload(@Req() req: FastifyRequest, @Res() res: FastifyReply) { // 初始化multipart流式解析 const parts = req.multipart( (fieldName, fileStream, filename, encoding, mimeType) => { // 创建目标服务请求 const targetReq = http.request({ hostname: 'your-target-service.com', port: 80, path: '/api/upload', method: 'POST', headers: { 'Content-Type': `multipart/form-data; boundary=${req.headers['content-type'].split('boundary=')[1]}`, // 按需添加其他headers }, }); // 将当前文件流直接转发到目标服务 fileStream.pipe(targetReq); // 监听目标服务响应,回发给客户端 targetReq.on('response', (targetRes) => { res.status(targetRes.statusCode); targetRes.pipe(res.raw); }); }, (err) => { // 处理解析错误 res.status(500).send(`Multipart解析失败: ${err.message}`); }, ); // 等待所有部分处理完成 await parts; } }
配置Fastify适配器:
在main.ts中注册@fastify/multipart插件:
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { FastifyAdapter } from '@nestjs/platform-fastify'; async function bootstrap() { const app = await NestFactory.create<NestFastifyApplication>( AppModule, new FastifyAdapter(), ); // 注册multipart插件 await app.register(require('@fastify/multipart')); await app.listen(3000); } bootstrap();
额外优化建议
- 超时设置:对于大文件传输,需要调整请求超时时间,避免中途断开。例如在http.request中添加
timeout: 300000(5分钟)。 - 错误捕获:务必监听所有流的
error事件,避免未处理的异常导致服务崩溃。 - 资源释放:在请求结束或出错时,及时销毁未完成的流,释放系统资源。
内容的提问来源于stack exchange,提问作者redeyeth
相关产品推荐
相关产品推荐

