Status: ✅ Active Applies To: All API endpoints Effective: October 16, 2025 Related Issue: #215 (CORE-ERROR-STANDARDS) Pattern Number: 034 (next in sequence)
All API endpoints MUST return appropriate HTTP status codes for errors. The response body format remains consistent for backward compatibility.
Core Principle: HTTP status codes MUST accurately reflect the outcome of the request. Never return 200 OK for error conditions.
Use For: Successful operations only
Response Format:
{
"status": "success",
"data": { ... }
}
Never use 200 for errors - this violates REST principles and breaks HTTP client error handling.
Use For: Malformed request syntax
Examples:
Response Format:
{
"status": "error",
"code": "BAD_REQUEST",
"message": "Request syntax is malformed",
"details": {
"issue": "Invalid JSON: unexpected token at line 5"
}
}
Implementation:
from web.utils.error_responses import bad_request_error
return bad_request_error(
"Invalid JSON",
{"issue": "Syntax error at line 5"}
)
Use For: Syntactically valid but semantically invalid
Examples:
Response Format:
{
"status": "error",
"code": "VALIDATION_ERROR",
"message": "Validation failed",
"details": {
"field": "intent",
"issue": "Cannot be empty"
}
}
Implementation:
from web.utils.error_responses import validation_error
if not data.get("intent"):
return validation_error(
"Required field missing",
{"field": "intent", "issue": "Cannot be empty"}
)
Use For: Resource doesn’t exist
Examples:
Response Format:
{
"status": "error",
"code": "NOT_FOUND",
"message": "Resource not found",
"details": {
"resource": "workflow",
"id": "12345"
}
}
Implementation:
from web.utils.error_responses import not_found_error
return not_found_error(
"Workflow not found",
{"resource": "workflow", "id": workflow_id}
)
Use For: Unexpected server errors
Examples:
Response Format:
{
"status": "error",
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"details": {
"error_id": "uuid-for-log-correlation"
}
}
Implementation:
from web.utils.error_responses import internal_error
import logging
logger = logging.getLogger(__name__)
try:
# operation
except Exception as e:
logger.error(f"Unexpected error: {e}", exc_info=True)
return internal_error() # Never expose details to client
CRITICAL: Never expose stack traces or internal details in 500 errors.
Use For: Backend/upstream service issues
502 Bad Gateway:
503 Service Unavailable:
Implementation:
try:
response = await http_client.get(backend_url)
except Exception as e:
logger.error(f"Backend unavailable: {e}")
return internal_error("Backend service unavailable")
Location: web/utils/error_responses.py
from enum import Enum
class ErrorCode(str, Enum):
"""Standard error codes for API responses."""
BAD_REQUEST = "BAD_REQUEST"
VALIDATION_ERROR = "VALIDATION_ERROR"
NOT_FOUND = "NOT_FOUND"
INTERNAL_ERROR = "INTERNAL_ERROR"
Extensibility: Add new codes as needed for specific error types.
Always use utility functions from web/utils/error_responses.py:
from web.utils.error_responses import (
bad_request_error,
validation_error,
not_found_error,
internal_error
)
@app.post("/api/v1/endpoint")
async def endpoint(request: Request):
try:
data = await request.json()
# Validation
if not data.get("required_field"):
return validation_error(
"Required field missing",
{"field": "required_field", "issue": "Cannot be empty"}
)
# Process...
result = process_data(data)
return {"status": "success", "data": result}
except ValueError as e:
# Bad input
return validation_error(str(e))
except Exception as e:
# Unexpected error
logger.error(f"Endpoint error: {e}", exc_info=True)
return internal_error()
Always log errors with appropriate level:
import logging
logger = logging.getLogger(__name__)
# Validation errors: INFO or WARNING
logger.warning(f"Validation failed: {field}")
# Internal errors: ERROR with stack trace
logger.error(f"Unexpected error: {e}", exc_info=True)
All error responses MUST include:
status: Always “error”code: Error code from ErrorCode enummessage: User-friendly messagedetails: Optional additional information (dict)Never expose:
Safe to expose:
try:
result = operation()
return result
except Exception as e:
return {"status": "error", "error": str(e)} # Returns 200!
Problems:
try:
result = operation()
return result
except ValueError as e:
# Semantic/validation error
return validation_error(str(e)) # Returns 422
except Exception as e:
# Unexpected error
logger.error(f"Unexpected error: {e}", exc_info=True)
return internal_error() # Returns 500
Benefits:
Response Format: ✅ UNCHANGED
{"status": "error", ...} format maintainedBreaking Change: ❌ HTTP Status Codes
Impact on Clients:
response.status_code firstMigration Guide:
Old client code:
response = requests.post(url, json=data)
if response.json().get("status") == "error":
handle_error()
New client code (better):
response = requests.post(url, json=data)
if response.status_code != 200:
handle_error()
elif response.json().get("status") == "error":
# Defensive check (should not happen with proper implementation)
handle_error()
Test each error path:
def test_endpoint_validation_error():
"""Test endpoint returns 422 for invalid input."""
response = client.post("/api/v1/endpoint", json={"invalid": "data"})
assert response.status_code == 422
assert response.json()["status"] == "error"
assert response.json()["code"] == "VALIDATION_ERROR"
Test error responses end-to-end:
def test_endpoint_error_format():
"""Test error response format is consistent."""
response = client.post("/api/v1/endpoint", json={})
# Check status code
assert response.status_code in [400, 422, 500]
# Check required fields
body = response.json()
assert "status" in body
assert body["status"] == "error"
assert "code" in body
assert "message" in body
@app.post("/api/v1/intent")
async def process_intent(request: Request):
try:
data = await request.json()
message = data.get("message", "")
# Validation
if not message:
return validation_error(
"Message required",
{"field": "message", "issue": "Cannot be empty"}
)
# Service unavailable
if intent_service is None:
return internal_error("Intent service unavailable")
# Process
result = await intent_service.process_intent(message)
# Service returned error
if result.error:
return validation_error(
result.error,
{"error_type": result.error_type}
)
return {"status": "success", "data": result}
except Exception as e:
logger.error(f"Intent processing error: {e}", exc_info=True)
return internal_error()
@app.get("/api/v1/workflows/{workflow_id}")
async def get_workflow(workflow_id: str):
try:
workflow = await workflow_service.get(workflow_id)
if not workflow:
return not_found_error(
"Workflow not found",
{"resource": "workflow", "id": workflow_id}
)
return {"status": "success", "data": workflow}
except Exception as e:
logger.error(f"Workflow retrieval error: {e}", exc_info=True)
return internal_error()
@app.get("/api/proxy/external")
async def proxy_endpoint():
try:
async with httpx.AsyncClient() as client:
response = await client.get(external_url)
return response.json()
except httpx.ConnectError:
logger.error("Backend connection failed")
return internal_error("Backend service unavailable")
except Exception as e:
logger.error(f"Proxy error: {e}", exc_info=True)
return internal_error()
400 Bad Request: Syntax errors (invalid JSON, missing headers) 422 Unprocessable Entity: Semantic errors (empty fields, invalid values)
This distinction helps clients understand whether to:
Consistency: All errors follow same structure Parsing: Clients can parse errors uniformly Monitoring: Tools can detect patterns Debugging: Clear error identification
We will: For request models This handles: Business logic errors, service errors, unexpected exceptions
Both approaches complement each other.
Document Owner: Lead Developer Last Updated: October 16, 2025 Review Date: Sprint A3 Status: Active (Effective October 16, 2025)