8ece0009ee
* auto-claude: subtask-1-1 - Add GitHubErrorType and GitHubErrorInfo types Add error classification types for GitHub API error handling: - GitHubErrorType: Discriminated union for error categories (rate_limit, auth, permission, network, not_found, unknown) - GitHubErrorInfo: Structured error info with user-friendly message, raw error, rate limit reset time, required OAuth scopes, and status code These types will be used by the github-error-parser utility and GitHubApiErrorDisplay component for consistent error handling. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-1-2 - Create github-error-parser.ts utility with parseGitHubError function - Create github-error-parser.ts utility to classify GitHub API errors - Implement parseGitHubError() to detect error types: rate_limit, auth, permission, not_found, network, unknown - Extract metadata from errors (rate limit reset times, required scopes, status codes) - Add convenience functions: isRateLimitError, isAuthError, isNetworkError, isRecoverableError, requiresSettingsAction - Export all functions from utils/index.ts barrel file - Follow patterns from rate-limit-detector.ts with pattern arrays and classification functions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-2-1 - Create GitHubErrorDisplay.tsx component Add GitHubErrorDisplay component with error-type-specific rendering: - Different icons per error type (Clock, Key, Shield, WifiOff, SearchX, AlertTriangle) - Rate limit countdown timer with useEffect cleanup - Conditional action buttons (retry for recoverable, settings for auth/permission) - Compact and full card display variants - i18n-ready with common namespace translation keys Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-2-2 - Add rate limit countdown timer with useEffect cleanup - Fixed non-null assertion lint warning in countdown useEffect - Extract resetTime to local variable with conditional check - Maintains proper cleanup pattern with clearInterval on unmount * auto-claude: subtask-2-3 - Export GitHubErrorDisplay from components/index.ts * auto-claude: subtask-3-1 - Update IssueList.tsx to use GitHubErrorDisplay for blocking errors - Added onRetry and onOpenSettings props to IssueListProps interface - Updated IssueList component to use GitHubErrorDisplay for blocking errors (when issues.length === 0) - Updated GitHubIssues.tsx to pass handleRefresh and onOpenSettings callbacks to IssueList - Blocking errors now show user-friendly messages with retry/settings buttons based on error type Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-3-2 - Update IssueList.tsx to use GitHubErrorDisplay for inline load-more errors Replace the simple inline error div with GitHubErrorDisplay component using the compact prop for better error handling when issues are already loaded. This provides consistent error display with retry/settings actions. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-4-1 - Add githubErrors.* translation keys to en/common.json Added translation keys for GitHub error display component: - rateLimitTitle, authTitle, permissionTitle, notFoundTitle - networkTitle, unknownTitle for error type titles - resetsIn for rate limit countdown display - rateLimitExpired for when rate limit has reset - requiredScopes for permission error details * auto-claude: subtask-4-2 - Add githubErrors.* translation keys to fr/common.json Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-5-1 - Create unit tests for github-error-parser.ts Add comprehensive unit tests covering all error types and helper functions: - parseGitHubError: rate_limit, auth, permission, not_found, network, unknown - Helper functions: isRateLimitError, isAuthError, isNetworkError - isRecoverableError, requiresSettingsAction - Edge cases: null/undefined/empty, case insensitivity, multiline, JSON - Cross-cutting concerns: consistency, status code extraction 92 tests total covering all patterns and behaviors. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * auto-claude: subtask-5-2 - Create unit tests for GitHubErrorDisplay.tsx component Added comprehensive unit tests covering: - Null/empty error state handling - String error and GitHubErrorInfo object parsing - All error types (rate_limit, auth, permission, not_found, network, unknown) - Compact mode vs full card mode rendering - Retry and Settings button visibility based on error type - Rate limit countdown display - Required scopes display for permission errors - Custom className prop support - Callback stability and accessibility Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * fix: address lint and TypeScript issues in GitHub error handling - Fix incorrect import path in test file (../../../types -> ../../types) - Replace isNaN with Number.isNaN for safer type checking - Fix unused parameter by prefixing with underscore - Remove redundant switch case (case 'unknown' with default) - Remove unused imports in test file (beforeEach, afterEach) - Add comments to empty arrow functions in tests - Use optional chaining instead of non-null assertion Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: address CodeRabbit review feedback on GitHub error handling - GitHubErrorDisplay.tsx: - Memoize errorInfo with useMemo to prevent useEffect churn - Remove unnecessary useCallback wrappers for trivial handlers - Simplify dead code conditional (if (!error) return null) - Use i18n keys for error messages instead of hardcoded strings - github-error-parser.ts: - Add word boundaries to numeric regex patterns (401, 403, 404) - Make STATUS_CODE_PATTERN context-aware to avoid false positives - Tests: - Add fake timer tests for countdown interval behavior - Add clearInterval spy for unmount cleanup verification - Add overlapping pattern priority tests - Update translation mock with new message keys - i18n: - Add githubErrors.*Message keys to en/common.json and fr/common.json * fix: address additional CodeRabbit review feedback - GitHubErrorDisplay.tsx: - Stop interval when countdown expires (clearInterval on empty formatted) - Select specific message keys based on metadata (rateLimitMessageMinutes/Hours, permissionMessageScopes) - github-error-parser.ts: - Tighten REQUIRED_SCOPES_PATTERN to stop at sentence boundaries - Tests: - Update interval test to verify timer count - Update permission tests to avoid duplicate text matching - Add missing translation mocks for specific message keys * fix: address final CodeRabbit review feedback - GitHubErrorDisplay.tsx: - Extract getMessageKey to module scope (pure function) - Use cn() utility for className merging - Add title tooltip to compact variant for full error message - github-error-parser.ts: - Fix extractRateLimitResetTime to handle relative durations ("in X seconds") - Separate relative vs absolute timestamp patterns - Remove unused RATE_LIMIT_RESET_PATTERN constant - Tests: - Update mock type to Record<string, unknown> for accuracy - Add test for empty string error input * fix: address CodeRabbit review feedback - accessibility and optimization - GitHubErrorDisplay.tsx: - Add role="alert" to compact and full card variants for screen readers - Fix minutes/hours calculation to be undefined when <= 0 (avoid stale values) - github-error-parser.ts: - Add optional parsedInfo parameter to convenience predicates - Avoids re-classification when caller already has parsed info - Updated: isRateLimitError, isAuthError, isNetworkError, isRecoverableError, requiresSettingsAction - Tests: - Add tests for role="alert" accessibility in both full and compact modes * fix: address CodeRabbit feedback - i18n countdown and pattern order - GitHubErrorDisplay.tsx: - Hoist BASE_MESSAGE_KEYS to module scope to avoid recreation - Replace formatCountdown with getCountdownComponents returning numeric values - Add formatCountdownDisplay using i18n keys for hours/minutes/seconds - github-error-parser.ts: - Reorder classifyError to check PERMISSION_PATTERNS before NOT_FOUND_PATTERNS - Properly classifies 403 responses that might contain "not found" text - i18n: - Add countdownHoursMinutes and countdownMinutesSeconds keys (en/fr) - Enables locale-aware countdown formatting - Tests: - Add mock translations for countdown formatting keys * docs: clarify i18n usage for GitHubErrorInfo message field - Add comprehensive JSDoc to GitHubErrorInfo interface explaining that the `message` field should only be used as i18n fallback defaultValue - Update parseGitHubError function documentation with translation key mapping and proper usage example - Addresses concern about direct consumers bypassing i18n Note: role="alert" accessibility fix was already present on both compact and full card variants (lines 272 and 311). * fix: address Auto Claude PR review findings - GitHubErrorDisplay.tsx: - Clear stale countdown state when error type changes away from rate_limit - Prevents stale countdown data from persisting across error type transitions - github-error-parser.ts: - Add MAX_RESET_SECONDS constant (86400 seconds = 24 hours) - Validate relative duration seconds are within reasonable bounds - Prevents malformed error strings from creating far-future dates * fix: address Auto Claude PR review findings - bounds validation and pattern fixes - Add upper-bound validation (MAX_RESET_SECONDS=86400) on absolute timestamps in extractRateLimitResetTime to prevent far-future dates from malformed input - Remove bare status code patterns (401/403/404) from AUTH_PATTERNS, PERMISSION_PATTERNS, and NOT_FOUND_PATTERNS to avoid misclassification (e.g., Issue #401 not found classified as auth instead of not_found) - STATUS_CODE_PATTERN already handles HTTP-context-aware matching - Unify time-remaining calculation: compute diffMs once and pass to both getMessageKey() and translation interpolation to avoid boundary edge cases - Fix useEffect dependency: use getTime() instead of Date object reference to prevent interval churn when callers pass new GitHubErrorInfo each render * fix: restore status code classification via HTTP context-aware fallback - Add 'requires:' pattern to PERMISSION_PATTERNS for scope context matching - Modify classifyError to accept extracted status code as fallback - Extract status code before classification to enable fallback logic - Move status code fallback before network patterns to prioritize HTTP status (e.g., 'Network error: HTTP 401' now correctly classifies as auth) - Preserves protection against bare number false positives while still supporting HTTP-context-aware status code classification * fix: address LOW severity findings - accessibility and dead code - Add aria-label to compact mode container for screen reader accessibility (title attribute alone is not reliably announced by screen readers) - Simplify RATE_LIMIT_PATTERNS by removing unreachable patterns: - /rate\s*limit/i is a superset that matches all rate limit variations - Removed redundant: api rate limit exceeded, rate limit exceeded, abuse rate limit, secondary rate limit - Kept unique patterns: too many requests, 403.*rate * fix: address PR review findings - pattern precision and helper consistency MEDIUM fixes: - Add 'requires authentication' pattern to AUTH_PATTERNS to catch GitHub 401 response - Narrow permission pattern to match only known OAuth scope names (repo, admin, write, read, workflow, org, gist, notification, user, project, package, delete, discussion) to avoid misclassifying 'Requires authentication' as permission error LOW fixes: - Update STATUS_CODE_PATTERN comment to accurately describe ^ anchor matching behavior (matches status codes at string start for formats like '403 Forbidden') - Fix helper functions (isRateLimitError, isAuthError, isNetworkError, isRecoverableError, requiresSettingsAction) to extract and pass status code to classifyError for consistent classification with parseGitHubError * fix: address PR review findings - test coverage and edge cases - Remove duplicate 'gist' from PERMISSION_PATTERNS regex - Fix error display visibility during active search - Extract resetTimeMs for stable useEffect dependency - Add test coverage for parsedInfo shortcut paths in all 5 helper functions --------- Co-authored-by: StillKnotKnown <stillknotknown@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
498 lines
17 KiB
TypeScript
498 lines
17 KiB
TypeScript
/**
|
|
* GitHub API error parser utility.
|
|
* Parses raw error strings to classify GitHub API errors and extract metadata.
|
|
*/
|
|
|
|
import type { GitHubErrorType, GitHubErrorInfo } from '../types';
|
|
|
|
/**
|
|
* Maximum length for raw error messages stored in GitHubErrorInfo.
|
|
* Truncates to prevent memory bloat and UI issues.
|
|
*/
|
|
const MAX_RAW_ERROR_LENGTH = 500;
|
|
|
|
/**
|
|
* Patterns for rate limit errors (HTTP 403 with rate limit context).
|
|
* Note: Pattern 1 covers all "rate limit" variations (api rate limit exceeded,
|
|
* abuse rate limit, secondary rate limit, etc.) via substring matching.
|
|
*/
|
|
const RATE_LIMIT_PATTERNS = [
|
|
/rate\s*limit/i, // Covers all variations containing "rate limit"
|
|
/too\s*many\s*requests/i,
|
|
/403.*rate/i,
|
|
];
|
|
|
|
/**
|
|
* Patterns for authentication errors (HTTP 401)
|
|
* Note: Bare status codes are intentionally omitted here - STATUS_CODE_PATTERN
|
|
* handles HTTP-context-aware matching to avoid false positives.
|
|
*/
|
|
const AUTH_PATTERNS = [
|
|
/unauthorized/i,
|
|
/bad\s*credentials/i,
|
|
/authentication\s*failed/i,
|
|
/invalid\s*(oauth\s*)?token/i,
|
|
/token\s*(is\s*)?(invalid|expired|required)/i,
|
|
/not\s*authenticated/i,
|
|
/requires\s*authentication/i, // GitHub 401 response body
|
|
];
|
|
|
|
/**
|
|
* Patterns for permission/scope errors (HTTP 403 with scope context)
|
|
* Note: Bare status codes are intentionally omitted here - STATUS_CODE_PATTERN
|
|
* handles HTTP-context-aware matching to avoid false positives.
|
|
*/
|
|
const PERMISSION_PATTERNS = [
|
|
/forbidden/i,
|
|
/permission\s*denied/i,
|
|
/insufficient\s*(scope|permission)/i,
|
|
/access\s*denied/i,
|
|
/repository\s*access\s*denied/i,
|
|
/not\s*authorized\s*to\s*access/i,
|
|
/requires\s*(admin|write|read)\s*access/i,
|
|
/missing\s*required\s*scope/i,
|
|
// Matches "requires: repo" or "requires workflow" for OAuth scope context
|
|
// Uses specific scope names to avoid matching "requires authentication" (auth error)
|
|
/requires[:\s]+(?:repo|admin|write|read|workflow|org|gist|notification|user|project|package|delete|discussion)/i,
|
|
];
|
|
|
|
/**
|
|
* Patterns for not found errors (HTTP 404)
|
|
* Note: Bare status codes are intentionally omitted here - STATUS_CODE_PATTERN
|
|
* handles HTTP-context-aware matching to avoid false positives (e.g., "Issue #404").
|
|
*/
|
|
const NOT_FOUND_PATTERNS = [
|
|
/not\s*found/i,
|
|
/no\s*such\s*(repository|repo|issue|resource)/i,
|
|
/does\s*not\s*exist/i,
|
|
/repository\s*not\s*found/i,
|
|
/user\s*not\s*found/i,
|
|
];
|
|
|
|
/**
|
|
* Patterns for network/connectivity errors
|
|
*/
|
|
const NETWORK_PATTERNS = [
|
|
/network\s*(error|failed|unreachable)/i,
|
|
/failed\s*to\s*fetch/i,
|
|
/enetunreach/i,
|
|
/econnrefused/i,
|
|
/econnreset/i,
|
|
/etimedout/i,
|
|
/dns\s*(error|failed)/i,
|
|
/offline/i,
|
|
/no\s*internet/i,
|
|
/unable\s*to\s*connect/i,
|
|
/connection\s*(refused|reset|timeout|failed)/i,
|
|
];
|
|
|
|
/**
|
|
* Pattern to extract required OAuth scopes from error messages
|
|
* Matches formats like:
|
|
* - "requires: repo, read:org"
|
|
* - "missing scopes: repo, workflow"
|
|
* - "X-Accepted-OAuth-Scopes: repo"
|
|
* Stops at sentence boundaries or non-scope characters
|
|
*/
|
|
const REQUIRED_SCOPES_PATTERN = /(?:requires?[:\s]*|missing\s*scopes?[:\s]*|X-Accepted-OAuth-Scopes[:\s]*)([a-z0-9_:]+(?:[,\s]+[a-z0-9_:]+)*)/i;
|
|
|
|
/**
|
|
* Pattern to extract HTTP status code from error messages.
|
|
* Matches status codes preceded by HTTP context keywords or at string start
|
|
* (for common error formats like "403 Forbidden").
|
|
*/
|
|
const STATUS_CODE_PATTERN = /(?:^|HTTP\s*|status[:\s]*|error[:\s]*|code[:\s]*)\b([1-5]\d{2})\b/i;
|
|
|
|
/**
|
|
* Sanitize error output to a reasonable length.
|
|
* Prevents memory bloat and UI issues from very long error messages.
|
|
*/
|
|
function sanitizeRawError(error: string): string {
|
|
if (error.length > MAX_RAW_ERROR_LENGTH) {
|
|
return error.substring(0, MAX_RAW_ERROR_LENGTH) + '...';
|
|
}
|
|
return error;
|
|
}
|
|
|
|
/**
|
|
* Maximum reasonable reset duration in seconds (24 hours).
|
|
* Prevents malformed error strings from creating far-future dates.
|
|
*/
|
|
const MAX_RESET_SECONDS = 86400;
|
|
|
|
/**
|
|
* Extract rate limit reset time from error message.
|
|
* Parses various formats and returns a Date object if found.
|
|
* Handles both absolute timestamps and relative durations ("in X seconds").
|
|
*/
|
|
function extractRateLimitResetTime(error: string): Date | undefined {
|
|
// First, try to match relative duration pattern (e.g., "reset in 3600 seconds")
|
|
const relativePattern = /reset[s]?\s*in[:\s]*(\d+)\s*seconds?/i;
|
|
const relativeMatch = error.match(relativePattern);
|
|
if (relativeMatch) {
|
|
const seconds = parseInt(relativeMatch[1], 10);
|
|
// Validate: positive, non-NaN, and within reasonable bounds (24 hours max)
|
|
if (!Number.isNaN(seconds) && seconds > 0 && seconds <= MAX_RESET_SECONDS) {
|
|
return new Date(Date.now() + seconds * 1000);
|
|
}
|
|
}
|
|
|
|
// Then try absolute timestamp pattern
|
|
const absolutePattern = /(?:reset[s]?\s*at[:\s]*|X-RateLimit-Reset[:\s]*)(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z?|\d+)/i;
|
|
const match = error.match(absolutePattern);
|
|
if (!match) {
|
|
return undefined;
|
|
}
|
|
|
|
const resetValue = match[1].trim();
|
|
|
|
// Check if it's an ISO date string
|
|
if (resetValue.includes('-') && resetValue.includes('T')) {
|
|
const date = new Date(resetValue);
|
|
if (Number.isNaN(date.getTime())) return undefined;
|
|
// Validate: within reasonable bounds (24 hours max from now)
|
|
if (date.getTime() - Date.now() > MAX_RESET_SECONDS * 1000) return undefined;
|
|
return date;
|
|
}
|
|
|
|
// Check if it's a Unix timestamp (seconds or milliseconds)
|
|
const numericValue = parseInt(resetValue, 10);
|
|
if (!Number.isNaN(numericValue)) {
|
|
// GitHub API uses seconds, JavaScript uses milliseconds
|
|
// Values > 1e12 are likely milliseconds already
|
|
const timestamp = numericValue > 1e12 ? numericValue : numericValue * 1000;
|
|
const date = new Date(timestamp);
|
|
if (Number.isNaN(date.getTime())) return undefined;
|
|
// Validate: within reasonable bounds (24 hours max from now)
|
|
if (date.getTime() - Date.now() > MAX_RESET_SECONDS * 1000) return undefined;
|
|
return date;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Extract required OAuth scopes from error message.
|
|
* Returns an array of scope strings if found.
|
|
*/
|
|
function extractRequiredScopes(error: string): string[] | undefined {
|
|
const match = error.match(REQUIRED_SCOPES_PATTERN);
|
|
if (!match) {
|
|
return undefined;
|
|
}
|
|
|
|
const scopes = match[1]
|
|
.split(/[,\s]+/)
|
|
.map(s => s.trim())
|
|
.filter(s => s.length > 0);
|
|
|
|
return scopes.length > 0 ? scopes : undefined;
|
|
}
|
|
|
|
/**
|
|
* Extract HTTP status code from error message.
|
|
*/
|
|
function extractStatusCode(error: string): number | undefined {
|
|
const match = error.match(STATUS_CODE_PATTERN);
|
|
if (!match) {
|
|
return undefined;
|
|
}
|
|
|
|
const code = parseInt(match[1], 10);
|
|
// Only return valid HTTP status codes
|
|
if (code >= 100 && code < 600) {
|
|
return code;
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Check if the error matches any of the given patterns.
|
|
*/
|
|
function matchesPatterns(error: string, patterns: RegExp[]): boolean {
|
|
return patterns.some(pattern => pattern.test(error));
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for rate limit errors.
|
|
*/
|
|
function getRateLimitMessage(_error: string, resetTime?: Date): string {
|
|
if (resetTime) {
|
|
const now = new Date();
|
|
const diffMs = resetTime.getTime() - now.getTime();
|
|
|
|
if (diffMs > 0) {
|
|
const diffMins = Math.ceil(diffMs / 60000);
|
|
if (diffMins < 60) {
|
|
return `GitHub API rate limit reached. Please wait ${diffMins} minute${diffMins !== 1 ? 's' : ''} before trying again.`;
|
|
}
|
|
const diffHours = Math.ceil(diffMins / 60);
|
|
return `GitHub API rate limit reached. Rate limit resets in approximately ${diffHours} hour${diffHours !== 1 ? 's' : ''}.`;
|
|
}
|
|
}
|
|
|
|
return 'GitHub API rate limit reached. Please wait a moment before trying again.';
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for authentication errors.
|
|
*/
|
|
function getAuthMessage(): string {
|
|
return 'GitHub authentication failed. Please check your GitHub token in Settings and try again.';
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for permission errors.
|
|
*/
|
|
function getPermissionMessage(scopes?: string[]): string {
|
|
if (scopes && scopes.length > 0) {
|
|
return `GitHub permission denied. Your token is missing required scopes: ${scopes.join(', ')}. Please update your GitHub token in Settings.`;
|
|
}
|
|
return 'GitHub permission denied. Your token may not have the required access. Please check your token permissions in Settings.';
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for not found errors.
|
|
*/
|
|
function getNotFoundMessage(): string {
|
|
return 'The requested GitHub resource was not found. Please verify the repository exists and you have access to it.';
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for network errors.
|
|
*/
|
|
function getNetworkMessage(): string {
|
|
return 'Unable to connect to GitHub. Please check your internet connection and try again.';
|
|
}
|
|
|
|
/**
|
|
* Get a user-friendly message for unknown errors.
|
|
*/
|
|
function getUnknownMessage(): string {
|
|
return 'An unexpected error occurred while communicating with GitHub. Please try again.';
|
|
}
|
|
|
|
/**
|
|
* Classify error type based on pattern matching and optional status code.
|
|
* Priority: rate_limit > auth > permission > not_found > network > unknown
|
|
* Note: Permission checks run before not_found to properly classify 403 responses.
|
|
* Status code fallback takes priority over network patterns since HTTP status
|
|
* codes are more specific than generic network error text.
|
|
* @param error - The error string to classify
|
|
* @param statusCode - Optional HTTP status code extracted with context (helps classify when text patterns don't match)
|
|
*/
|
|
function classifyError(error: string, statusCode?: number): GitHubErrorType {
|
|
// Check rate limit first (403 can also be permission, but rate limit is more specific)
|
|
if (matchesPatterns(error, RATE_LIMIT_PATTERNS)) {
|
|
return 'rate_limit';
|
|
}
|
|
|
|
// Check auth (401 is always auth)
|
|
if (matchesPatterns(error, AUTH_PATTERNS)) {
|
|
return 'auth';
|
|
}
|
|
|
|
// Check permission (403 without rate limit context) before not_found
|
|
// to properly classify 403 responses that might contain "not found" text
|
|
if (matchesPatterns(error, PERMISSION_PATTERNS)) {
|
|
return 'permission';
|
|
}
|
|
|
|
// Check not found (404 is always not_found)
|
|
if (matchesPatterns(error, NOT_FOUND_PATTERNS)) {
|
|
return 'not_found';
|
|
}
|
|
|
|
// Use status code fallback BEFORE network patterns
|
|
// HTTP status codes are more specific than generic network error text
|
|
if (statusCode === 401) return 'auth';
|
|
if (statusCode === 403) return 'permission';
|
|
if (statusCode === 404) return 'not_found';
|
|
|
|
// Check network errors (only if no status code fallback matched)
|
|
if (matchesPatterns(error, NETWORK_PATTERNS)) {
|
|
return 'network';
|
|
}
|
|
|
|
return 'unknown';
|
|
}
|
|
|
|
/**
|
|
* Parse a GitHub API error string and return classified error information.
|
|
*
|
|
* IMPORTANT: The returned `message` field contains hardcoded English strings
|
|
* intended ONLY as a fallback defaultValue for i18n translation. Consumers
|
|
* should use the `type` field to look up the appropriate translation key
|
|
* (e.g., 'githubErrors.rateLimitMessage') via react-i18next rather than
|
|
* displaying `message` directly. This ensures proper localization.
|
|
*
|
|
* Translation key mapping by type:
|
|
* - rate_limit → 'githubErrors.rateLimitMessage' (or rateLimitMessageMinutes/Hours)
|
|
* - auth → 'githubErrors.authMessage'
|
|
* - permission → 'githubErrors.permissionMessage' (or permissionMessageScopes)
|
|
* - not_found → 'githubErrors.notFoundMessage'
|
|
* - network → 'githubErrors.networkMessage'
|
|
* - unknown → 'githubErrors.unknownMessage'
|
|
*
|
|
* @param error - The raw error string (typically from issues-store error state)
|
|
* @returns GitHubErrorInfo object with classified type, user-friendly message, and metadata
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const errorInfo = parseGitHubError('GitHub API error: 403 - API rate limit exceeded');
|
|
* // Use type to get i18n key, message only as fallback:
|
|
* // t(`githubErrors.${errorInfo.type}Message`, { defaultValue: errorInfo.message })
|
|
* ```
|
|
*/
|
|
export function parseGitHubError(error: string | null | undefined): GitHubErrorInfo {
|
|
// Handle null/undefined/empty errors
|
|
if (!error || typeof error !== 'string' || error.trim() === '') {
|
|
return {
|
|
type: 'unknown',
|
|
message: getUnknownMessage(),
|
|
};
|
|
}
|
|
|
|
const trimmedError = error.trim();
|
|
// Extract status code first so we can use it for classification fallback
|
|
const statusCode = extractStatusCode(trimmedError);
|
|
const errorType = classifyError(trimmedError, statusCode);
|
|
|
|
switch (errorType) {
|
|
case 'rate_limit': {
|
|
const resetTime = extractRateLimitResetTime(trimmedError);
|
|
return {
|
|
type: 'rate_limit',
|
|
message: getRateLimitMessage(trimmedError, resetTime),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
rateLimitResetTime: resetTime,
|
|
statusCode: statusCode ?? 403,
|
|
};
|
|
}
|
|
|
|
case 'auth':
|
|
return {
|
|
type: 'auth',
|
|
message: getAuthMessage(),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
statusCode: statusCode ?? 401,
|
|
};
|
|
|
|
case 'permission': {
|
|
const scopes = extractRequiredScopes(trimmedError);
|
|
return {
|
|
type: 'permission',
|
|
message: getPermissionMessage(scopes),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
requiredScopes: scopes,
|
|
statusCode: statusCode ?? 403,
|
|
};
|
|
}
|
|
|
|
case 'not_found':
|
|
return {
|
|
type: 'not_found',
|
|
message: getNotFoundMessage(),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
statusCode: statusCode ?? 404,
|
|
};
|
|
|
|
case 'network':
|
|
return {
|
|
type: 'network',
|
|
message: getNetworkMessage(),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
};
|
|
|
|
default:
|
|
return {
|
|
type: 'unknown',
|
|
message: getUnknownMessage(),
|
|
rawMessage: sanitizeRawError(trimmedError),
|
|
statusCode,
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if an error is a rate limit error.
|
|
* Convenience function for quick checks without full parsing.
|
|
* @param error - Raw error string or null/undefined
|
|
* @param parsedInfo - Optional pre-parsed GitHubErrorInfo to avoid re-classification
|
|
*/
|
|
export function isRateLimitError(
|
|
error: string | null | undefined,
|
|
parsedInfo?: GitHubErrorInfo | null
|
|
): boolean {
|
|
if (parsedInfo) return parsedInfo.type === 'rate_limit';
|
|
if (!error) return false;
|
|
const trimmed = error.trim();
|
|
return classifyError(trimmed, extractStatusCode(trimmed)) === 'rate_limit';
|
|
}
|
|
|
|
/**
|
|
* Check if an error is an authentication error.
|
|
* Convenience function for quick checks without full parsing.
|
|
* @param error - Raw error string or null/undefined
|
|
* @param parsedInfo - Optional pre-parsed GitHubErrorInfo to avoid re-classification
|
|
*/
|
|
export function isAuthError(
|
|
error: string | null | undefined,
|
|
parsedInfo?: GitHubErrorInfo | null
|
|
): boolean {
|
|
if (parsedInfo) return parsedInfo.type === 'auth';
|
|
if (!error) return false;
|
|
const trimmed = error.trim();
|
|
return classifyError(trimmed, extractStatusCode(trimmed)) === 'auth';
|
|
}
|
|
|
|
/**
|
|
* Check if an error is a network error.
|
|
* Convenience function for quick checks without full parsing.
|
|
* @param error - Raw error string or null/undefined
|
|
* @param parsedInfo - Optional pre-parsed GitHubErrorInfo to avoid re-classification
|
|
*/
|
|
export function isNetworkError(
|
|
error: string | null | undefined,
|
|
parsedInfo?: GitHubErrorInfo | null
|
|
): boolean {
|
|
if (parsedInfo) return parsedInfo.type === 'network';
|
|
if (!error) return false;
|
|
const trimmed = error.trim();
|
|
return classifyError(trimmed, extractStatusCode(trimmed)) === 'network';
|
|
}
|
|
|
|
/**
|
|
* Check if an error is recoverable (user can retry).
|
|
* Rate limit, network, and unknown errors are considered recoverable.
|
|
* @param error - Raw error string or null/undefined
|
|
* @param parsedInfo - Optional pre-parsed GitHubErrorInfo to avoid re-classification
|
|
*/
|
|
export function isRecoverableError(
|
|
error: string | null | undefined,
|
|
parsedInfo?: GitHubErrorInfo | null
|
|
): boolean {
|
|
if (parsedInfo) return ['rate_limit', 'network', 'unknown'].includes(parsedInfo.type);
|
|
if (!error) return false;
|
|
const trimmed = error.trim();
|
|
const errorType = classifyError(trimmed, extractStatusCode(trimmed));
|
|
return ['rate_limit', 'network', 'unknown'].includes(errorType);
|
|
}
|
|
|
|
/**
|
|
* Check if an error requires user action in settings.
|
|
* Auth and permission errors require settings changes.
|
|
* @param error - Raw error string or null/undefined
|
|
* @param parsedInfo - Optional pre-parsed GitHubErrorInfo to avoid re-classification
|
|
*/
|
|
export function requiresSettingsAction(
|
|
error: string | null | undefined,
|
|
parsedInfo?: GitHubErrorInfo | null
|
|
): boolean {
|
|
if (parsedInfo) return ['auth', 'permission'].includes(parsedInfo.type);
|
|
if (!error) return false;
|
|
const trimmed = error.trim();
|
|
const errorType = classifyError(trimmed, extractStatusCode(trimmed));
|
|
return ['auth', 'permission'].includes(errorType);
|
|
}
|