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

NextJS使用formidable上传至GCS始终生成0字节文件问题排查

问题描述

我在NextJS中实现文件上传功能,将文件流式传输至Google Cloud Storage,使用的工具是formidable。formidable已返回filepath、newFilename、filesize等信息,但上传至Google Cloud Storage的文件始终为0字节,仅创建了存储位置但未实际写入文件内容。

我的API路由代码:

import { Storage } from "@google-cloud/storage";
import formidable from '../../lib/formidable-serverless'

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  console.log('req')
  const storage = new Storage({
    projectId: process.env.GOOGLE_SERVICE_ID,
    client_email: process.env.GOOGLE_CLIENT_EMAIL,
    private_key: process.env.GOOGLE_SERVICE_KEY,
  });
  const data = await new Promise(function(resolve, reject) { 
    const form = new formidable.IncomingForm();
    form.keepExtensions = true;
    form.parse(req, function(err, fields, files) {
      if (err) return reject(err);
      resolve({ fields, files });
    });
  });
  const file = data.files.file
    try {
      const bucket = storage.bucket(process.env.GCS_BUCKET)
      const blob = bucket.file(file.originalFilename);
     
      const blobStream = blob.createWriteStream({
        resumable: false,
      });
    
      blobStream.on("error", (err) => {
        console.log('ERR', err.message)
        res.status(500).send({ message: err.message });
      });
  
      blobStream.on("finish", async () => {
        const publicURL = `https://storage.googleapis.com/${bucket.name}/${blob.name}`
      ;
        try {
          await blob.makePublic();
        } catch {
          return res.status(500).send({
            message: `Uploaded the file successfully: ${file.newFilename}, but public access is denied!`,
            url: publicURL,
          });
        }
  
        res.status(200).send({
          message: "Uploaded the file successfully: " + file.newFilename,
          url: publicURL,
        });
      });
      blobStream.end();
    } catch (err) {
      if (err.code == "LIMIT_FILE_SIZE") {
        console.log('ERR', err.message)
        return res.status(500).send({
          message: "File size cannot be larger than 25MB!",
        });
      }
  
      res.status(500).send({
        message: `Could not upload the file: ${file.newFilename}. ${err}`,
      });
   }

创建后的blobStream对象信息:

Writable {
  _writableState: WritableState {
    objectMode: false,
    highWaterMark: 16384,
    finalCalled: false,
    needDrain: false,
    ending: false,
    ended: false,
    finished: false,
    destroyed: false,
    decodeStrings: true,
    defaultEncoding: 'utf8',
    length: 0,
    writing: false,
    corked: 0,
    sync: true,
    bufferProcessing: false,
    onwrite: [Function: bound onwrite],
    writecb: null,
    writelen: 0,
    afterWriteTickInfo: null,
    buffered: [],
    bufferedIndex: 0,
    allBuffers: true,
    allNoop: true,
    pendingcb: 0,
    prefinished: false,
    errorEmitted: false,
    emitClose: true,
    autoDestroy: true,
    errored: null,
    closed: false
  },

formidable解析后的文件对象信息:

PersistentFile {
  _events: [Object: null prototype] { error: [Function (anonymous)] },
  _eventsCount: 1,
  _maxListeners: undefined,
  lastModifiedDate: 2022-11-22T14:54:26.597Z,
  filepath: '/var/folders/yb/l636l56d7ng731jp24crv6lw0000gn/T/9e8fb9b8b1e0740b2622b990b',
  newFilename: '9e8fb9b8b1e0740b2622b990b',
  originalFilename: '17.mp3',
  mimetype: 'audio/mpeg',
  hashAlgorithm: false,
  size: 7020751,
  _writeStream: WriteStream {
    _writableState: WritableState {
      objectMode: false,
      highWaterMark: 16384,
      finalCalled: true,
      needDrain: true,
      ending: true,
      ended: true,
      finished: true,
      destroyed: true,
      decodeStrings: true,
      defaultEncoding: 'utf8',
      length: 0,
      writing: false,...
}

请问是否是我未将文件正确传入写入流导致的问题?


问题原因与解决方法

没错,问题核心就是你没有把formidable存在本地临时目录的文件内容写入到GCS的写入流中。当前代码仅创建了blobStream就直接调用blobStream.end(),没有读取临时文件的数据并传输到GCS,所以生成的文件是0字节。

修正步骤

  1. 引入Node.js内置的fs模块,用于读取本地临时文件
  2. 创建临时文件的可读流,通过管道(pipe)将数据传输到GCS的写入流中
  3. 额外优化:处理Google Cloud私钥的换行符,指定文件MIME类型让GCS正确识别文件

修改后的完整代码

import { Storage } from "@google-cloud/storage";
import formidable from '../../lib/formidable-serverless';
import fs from 'fs'; // 新增引入fs模块

export const config = {
  api: {
    bodyParser: false,
  },
};

export default async function handler(req, res) {
  const storage = new Storage({
    projectId: process.env.GOOGLE_SERVICE_ID,
    client_email: process.env.GOOGLE_CLIENT_EMAIL,
    private_key: process.env.GOOGLE_SERVICE_KEY.replace(/\\n/g, '\n'), // 处理私钥换行符
  });
  const data = await new Promise(function(resolve, reject) { 
    const form = new formidable.IncomingForm();
    form.keepExtensions = true;
    form.parse(req, function(err, fields, files) {
      if (err) return reject(err);
      resolve({ fields, files });
    });
  });
  const file = data.files.file;
  try {
    const bucket = storage.bucket(process.env.GCS_BUCKET);
    const blob = bucket.file(file.originalFilename);
     
    const blobStream = blob.createWriteStream({
      resumable: false,
      contentType: file.mimetype, // 指定文件MIME类型
    });
  
    blobStream.on("error", (err) => {
      console.log('ERR', err.message);
      res.status(500).send({ message: err.message });
    });

    blobStream.on("finish", async () => {
      const publicURL = `https://storage.googleapis.com/${bucket.name}/${blob.name}`;
      try {
        await blob.makePublic();
      } catch {
        return res.status(500).send({
          message: `文件上传成功:${file.newFilename},但无法设置公开访问权限!`,
          url: publicURL,
        });
      }

      res.status(200).send({
        message: "文件上传成功:" + file.newFilename,
        url: publicURL,
      });
    });

    // 核心修正:将本地临时文件流管道到GCS写入流
    fs.createReadStream(file.filepath).pipe(blobStream);
  } catch (err) {
    if (err.code == "LIMIT_FILE_SIZE") {
      console.log('ERR', err.message);
      return res.status(500).send({
        message: "文件大小不能超过25MB!",
      });
    }

    res.status(500).send({
      message: `文件上传失败:${file.newFilename}。错误信息:${err}`,
    });
 }
}

额外注意事项

  • 环境变量中的Google私钥通常会把换行符转义为\\n,需要替换为真实的\n才能通过认证
  • 指定contentType可以让GCS正确识别文件类型,避免下载时出现类型错误
  • formidable默认将文件存储在系统临时目录,file.filepath就是该文件的完整路径,确保此路径可读取

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.11 11:05:27