Skip to content

Commit 14f6ab5

Browse files
amcaplanclaude
andcommitted
Distinguish between transient and permanent network errors
Addresses PR review feedback about retrying permanent network errors. Changes: - Split isTransientNetworkError (for retry logic) from isNetworkError (for error classification) - isTransientNetworkError: Only includes retryable errors (timeouts, DNS failures, connection issues) - isNetworkError: Includes all network errors (transient + permanent SSL/TLS/certificate errors) - Updated fetchApiVersions to use isNetworkError for error classification - Eliminated code duplication by having isNetworkError call isTransientNetworkError Impact: - Certificate validation failures are still reported as user-facing AbortErrors (not bugs) - But they are no longer retried unnecessarily - All existing tests pass 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 60eb2a2 commit 14f6ab5

File tree

2 files changed

+43
-15
lines changed

2 files changed

+43
-15
lines changed

packages/cli-kit/src/private/node/api.ts

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,19 @@ type VerboseResponse<T> =
6969
| UnauthorizedErrorResponse
7070

7171
/**
72-
* Checks if an error is a transient network error (connection issues, timeouts, DNS failures, TLS errors, etc.)
73-
* rather than an application logic error. These errors are typically:
74-
* - Worth retrying (when retry logic is configured)
75-
* - Should be reported as user-facing errors (AbortError) rather than bugs (BugError)
72+
* Checks if an error is a transient network error that is likely to recover with retries.
73+
*
74+
* Use this function for retry logic. Use isNetworkError for error classification.
75+
*
76+
* Examples of transient errors (worth retrying):
77+
* - Connection timeouts, resets, and aborts
78+
* - DNS failures (enotfound, getaddrinfo, eai_again) - can be temporary
79+
* - Socket disconnects and hang ups
80+
* - Premature connection closes
7681
*/
7782
export function isTransientNetworkError(error: unknown): boolean {
7883
if (error instanceof Error) {
79-
const networkErrorMessages = [
84+
const transientErrorMessages = [
8085
'socket hang up',
8186
'econnreset',
8287
'econnaborted',
@@ -90,21 +95,43 @@ export function isTransientNetworkError(error: unknown): boolean {
9095
'the operation was aborted',
9196
'timeout',
9297
'premature close',
93-
'certificate',
94-
'cert',
95-
'tls',
96-
'ssl',
97-
'altnames',
9898
'getaddrinfo',
9999
]
100100
const errorMessage = error.message.toLowerCase()
101-
const anyMatches = networkErrorMessages.some((issueMessage) => errorMessage.includes(issueMessage))
101+
const anyMatches = transientErrorMessages.some((issueMessage) => errorMessage.includes(issueMessage))
102102
const missingReason = /^request to .* failed, reason:\s*$/.test(errorMessage)
103103
return anyMatches || missingReason
104104
}
105105
return false
106106
}
107107

108+
/**
109+
* Checks if an error is any kind of network-related error (connection issues, timeouts, DNS failures,
110+
* TLS/certificate errors, etc.) rather than an application logic error.
111+
*
112+
* These errors should be reported as user-facing errors (AbortError) rather than bugs (BugError),
113+
* regardless of whether they are transient or permanent.
114+
*
115+
* Examples include:
116+
* - Transient: connection timeouts, socket hang ups, temporary DNS failures
117+
* - Permanent: certificate validation failures, misconfigured SSL
118+
*/
119+
export function isNetworkError(error: unknown): boolean {
120+
// First check if it's a transient network error
121+
if (isTransientNetworkError(error)) {
122+
return true
123+
}
124+
125+
// Then check for permanent network errors (SSL/TLS/certificate issues)
126+
if (error instanceof Error) {
127+
const permanentNetworkErrorMessages = ['certificate', 'cert', 'tls', 'ssl', 'altnames']
128+
const errorMessage = error.message.toLowerCase()
129+
return permanentNetworkErrorMessages.some((issueMessage) => errorMessage.includes(issueMessage))
130+
}
131+
132+
return false
133+
}
134+
108135
async function runRequestWithNetworkLevelRetry<T extends {headers: Headers; status: number}>(
109136
requestOptions: RequestOptions<T>,
110137
): Promise<T> {

packages/cli-kit/src/public/node/api/admin.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import {
1414
restRequestUrl,
1515
isThemeAccessSession,
1616
} from '../../../private/node/api/rest.js'
17-
import {isTransientNetworkError} from '../../../private/node/api.js'
17+
import {isNetworkError} from '../../../private/node/api.js'
1818
import {RequestModeInput, shopifyFetch} from '../http.js'
1919
import {PublicApiVersions} from '../../../cli/api/graphql/admin/generated/public_api_versions.js'
2020

@@ -185,9 +185,10 @@ async function fetchApiVersions(session: AdminSession, preferredBehaviour?: Requ
185185
)
186186
}
187187

188-
// Check for network-level errors (connection issues, timeouts, DNS failures, TLS errors, etc.)
189-
// These are transient errors that may have been retried already by lower-level retry logic
190-
if (isTransientNetworkError(error)) {
188+
// Check for network-level errors (connection issues, timeouts, DNS failures, TLS/certificate errors, etc.)
189+
// All network errors should be treated as user-facing errors, not CLI bugs
190+
// Note: Some of these may have been retried already by lower-level retry logic
191+
if (isNetworkError(error)) {
191192
throw new AbortError(
192193
`Network error connecting to your store ${session.storeFqdn}: ${
193194
error instanceof Error ? error.message : String(error)

0 commit comments

Comments
 (0)