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

Nest.js向Google Cloud Storage上传大文件失败问题排查

问题原因分析
  1. Google App Engine 请求体大小限制:GAE默认允许的最大请求体大小为64MB,当上传文件超过这个阈值时,GAE会直接拦截请求,不会将其转发到你的Nest.js应用,因此后端日志中只会看到OPTIONS预检请求,而没有实际的PATCH请求。
  2. Multer 内存存储瓶颈:Multer默认使用内存存储上传的文件内容,当文件超过32MB时,不仅会占用大量内存,还会触发Node.js的内存限制,同时结合GAE的请求限制,直接导致请求被中断。
  3. 前端请求配置错误:手动设置Content-Type: multipart/form-data会覆盖axios自动生成的请求头(包含正确的边界标识),大文件上传时,服务器无法正确解析multipart数据,间接导致请求失败。
解决方案

1. 调整GAE的请求大小限制

在你的GAE项目的app.yaml文件中添加max_request_size配置,允许更大的请求体(根据需求设置,比如200MB):

runtime: nodejs20 # 对应你的Node.js版本
service: default
max_request_size: 200M

2. 后端代码优化

2.1 配置Multer使用磁盘存储(避免内存溢出)

修改文件拦截器,配置Multer使用临时磁盘存储,并设置合理的文件大小限制:

@Patch(':id/audio')
@Roles(Role.MOD, Role.ADMIN)
@UseInterceptors(
  FileInterceptor('audio', {
    fileFilter: audioFileFilter,
    storage: diskStorage({
      destination: '/tmp', // 使用GAE的临时目录
      filename: (req, file, cb) => {
        cb(null, file.originalname);
      },
    }),
    limits: {
      fileSize: 200 * 1024 * 1024, // 200MB,和GAE配置一致
    },
  }),
)
async addAudio(
  @Req() req: Express.Request,
  @UploadedFile() audio: Express.Multer.File,
) {
  return await this.bookService.uploadFile(req, audio, 'audio');
}

2.2 改用GCS可恢复上传+流传输(避免内存加载整个文件)

修改uploadFile方法,使用文件流直接pipe到GCS,而不是读取整个文件到buffer,同时启用可恢复上传:

import * as fs from 'fs';

async uploadFile(
  req,
  file: Express.Multer.File,
  type: 'audio' | 'lyrics',
): Promise<any> {
  const id = req.params['id'];
  const lang = req.body['language'];
  let fullName: string;
  if (type === 'audio') {
    const voice_id = req.body['voice_id'];
    const audioType = req.body['type'];
    fullName =
      this.setDestination(id, lang) +
      this.audioFileName(voice_id, audioType, file);
  } else {
    fullName = this.setDestination(id, lang) + this.lyricsFileName(file);
  }
  const blob = this.bucket.file(fullName);
  if (await blob.exists()) {
    await this.removeFile(fullName);
  }

  return new Promise((resolve, reject) => {
    const blobStream = blob.createWriteStream({
      resumable: true, // 启用可恢复上传,适配大文件
      contentType: file.mimetype, // 正确设置文件MIME类型
    });

    const fileStream = fs.createReadStream(file.path); // 读取本地临时文件流

    fileStream.pipe(blobStream);

    blobStream.on('error', (error) => {
      console.error('上传错误:', error);
      fs.unlinkSync(file.path); // 清理临时文件
      reject(error);
    });

    blobStream.on('finish', async () => {
      fs.unlinkSync(file.path); // 上传完成后清理临时文件
      try {
        await blob.makePublic();
        resolve({
          publicUrl: `https://storage.googleapis.com/${this.bucket.name}/${fullName}`,
        });
      } catch (e) {
        console.error('设置公开权限错误:', e);
        reject(e);
      }
    });
  });
}

3. 前端代码修复

移除手动设置的Content-Type,让axios自动处理multipart请求头:

async function addAudio(
  this: Server,
  id: string,
  token: string,
  audio: ServerDto.AddAudio
) {
  let config = {
    headers: {
      "x-access-token": token,
      // 移除手动设置的Content-Type,axios会自动生成包含边界的正确头
    },
    onUploadProgress: (progressEvent: any) => {
      const percentCompleted = Math.round(
        (progressEvent.loaded * 100) / progressEvent.total
      );
      console.log(`Upload progress: ${percentCompleted}%`);
    },
  };
  const { data } = await axios.patch(
    `${this.baseUrl}/books/${id}/audio`,
    audio,
    config
  );
  return data;
}

确保audio参数是FormData实例,axios才能正确识别并生成multipart请求。

4. 可选:大文件分片上传(推荐)

如果文件超过100MB以上,建议实现分片上传逻辑:

  • 前端将文件分割成多个小块(比如10MB/块)
  • 后端接收每个分片,上传到GCS的临时位置
  • 所有分片上传完成后,后端调用GCS的合并接口将分片合并为完整文件

这样可以进一步降低单个请求的大小,避免各种服务器限制,同时提升上传的可靠性。


内容的提问来源于stack exchange,提问作者Efim Rubin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 03:59:55