Skip to content

Report unavailable shops and Admin API server errors instead of unknown CLI bugs - #8612

Merged
Suleimanlatrsh merged 3 commits into
mainfrom
suleiman/unavailable-shop-not-a-cli-bug
Sep 22, 2026
Merged

Suleimanlatrsh merged 3 commits into
mainfrom
suleiman/unavailable-shop-not-a-cli-bug

Conversation

@Suleimanlatrsh

Copy link
Copy Markdown

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 — fetchApiVersions in @shopify/cli-kit wraps it in a BugError and prints:

Unknown error connecting to your store <shop>: GraphQL Error (Code: 402): {"response":{"errors":"Unavailable Shop","status":402,...}}

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 BugError at the same line, so Observe groups them together. That conflation is itself part of the bug.

WHAT is this pull request doing?

@shopify/cli-kitfetchApiVersions now classifies three cases before falling through to the catch-all BugError:

  • 402The store <shop> is currently unavailable. (frozen, paused, or closed)
  • 5xxThe 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.
  • aborted requestRequest to <shop> was aborted before it completed.

Adds isAbortedFetchError to private/node/api.ts, deliberately kept out of isTransientNetworkError so a deliberately cancelled request is never retried.

@shopify/storefetchPublicApiVersions wraps a 5xx instead of re-throwing a raw ClientError. 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 in classifyAdminApiError — which runAdminStoreGraphQLOperation also calls — so that function's GraphQL operation failed. branch stays reachable for errors about the user's own query.

Two regression guards included: a genuinely unknown failure still produces a BugError with shouldReportErrorAsUnexpected === true, and the existing 401/403 classifications are unchanged.

How to manually test your changes?

Against a frozen, paused, or closed store:

shopify theme list --store <frozen-store>.myshopify.com
  • Before: Unknown error connecting to your store … and a filed crash report
  • After: The store <shop> is currently unavailable. with instructions to reactivate it

Post-release steps

None.

Checklist

  • I've considered possible cross-platform impacts (Mac, Linux, Windows) — error classification only, no platform-specific code paths
  • I've considered possible documentation changes — user-facing error copy only
  • I've considered analytics changes to measure impact — these three cases stop being reported as unexpected errors, which is the intent; Observe hash 3141726163689320914 should go to zero
  • The change is user-facing — patch bump for both packages, changeset included

Provenance: 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, and type-check and lint are green on both packages.

🤖 Generated with Claude Code

…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>
@Suleimanlatrsh
Suleimanlatrsh requested a review from a team as a code owner September 21, 2026 18:43
@github-actions github-actions Bot added the Area: @shopify/cli @shopify/cli package issues label Sep 21, 2026
}

/**
* Lower-cased substrings the fetch implementations we run on use to signal that a request was

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests use the runtime messages directly, and no code imports this constant. Please remove export and this sentence.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please narrow this claim. User-cancel aborts are not retried, but the CLI timeout message still matches isTransientNetworkError and uses its retry policy.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Suleimanlatrsh and others added 2 commits September 22, 2026 10:04
- 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>
@github-actions

Copy link
Copy Markdown
Contributor

Differences in type declarations

We 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:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

We found no new type declarations in this PR

Existing type declarations

packages/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;

@Suleimanlatrsh
Suleimanlatrsh added this pull request to the merge queue Sep 22, 2026
Merged via the queue into main with commit 4b99712 Sep 22, 2026
30 checks passed
@Suleimanlatrsh
Suleimanlatrsh deleted the suleiman/unavailable-shop-not-a-cli-bug branch September 22, 2026 15:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants