Error Handling
This guide explains the error response format, common error codes, and strategies for handling errors in your integration.
Error Response Format
All error responses follow a consistent structure:
{
"error": {
"code": "invalid_api_key",
"message": "Invalid API key",
"details": {},
"retry_after": 45,
"request_id": "req_abc123"
}
}
| Field | Type | Description |
|---|---|---|
code | string | Machine-readable error code for programmatic handling |
message | string | Human-readable description |
details | object | Additional context (varies by error) |
retry_after | number | Seconds to wait before retrying (for 429 errors) |
request_id | string | Unique ID for support debugging |
Error Codes Reference
Authentication Errors (401)
| Code | Message | Cause | Solution |
|---|---|---|---|
missing_auth | Authorization header required | No Authorization header | Add Authorization: Bearer <key> header |
invalid_auth_format | Expected 'Bearer <api_key>' | Wrong header format | Use Bearer <key>, not just the key |
invalid_api_key | Invalid API key | Key doesn't exist or is incorrect | Check for typos, verify key |
revoked_api_key | API key has been revoked | Key was revoked in portal | Create a new key |
Example handling:
- JavaScript
- Python
- Go
- cURL
async function makeRequest(endpoint, data) {
const response = await fetch(`https://api.cmfy.cloud${endpoint}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
if (response.status === 401) {
const { error } = await response.json();
switch (error.code) {
case 'missing_auth':
case 'invalid_auth_format':
throw new Error('API key not properly configured');
case 'invalid_api_key':
throw new Error('Check your API key is correct');
case 'revoked_api_key':
throw new Error('Create a new API key in the portal');
}
}
return response.json();
}
import requests
def make_request(endpoint, data):
response = requests.post(
f"https://api.cmfy.cloud{endpoint}",
headers={
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json"
},
json=data
)
if response.status_code == 401:
error = response.json()["error"]
if error["code"] in ("missing_auth", "invalid_auth_format"):
raise Exception("API key not properly configured")
elif error["code"] == "invalid_api_key":
raise Exception("Check your API key is correct")
elif error["code"] == "revoked_api_key":
raise Exception("Create a new API key in the portal")
return response.json()
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
)
func makeRequest(endpoint string, data interface{}) (map[string]interface{}, error) {
body, _ := json.Marshal(data)
req, _ := http.NewRequest("POST", "https://api.cmfy.cloud"+endpoint, bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 401 {
var errResp struct {
Error struct {
Code string `json:"code"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
switch errResp.Error.Code {
case "missing_auth", "invalid_auth_format":
return nil, errors.New("API key not properly configured")
case "invalid_api_key":
return nil, errors.New("check your API key is correct")
case "revoked_api_key":
return nil, errors.New("create a new API key in the portal")
}
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
# Example: Check for 401 errors
response=$(curl -s -w "\n%{http_code}" -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": {...}}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "401" ]; then
echo "Authentication error: $body"
exit 1
fi
Authorization Errors (403)
| Code | Message | Cause | Solution |
|---|---|---|---|
account_suspended | Account suspended - contact support | Account issue | Contact support@cmfy.cloud |
insufficient_permissions | Insufficient permissions | Action not allowed for your tier | Upgrade your plan |
Validation Errors (400)
| Code | Message | Cause |
|---|---|---|
invalid_json | Request body is not valid JSON | Malformed JSON |
missing_prompt | Required 'prompt' field missing | No prompt in request body |
invalid_prompt_type | 'prompt' must be an object | prompt is not a JSON object |
empty_workflow | Workflow has no nodes | prompt object is empty |
too_many_nodes | Workflow exceeds 500 nodes | Too many nodes in workflow |
missing_class_type | Node missing 'class_type' | Node lacks required field |
missing_inputs | Node missing 'inputs' | Node lacks required field |
invalid_url | URL in workflow is invalid | Malformed URL |
insecure_url | Only HTTPS URLs are allowed | HTTP URL in workflow |
webhook_must_be_https | Webhook URL must use HTTPS | HTTP webhook URL |
webhook_private_host | Webhook cannot use private IP | localhost or private IP |
Example handling:
- Python
- JavaScript
- Go
- cURL
import requests
import logging
logger = logging.getLogger(__name__)
def submit_job(workflow, webhook_url=None):
response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'prompt': workflow, 'webhook_url': webhook_url}
)
if response.status_code == 400:
error = response.json()['error']
# Log detailed error for debugging
logger.error(f"Validation error: {error['code']} - {error['message']}")
# Handle specific cases
if error['code'] == 'missing_prompt':
raise ValueError("Workflow cannot be empty")
elif error['code'] == 'too_many_nodes':
raise ValueError("Workflow too complex, split into smaller jobs")
else:
raise ValueError(f"Invalid request: {error['message']}")
response.raise_for_status()
return response.json()
async function submitJob(workflow, webhookUrl = null) {
const response = await fetch('https://api.cmfy.cloud/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
prompt: workflow,
webhook_url: webhookUrl
})
});
if (response.status === 400) {
const { error } = await response.json();
// Log detailed error for debugging
console.error(`Validation error: ${error.code} - ${error.message}`);
// Handle specific cases
switch (error.code) {
case 'missing_prompt':
throw new Error('Workflow cannot be empty');
case 'too_many_nodes':
throw new Error('Workflow too complex, split into smaller jobs');
default:
throw new Error(`Invalid request: ${error.message}`);
}
}
return response.json();
}
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
)
func submitJob(workflow map[string]interface{}, webhookURL string) (map[string]interface{}, error) {
payload := map[string]interface{}{
"prompt": workflow,
"webhook_url": webhookURL,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://api.cmfy.cloud/v1/jobs", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 400 {
var errResp struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
// Log detailed error for debugging
log.Printf("Validation error: %s - %s", errResp.Error.Code, errResp.Error.Message)
// Handle specific cases
switch errResp.Error.Code {
case "missing_prompt":
return nil, errors.New("workflow cannot be empty")
case "too_many_nodes":
return nil, errors.New("workflow too complex, split into smaller jobs")
default:
return nil, fmt.Errorf("invalid request: %s", errResp.Error.Message)
}
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
# Submit a job and check for validation errors
response=$(curl -s -w "\n%{http_code}" -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": {...}}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "400" ]; then
echo "Validation error: $body"
# Extract error code with jq
error_code=$(echo "$body" | jq -r '.error.code')
echo "Error code: $error_code"
exit 1
fi
Rate Limit Errors (429)
| Code | Message | Cause |
|---|---|---|
rate_limit_exceeded | Rate limit exceeded | Too many requests per minute |
concurrent_limit_reached | Maximum concurrent jobs reached | Too many running jobs |
queue_full | Queue limit reached | Personal queue is full |
Rate limit responses include helpful headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1699574400
Example handling with retry:
- JavaScript
- Python
- Go
- cURL
async function submitWithRetry(workflow, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch('https://api.cmfy.cloud/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt: workflow })
});
if (response.status !== 429) {
return response.json();
}
const { error } = await response.json();
// Handle different 429 types
if (error.code === 'rate_limit_exceeded') {
// Wait for rate limit window to reset
const retryAfter = error.retry_after || 60;
console.log(`Rate limited, waiting ${retryAfter}s`);
await sleep(retryAfter * 1000);
} else if (error.code === 'concurrent_limit_reached') {
// Wait for a running job to complete
console.log('At concurrent limit, waiting for job to finish');
await sleep(5000);
} else if (error.code === 'queue_full') {
// Queue is full, wait longer
console.log('Queue full, waiting for jobs to process');
await sleep(10000);
}
}
throw new Error('Max retries exceeded');
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
import time
import requests
def submit_with_retry(workflow, max_retries=3):
for attempt in range(max_retries):
response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'prompt': workflow}
)
if response.status_code != 429:
return response.json()
error = response.json()['error']
# Handle different 429 types
if error['code'] == 'rate_limit_exceeded':
# Wait for rate limit window to reset
retry_after = error.get('retry_after', 60)
print(f"Rate limited, waiting {retry_after}s")
time.sleep(retry_after)
elif error['code'] == 'concurrent_limit_reached':
# Wait for a running job to complete
print("At concurrent limit, waiting for job to finish")
time.sleep(5)
elif error['code'] == 'queue_full':
# Queue is full, wait longer
print("Queue full, waiting for jobs to process")
time.sleep(10)
raise Exception("Max retries exceeded")
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
func submitWithRetry(workflow map[string]interface{}, maxRetries int) (map[string]interface{}, error) {
for attempt := 0; attempt < maxRetries; attempt++ {
body, _ := json.Marshal(map[string]interface{}{"prompt": workflow})
req, _ := http.NewRequest("POST", "https://api.cmfy.cloud/v1/jobs", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != 429 {
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
return result, nil
}
var errResp struct {
Error struct {
Code string `json:"code"`
RetryAfter int `json:"retry_after"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
resp.Body.Close()
// Handle different 429 types
switch errResp.Error.Code {
case "rate_limit_exceeded":
retryAfter := errResp.Error.RetryAfter
if retryAfter == 0 {
retryAfter = 60
}
fmt.Printf("Rate limited, waiting %ds\n", retryAfter)
time.Sleep(time.Duration(retryAfter) * time.Second)
case "concurrent_limit_reached":
fmt.Println("At concurrent limit, waiting for job to finish")
time.Sleep(5 * time.Second)
case "queue_full":
fmt.Println("Queue full, waiting for jobs to process")
time.Sleep(10 * time.Second)
}
}
return nil, errors.New("max retries exceeded")
}
#!/bin/bash
MAX_RETRIES=3
attempt=0
while [ $attempt -lt $MAX_RETRIES ]; do
response=$(curl -s -w "\n%{http_code}" -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": {...}}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" != "429" ]; then
echo "$body"
exit 0
fi
# Extract retry_after from response
retry_after=$(echo "$body" | jq -r '.error.retry_after // 60')
echo "Rate limited, waiting ${retry_after}s"
sleep "$retry_after"
attempt=$((attempt + 1))
done
echo "Max retries exceeded"
exit 1
Not Found Errors (404)
| Code | Message | Cause |
|---|---|---|
job_not_found | Job not found | Job ID doesn't exist or belongs to another user |
- Python
- JavaScript
- Go
- cURL
def get_job_status(job_id):
response = requests.get(
f'https://api.cmfy.cloud/v1/jobs/{job_id}',
headers={'Authorization': f'Bearer {API_KEY}'}
)
if response.status_code == 404:
raise JobNotFoundError(f"Job {job_id} not found")
return response.json()
async function getJobStatus(jobId) {
const response = await fetch(`https://api.cmfy.cloud/v1/jobs/${jobId}`, {
headers: {
'Authorization': `Bearer ${apiKey}`
}
});
if (response.status === 404) {
throw new Error(`Job ${jobId} not found`);
}
return response.json();
}
func getJobStatus(jobID string) (map[string]interface{}, error) {
req, _ := http.NewRequest("GET", "https://api.cmfy.cloud/v1/jobs/"+jobID, nil)
req.Header.Set("Authorization", "Bearer "+apiKey)
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode == 404 {
return nil, fmt.Errorf("job %s not found", jobID)
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
response=$(curl -s -w "\n%{http_code}" https://api.cmfy.cloud/v1/jobs/YOUR_JOB_ID \
-H "Authorization: Bearer YOUR_API_KEY")
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "404" ]; then
echo "Job not found"
exit 1
fi
echo "$body"
Server Errors (5xx)
| HTTP | Code | Message | Cause |
|---|---|---|---|
| 500 | internal_error | Internal server error | Unexpected error |
| 503 | service_unavailable | Service temporarily unavailable | Maintenance or overload |
Server errors are rare but should be handled:
- Python
- JavaScript
- Go
- cURL
def submit_job_with_error_handling(workflow):
try:
response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'prompt': workflow},
timeout=30
)
if response.status_code >= 500:
# Server error - retry with backoff
raise ServerError(response.json()['error']['message'])
response.raise_for_status()
return response.json()
except requests.exceptions.Timeout:
raise ServerError("Request timed out")
except requests.exceptions.ConnectionError:
raise ServerError("Could not connect to server")
async function submitJobWithErrorHandling(workflow) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch('https://api.cmfy.cloud/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt: workflow }),
signal: controller.signal
});
clearTimeout(timeoutId);
if (response.status >= 500) {
const { error } = await response.json();
throw new Error(`Server error: ${error.message}`);
}
return response.json();
} catch (err) {
if (err.name === 'AbortError') {
throw new Error('Request timed out');
}
throw err;
}
}
func submitJobWithErrorHandling(workflow map[string]interface{}) (map[string]interface{}, error) {
body, _ := json.Marshal(map[string]interface{}{"prompt": workflow})
req, _ := http.NewRequest("POST", "https://api.cmfy.cloud/v1/jobs", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
if os.IsTimeout(err) {
return nil, errors.New("request timed out")
}
return nil, fmt.Errorf("could not connect to server: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode >= 500 {
var errResp struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
return nil, fmt.Errorf("server error: %s", errResp.Error.Message)
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
# Submit with timeout and check for server errors
response=$(curl -s -w "\n%{http_code}" --max-time 30 -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": {...}}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" -ge 500 ]; then
echo "Server error: $body"
exit 1
fi
echo "$body"
Retry Strategies
Exponential Backoff
The recommended retry strategy for transient errors:
- Python
- JavaScript
- Go
- cURL
import time
import random
def exponential_backoff(attempt, base_delay=1, max_delay=60):
"""Calculate delay with jitter."""
delay = min(base_delay * (2 ** attempt), max_delay)
jitter = random.uniform(0, delay * 0.1)
return delay + jitter
def submit_with_backoff(workflow, max_retries=5):
for attempt in range(max_retries):
try:
response = requests.post(
'https://api.cmfy.cloud/v1/jobs',
headers={'Authorization': f'Bearer {API_KEY}'},
json={'prompt': workflow}
)
if response.status_code == 429:
# Use Retry-After if provided, otherwise backoff
retry_after = response.json()['error'].get('retry_after')
delay = retry_after or exponential_backoff(attempt)
time.sleep(delay)
continue
if response.status_code >= 500:
# Server error - retry with backoff
time.sleep(exponential_backoff(attempt))
continue
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException:
if attempt == max_retries - 1:
raise
time.sleep(exponential_backoff(attempt))
raise Exception("Max retries exceeded")
function exponentialBackoff(attempt, baseDelay = 1, maxDelay = 60) {
const delay = Math.min(baseDelay * Math.pow(2, attempt), maxDelay);
const jitter = Math.random() * delay * 0.1;
return delay + jitter;
}
async function submitWithBackoff(workflow, maxRetries = 5) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch('https://api.cmfy.cloud/v1/jobs', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ prompt: workflow })
});
if (response.status === 429) {
const { error } = await response.json();
const delay = error.retry_after || exponentialBackoff(attempt);
await sleep(delay * 1000);
continue;
}
if (response.status >= 500) {
await sleep(exponentialBackoff(attempt) * 1000);
continue;
}
return response.json();
} catch (err) {
if (attempt === maxRetries - 1) throw err;
await sleep(exponentialBackoff(attempt) * 1000);
}
}
throw new Error('Max retries exceeded');
}
package main
import (
"bytes"
"encoding/json"
"errors"
"math"
"math/rand"
"net/http"
"time"
)
func exponentialBackoff(attempt int, baseDelay, maxDelay float64) time.Duration {
delay := math.Min(baseDelay*math.Pow(2, float64(attempt)), maxDelay)
jitter := rand.Float64() * delay * 0.1
return time.Duration((delay + jitter) * float64(time.Second))
}
func submitWithBackoff(workflow map[string]interface{}, maxRetries int) (map[string]interface{}, error) {
for attempt := 0; attempt < maxRetries; attempt++ {
body, _ := json.Marshal(map[string]interface{}{"prompt": workflow})
req, _ := http.NewRequest("POST", "https://api.cmfy.cloud/v1/jobs", bytes.NewBuffer(body))
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
if attempt == maxRetries-1 {
return nil, err
}
time.Sleep(exponentialBackoff(attempt, 1, 60))
continue
}
if resp.StatusCode == 429 {
var errResp struct {
Error struct {
RetryAfter float64 `json:"retry_after"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
resp.Body.Close()
delay := errResp.Error.RetryAfter
if delay == 0 {
delay = exponentialBackoff(attempt, 1, 60).Seconds()
}
time.Sleep(time.Duration(delay) * time.Second)
continue
}
if resp.StatusCode >= 500 {
resp.Body.Close()
time.Sleep(exponentialBackoff(attempt, 1, 60))
continue
}
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
resp.Body.Close()
return result, nil
}
return nil, errors.New("max retries exceeded")
}
#!/bin/bash
MAX_RETRIES=5
attempt=0
exponential_backoff() {
local attempt=$1
local base_delay=1
local max_delay=60
local delay=$((base_delay * (2 ** attempt)))
[ $delay -gt $max_delay ] && delay=$max_delay
echo $delay
}
while [ $attempt -lt $MAX_RETRIES ]; do
response=$(curl -s -w "\n%{http_code}" -X POST https://api.cmfy.cloud/v1/jobs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"prompt": {...}}')
http_code=$(echo "$response" | tail -n1)
body=$(echo "$response" | sed '$d')
if [ "$http_code" = "429" ]; then
retry_after=$(echo "$body" | jq -r '.error.retry_after // empty')
delay=${retry_after:-$(exponential_backoff $attempt)}
echo "Rate limited, waiting ${delay}s"
sleep "$delay"
elif [ "$http_code" -ge 500 ]; then
delay=$(exponential_backoff $attempt)
echo "Server error, retrying in ${delay}s"
sleep "$delay"
else
echo "$body"
exit 0
fi
attempt=$((attempt + 1))
done
echo "Max retries exceeded"
exit 1
Which Errors to Retry
| Error Type | Should Retry? | Notes |
|---|---|---|
| 400 (validation) | No | Fix the request first |
| 401 (auth) | No | Fix credentials |
| 403 (forbidden) | No | Contact support |
| 404 (not found) | No | Resource doesn't exist |
| 429 (rate limit) | Yes | Use Retry-After header |
| 500 (server error) | Yes | With exponential backoff |
| 503 (unavailable) | Yes | With exponential backoff |
| Timeout | Yes | May need longer timeout |
| Connection error | Yes | With exponential backoff |
Error Logging Best Practices
Log Useful Context
- Python
- JavaScript
- Go
import logging
logger = logging.getLogger(__name__)
def handle_api_error(response, context=None):
error = response.json().get('error', {})
log_data = {
'status_code': response.status_code,
'error_code': error.get('code'),
'error_message': error.get('message'),
'request_id': error.get('request_id'),
**(context or {})
}
if response.status_code >= 500:
logger.error("Server error", extra=log_data)
elif response.status_code == 429:
logger.warning("Rate limited", extra=log_data)
else:
logger.info("API error", extra=log_data)
function logError(error, context) {
console.error({
timestamp: new Date().toISOString(),
errorCode: error.code,
errorMessage: error.message,
requestId: error.request_id, // Include this when contacting support
...context
});
}
package main
import (
"log"
"net/http"
)
func handleAPIError(resp *http.Response, errResp map[string]interface{}, context map[string]interface{}) {
errorData := errResp["error"].(map[string]interface{})
logData := map[string]interface{}{
"status_code": resp.StatusCode,
"error_code": errorData["code"],
"error_message": errorData["message"],
"request_id": errorData["request_id"],
}
for k, v := range context {
logData[k] = v
}
if resp.StatusCode >= 500 {
log.Printf("Server error: %v", logData)
} else if resp.StatusCode == 429 {
log.Printf("Rate limited: %v", logData)
} else {
log.Printf("API error: %v", logData)
}
}
Building Robust Clients
Complete Error Handler
- Python
- JavaScript
- Go
class CmfyCloudError(Exception):
"""Base exception for cmfy.cloud API errors."""
def __init__(self, code, message, status_code=None, request_id=None):
super().__init__(message)
self.code = code
self.message = message
self.status_code = status_code
self.request_id = request_id
class AuthenticationError(CmfyCloudError):
"""Raised for 401 errors."""
pass
class ValidationError(CmfyCloudError):
"""Raised for 400 errors."""
pass
class RateLimitError(CmfyCloudError):
"""Raised for 429 errors."""
def __init__(self, code, message, retry_after=None, **kwargs):
super().__init__(code, message, **kwargs)
self.retry_after = retry_after
class ServerError(CmfyCloudError):
"""Raised for 5xx errors."""
pass
def handle_response(response):
"""Convert API response to appropriate exception or return data."""
if response.ok:
return response.json()
error = response.json().get('error', {})
kwargs = {
'code': error.get('code', 'unknown'),
'message': error.get('message', 'Unknown error'),
'status_code': response.status_code,
'request_id': error.get('request_id')
}
if response.status_code == 401:
raise AuthenticationError(**kwargs)
elif response.status_code == 400:
raise ValidationError(**kwargs)
elif response.status_code == 429:
raise RateLimitError(retry_after=error.get('retry_after'), **kwargs)
elif response.status_code >= 500:
raise ServerError(**kwargs)
else:
raise CmfyCloudError(**kwargs)
class CmfyCloudError extends Error {
constructor(code, message, statusCode, requestId) {
super(message);
this.name = 'CmfyCloudError';
this.code = code;
this.statusCode = statusCode;
this.requestId = requestId;
}
}
class AuthenticationError extends CmfyCloudError {
constructor(code, message, statusCode, requestId) {
super(code, message, statusCode, requestId);
this.name = 'AuthenticationError';
}
}
class ValidationError extends CmfyCloudError {
constructor(code, message, statusCode, requestId) {
super(code, message, statusCode, requestId);
this.name = 'ValidationError';
}
}
class RateLimitError extends CmfyCloudError {
constructor(code, message, statusCode, requestId, retryAfter) {
super(code, message, statusCode, requestId);
this.name = 'RateLimitError';
this.retryAfter = retryAfter;
}
}
class ServerError extends CmfyCloudError {
constructor(code, message, statusCode, requestId) {
super(code, message, statusCode, requestId);
this.name = 'ServerError';
}
}
async function handleResponse(response) {
if (response.ok) {
return response.json();
}
const { error } = await response.json();
const code = error?.code || 'unknown';
const message = error?.message || 'Unknown error';
const requestId = error?.request_id;
switch (response.status) {
case 401:
throw new AuthenticationError(code, message, response.status, requestId);
case 400:
throw new ValidationError(code, message, response.status, requestId);
case 429:
throw new RateLimitError(code, message, response.status, requestId, error?.retry_after);
default:
if (response.status >= 500) {
throw new ServerError(code, message, response.status, requestId);
}
throw new CmfyCloudError(code, message, response.status, requestId);
}
}
package main
import (
"encoding/json"
"fmt"
"net/http"
)
type CmfyCloudError struct {
Code string
Message string
StatusCode int
RequestID string
}
func (e *CmfyCloudError) Error() string {
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
type AuthenticationError struct{ CmfyCloudError }
type ValidationError struct{ CmfyCloudError }
type ServerError struct{ CmfyCloudError }
type RateLimitError struct {
CmfyCloudError
RetryAfter int
}
func handleResponse(resp *http.Response) (map[string]interface{}, error) {
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
var result map[string]interface{}
json.NewDecoder(resp.Body).Decode(&result)
return result, nil
}
var errResp struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
RequestID string `json:"request_id"`
RetryAfter int `json:"retry_after"`
} `json:"error"`
}
json.NewDecoder(resp.Body).Decode(&errResp)
baseErr := CmfyCloudError{
Code: errResp.Error.Code,
Message: errResp.Error.Message,
StatusCode: resp.StatusCode,
RequestID: errResp.Error.RequestID,
}
switch resp.StatusCode {
case 401:
return nil, &AuthenticationError{baseErr}
case 400:
return nil, &ValidationError{baseErr}
case 429:
return nil, &RateLimitError{baseErr, errResp.Error.RetryAfter}
default:
if resp.StatusCode >= 500 {
return nil, &ServerError{baseErr}
}
return nil, &baseErr
}
}
Using the Error Handler
- Python
- JavaScript
- Go
def main():
try:
result = submit_job(my_workflow)
print(f"Job submitted: {result['job_id']}")
except AuthenticationError as e:
print(f"Auth failed: {e.message}")
print("Check your API key in environment variables")
except ValidationError as e:
print(f"Invalid request: {e.message}")
# Log the full error for debugging
logger.error(f"Validation error: {e.code}", extra={
'request_id': e.request_id
})
except RateLimitError as e:
print(f"Rate limited: {e.message}")
if e.retry_after:
print(f"Retry after {e.retry_after} seconds")
except ServerError as e:
print("Server error, please try again later")
# Report to monitoring
report_server_error(e.request_id)
async function main() {
try {
const result = await submitJob(myWorkflow);
console.log(`Job submitted: ${result.job_id}`);
} catch (e) {
if (e instanceof AuthenticationError) {
console.log(`Auth failed: ${e.message}`);
console.log('Check your API key in environment variables');
} else if (e instanceof ValidationError) {
console.log(`Invalid request: ${e.message}`);
console.error(`Validation error: ${e.code}`, { requestId: e.requestId });
} else if (e instanceof RateLimitError) {
console.log(`Rate limited: ${e.message}`);
if (e.retryAfter) {
console.log(`Retry after ${e.retryAfter} seconds`);
}
} else if (e instanceof ServerError) {
console.log('Server error, please try again later');
reportServerError(e.requestId);
} else {
throw e;
}
}
}
func main() {
result, err := submitJob(myWorkflow)
if err != nil {
switch e := err.(type) {
case *AuthenticationError:
fmt.Printf("Auth failed: %s\n", e.Message)
fmt.Println("Check your API key in environment variables")
case *ValidationError:
fmt.Printf("Invalid request: %s\n", e.Message)
log.Printf("Validation error: %s, request_id: %s", e.Code, e.RequestID)
case *RateLimitError:
fmt.Printf("Rate limited: %s\n", e.Message)
if e.RetryAfter > 0 {
fmt.Printf("Retry after %d seconds\n", e.RetryAfter)
}
case *ServerError:
fmt.Println("Server error, please try again later")
reportServerError(e.RequestID)
default:
panic(err)
}
return
}
fmt.Printf("Job submitted: %s\n", result["job_id"])
}
What's Next?
- Webhooks - Receive results asynchronously
- Idempotency - Safely retry requests
- Rate Limiting - Understand limits
Was this page helpful?