Skip to content

require-fetch-try-catch: member-chained await fetch(...).then()/.catch()/.<responseMethod>() escapes detection #47932

Description

@github-actions

Rule

require-fetch-try-catch (eslint-factory/src/rules/require-fetch-try-catch.ts)

Summary

isAwaitFetchCall (lines 13–19) only matches an AwaitExpression whose argument is a CallExpression whose callee is the bare fetch Identifier:

const callee = argument.callee;
return callee.type === AST_NODE_TYPES.Identifier && callee.name === "fetch";

Any method chain on the fetch call moves the awaited CallExpression's callee to a MemberExpression, so the rule never fires:

  • await fetch(url).then(r => r.json()) — callee is fetch(url).thennot detected.
  • await fetch(url).catch(handler) — callee is fetch(url).catchnot detected.

The .then()-only chain (no rejection handler) still throws a TypeError on network failure and would crash the action unhandled — the exact case the rule exists to prevent — yet it is silently ignored.

Grounded occurrences of the escaping shape

actions/setup/js/validate_secrets.cjs:65 and :85:

const res = await fetch(`https://${hostname}${path}`, {
  method: "GET",
  headers,
  signal: AbortSignal.timeout(10000),
}).catch(rethrowAbortError);   // MemberExpression callee -> rule never fires

These specific sites happen to be safe today (the .catch(rethrowAbortError) rethrows and the callers wrap the call in try/catch — testGitHubRESTAPI at validate_secrets.cjs:107), so this is a detection blind spot rather than a live crash. But the shape occurs live, and a bare await fetch(url).then(r => r.json()) with no rejection handling would be missed with no diagnostic.

Nuance — do NOT flag chains that already handle rejection

Adding blanket member-chain matching would create false positives on the validate_secrets.cjs sites (they attach .catch(...)). The refinement must recognize a rejection handler as satisfying the requirement, mirroring how no-json-stringify-error treats .then(onFulfilled, onRejected) as a catch-equivalent:

  • Chain ending in .catch(...) → handled → not flagged.
  • .then(onFulfilled, onRejected) (2-arg) → handled → not flagged.
  • .then(onFulfilled) (1-arg) or response-method chains (.json(), .text()) with no rejection handler, not inside try/catch → flagged.

Proposed refinement

Walk outward from the fetch CallExpression through any MemberExpression/CallExpression chain to find the awaited outer expression. Detect fetch even when it is the object of a member chain, then check the chain for a terminal .catch(...) or a two-argument .then(onF, onR) (or an enclosing try/catch) before reporting.

Acceptance criteria

  • async function f() { const r = await fetch(url).then(x => x.json()); } is flagged (add invalid test).
  • async function f() { await fetch(url).catch(handler); } is valid (add valid test).
  • async function f() { await fetch(url).then(ok, onErr); } is valid (2-arg .then).
  • try { await fetch(url).then(x => x.json()); } catch (e) {} is valid (enclosing try).
  • Re-verify validate_secrets.cjs:65 and :85 are not newly flagged (they carry .catch(rethrowAbortError)).
  • Document the chained-fetch scope in the README section for the rule.

Grounded via static review (npm/lint firewalled in this environment): validate_secrets.cjs:53,65,69,85,107.

Generated by 🤖 ESLint Refiner · age00 · 317.3 AIC · ⌖ 13 AIC · ⊞ 4.6K ·

  • expires on Jul 31, 2026, 10:13 PM UTC-08:00

Activity

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

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions