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>
299 lines
6.3 KiB
Markdown
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.
|