Skip to content

Add error handling for network timeouts #10

@BekahHW

Description

@BekahHW

Description

RepoReady currently lacks robust error handling for network-related issues like timeouts, connection errors, and intermittent API failures. Users may experience cryptic error messages or hanging operations when network conditions are poor.

Current State

  • ❌ No explicit timeout handling for GitHub API calls
  • ❌ No retry logic for temporary failures
  • ❌ Generic error messages for network issues
  • ❌ No graceful degradation for slow connections
  • ✅ Basic error handling exists in src/utils/github.ts
  • ✅ Octokit handles some network errors automatically

Acceptance Criteria

Timeout Handling

  • Set reasonable timeout values for all API calls (30-60 seconds)
  • Implement custom timeout logic where needed
  • Provide clear timeout error messages to users
  • Allow timeout configuration via environment variables

Retry Logic

  • Implement exponential backoff for retryable errors
  • Retry on specific HTTP status codes (500, 502, 503, 504)
  • Limit retry attempts (3-5 attempts max)
  • Skip retries for non-retryable errors (404, 401, 403)

User Experience

  • Show progress indicators for long operations
  • Provide helpful error messages with next steps
  • Suggest solutions for common network issues
  • Gracefully handle partial failures in batch operations

Implementation Suggestions

Enhanced GitHubService with Timeout Support

// src/utils/github.ts - Enhanced version
import { Octokit } from '@octokit/rest';
import ora from 'ora';

export class GitHubService {
  private octokit: Octokit;
  private timeout: number;
  private maxRetries: number;
  private retryDelay: number;

  constructor(token?: string, options: {
    timeout?: number;
    maxRetries?: number;
    retryDelay?: number;
  } = {}) {
    this.timeout = options.timeout ?? parseInt(process.env.GITHUB_TIMEOUT ?? '30000'); // 30s default
    this.maxRetries = options.maxRetries ?? parseInt(process.env.GITHUB_MAX_RETRIES ?? '3');
    this.retryDelay = options.retryDelay ?? 1000; // 1s base delay
    
    this.octokit = new Octokit({
      auth: token || process.env.GITHUB_TOKEN,
      request: {
        timeout: this.timeout,
        retries: 0 // We'll handle retries ourselves
      }
    });
  }

  private async withRetry<T>(
    operation: () => Promise<T>,
    context: string
  ): Promise<T> {
    let lastError: Error;
    
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      try {
        return await Promise.race([
          operation(),
          this.createTimeoutPromise(context)
        ]);
      } catch (error) {
        lastError = error as Error;
        
        if (!this.isRetryableError(error) || attempt === this.maxRetries) {
          throw this.enhanceError(error, context, attempt);
        }
        
        const delay = this.calculateBackoffDelay(attempt);
        console.warn(`⚠️  ${context} failed (attempt ${attempt}/${this.maxRetries}), retrying in ${delay}ms...`);
        await this.sleep(delay);
      }
    }
    
    throw this.enhanceError(lastError!, context, this.maxRetries);
  }

  private createTimeoutPromise<T>(context: string): Promise<T> {
    return new Promise((_, reject) => {
      setTimeout(() => {
        reject(new Error(`Operation timed out after ${this.timeout}ms: ${context}`));
      }, this.timeout);
    });
  }

  private isRetryableError(error: any): boolean {
    // Retry on network errors and server errors
    if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') return true;
    if (error.status >= 500 && error.status < 600) return true;
    if (error.status === 429) return true; // Rate limit
    
    // Don't retry client errors
    if (error.status >= 400 && error.status < 500) return false;
    
    return true;
  }

  private calculateBackoffDelay(attempt: number): number {
    // Exponential backoff: 1s, 2s, 4s, 8s...
    return this.retryDelay * Math.pow(2, attempt - 1) + Math.random() * 1000;
  }

  private enhanceError(error: any, context: string, attempts: number): Error {
    const baseMessage = `Failed to ${context}`;
    
    if (error.message?.includes('timeout')) {
      return new Error(
        `${baseMessage}: Request timed out after ${this.timeout}ms. ` +
        'Try increasing the timeout with GITHUB_TIMEOUT environment variable or check your internet connection.'
      );
    }
    
    if (error.status === 403 && error.message?.includes('rate limit')) {
      return new Error(
        `${baseMessage}: GitHub API rate limit exceeded. ` +
        'Use a personal access token with --token flag for higher limits.'
      );
    }
    
    if (error.code === 'ECONNRESET' || error.code === 'ETIMEDOUT') {
      return new Error(
        `${baseMessage}: Network connection error after ${attempts} attempts. ` +
        'Please check your internet connection and try again.'
      );
    }
    
    if (error.status === 404) {
      return new Error(
        `${baseMessage}: Repository not found. Please check the repository name and your access permissions.`
      );
    }
    
    return new Error(`${baseMessage}: ${error.message} (after ${attempts} attempts)`);
  }

  private sleep(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }

  async getRepositoryInfo(owner: string, repo: string): Promise<RepositoryInfo> {
    return this.withRetry(async () => {
      const spinner = ora('Fetching repository information...').start();
      
      try {
        // Your existing implementation, but wrapped in retry logic
        const repoData = await this.octokit.rest.repos.get({ owner, repo });
        // ... rest of the implementation
        
        spinner.succeed('Repository information fetched successfully');
        return repositoryInfo;
      } catch (error) {
        spinner.fail('Failed to fetch repository information');
        throw error;
      }
    }, `fetch repository information for ${owner}/${repo}`);
  }
}

