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字节。
修正步骤
- 引入Node.js内置的
fs模块,用于读取本地临时文件 - 创建临时文件的可读流,通过管道(pipe)将数据传输到GCS的写入流中
- 额外优化:处理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
相关产品推荐
相关产品推荐

