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

NestJS+Fastify文件上传问题:@fastify/multipart使用及最佳方案咨询

NestJS + Fastify 文件上传问题解决与最佳实践

问题描述

  • Multer无法处理非multipart/form-data格式的数据,且与FastifyAdapter不兼容;
  • 使用@fastify/multipart时,上传3个文件未触发预期报错,且文件大小限制设置无效。

尝试过的代码

async uploadFile(req: FastifyRequest, res: FastifyReply<any>): Promise<any> {
  // upload file
  try {
    //Check request is multipart
    if (!req.isMultipart()) {
      res.send(
        new BadRequestException(
          new AppResponseDto(400, undefined, 'Request is not multipart'),
        ),
      );
      return;
    }
    //
    const options = {
      limits: {
        fieldSize: 1, // Max field value size in bytes
      },
    };
    const mp = await req.multipart(handler, onEnd, options);
    // for key value pairs in request
    mp.on('file', function (key: any, value: any) {
      // console.log('form-data', key, value,'<');
    });
    //Save files in directory
    async function handler(
      field: string,
      file: any,
      filename: string,
      encoding: string,
      mimetype: string,
    ): Promise<void> {
      const pipeline = util.promisify(stream.pipeline);
      const writeStream = fs.createWriteStream(`uploads/${filename}`); //File path
      try {
        await pipeline(file, writeStream);
      } catch (err) {
        console.error('Pipeline failed', err);
      }
    }
    // Uploading finished
    async function onEnd(err: any) {
      if (err) {
        res.send(new HttpException('Internal server error', 500));
        return;
      }
      res
        .code(200)
        .send(
          new AppResponseDto(200, undefined, 'Data uploaded successfully'),
        );
    }
  } catch (err) {
    console.log(err);
  }
}

最佳实现方案

1. 全局注册@fastify/multipart插件

首先在main.ts中正确注册插件并配置全局限制,这是文件大小、数量限制生效的基础:

import { NestFactory } from '@nestjs/core';
import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
import fastifyMultipart from '@fastify/multipart';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create<NestFastifyApplication>(AppModule, new FastifyAdapter());
  
  // 注册multipart插件,配置全局限制
  await app.register(fastifyMultipart, {
    limits: {
      fileSize: 1024 * 1024, // 单文件最大1MB
      files: 2, // 最多允许上传2个文件,超出自动报错
      fieldSize: 1024, // 表单字段值最大1KB
    },
  });

  await app.listen(3000);
}
bootstrap();

2. 修正路由处理逻辑

之前的代码存在参数顺序错误,导致限制配置不生效。调整后的路由函数完善了错误捕获逻辑:

import { Controller, Post, HttpException, HttpStatus } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from '@nestjs/platform-fastify';
import * as fs from 'fs';
import * as stream from 'stream';
import * as util from 'util';
import { AppResponseDto } from './app-response.dto';

const pipeline = util.promisify(stream.pipeline);

@Controller('upload')
export class UploadController {
  async uploadFile(req: FastifyRequest, res: FastifyReply): Promise<void> {
    try {
      if (!req.isMultipart()) {
        return res.status(HttpStatus.BAD_REQUEST).send(
          new AppResponseDto(400, undefined, '请求不是multipart格式'),
        );
      }

      // 修正参数顺序:文件处理回调 -> 配置选项 -> 结束回调
      const mp = await req.multipart(
        // 文件处理回调
        async (field: string, file: any, filename: string, encoding: string, mimetype: string) => {
          // 可选:添加文件类型校验
          const allowedTypes = ['image/jpeg', 'image/png'];
          if (!allowedTypes.includes(mimetype)) {
            throw new HttpException('仅支持JPG/PNG格式', HttpStatus.BAD_REQUEST);
          }

          const writeStream = fs.createWriteStream(`uploads/${filename}`);
          try {
            await pipeline(file, writeStream);
          } catch (err) {
            console.error('文件写入失败:', err);
            throw new HttpException('文件保存失败', HttpStatus.INTERNAL_SERVER_ERROR);
          }
        },
        // 路由级限制(可覆盖全局配置)
        {
          limits: {
            fileSize: 1024 * 1024,
          },
        },
        // 上传结束回调,捕获插件抛出的限制错误
        (err: any) => {
          if (err) {
            console.error('上传错误:', err);
            return res.status(HttpStatus.BAD_REQUEST).send(
              new AppResponseDto(400, undefined, err.message || '文件上传失败'),
            );
          }
          return res.status(HttpStatus.OK).send(
            new AppResponseDto(200, undefined, '文件上传成功'),
          );
        },
      );

      // 监听表单字段(如果需要处理非文件参数)
      mp.on('field', (key, value) => {
        console.log('表单字段:', key, value);
      });
    } catch (err) {
      console.error('控制器错误:', err);
      res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(
        new AppResponseDto(500, undefined, '服务器内部错误'),
      );
    }
  }
}

3. 关键问题修复点

  • 限制不生效:之前req.multipart的参数顺序错误,options应放在第二个位置;需使用fileSize(单文件大小)和files(文件数量)配置限制,而非仅fieldSize。
  • 多文件不报错:当文件数量超出limits.files时,插件会自动抛出错误,需在onEnd回调中捕获并返回对应响应。
  • Multer兼容性:Fastify官方推荐用@fastify/multipart替代Multer,后者基于Express开发,与Fastify适配器存在兼容性问题,且不支持非multipart/form-data的二进制流上传。

4. 支持二进制流上传(非multipart格式)

如果需要接收二进制格式的文件上传,可直接读取请求流:

@Post('binary')
async binaryUpload(req: FastifyRequest, res: FastifyReply): Promise<void> {
  const writeStream = fs.createWriteStream('uploads/binary-file');
  try {
    await pipeline(req.raw, writeStream);
    res.status(HttpStatus.OK).send(new AppResponseDto(200, undefined, '二进制文件上传成功'));
  } catch (err) {
    console.error('二进制上传失败:', err);
    res.status(HttpStatus.INTERNAL_SERVER_ERROR).send(new AppResponseDto(500, undefined, '上传失败'));
  }
}

内容的提问来源于stack exchange,提问作者Ihor Havryliak

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 20:39:55