Node.js+Express+Firebase Functions上传Firebase Storage生产环境报错排查
问题
我用Firebase Functions搭配Express实现图片、PDF等文件上传到Firebase Storage的功能,本地模拟器测试完全正常,但部署到生产环境后出现如下错误:
{ error1: TypeError [ERR_INVALID_ARG_TYPE]: The "paths[0]" argument must be of type string. Received an instance of Object at Object.resolve (node:path:1129:7) at GoogleAuth.getClient (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:690:39) at GoogleAuth.request (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:748:35) at Upload.makeRequest (/workspace/node_modules/@google-cloud/storage/build/cjs/src/resumable-upload.js:744:43) at uri.retries (/workspace/node_modules/@google-cloud/storage/build/cjs/src/resumable-upload.js:410:40) at RetryOperation.runAttempt [as _fn] (/workspace/node_modules/async-retry/lib/index.js:42:15) at RetryOperation.attempt (/workspace/node_modules/retry/lib/retry_operation.js:116:8) at run (/workspace/node_modules/async-retry/lib/index.js:55:8) at new Promise (<anonymous>) at retry (/workspace/node_modules/async-retry/lib/index.js:58:10) { code: 'ERR_INVALID_ARG_TYPE' }}
代码实现如下:
require('dotenv').config(); const functions = require('firebase-functions'); const admin = require("firebase-admin"); const bcrypt = require("bcrypt"); const _ = require('lodash'); const Joi = require("joi"); const jwt = require("jsonwebtoken"); const express = require("express"); const formidable = require('formidable-serverless'); const bucket = require('../utils/clouStorage'); const { v4: uuidv4 } = require('uuid'); const os = require('os'); const path = require('path'); const fs = require('fs'); const sendEMail = require('../utils/emailService'); const router = express.Router(); const JWT_SECRET = process.env.JWT_SECRET || functions.config().jwt.secret; // DOCTORS REGISTRATION LOGIC router.post("/register", (req, res, next) => { // Had to validate the request inputs in the formidable lib because the multipart/form-data // was a bit challenging to parse with only the request object. const form = new formidable.IncomingForm(); form.parse(req, async (err, fields, files) => { if (err) { console.error('Error parsing form data:', err); next(err) return res.status(500).send("Internal server error"); } const { name, surname, email, phone, gender, password, nationalID, passport, citizenship, city, speciality, school, school_from, school_to, license_number, yearsOfExperience } = fields; let doctor; const { avatar, license, resume } = files; console.log(files); const { error } = validate(fields); if (error) return res.status(400).send(error.details[0].message); const filesError = validateFile(files); if (filesError.error) return res.status(400).send(filesError.error.details[0].message); try { const doctorSnapshot = await admin.firestore().collection('doctors').where('email', '==', email).get(); if (!doctorSnapshot.empty) return res.status(400).send("Doctor already registered."); doctor = await admin.auth().createUser({ email, password }); const userId = doctor.uid; const salt = await bcrypt.genSalt(10); const hashed_password = await bcrypt.hash(password, salt); let avatarUrl = ""; let licenseUrl = ""; let resumeUrl = ""; // Upload avatar if (avatar) { const avatarFileName = `${uuidv4()}_${avatar.name}`; try { console.log({path1: avatar.path}) const avatarUpload = await bucket.upload(avatar.path, { destination: `doctorsImage/${avatarFileName}`, metadata: { contentType: avatar.type, }, }); avatarUrl = `https://storage.googleapis.com/${bucket.name}/${avatarUpload[0].name}`; } catch (error) { // console.log({ error1: error }) // return res.status(400).send(error.message); } } // Upload license if (license) { const licenseFileName = `${uuidv4()}_${license.name}`; try { console.log({path2: license.path}) const licenseUpload = await bucket.upload(license.path, { destination: `licenses/${licenseFileName}`, metadata: { contentType: license.type, }, }); licenseUrl = `https://storage.googleapis.com/${bucket.name}/${licenseUpload[0].name}`; } catch (error) { // console.log({ error2: error }) // return res.status(400).send(error.message); } } // Upload resume if (resume) { const resumeFileName = `${uuidv4()}_${resume.name}`; try { console.log({path3: resume.path}) const resumeUpload = await bucket.upload(resume.path, { destination: `resumes/${resumeFileName}`, metadata: { contentType: resume.type, }, }); resumeUrl = `https://storage.googleapis.com/${bucket.name}/${resumeUpload[0].name}`; } catch (error) { // console.log({ error3: error }) // return res.status(400).send(error.message); } }; await admin.firestore().collection("doctors").doc(userId).set({ id: userId, name, surname, email, phone, }); const adminsSnapshot = await admin.firestore().collection("admins").get(); const adminEmails = adminsSnapshot.docs.map(doc => doc.data().email); res.status(201).send({ message: 'Please check you email to verify your account.', userId }); } catch (error) { if (doctor && doctor.uid) { await admin.auth().deleteUser(doctor.uid); } console.log({ error }) if (error.code) { return res.status(400).send(error.message); } res.status(500).send("Internal server error"); } }) });
我打印文件路径的类型显示为字符串,但仍不清楚报错中提到的Object实例来源,使用formidable库解析multipart/form-data格式的请求。
解决方案
核心原因分析
报错指向paths[0]参数类型错误,本质是生产环境中bucket.upload()接收的路径参数并非预期的字符串,而是对象。这和formidable-serverless在Firebase Functions生产环境的临时文件处理逻辑有关——本地环境中文件路径是字符串,但生产环境可能因权限或库的兼容性问题,导致路径返回异常。
解决办法1:改用文件缓冲区直接上传
绕开文件路径读取,直接读取文件内容为缓冲区,调用bucket.file().save()上传:
// 替换原有的avatar上传代码 if (avatar) { const avatarFileName = `${uuidv4()}_${avatar.name}`; try { // 读取文件为缓冲区 const fileBuffer = fs.readFileSync(avatar.path); // 创建Storage文件实例 const targetFile = bucket.file(`doctorsImage/${avatarFileName}`); // 上传缓冲区内容 await targetFile.save(fileBuffer, { metadata: { contentType: avatar.type, }, }); avatarUrl = `https://storage.googleapis.com/${bucket.name}/${targetFile.name}`; } catch (error) { console.log({ error1: error }); } }
按同样逻辑修改license和resume的上传代码。
解决办法2:替换formidable库
formidable-serverless可能存在环境兼容问题,建议改用Firebase官方推荐的busboy库处理multipart/form-data请求,或者升级formidable-serverless到最新版本。
解决办法3:检查临时目录权限
Firebase Functions生产环境的临时目录(/tmp)权限有限,确保代码中没有修改临时目录权限的操作,且文件路径指向的是/tmp下的有效文件。
内容的提问来源于stack exchange,提问作者christian
相关产品推荐
相关产品推荐

