Initial commit
This commit is contained in:
635
skills/mxcp-expert/references/error-handling-guide.md
Normal file
635
skills/mxcp-expert/references/error-handling-guide.md
Normal file
@@ -0,0 +1,635 @@
|
||||
# Error Handling Guide
|
||||
|
||||
**Comprehensive error handling for MXCP servers: SQL errors (managed by MXCP) and Python errors (YOU must handle).**
|
||||
|
||||
## Two Types of Error Handling
|
||||
|
||||
### 1. SQL Errors (Managed by MXCP)
|
||||
|
||||
**MXCP automatically handles**:
|
||||
- SQL syntax errors
|
||||
- Type mismatches
|
||||
- Parameter binding errors
|
||||
- Database connection errors
|
||||
|
||||
**Your responsibility**:
|
||||
- Write correct SQL
|
||||
- Use proper parameter binding (`$param`)
|
||||
- Match return types to actual data
|
||||
|
||||
### 2. Python Errors (YOU Must Handle)
|
||||
|
||||
**You MUST handle**:
|
||||
- External API failures
|
||||
- Invalid input
|
||||
- Resource not found
|
||||
- Business logic errors
|
||||
- Async/await errors
|
||||
|
||||
**Return structured error objects, don't raise exceptions to MXCP.**
|
||||
|
||||
## Python Error Handling Pattern
|
||||
|
||||
### ❌ WRONG: Let Exceptions Bubble Up
|
||||
|
||||
```python
|
||||
# python/api_wrapper.py
|
||||
async def fetch_user(user_id: int) -> dict:
|
||||
async with httpx.AsyncClient() as client:
|
||||
response = await client.get(f"https://api.example.com/users/{user_id}")
|
||||
response.raise_for_status() # ❌ Will crash if 404/500!
|
||||
return response.json()
|
||||
```
|
||||
|
||||
**Problem**: When API returns 404, exception crashes the tool. LLM gets unhelpful error.
|
||||
|
||||
### ✅ CORRECT: Return Structured Errors
|
||||
|
||||
```python
|
||||
# python/api_wrapper.py
|
||||
import httpx
|
||||
|
||||
async def fetch_user(user_id: int) -> dict:
|
||||
"""
|
||||
Fetch user from external API.
|
||||
|
||||
Returns:
|
||||
Success: {"success": true, "user": {...}}
|
||||
Error: {"success": false, "error": "User not found", "error_code": "NOT_FOUND"}
|
||||
"""
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
f"https://api.example.com/users/{user_id}"
|
||||
)
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"User with ID {user_id} not found",
|
||||
"error_code": "NOT_FOUND",
|
||||
"user_id": user_id
|
||||
}
|
||||
|
||||
if response.status_code >= 500:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "External API is currently unavailable. Please try again later.",
|
||||
"error_code": "API_ERROR",
|
||||
"status_code": response.status_code
|
||||
}
|
||||
|
||||
response.raise_for_status() # Other HTTP errors
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user": response.json()
|
||||
}
|
||||
|
||||
except httpx.TimeoutException:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Request timed out after 10 seconds. The API may be slow or unavailable.",
|
||||
"error_code": "TIMEOUT"
|
||||
}
|
||||
|
||||
except httpx.HTTPError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"HTTP error occurred: {str(e)}",
|
||||
"error_code": "HTTP_ERROR"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"error_code": "UNKNOWN_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
**Why good**:
|
||||
- ✅ LLM gets clear error message
|
||||
- ✅ LLM knows what went wrong (error_code)
|
||||
- ✅ LLM can take action (retry, try different ID, etc.)
|
||||
- ✅ Tool never crashes
|
||||
|
||||
## Error Response Structure
|
||||
|
||||
### Standard Error Format
|
||||
|
||||
```python
|
||||
{
|
||||
"success": False,
|
||||
"error": "Human-readable error message for LLM",
|
||||
"error_code": "MACHINE_READABLE_CODE",
|
||||
"details": { # Optional: additional context
|
||||
"attempted_value": user_id,
|
||||
"valid_range": "1-1000"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Standard Success Format
|
||||
|
||||
```python
|
||||
{
|
||||
"success": True,
|
||||
"data": {
|
||||
# Actual response data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Common Error Scenarios
|
||||
|
||||
### 1. Input Validation Errors
|
||||
|
||||
```python
|
||||
def process_order(order_id: str, quantity: int) -> dict:
|
||||
"""Process an order with validation"""
|
||||
|
||||
# Validate order_id format
|
||||
if not order_id.startswith("ORD_"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid order ID format. Expected format: 'ORD_XXXXX', got: '{order_id}'",
|
||||
"error_code": "INVALID_FORMAT",
|
||||
"expected_format": "ORD_XXXXX",
|
||||
"provided": order_id
|
||||
}
|
||||
|
||||
# Validate quantity range
|
||||
if quantity <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Quantity must be positive. Got: {quantity}",
|
||||
"error_code": "INVALID_QUANTITY",
|
||||
"provided": quantity,
|
||||
"valid_range": "1 or greater"
|
||||
}
|
||||
|
||||
if quantity > 1000:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Quantity {quantity} exceeds maximum allowed (1000). Please split into multiple orders.",
|
||||
"error_code": "QUANTITY_EXCEEDED",
|
||||
"provided": quantity,
|
||||
"maximum": 1000
|
||||
}
|
||||
|
||||
# Process order...
|
||||
return {"success": True, "order_id": order_id, "quantity": quantity}
|
||||
```
|
||||
|
||||
### 2. Resource Not Found Errors
|
||||
|
||||
```python
|
||||
from mxcp.runtime import db
|
||||
|
||||
def get_customer(customer_id: str) -> dict:
|
||||
"""Get customer by ID with proper error handling"""
|
||||
|
||||
try:
|
||||
result = db.execute(
|
||||
"SELECT * FROM customers WHERE customer_id = $1",
|
||||
{"customer_id": customer_id}
|
||||
)
|
||||
|
||||
customer = result.fetchone()
|
||||
|
||||
if customer is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Customer '{customer_id}' not found in database. Use list_customers to see available customers.",
|
||||
"error_code": "CUSTOMER_NOT_FOUND",
|
||||
"customer_id": customer_id,
|
||||
"suggestion": "Call list_customers tool to see all available customer IDs"
|
||||
}
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer": dict(customer)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Database error while fetching customer: {str(e)}",
|
||||
"error_code": "DATABASE_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. External API Errors
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
async def create_customer_in_stripe(email: str, name: str) -> dict:
|
||||
"""Create Stripe customer with comprehensive error handling"""
|
||||
|
||||
try:
|
||||
import stripe
|
||||
from mxcp.runtime import get_secret
|
||||
|
||||
# Get API key
|
||||
secret = get_secret("stripe")
|
||||
if not secret:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe API key not configured. Please set up 'stripe' secret in config.yml",
|
||||
"error_code": "MISSING_CREDENTIALS",
|
||||
"required_secret": "stripe"
|
||||
}
|
||||
|
||||
stripe.api_key = secret.get("api_key")
|
||||
|
||||
# Create customer
|
||||
customer = stripe.Customer.create(
|
||||
email=email,
|
||||
name=name
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"customer_id": customer.id,
|
||||
"email": customer.email
|
||||
}
|
||||
|
||||
except stripe.error.InvalidRequestError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Invalid request to Stripe: {str(e)}",
|
||||
"error_code": "INVALID_REQUEST",
|
||||
"details": str(e)
|
||||
}
|
||||
|
||||
except stripe.error.AuthenticationError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe API key is invalid or expired. Please update credentials.",
|
||||
"error_code": "AUTHENTICATION_FAILED"
|
||||
}
|
||||
|
||||
except stripe.error.RateLimitError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe rate limit exceeded. Please try again in a few seconds.",
|
||||
"error_code": "RATE_LIMIT",
|
||||
"suggestion": "Wait 5-10 seconds and retry"
|
||||
}
|
||||
|
||||
except stripe.error.StripeError as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Stripe error: {str(e)}",
|
||||
"error_code": "STRIPE_ERROR"
|
||||
}
|
||||
|
||||
except ImportError:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "Stripe library not installed. Run: pip install stripe",
|
||||
"error_code": "MISSING_DEPENDENCY",
|
||||
"fix": "pip install stripe>=5.0.0"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Unexpected error: {str(e)}",
|
||||
"error_code": "UNKNOWN_ERROR"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Business Logic Errors
|
||||
|
||||
```python
|
||||
def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
|
||||
"""Transfer funds with business logic validation"""
|
||||
|
||||
# Check amount
|
||||
if amount <= 0:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Transfer amount must be positive. Got: ${amount}",
|
||||
"error_code": "INVALID_AMOUNT"
|
||||
}
|
||||
|
||||
# Check account exists and get balance
|
||||
from_balance = db.execute(
|
||||
"SELECT balance FROM accounts WHERE account_id = $1",
|
||||
{"account_id": from_account}
|
||||
).fetchone()
|
||||
|
||||
if from_balance is None:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Source account '{from_account}' not found",
|
||||
"error_code": "ACCOUNT_NOT_FOUND",
|
||||
"account_id": from_account
|
||||
}
|
||||
|
||||
# Check sufficient funds
|
||||
if from_balance["balance"] < amount:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Insufficient funds. Available: ${from_balance['balance']:.2f}, Requested: ${amount:.2f}",
|
||||
"error_code": "INSUFFICIENT_FUNDS",
|
||||
"available": from_balance["balance"],
|
||||
"requested": amount,
|
||||
"shortfall": amount - from_balance["balance"]
|
||||
}
|
||||
|
||||
# Perform transfer...
|
||||
return {
|
||||
"success": True,
|
||||
"transfer_id": "TXN_12345",
|
||||
"from_account": from_account,
|
||||
"to_account": to_account,
|
||||
"amount": amount
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Async/Await Errors
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
|
||||
async def fetch_multiple_users(user_ids: list[int]) -> dict:
|
||||
"""Fetch multiple users concurrently with error handling"""
|
||||
|
||||
async def fetch_one(user_id: int) -> dict:
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||
response = await client.get(f"https://api.example.com/users/{user_id}")
|
||||
|
||||
if response.status_code == 404:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": f"User {user_id} not found"
|
||||
}
|
||||
|
||||
response.raise_for_status()
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": True,
|
||||
"user": response.json()
|
||||
}
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": f"Timeout fetching user {user_id}"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
return {
|
||||
"user_id": user_id,
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
# Fetch all concurrently
|
||||
results = await asyncio.gather(*[fetch_one(uid) for uid in user_ids])
|
||||
|
||||
# Separate successes and failures
|
||||
successes = [r for r in results if r["success"]]
|
||||
failures = [r for r in results if not r["success"]]
|
||||
|
||||
return {
|
||||
"success": len(failures) == 0,
|
||||
"total_requested": len(user_ids),
|
||||
"successful": len(successes),
|
||||
"failed": len(failures),
|
||||
"users": [r["user"] for r in successes],
|
||||
"errors": [{"user_id": r["user_id"], "error": r["error"]} for r in failures]
|
||||
}
|
||||
```
|
||||
|
||||
## Error Messages for LLMs
|
||||
|
||||
### Principles for Good Error Messages
|
||||
|
||||
1. **Be Specific**: Tell exactly what went wrong
|
||||
2. **Be Actionable**: Suggest what to do next
|
||||
3. **Provide Context**: Include relevant values/IDs
|
||||
4. **Use Plain Language**: Avoid technical jargon
|
||||
|
||||
### ❌ BAD Error Messages
|
||||
|
||||
```python
|
||||
return {"error": "Error"} # ❌ Useless
|
||||
return {"error": "Invalid input"} # ❌ Which input? Why invalid?
|
||||
return {"error": "DB error"} # ❌ What kind of error?
|
||||
return {"error": str(e)} # ❌ Raw exception message (often cryptic)
|
||||
```
|
||||
|
||||
### ✅ GOOD Error Messages
|
||||
|
||||
```python
|
||||
return {
|
||||
"error": "Customer ID 'CUST_999' not found. Use list_customers to see available IDs."
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "Date format invalid. Expected 'YYYY-MM-DD' (e.g., '2024-01-15'), got: '01/15/2024'"
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "Quantity 5000 exceeds maximum allowed (1000). Split into multiple orders or contact support."
|
||||
}
|
||||
|
||||
return {
|
||||
"error": "API rate limit exceeded. Please wait 30 seconds and try again."
|
||||
}
|
||||
```
|
||||
|
||||
## SQL Error Handling (MXCP Managed)
|
||||
|
||||
### You Don't Handle These (MXCP Does)
|
||||
|
||||
MXCP automatically handles and returns errors for:
|
||||
- Invalid SQL syntax
|
||||
- Missing tables/columns
|
||||
- Type mismatches
|
||||
- Parameter binding errors
|
||||
|
||||
**Your job**: Write correct SQL and let MXCP handle errors.
|
||||
|
||||
### Prevent SQL Errors
|
||||
|
||||
#### 1. Validate Schema
|
||||
|
||||
```yaml
|
||||
# Always define return types to match SQL output
|
||||
tool:
|
||||
name: get_stats
|
||||
return:
|
||||
type: object
|
||||
properties:
|
||||
total: { type: number } # Matches SQL: SUM(amount)
|
||||
count: { type: integer } # Matches SQL: COUNT(*)
|
||||
source:
|
||||
code: |
|
||||
SELECT
|
||||
SUM(amount) as total,
|
||||
COUNT(*) as count
|
||||
FROM orders
|
||||
```
|
||||
|
||||
#### 2. Handle NULL Values
|
||||
|
||||
```sql
|
||||
-- BAD: Might return NULL which breaks type system
|
||||
SELECT amount FROM orders WHERE id = $order_id
|
||||
|
||||
-- GOOD: Handle potential NULL
|
||||
SELECT COALESCE(amount, 0) as amount
|
||||
FROM orders
|
||||
WHERE id = $order_id
|
||||
|
||||
-- GOOD: Use IFNULL/COALESCE for aggregations
|
||||
SELECT
|
||||
COALESCE(SUM(amount), 0) as total,
|
||||
COALESCE(AVG(amount), 0) as average
|
||||
FROM orders
|
||||
WHERE status = $status
|
||||
```
|
||||
|
||||
#### 3. Handle Empty Results
|
||||
|
||||
```sql
|
||||
-- If no results, return empty array (not NULL)
|
||||
SELECT * FROM customers WHERE city = $city
|
||||
-- Returns: [] if no customers (MXCP handles this)
|
||||
|
||||
-- For aggregations, always return a row
|
||||
SELECT
|
||||
COUNT(*) as count,
|
||||
COALESCE(SUM(amount), 0) as total
|
||||
FROM orders
|
||||
WHERE status = $status
|
||||
-- Always returns one row, even if no matching orders
|
||||
```
|
||||
|
||||
## Error Codes Convention
|
||||
|
||||
**Use consistent error codes across your tools**:
|
||||
|
||||
```python
|
||||
# Standard error codes
|
||||
ERROR_CODES = {
|
||||
# Input validation
|
||||
"INVALID_FORMAT": "Input format is incorrect",
|
||||
"INVALID_RANGE": "Value outside valid range",
|
||||
"MISSING_REQUIRED": "Required parameter missing",
|
||||
|
||||
# Resource errors
|
||||
"NOT_FOUND": "Resource not found",
|
||||
"ALREADY_EXISTS": "Resource already exists",
|
||||
"DELETED": "Resource has been deleted",
|
||||
|
||||
# Permission errors
|
||||
"UNAUTHORIZED": "User not authenticated",
|
||||
"FORBIDDEN": "User lacks permission",
|
||||
|
||||
# External service errors
|
||||
"API_ERROR": "External API error",
|
||||
"TIMEOUT": "Request timed out",
|
||||
"RATE_LIMIT": "Rate limit exceeded",
|
||||
|
||||
# System errors
|
||||
"DATABASE_ERROR": "Database operation failed",
|
||||
"CONFIGURATION_ERROR": "Missing or invalid configuration",
|
||||
"DEPENDENCY_ERROR": "Required library not installed",
|
||||
|
||||
# Business logic errors
|
||||
"INSUFFICIENT_FUNDS": "Not enough balance",
|
||||
"INVALID_STATE": "Operation not allowed in current state",
|
||||
"QUOTA_EXCEEDED": "Usage quota exceeded",
|
||||
|
||||
# Unknown
|
||||
"UNKNOWN_ERROR": "Unexpected error occurred"
|
||||
}
|
||||
```
|
||||
|
||||
## Testing Error Handling
|
||||
|
||||
### Unit Tests for Error Cases
|
||||
|
||||
```python
|
||||
# tests/test_error_handling.py
|
||||
import pytest
|
||||
from python.my_module import fetch_user
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_user_not_found(httpx_mock):
|
||||
"""Test 404 error handling"""
|
||||
httpx_mock.add_response(
|
||||
url="https://api.example.com/users/999",
|
||||
status_code=404
|
||||
)
|
||||
|
||||
result = await fetch_user(999)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "NOT_FOUND"
|
||||
assert "999" in result["error"] # Error mentions the ID
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fetch_user_timeout(httpx_mock):
|
||||
"""Test timeout handling"""
|
||||
httpx_mock.add_exception(httpx.TimeoutException("Timeout"))
|
||||
|
||||
result = await fetch_user(123)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "TIMEOUT"
|
||||
assert "timeout" in result["error"].lower()
|
||||
|
||||
def test_invalid_input():
|
||||
"""Test input validation"""
|
||||
result = process_order("INVALID", quantity=5)
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["error_code"] == "INVALID_FORMAT"
|
||||
assert "ORD_" in result["error"] # Mentions expected format
|
||||
```
|
||||
|
||||
## Error Handling Checklist
|
||||
|
||||
Before declaring Python tool complete:
|
||||
|
||||
- [ ] All external API calls wrapped in try/except
|
||||
- [ ] All exceptions return structured error objects
|
||||
- [ ] Error messages are clear and actionable
|
||||
- [ ] Error codes are consistent
|
||||
- [ ] Input validation with helpful error messages
|
||||
- [ ] NULL/None values handled gracefully
|
||||
- [ ] Timeout handling for network calls
|
||||
- [ ] Missing dependencies handled (ImportError)
|
||||
- [ ] Database errors caught and explained
|
||||
- [ ] Success/failure clearly indicated in response
|
||||
- [ ] Unit tests for error scenarios
|
||||
- [ ] Error messages help LLM understand what to do next
|
||||
|
||||
## Summary
|
||||
|
||||
**SQL Tools (MXCP Handles)**:
|
||||
- Write correct SQL
|
||||
- Handle NULL values with COALESCE
|
||||
- Match return types to SQL output
|
||||
|
||||
**Python Tools (YOU Handle)**:
|
||||
- ✅ Wrap ALL external calls in try/except
|
||||
- ✅ Return structured error objects (`{"success": False, "error": "...", "error_code": "..."}`)
|
||||
- ✅ Validate inputs with clear error messages
|
||||
- ✅ Be specific and actionable in error messages
|
||||
- ✅ Use consistent error codes
|
||||
- ✅ Test error scenarios
|
||||
- ✅ NEVER let exceptions bubble up to MXCP
|
||||
|
||||
**Golden Rule**: Errors should help the LLM understand what went wrong and what to do next.
|
||||
Reference in New Issue
Block a user