ruvnet/ruflo · error · Error

Pinata upload failed (HTTP ${res.status}): ${JSON.stringify(

Error message

Pinata upload failed (HTTP ${res.status}): ${JSON.stringify(res.data)}

What it means

Thrown by RvfaPublisher.upload() when Pinata's POST /pinning/pinFileToIPFS returns a non-200 status. The error embeds both the HTTP status and the parsed response body (JSON or raw) so the caller can diagnose the upstream cause. Upload is the backing call for publish() and publishPatch().

Source

Thrown at v3/@claude-flow/cli/src/appliance/rvfa-distribution.ts:344

  private jwt: string;
  private gw: string;
  private api: string;

  constructor(config: PublishConfig) {
    this.jwt = config.pinataJwt || process.env.PINATA_API_JWT || '';
    this.gw = (config.gatewayUrl || DEFAULT_GW).replace(/\/+$/, '');
    this.api = (config.apiUrl || DEFAULT_API).replace(/\/+$/, '');
    if (!this.jwt) throw new Error('Pinata JWT required (config.pinataJwt or PINATA_API_JWT)');
  }

  private async upload(
    fileName: string, data: Buffer, kv: Record<string, string>,
  ): Promise<PublishResult> {
    const meta = JSON.stringify({ name: fileName, keyvalues: kv });
    const { body, ct } = multipart('file', fileName, data, meta);
    const res = await pinataReq('POST', `${this.api}/pinning/pinFileToIPFS`, this.jwt, body, ct);
    if (res.status !== 200)
      throw new Error(`Pinata upload failed (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
    const r = res.data as { IpfsHash: string; PinSize: number };
    return {
      cid: r.IpfsHash, size: r.PinSize,
      gatewayUrl: `${this.gw}/ipfs/${r.IpfsHash}`, pinataUrl: `${this.api}/pinning/pins/${r.IpfsHash}`,
    };
  }

  async publish(rvfaPath: string, meta?: PublishMetadata): Promise<PublishResult> {
    const data = await readFile(rvfaPath);
    const name = meta?.name || rvfaPath.split('/').pop() || 'appliance.rvf';
    return this.upload(name, data, {
      type: 'rvfa-appliance', version: meta?.version || '',
      profile: meta?.profile || '', description: meta?.description || '',
    });
  }

  async publishPatch(patchBuf: Buffer, meta?: PublishMetadata): Promise<PublishResult> {
    const name = meta?.name || `patch-${Date.now()}.rvfp`;

View on GitHub (pinned to 6b01dc5a68)

Solutions

  1. Inspect the embedded status code and JSON body in the error message to identify the cause.
  2. For 401/403: regenerate the JWT from the Pinata dashboard and ensure it has pinning permissions.
  3. For 413: reduce appliance size (strip sections) or upgrade the Pinata plan.
  4. For 429/5xx: retry with exponential backoff (the call is idempotent on distinct filenames).
Defensive patterns

Strategy: retry

Try / catch

async function safePublish(pub: RvfaPublisher, path: string, meta?: PublishMetadata) {
  const maxAttempts = 4;
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      return await pub.publish(path, meta);
    } catch (e) {
      const msg = (e as Error).message;
      const transient = /HTTP 429|HTTP 5\d\d/.test(msg);
      if (!transient || attempt === maxAttempts) throw e;
      await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
    }
  }
  throw new Error('unreachable');
}

Prevention

When it happens

Trigger: HTTP 401 (invalid/expired JWT), 403 (JWT lacks pinning scope), 413 (file exceeds Pinata plan size limit), 429 (rate limited), 5xx (Pinata outage), or 400 (malformed multipart body).

Common situations: JWT expired since the publisher was constructed; appliance file is larger than the Pinata plan's per-file cap; burst publishing trips the rate limiter; transient Pinata 5xx during a release window.

Related errors


AI-assisted analysis of ruvnet/ruflo@6b01dc5a68 (2026-08-12). Data as JSON: /api/errors/ac96e1f1d8e83e52. Report an issue: GitHub.