rdev/docs/api/errors.md
jordan 72d16929ca feat: Implement hexagonal architecture with services, webhooks, queue, and telemetry
Major refactoring to hexagonal (ports & adapters) architecture:

- Add service layer (apikey_service, project_service) for business logic
- Add webhook system with dispatcher and delivery tracking
- Add command queue with priority-based processing
- Add rate limiting with sliding window algorithm
- Add audit logging for command execution
- Add OpenTelemetry integration (traces, metrics, spans)
- Add circuit breaker for fault tolerance
- Add cached repository wrapper for performance
- Add comprehensive validation package
- Add Kubernetes client integration for pod management
- Add database migrations (allowed_ips, audit_log, rate_limiting, queue, webhooks)
- Add network policy and PodDisruptionBudget for k8s
- Remove legacy executor and projects/registry packages
- Untrack secrets.yaml (now managed via envault)
- Add coverage.out to .gitignore
- Add e2e test infrastructure with docker-compose
- Add comprehensive documentation (API, architecture, operations, plans)
- Add golangci-lint config and pre-commit hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 19:57:46 -07:00

6.3 KiB

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:

{
  "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

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

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

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

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

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

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

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

curl http://localhost:8080/projects

# Response: 401
{
  "error": {
    "code": "UNAUTHORIZED",
    "message": "Missing API key"
  }
}

Fix: Add the X-API-Key header.

Invalid Command

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

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

# After too many requests
# Response: 429
{
  "error": {
    "code": "TOO_MANY_REQUESTS",
    "message": "Rate limit exceeded"
  }
}

Fix: Wait for X-RateLimit-Reset timestamp, then retry.