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

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions