Skip to content

Commit c41c10e

Browse files
committed
feat(cli): enhance error handling with network diagnostics and timeout errors
Add comprehensive error handling improvements aligned with socket-sdk-js: - Add TimeoutError class with elapsed time tracking and retry guidance - Add network error diagnostics with specific guidance for: - Timeout errors (ETIMEDOUT, ESOCKETTIMEDOUT, ECONNRESET) - Connection failures (ECONNREFUSED) - DNS issues (ENOTFOUND, EAI_AGAIN) - SSL/TLS certificate errors - Network unreachable scenarios - Add type guards: isNetworkError(), isTimeoutError() - Add getNetworkErrorDiagnostics() helper with actionable recovery steps - Centralize Socket.dev URLs as constants (status, dashboard, pricing, etc.) - Enhance queryApiSafeText() and sendApiRequest() with network diagnostics - Add comprehensive test coverage (61 tests, all passing) Benefits: - Users get specific, actionable guidance for network errors - Faster troubleshooting with clear diagnostics - Consistent messaging via URL constants - Better alignment with socket-sdk-js and socket-lib
1 parent 71caa25 commit c41c10e

File tree

4 files changed

+365
-20
lines changed

4 files changed

+365
-20
lines changed

packages/cli/src/constants/socket.mts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ export { NPM_REGISTRY_URL } from '@socketsecurity/lib/constants/agents'
88
// Socket API URLs
99
export const API_V0_URL = 'https://api.socket.dev/v0/'
1010
export const SOCKET_WEBSITE_URL = 'https://socket.dev'
11+
export const SOCKET_CONTACT_URL = 'https://socket.dev/contact'
12+
export const SOCKET_DASHBOARD_URL = 'https://socket.dev/dashboard'
13+
export const SOCKET_PRICING_URL = 'https://socket.dev/pricing'
14+
export const SOCKET_SETTINGS_API_TOKENS_URL =
15+
'https://socket.dev/settings/api-tokens'
16+
export const SOCKET_STATUS_URL = 'https://status.socket.dev'
1117

1218
// Socket Configuration Files
1319
export const SOCKET_JSON = 'socket.json'
@@ -25,3 +31,7 @@ export const TOKEN_PREFIX_LENGTH = TOKEN_PREFIX.length
2531
// Documentation
2632
export const V1_MIGRATION_GUIDE_URL =
2733
'https://docs.socket.dev/docs/v1-migration-guide'
34+
35+
// GitHub
36+
export const SOCKET_CLI_ISSUES_URL =
37+
'https://github.com/SocketDev/socket-cli/issues'

packages/cli/src/utils/error/errors.mts

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ import {
2727
import { debugNs } from '@socketsecurity/lib/debug'
2828

2929
import ENV from '../../constants/env.mts'
30+
import {
31+
SOCKET_DASHBOARD_URL,
32+
SOCKET_PRICING_URL,
33+
SOCKET_STATUS_URL,
34+
} from '../../constants/socket.mts'
3035

3136
import type { RegistryInternals } from '../../constants/types.mts'
3237

@@ -111,8 +116,8 @@ export class RateLimitError extends Error {
111116
retryAfter
112117
? `Wait ${retryAfter} seconds before retrying`
113118
: 'Wait a few minutes before retrying',
114-
'Check your API quota at https://socket.dev/dashboard',
115-
'Consider upgrading your plan for higher limits',
119+
`Check your API quota at ${SOCKET_DASHBOARD_URL}`,
120+
`Consider upgrading your plan for higher limits at ${SOCKET_PRICING_URL}`,
116121
]
117122
}
118123
}
@@ -190,6 +195,33 @@ export class ConfigError extends Error {
190195
}
191196
}
192197

198+
/**
199+
* Timeout error with retry guidance.
200+
* Thrown when operations exceed time limits.
201+
*/
202+
export class TimeoutError extends Error {
203+
public readonly timeoutMs?: number | undefined
204+
public readonly elapsedMs?: number | undefined
205+
public readonly recovery: string[]
206+
207+
constructor(
208+
message: string,
209+
timeoutMs?: number | undefined,
210+
elapsedMs?: number | undefined,
211+
recovery?: string[] | undefined,
212+
) {
213+
super(message)
214+
this.name = 'TimeoutError'
215+
this.timeoutMs = timeoutMs
216+
this.elapsedMs = elapsedMs
217+
this.recovery = recovery || [
218+
'Check your internet connection speed',
219+
'Try again when network conditions improve',
220+
'Contact support if timeouts persist',
221+
]
222+
}
223+
}
224+
193225
export async function captureException(
194226
exception: unknown,
195227
hint?: EventHintOrCaptureContext | undefined,
@@ -353,3 +385,133 @@ export async function buildErrorCause(
353385
? `${message} (reason: ${reason})`
354386
: message
355387
}
388+
389+
/**
390+
* Type guard to check if an error is a network error.
391+
*/
392+
export function isNetworkError(error: unknown): error is NetworkError {
393+
return error instanceof NetworkError
394+
}
395+
396+
/**
397+
* Type guard to check if an error is a timeout error.
398+
*/
399+
export function isTimeoutError(error: unknown): error is TimeoutError {
400+
return error instanceof TimeoutError
401+
}
402+
403+
/**
404+
* Detect network-related error codes from Node.js errors.
405+
*/
406+
export function getNetworkErrorCode(error: unknown): string | undefined {
407+
if (!isErrnoException(error)) {
408+
return undefined
409+
}
410+
return error.code
411+
}
412+
413+
/**
414+
* Get network error diagnostics with actionable guidance.
415+
* Provides specific recovery steps based on error type.
416+
*
417+
* @param error - The error to diagnose
418+
* @param durationMs - Optional request duration in milliseconds
419+
* @returns Diagnostic message with recovery suggestions
420+
*
421+
* @example
422+
* const diagnostics = getNetworkErrorDiagnostics(error, 5000)
423+
* // Returns: "Connection refused. The server may be down..."
424+
*/
425+
export function getNetworkErrorDiagnostics(
426+
error: unknown,
427+
durationMs?: number | undefined,
428+
): string {
429+
const errorCode = getNetworkErrorCode(error)
430+
const errorMessage = getErrorMessage(error) || String(error)
431+
432+
// Timeout errors.
433+
if (
434+
errorCode === 'ETIMEDOUT' ||
435+
errorCode === 'ESOCKETTIMEDOUT' ||
436+
errorCode === 'ECONNRESET' ||
437+
(durationMs && durationMs > 30_000)
438+
) {
439+
const timeInfo = durationMs ? ` after ${Math.round(durationMs / 1000)}s` : ''
440+
return (
441+
`Request timeout${timeInfo}. The server took too long to respond.\n` +
442+
'💡 Try:\n' +
443+
' • Check your internet connection speed\n' +
444+
' • Retry the request - the server may be temporarily slow\n' +
445+
` • Check Socket status: ${SOCKET_STATUS_URL}\n` +
446+
' • Contact support if timeouts persist'
447+
)
448+
}
449+
450+
// Connection refused.
451+
if (errorCode === 'ECONNREFUSED') {
452+
return (
453+
'Connection refused. The server actively rejected the connection.\n' +
454+
'💡 Try:\n' +
455+
' • Check if you are using a proxy or VPN that may be blocking the connection\n' +
456+
' • Verify your firewall settings\n' +
457+
` • Check Socket status: ${SOCKET_STATUS_URL}\n` +
458+
' • Ensure SOCKET_CLI_API_BASE_URL is set correctly (if configured)'
459+
)
460+
}
461+
462+
// DNS resolution failures.
463+
if (
464+
errorCode === 'ENOTFOUND' ||
465+
errorCode === 'EAI_AGAIN' ||
466+
errorMessage.includes('getaddrinfo')
467+
) {
468+
return (
469+
'DNS resolution failed. Unable to resolve the server hostname.\n' +
470+
'💡 Try:\n' +
471+
' • Check your internet connection\n' +
472+
' • Verify DNS settings (try 8.8.8.8 or 1.1.1.1)\n' +
473+
' • Check if a VPN or proxy is interfering\n' +
474+
' • Ensure SOCKET_CLI_API_BASE_URL is correct (if configured)\n' +
475+
' • Try again in a few moments'
476+
)
477+
}
478+
479+
// Certificate/SSL errors.
480+
if (
481+
errorCode === 'CERT_HAS_EXPIRED' ||
482+
errorCode === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE' ||
483+
errorCode === 'SELF_SIGNED_CERT_IN_CHAIN' ||
484+
errorMessage.includes('certificate')
485+
) {
486+
return (
487+
'SSL/TLS certificate error. Unable to verify server identity.\n' +
488+
'💡 Try:\n' +
489+
' • Check your system date and time are correct\n' +
490+
' • Update your system certificates\n' +
491+
' • Check if a proxy is intercepting HTTPS traffic\n' +
492+
' • Contact your IT department if behind corporate firewall'
493+
)
494+
}
495+
496+
// Network unreachable.
497+
if (errorCode === 'ENETUNREACH' || errorCode === 'EHOSTUNREACH') {
498+
return (
499+
'Network unreachable. Cannot reach the destination network.\n' +
500+
'💡 Try:\n' +
501+
' • Check your internet connection\n' +
502+
' • Verify network/WiFi is connected\n' +
503+
' • Check if VPN or firewall is blocking access\n' +
504+
' • Try a different network'
505+
)
506+
}
507+
508+
// Generic network error with basic guidance.
509+
return (
510+
`Network error: ${errorMessage}\n` +
511+
'💡 Try:\n' +
512+
' • Check your internet connection\n' +
513+
' • Verify proxy settings if using a proxy\n' +
514+
` • Check Socket status: ${SOCKET_STATUS_URL}\n` +
515+
' • Try again in a few moments'
516+
)
517+
}

0 commit comments

Comments
 (0)