Firebase Cloud Function(TypeScript)调用Pinata API报400错误排查
核心问题分析
你的代码在本地Node.js环境正常,但部署到Firebase云函数后触发400错误,主要是云函数环境下的请求体处理、异步流程控制以及Busboy参数使用这几个环节出了问题,以下是具体排查点和修复方案:
1. 移除冲突的Express中间件
你添加的express.urlencoded中间件会提前解析application/x-www-form-urlencoded类型请求体,但你的接口接收的是multipart/form-data格式,这会导致Busboy无法获取原始请求体,进而无法正确解析文件。
修复:
删除这行代码:
app.use(express.urlencoded({ extended: true }));
2. 修正Busboy的filename参数使用
Busboy的file事件中,第三个参数filename本身就是字符串类型的文件名,你错误地使用了filename.filename,这会导致生成的临时文件路径无效,上传时读取的是错误的文件(或空文件),触发Pinata的400错误。
修复:
将:
const tempFilePath = path.join(os.tmpdir(), filename.filename);
改为:
const tempFilePath = path.join(os.tmpdir(), filename);
3. 确保文件写入完成后再执行上传
Busboy的finish事件触发时,文件可能还没完全写入磁盘(因为fs.createWriteStream是异步操作),直接读取文件上传会导致文件内容不完整。需要用Promise等待所有文件写入完成。
修复:
修改文件处理逻辑,用Promise数组跟踪所有文件的写入状态:
// 替换原来的tempFileList和file事件处理 const fileWritePromises: Promise<string>[] = []; busboy.on("file", (fieldname: string, file: any, filename: string, encoding: string, mimetype: string) => { const tempFilePath = path.join(os.tmpdir(), filename); const writeStream = fs.createWriteStream(tempFilePath); // 用Promise包裹文件写入流程 const writePromise = new Promise<string>((resolve, reject) => { file.pipe(writeStream); writeStream.on("finish", () => { console.log("File written to temp folder:", tempFilePath); resolve(tempFilePath); }); writeStream.on("error", reject); }); fileWritePromises.push(writePromise); }); busboy.on("finish", async () => { try { // 等待所有文件写入完成 const tempFileList = await Promise.all(fileWritePromises); // 后续上传逻辑...
4. 给FormData的文件流添加文件名标识
Pinata API需要明确的文件名信息,直接传入读取流可能导致服务端无法识别文件类型,触发400错误。
修复:
在append文件时指定文件名:
formData.append("file", readStream, { filename: path.basename(tempFilePath) });
5. 确认Pinata API端点正确性
确保你的PINATA_API配置的是正确的文件上传端点:https://api.pinata.cloud/pinning/pinFileToIPFS,如果配置错误会直接返回400。
完整修复后的代码
import * as functions from "firebase-functions"; import * as Busboy from "busboy"; import * as fs from "fs"; import * as os from "os"; import * as path from "path"; import * as FormData from "form-data"; import axios from "axios"; import * as cors from "cors"; import * as express from "express"; const app = express(); app.use(cors({ origin: true })); const JWT = functions.config().pinata.jwt; const PINATA_API = functions.config().pinata.api || "https://api.pinata.cloud/pinning/pinFileToIPFS"; app.post("/upload", async (req: any, res: any) => { if (req.method !== "POST") { return res.status(400).json({ error: "Invalid request method. Please use POST." }); } const busboy = (Busboy as any)({ headers: req.headers }); req.pipe(busboy); const fileWritePromises: Promise<string>[] = []; busboy.on("file", (fieldname: string, file: any, filename: string, encoding: string, mimetype: string) => { const tempFilePath = path.join(os.tmpdir(), filename); const writeStream = fs.createWriteStream(tempFilePath); const writePromise = new Promise<string>((resolve, reject) => { file.pipe(writeStream); writeStream.on("finish", () => { console.log("File written to temp folder:", tempFilePath); resolve(tempFilePath); }); writeStream.on("error", (err: Error) => { console.error("Error writing temp file:", err); reject(err); }); }); fileWritePromises.push(writePromise); }); busboy.on("finish", async () => { try { const tempFileList = await Promise.all(fileWritePromises); for (const tempFilePath of tempFileList) { const formData = new FormData(); const readStream = fs.createReadStream(tempFilePath); formData.append("file", readStream, { filename: path.basename(tempFilePath) }); formData.append( "pinataOptions", JSON.stringify({ wrapWithDirectory: true, }) ); const response = await axios.post(PINATA_API, formData, { headers: { ...formData.getHeaders(), "Authorization": `Bearer ${JWT}` }, }); console.log("File uploaded to IPFS:", response.data); } // 清理临时文件 await Promise.all(tempFileList.map(filePath => new Promise<void>((resolve, reject) => { fs.unlink(filePath, (err) => { if (err) { console.error("Error deleting temp file:", err); reject(err); } else { resolve(); } }); }) )); res.status(200).json({ success: true, message: "All files uploaded successfully" }); } catch (error: any) { console.error("Upload error details:", error.response?.data || error.message); res.status(500).send(`Error uploading files to IPFS: ${error.message}`); } }); busboy.end(req.rawBody); }); export const uploadNFTs = functions.https.onRequest(app);
额外调试建议
- 在云函数日志中查看
error.response?.data,Pinata的400错误通常会返回具体的错误信息(比如文件为空、参数格式错误等),这能帮你更快定位问题。 - 确认云函数的内存配置,上传大文件时建议至少配置512MB内存,避免因内存不足导致文件写入失败。
内容的提问来源于stack exchange,提问作者Omar Osman

