You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 08:25:57