Report unavailable shops and Admin API server errors instead of unknown CLI bugs - #8612
Conversation
…wn CLI bugs Admin API version discovery wrapped every unclassified failure in a BugError, so a frozen shop (HTTP 402), a Shopify-side 5xx, and a cancelled request all surfaced as "Unknown error connecting to your store" and were filed as CLI bugs. Classify all three at the call site in cli-kit's fetchApiVersions, and wrap 5xx in @shopify/store's fetchPublicApiVersions so a raw ClientError no longer reaches the error reporter unwrapped. Patch authored by River (Pit Crew). Vault-Issue: 31609 Vault-Issue: 78419 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| } | ||
|
|
||
| /** | ||
| * Lower-cased substrings the fetch implementations we run on use to signal that a request was |
There was a problem hiding this comment.
Nit: let's reduce the size of these comments. I find running them through ASD-STE100 to be a helpful reframing of sentences with points and decent structure.
There was a problem hiding this comment.
Done, trimmed both blocks.
| * Lower-cased substrings the fetch implementations we run on use to signal that a request was | ||
| * aborted rather than failing on its own: `node-fetch` says 'The user aborted a request.', undici | ||
| * says 'This operation was aborted', and cli-kit's own request-timeout signal says 'The operation | ||
| * was aborted'. Exported so tests assert against the same source of truth as production. |
There was a problem hiding this comment.
Tests use the runtime messages directly, and no code imports this constant. Please remove export and this sentence.
| * Checks if an error is an aborted request: the user cancelling the command, the host process | ||
| * cancelling it, or one of the CLI's own request timeouts firing. | ||
| * | ||
| * Deliberately kept out of `isTransientNetworkError`, which drives retry behaviour — a request that |
There was a problem hiding this comment.
Please narrow this claim. User-cancel aborts are not retried, but the CLI timeout message still matches isTransientNetworkError and uses its retry policy.
There was a problem hiding this comment.
Good catch, thank you. Narrowed to user-cancel.
Side note: timeout vs user-cancel differ by one word ("The" vs "This"). error.name would be sturdier. Pre-existing though, worth a follow-up?
- Shorten the two JSDoc blocks on the aborted-fetch helpers. - Drop `export` from ABORTED_FETCH_MESSAGE_FRAGMENTS; nothing imports it. - Correct the claim that aborted requests are never retried. The CLI's own timeout message matches isTransientNetworkError, so those still retry. Only user-cancelled requests are excluded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same treatment as the api.ts blocks: shorten the three inline comments in admin.ts and the nine-line block in admin-transport.ts. The reasoning that belongs in the PR description rather than the code is dropped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Differences in type declarationsWe detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:
New type declarationsWe found no new type declarations in this PR Existing type declarationspackages/cli-kit/dist/private/node/api.d.ts@@ -37,6 +37,21 @@ export declare function isTransientNetworkError(error: unknown): boolean;
* - Permanent: certificate validation failures, misconfigured SSL
*/
export declare function isNetworkError(error: unknown): boolean;
+/**
+ * Checks if an error is an aborted request: a user cancelling the command, the host process
+ * cancelling it, or one of the CLI's own request timeouts firing.
+ *
+ * Not used by the retry logic, because a user-cancelled request must not be retried.
+ * `isTransientNetworkError` separately matches the CLI's own timeout message, so timeouts do
+ * still retry.
+ *
+ * The `name` check matches the `AbortError` shape that fetch throws, not cli-kit's own
+ * `AbortError`, which leaves `name` as 'Error'.
+ *
+ * @param error - Error to be checked.
+ * @returns A boolean indicating if the request was aborted.
+ */
+export declare function isAbortedFetchError(error: unknown): boolean;
export declare function simpleRequestWithDebugLog<T extends {
headers: Headers;
status: number;
|
WHY are these changes introduced?
Fixes shop/issues#32996 and shop/issues#85279 — Vault 31609 (P1, fix due 2026-09-22) and 78419 (P2, fix due 2026-09-28).
When the Admin API returns 402 "Unavailable Shop" — the shop is frozen, paused, or closed —
fetchApiVersionsin@shopify/cli-kitwraps it in aBugErrorand prints:A frozen shop is an expected, user-actionable condition, not a CLI defect. Reporting it as a bug both misleads the user and files a crash report on every occurrence — roughly 400/day, 13,743 events across ≥1,170 users and ≥292 shops in the last 30 days. It is currently the highest-volume error report from the CLI.
Worth noting: 31609's title says "The user aborted a request" while its current events carry the 402 message. Both are true — two different underlying errors land in the same
BugErrorat the same line, so Observe groups them together. That conflation is itself part of the bug.WHAT is this pull request doing?
@shopify/cli-kit—fetchApiVersionsnow classifies three cases before falling through to the catch-allBugError:The store <shop> is currently unavailable.(frozen, paused, or closed)The Admin API for <shop> returned a server error (HTTP N).This query is a constant with no user-supplied input, so a server error here cannot have been caused by what the user typed.Request to <shop> was aborted before it completed.Adds
isAbortedFetchErrortoprivate/node/api.ts, deliberately kept out ofisTransientNetworkErrorso a deliberately cancelled request is never retried.@shopify/store—fetchPublicApiVersionswraps a 5xx instead of re-throwing a rawClientError. Previously that escaped unwrapped and reached the error reporter as an unexpected CLI bug, echoing the whole request back at the user. Handled here rather than inclassifyAdminApiError— whichrunAdminStoreGraphQLOperationalso calls — so that function'sGraphQL operation failed.branch stays reachable for errors about the user's own query.Two regression guards included: a genuinely unknown failure still produces a
BugErrorwithshouldReportErrorAsUnexpected === true, and the existing 401/403 classifications are unchanged.How to manually test your changes?
Against a frozen, paused, or closed store:
Unknown error connecting to your store …and a filed crash reportThe store <shop> is currently unavailable.with instructions to reactivate itPost-release steps
None.
Checklist
3141726163689320914should go to zeropatchbump for both packages, changeset includedProvenance: the patch was authored by River (Pit Crew) overnight on 2026-09-21 and posted in #devtools-gardener-backlog; the full derivation and evidence live in that thread. I applied it, and verified locally against current
main: 36 tests pass in the two affected suites, andtype-checkandlintare green on both packages.🤖 Generated with Claude Code