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

Firebase Cloud Function(TypeScript)调用Pinata API报400错误排查

问题排查与修复:Firebase云函数调用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 11:03:16