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

299 lines
6.3 KiB
Markdown

# 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.