# Error Handling Guide This guide covers error responses from the rdev API and how to handle them. ## Error Response Format All errors follow this format: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable error description" }, "meta": { "request_id": "req-abc123", "timestamp": "2024-01-15T10:30:00Z" } } ``` ## Error Codes ### Authentication Errors (4xx) | Code | HTTP Status | Description | Resolution | |------|-------------|-------------|------------| | `UNAUTHORIZED` | 401 | Missing or invalid API key | Check API key header | | `KEY_REVOKED` | 401 | API key has been revoked | Request new key | | `KEY_EXPIRED` | 401 | API key has expired | Request new key | | `FORBIDDEN` | 403 | Insufficient permissions | Use key with required scope | | `IP_NOT_ALLOWED` | 403 | IP not in allowlist | Use allowed IP or update key | ### Resource Errors (4xx) | Code | HTTP Status | Description | Resolution | |------|-------------|-------------|------------| | `BAD_REQUEST` | 400 | Invalid request body | Check request format | | `NOT_FOUND` | 404 | Resource not found | Verify resource ID | | `TOO_MANY_REQUESTS` | 429 | Rate limit exceeded | Wait and retry | ### Server Errors (5xx) | Code | HTTP Status | Description | Resolution | |------|-------------|-------------|------------| | `INTERNAL_ERROR` | 500 | Server error | Retry later, contact support | | `SERVICE_UNAVAILABLE` | 503 | Service not ready | Wait for service to be ready | ## Handling Errors by Type ### Authentication Errors ```javascript async function handleAuthError(response) { const { error } = await response.json(); switch (error.code) { case 'UNAUTHORIZED': // Key is missing or invalid throw new Error('Invalid API key. Check your configuration.'); case 'KEY_REVOKED': // Key was revoked by admin throw new Error('API key was revoked. Request a new key.'); case 'KEY_EXPIRED': // Key has expired throw new Error('API key expired. Request a new key.'); case 'FORBIDDEN': // Key lacks required scope throw new Error(`Insufficient permissions: ${error.message}`); case 'IP_NOT_ALLOWED': // IP not in allowlist throw new Error('Your IP is not allowed for this API key.'); default: throw new Error(error.message); } } ``` ### Rate Limiting ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = response.headers.get('X-RateLimit-Reset'); const waitMs = retryAfter ? (parseInt(retryAfter) * 1000) - Date.now() : 1000 * Math.pow(2, i); // Exponential backoff console.log(`Rate limited. Waiting ${waitMs}ms...`); await new Promise(resolve => setTimeout(resolve, waitMs)); continue; } return response; } throw new Error('Max retries exceeded'); } ``` ### Validation Errors ```javascript async function handleValidationError(response) { const { error } = await response.json(); // Error message contains field-specific info // e.g., "prompt: is required" // e.g., "command: contains dangerous characters" const match = error.message.match(/^(\w+): (.+)$/); if (match) { const [, field, message] = match; return { field, message, }; } return { message: error.message }; } ``` ## Error Handling Best Practices ### 1. Always Check Status Code ```javascript const response = await fetch(url, options); if (!response.ok) { const error = await response.json(); throw new APIError(response.status, error.error); } return response.json(); ``` ### 2. Use Custom Error Class ```javascript class APIError extends Error { constructor(status, error) { super(error.message); this.name = 'APIError'; this.status = status; this.code = error.code; } isRetryable() { return this.status >= 500 || this.status === 429; } isAuthError() { return this.status === 401 || this.status === 403; } } ``` ### 3. Log Request IDs ```javascript async function logError(response, error) { const requestId = error.meta?.request_id; console.error(`Request ${requestId} failed:`, error.error); // Include in bug reports return { requestId, error: error.error, url: response.url, timestamp: new Date().toISOString(), }; } ``` ### 4. Implement Circuit Breaker ```javascript class CircuitBreaker { constructor(threshold = 5, timeout = 60000) { this.failures = 0; this.threshold = threshold; this.timeout = timeout; this.lastFailure = null; } async execute(fn) { if (this.isOpen()) { throw new Error('Circuit breaker is open'); } try { const result = await fn(); this.reset(); return result; } catch (error) { this.recordFailure(); throw error; } } isOpen() { if (this.failures < this.threshold) return false; if (Date.now() - this.lastFailure > this.timeout) { this.reset(); return false; } return true; } recordFailure() { this.failures++; this.lastFailure = Date.now(); } reset() { this.failures = 0; this.lastFailure = null; } } ``` ## Common Error Scenarios ### Missing API Key ```bash curl http://localhost:8080/projects # Response: 401 { "error": { "code": "UNAUTHORIZED", "message": "Missing API key" } } ``` **Fix**: Add the `X-API-Key` header. ### Invalid Command ```bash curl -X POST http://localhost:8080/projects/test/shell \ -H "X-API-Key: rdev_xxx" \ -d '{"command": "rm -rf /"}' # Response: 400 { "error": { "code": "BAD_REQUEST", "message": "destructive rm command not allowed" } } ``` **Fix**: Use safe commands. See security documentation for allowed patterns. ### Project Not Found ```bash curl http://localhost:8080/projects/nonexistent \ -H "X-API-Key: rdev_xxx" # Response: 404 { "error": { "code": "NOT_FOUND", "message": "project not found: nonexistent" } } ``` **Fix**: Check project ID. List projects to see available ones. ### Rate Limited ```bash # After too many requests # Response: 429 { "error": { "code": "TOO_MANY_REQUESTS", "message": "Rate limit exceeded" } } ``` **Fix**: Wait for `X-RateLimit-Reset` timestamp, then retry.