Environment Variable Configuration

# .env.example
GITHUB_TOKEN=your_token_here
GITHUB_TIMEOUT=30000          # 30 seconds
GITHUB_MAX_RETRIES=3          # 3 retry attempts
GITHUB_RETRY_DELAY=1000       # 1 second base delay

Enhanced CLI Error Messages

// src/commands/evaluate.ts - Enhanced error handling
catch (error) {
  spinner.fail('Evaluation failed');
  
  if (error instanceof Error) {
    // Network/timeout specific handling
    if (error.message.includes('timeout')) {
      console.error('\n🕒 Request Timeout');
      console.error('The request took too long to complete. This might be due to:');
      console.error('• Slow internet connection');
      console.error('• GitHub API being slow');
      console.error('• Large repository with many files to check');
      console.error('\n💡 Try:');
      console.error('• Check your internet connection');
      console.error('• Use a GitHub token for better performance: --token YOUR_TOKEN');
      console.error('• Increase timeout: GITHUB_TIMEOUT=60000 rr evaluate ...');
    } else if (error.message.includes('rate limit')) {
      console.error('\n⏱️  Rate Limit Exceeded');
      console.error('GitHub API rate limit has been exceeded.');
      console.error('\n💡 Try:');
      console.error('• Use a personal access token: --token YOUR_TOKEN');
      console.error('• Wait an hour for the rate limit to reset');
      console.error('• Create a token at: https://github.com/settings/tokens');
    } else if (error.message.includes('Network connection error')) {
      console.error('\n🌐 Network Connection Error');
      console.error('Unable to connect to GitHub after multiple attempts.');
      console.error('\n💡 Try:');
      console.error('• Check your internet connection');
      console.error('• Verify GitHub.com is accessible');
      console.error('• Try again in a few minutes');
    }
    
    console.error(`\n❌ Error: ${error.message}`);
  }
  
  process.exit(1);
}

Files to Modify

  • src/utils/github.ts - Add timeout and retry logic
  • src/commands/evaluate.ts - Enhanced error messages
  • src/commands/create.ts - Enhanced error messages
  • README.md - Document timeout configuration
  • package.json - Add timeout-related dependencies if needed

Configuration Options

Environment Variables

  • GITHUB_TIMEOUT - Request timeout in milliseconds (default: 30000)
  • GITHUB_MAX_RETRIES - Maximum retry attempts (default: 3)
  • GITHUB_RETRY_DELAY - Base delay between retries in milliseconds (default: 1000)

CLI Options (Optional)

rr evaluate owner/repo --timeout 60000 --max-retries 5

Benefits

  • 🔄 More reliable operation in poor network conditions
  • ⏱️ Clear timeout behavior instead of hanging
  • 🔁 Automatic recovery from temporary failures
  • 📝 Better user experience with helpful error messages
  • ⚙️ Configurable for different environments
  • 🎯 Distinguishes between temporary and permanent errors

Testing Considerations

  • Test with simulated network delays
  • Test with intermittent connection failures
  • Test timeout behavior
  • Test retry logic with different error codes
  • Mock slow API responses

Resources

Estimated Effort

Medium - Requires understanding async patterns and error handling strategies.


Great for contributors who want to improve reliability and user experience! 🔄

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions