Skip to content

cloudflare-export: GraphQL errors assumed to be an array of {message: string} — other shapes throw TypeErrors misclassified as fetch-failed, and a single-object errors is silently ignored #2608

Closed

Description

@dezren39

cloudflare-export: GraphQL errors is assumed to be an array of {message: string} — other shapes throw TypeErrors misclassified as fetch-failed, and a single-object errors is silently ignored

Evidence (verified this run, main 819889b)

pm-stack/cloudflare-export/cloudflare-quota-export.mjs, in run():

  • Line 407: const gqlErrors = gql.body?.errors ?? [];
  • Line 408-409: if (gqlErrors.length) { const msg = gqlErrors.map((e) => e.message.split("\n")[0]).join(" ; "); ... } — this assumes errors is an array whose every entry is an object with a string message. Nothing validates that shape, in a file whose other parsers (parseGraphqlUsage, classifyR2, foldKv, firstSum) are deliberately fail-closed on shape drift.
  • Line 451 (main()): any non-ExportError escaping run() is re-wrapped as new ExportError("fetch-failed", 7, "unexpected: ...").

Probe (laptop ~/git/fleet/probe-2026-10-30-laneB-cfgqlerr/probe.mjs, Bun, the real run() imported from the laptop clone — md5 0fbd8dae4f5eba83adb88ec6092d1039, byte-identical to sandbox main — with fetch stubbed; billing probes 200, buckets well-formed):

  • Control, valid payload with no errors: status: ok, 11 metrics.
  • Control, errors: [{message: "auth: permission denied"}]: correctly throws ExportError token-scope exit 3.
  • errors: "upstream exploded" (a string): TypeError — gqlErrors.map is not a function escapes run(); main() reclassifies it as fetch-failed / exit 7 with detail unexpected: ..., instead of the file's own unparseable / exit 6 for an unrecognised shape, and the GraphQL error text is lost.
  • errors: [null]: TypeError — null is not an object (evaluating 'e.message'), same fetch-failed/7 misclassification.
  • errors: [{message: 12345}]: TypeError — e.message.split is not a function, same misclassification.
  • errors: {message: "auth: permission denied"} (single object, no array) with a valid data payload alongside (GraphQL permits partial data + errors): the error is silently ignored — gqlErrors.length is undefined, so the branch never runs — and run() publishes a status: ok document with 11 metrics despite an auth/permission error entry. Without data, the same shape falls through to the generic "no account node in data" unparseable, and the token-scope classification (exit 3) the array form gets never fires.

Why it matters

This exporter's whole contract is its failure taxonomy (header comment, lines 36-40): token-scope (exit 3) tells the operator the token needs scopes, unparseable (6) says an endpoint answered an unknown shape, fetch-failed (7) says the network/other failed. A shape-drifted errors field currently lands in the wrong bucket — an on-call reader chasing a "fetch failure" finds a healthy network and no error text — and the single-object form can either hide an auth error behind a green status: ok document or strip it of its scope classification. The aggregator/gatherer side trusts these statuses to decide whether a Cloudflare reading is real.

Acceptance criteria

  • The GraphQL errors value is normalised/validated before use: a non-array errors, or entries that are not objects with a string message, produce ExportError("unparseable", 6, ...) naming the shape — never a bare TypeError escaping run().
  • A single-object errors (or any truthy non-array form triage decides to accept) is not silently ignored: its message goes through the same auth/permission/scope classification as the array form, or it is rejected as unparseable — triage pins which; an auth/permission error must never coexist with a published status: ok document.
  • main()'s classification of a GraphQL shape problem is unparseable/6, not fetch-failed/7.
  • Regression tests in cloudflare-quota-export.test.mjs cover: errors as a string, [null], [{message: <number>}], and a single-object errors, alongside the existing array controls (valid payload still exports status: ok; [{message: "auth: ..."}] still yields token-scope/3).
  • Well-formed responses are unaffected: the existing cloudflare-export test suite passes unchanged.

Dedup: #2574 covers the R2 bucket enumeration in the same run() (fails-open r2Buckets: [] + pagination) — a different field and failure mode; this issue is only the GraphQL errors handling at lines 407-416. #2460 is the meta-exporter's timestamp misclassification, a different file.

Activity

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    source/creatorFiled by the new-issue-creator job
    priority/normalOrdinary priority (the default)
    queue/queuedHas a queued task in the Supabase agent queue
    effort/xsTrivial: expected under 30 minutes of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 30, 2026
  2. dezren39 commented on Oct 30, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

    • Agent: new-issue-creator job (new-issue-creator-code-b)
    • Session / ID: cron job new-issue-creator-code-b, run 2026-10-30 01:50 CDT
    • Found via: filed this run
    • Task: Filed this issue after verifying the GraphQL errors shape handling in cloudflare-quota-export.mjs against the real run() on the laptop (stubbed fetch, main 819889b).
    • Expectation: effort/xs triage — pin the normalisation/treatment for non-array and malformed errors shapes, then route a small_fix packet.
    • Links: packet packet-2608-triage.json (hidden_files, goal keep-the-four-repos-healthy); probe ~/git/fleet/probe-2026-10-30-laneB-cfgqlerr/ (laptop)
  3. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 1, 2026
  4. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  5. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    Triage findings — CONFIRMED

    Fresh verification at current main 199d9f2e2 (laptop primary clone, clean main):

    • pm-stack/cloudflare-export/cloudflare-quota-export.mjs:407-413 — const gqlErrors = gql.body?.errors ?? []; if (gqlErrors.length) { const msg = gqlErrors.map((e) => e.message.split("\n")[0]).join(" ; "); … } with the auth|permission|scope → token-scope classification and unparseable/6 fallback inside the branch. No shape validation anywhere.
    • main()'s catch wraps any non-ExportError as fetch-failed/7 (unexpected: …), so the bare TypeErrors from drifted shapes land in the wrong taxonomy bucket with the real error text mangled.
    • A single-object errors has no .length → the branch is skipped entirely → a valid data payload publishes status: ok with the error invisible.

    The file is byte-identical between the filing probe's main (819889bb) and current main (empty git diff), so the probe carries verbatim (~/git/fleet/probe-2026-10-30-laneB-cfgqlerr/probe.mjs, real run(), stubbed fetch): valid control → ok, 11 metrics; array auth error → token-scope/3; errors as string → TypeError gqlErrors.map is not a function; [null] → TypeError on e.message; [{message: 12345}] → TypeError e.message.split is not a function; single-object auth error + valid data → status ok (silently ignored).

    Pinned normalisation (task 5394)

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Nov 1, 2026
  7. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

  8. added a commit that references this issue on Nov 1, 2026
  9. dezren39 commented on Nov 1, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/xsTrivial: expected under 30 minutes of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/normalOrdinary priority (the default)source/creatorFiled by the new-issue-creator jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions