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).then → not detected.
await fetch(url).catch(handler) — callee is fetch(url).catch → not 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
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 · ◷
Rule
require-fetch-try-catch(eslint-factory/src/rules/require-fetch-try-catch.ts)Summary
isAwaitFetchCall(lines 13–19) only matches anAwaitExpressionwhose argument is aCallExpressionwhose callee is the barefetchIdentifier:Any method chain on the fetch call moves the awaited
CallExpression's callee to aMemberExpression, so the rule never fires:await fetch(url).then(r => r.json())— callee isfetch(url).then→ not detected.await fetch(url).catch(handler)— callee isfetch(url).catch→ not detected.The
.then()-only chain (no rejection handler) still throws aTypeErroron 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:65and:85:These specific sites happen to be safe today (the
.catch(rethrowAbortError)rethrows and the callers wrap the call in try/catch —testGitHubRESTAPIatvalidate_secrets.cjs:107), so this is a detection blind spot rather than a live crash. But the shape occurs live, and a bareawait 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.cjssites (they attach.catch(...)). The refinement must recognize a rejection handler as satisfying the requirement, mirroring howno-json-stringify-errortreats.then(onFulfilled, onRejected)as a catch-equivalent:.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
fetchCallExpressionthrough anyMemberExpression/CallExpressionchain 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).validate_secrets.cjs:65and:85are not newly flagged (they carry.catch(rethrowAbortError)).Grounded via static review (npm/lint firewalled in this environment):
validate_secrets.cjs:53,65,69,85,107.