ruvnet/ruflo · error

fetchAgentCard: ${sourceUrl} did not return valid JSON

Error message

fetchAgentCard: ${sourceUrl} did not return valid JSON

What it means

The Agent Card endpoint returned 2xx but JSON.parse of the body threw, so the payload is not valid JSON. Typical bodies are HTML (login pages, error templates), plain-text errors, empty responses, or JSON with syntax problems. The abort-timer cleanup has already run by the time this is thrown.

Source

Thrown at v3/@claude-flow/plugin-agent-federation/src/a2a/consume.ts:77

      signal: controller.signal,
      headers: { accept: 'application/json' },
    });
    if (!res.ok) {
      throw new Error(`fetchAgentCard: ${sourceUrl} returned HTTP ${res.status}`);
    }
    text = await res.text();
  } finally {
    clearTimeout(timer);
  }
  if (text.length > maxBytes) {
    throw new Error(`fetchAgentCard: card exceeds ${maxBytes} bytes`);
  }

  let parsed: unknown;
  try {
    parsed = JSON.parse(text);
  } catch {
    throw new Error(`fetchAgentCard: ${sourceUrl} did not return valid JSON`);
  }

  const validation = validateAgentCard(parsed);
  if (!validation.valid) {
    throw new Error(
      `fetchAgentCard: invalid A2A agent card from ${sourceUrl}: ${validation.errors.join('; ')}`,
    );
  }
  return { card: parsed as A2AAgentCard, sourceUrl };
}

/**
 * Fetch a remote Agent Card and register the peer in federation discovery.
 * The peer enters at TrustLevel.UNTRUSTED (see fromAgentCard) — the card is
 * self-asserted metadata, and trust is earned through the normal handshake.
 */
export async function consumeAgentCard(
  discovery: DiscoveryService,

View on GitHub (pinned to fa13ee4ad6)

Solutions

  1. curl the exact sourceUrl and inspect the raw body — the first 200 bytes usually reveal the problem
  2. Fix the serving side to emit application/json on the well-known card route
  3. If a proxy rewrites responses, bypass it or configure an exception for the card URL
  4. Check for a trailing BOM or encoding mismatch if the body looks like JSON but still fails

Example fix

// before
await fetchAgentCard(url);
// after — inspect what the endpoint really returns
const res = await fetch(resolveAgentCardUrl(url));
console.log(res.headers.get('content-type'));
console.log((await res.text()).slice(0, 200));
Defensive patterns

Strategy: try-catch

Try / catch

try {
  const { card } = await fetchAgentCard(url, opts);
} catch (e) {
  if (String(e).includes('did not return valid JSON')) {
    // fetch the body yourself once to see what the endpoint really returns
    const res = await fetchImpl(resolveAgentCardUrl(url));
    console.error('non-JSON body:', (await res.text()).slice(0, 200));
  }
  throw e;
}

Prevention

When it happens

Trigger: Auth walls or captive portals returning HTML with status 200; the server returning JSONP or commented JSON; an empty 200 response; corporate proxies rewriting the body despite the accept header.

Common situations: Intercepting proxies that inject terms-of-service pages; misconfigured content negotiation on the peer server; endpoints that serve human-readable status text on the card route.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/9144f086e37caacd. Report an issue: GitHub.