200 OK: The standard success response for most GET, PUT, and PATCH requests. Indicates the request succeeded and the response body contains the requested data.
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = db.get_user(user_id)
if user:
return jsonify(user), 200
return {"error": "User not found"}, 404
201 Created: Successful resource creation, typically used with POST requests. Should include a Location header pointing to the new resource.
@app.post("/users")
def create_user():
user_data = request.get_json()
new_user = User.create(user_data)
return jsonify(new_user.to_dict()), 201, {
'Location': f'/users/{new_user.id}'
}
204 No Content: Success but no response body, commonly used for DELETE operations or updates where no data needs to be returned.
@app.delete("/users/{user_id}")
def delete_user(user_id: int):
user = db.get_user(user_id)
if user:
db.delete_user(user_id)
return '', 204
return {"error": "User not found"}, 404
400 Bad Request: Generic client error when the server cannot process the request due to malformed syntax, invalid parameters, or other client-side issues.
@app.post("/orders")
def create_order():
try:
order_data = request.get_json()
validate_order_data(order_data) # Raises ValidationError
order = Order.create(order_data)
return jsonify(order.to_dict()), 201
except ValidationError as e:
return {
"error": "Invalid order data",
"details": str(e)
}, 400
401 Unauthorized: Authentication is required and has failed or not been provided. Should include a WWW-Authenticate header when appropriate.
@app.get("/protected")
def protected_resource():
auth_header = request.headers.get('Authorization')
if not auth_header or not validate_token(auth_header):
return {
"error": "Authentication required"
}, 401, {
'WWW-Authenticate': 'Bearer realm="api"'
}
# Process request...
403 Forbidden: The client is authenticated but doesn't have permission to access the resource.
@app.get("/admin/users")
def get_all_users():
if not current_user.is_admin:
return {
"error": "Insufficient permissions"
}, 403
users = db.get_all_users()
return jsonify(users), 200
404 Not Found: The requested resource doesn't exist at the specified URL.
@app.get("/products/{product_id}")
def get_product(product_id: int):
product = db.get_product(product_id)
if not product:
return {
"error": f"Product {product_id} not found"
}, 404
return jsonify(product.to_dict()), 200
429 Too Many Requests: The client has exceeded rate limits. Should include Retry-After header indicating when to retry.
def rate_limit_middleware():
client_ip = request.remote_addr
if rate_limiter.is_limited(client_ip):
return {
"error": "Rate limit exceeded"
}, 429, {
'Retry-After': str(rate_limiter.get_retry_after(client_ip))
}
500 Internal Server Error: Generic server error when no more specific error is appropriate.
@app.post("/process")
def process_data():
try:
result = complex_processing(request.get_json())
return jsonify(result), 200
except Exception as e:
logger.error(f"Unexpected error: {e}")
return {
"error": "Internal server error"
}, 500
503 Service Unavailable: The server is temporarily unavailable, often due to maintenance or overload.
@app.before_request
def check_maintenance():
if maintenance_mode.is_active():
return {
"error": "Service temporarily unavailable",
"estimated_recovery": maintenance_mode.estimated_recovery_time()
}, 503, {
'Retry-After': str(maintenance_mode.retry_after_seconds())
}
Consistent error response formatting improves developer experience and simplifies client-side error handling. A well-structured error response should include:
class APIError(Exception):
def __init__(self, message, status_code=400, error_code=None, details=None):
self.message = message
self.status_code = status_code
self.error_code = error_code
self.details = details
def error_handler(error):
response = {
"error": {
"code": error.error_code or f"HTTP_{error.status_code}",
"message": error.message,
"timestamp": datetime.utcnow().isoformat()
}
}
if error.details:
response["error"]["details"] = error.details
return jsonify(response), error.status_code
@app.errorhandler(APIError)
def handle_api_error(error):
return error_handler(error)
@app.errorhandler(404)
def handle_not_found(error):
return error_handler(APIError(
"Resource not found",
status_code=404,
error_code="RESOURCE_NOT_FOUND"
))
from pydantic import BaseModel, ValidationError, validator
from typing import List
class CreateUserRequest(BaseModel):
email: str
password: str
roles: List[str] = []
@validator('email')
def validate_email(cls, v):
if not re.match(r'[^@]+@[^@]+\.[^@]+', v):
raise ValueError('Invalid email format')
return v.lower()
@app.post("/users")
def create_user():
try:
user_data = CreateUserRequest.parse_obj(request.get_json())
user = User.create(user_data.dict())
return jsonify(user.to_dict()), 201
except ValidationError as e:
return {
"error": "Validation failed",
"details": e.errors()
}, 400
Implement comprehensive auth error handling with appropriate status codes and security headers.
def require_auth(f):
@wraps(f)
def decorated(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header:
raise APIError(
"Authentication required",
status_code=401,
error_code="AUTH_REQUIRED"
)
try:
token = auth_header.split(' ')[1]
user = verify_token(token)
if not user:
raise APIError(
"Invalid token",
status_code=401,
error_code="INVALID_TOKEN"
)
request.current_user = user
except IndexError:
raise APIError(
"Invalid authorization header format",
status_code=401,
error_code="INVALID_AUTH_HEADER"
)
return f(*args, **kwargs)
return decorated
Implement comprehensive rate limiting with clear error responses and retry information.
class RateLimiter:
def __init__(self, redis_client, limits):
self.redis = redis_client
self.limits = limits # {'ip': 100, 'user': 1000} per hour
def is_limited(self, identifier, limit_type):
key = f"rate_limit:{limit_type}:{identifier}"
current = self.redis.incr(key)
if current == 1:
self.redis.expire(key, 3600) # 1 hour TTL
limit = self.limits.get(limit_type, 100)
return current > limit
def get_retry_after(self, identifier, limit_type):
key = f"rate_limit:{limit_type}:{identifier}"
ttl = self.redis.ttl(key)
return max(ttl, 1)
@app.before_request
def check_rate_limit():
limiter = current_app.rate_limiter
client_ip = request.remote_addr
if limiter.is_limited(client_ip, 'ip'):
raise APIError(
"IP rate limit exceeded",
status_code=429,
error_code="RATE_LIMIT_EXCEEDED",
details={
"retry_after": limiter.get_retry_after(client_ip, 'ip'),
"limit_type": "ip"
}
)
if hasattr(request, 'current_user'):
user_id = request.current_user.id
if limiter.is_limited(user_id, 'user'):
raise APIError(
"User rate limit exceeded",
status_code=429,
error_code="RATE_LIMIT_EXCEEDED",
details={
"retry_after": limiter.get_retry_after(user_id, 'user'),
"limit_type": "user"
}
)
Handle database and external service failures gracefully with appropriate status codes and logging.
@app.post("/orders")
def create_order():
try:
order_data = request.get_json()
with db.transaction():
order = Order.create(order_data)
inventory_service.reserve_items(order.items)
payment_service.process_payment(order.payment_info)
return jsonify(order.to_dict()), 201
except db.IntegrityError as e:
logger.warning(f"Database integrity error: {e}")
raise APIError(
"Data conflict occurred",
status_code=409,
error_code="DATA_CONFLICT"
)
except inventory_service.OutOfStockError as e:
raise APIError(
"Items out of stock",
status_code=422,
error_code="OUT_OF_STOCK",
details={"items": e.out_of_stock_items}
)
except payment_service.PaymentError as e:
raise APIError(
"Payment processing failed",
status_code=402,
error_code="PAYMENT_FAILED",
details={"reason": e.reason}
)
except ExternalServiceError as e:
logger.error(f"External service error: {e}")
raise APIError(
"Service temporarily unavailable",
status_code=503,
error_code="EXTERNAL_SERVICE_UNAVAILABLE"
)
Handle unsupported media types and content negotiation errors appropriately.
@app.before_request
def check_content_type():
if request.method in ['POST', 'PUT', 'PATCH']:
content_type = request.content_type
if not content_type or not content_type.startswith('application/json'):
raise APIError(
"Unsupported media type",
status_code=415,
error_code="UNSUPPORTED_MEDIA_TYPE",
details={"supported_types": ["application/json"]}
)
@app.after_request
def set_content_type(response):
if response.status_code == 406:
response.headers['Content-Type'] = 'application/json'
return response
Create a comprehensive error handling system with custom exception classes and centralized error handling.
class APIError(Exception):
base_message = "An error occurred"
def __init__(self, message=None, status_code=400, error_code=None,
details=None, headers=None):
self.message = message or self.base_message
self.status_code = status_code
self.error_code = error_code or self.__class__.__name__
self.details = details
self.headers = headers or {}
super().__init__(self.message)
class ValidationError(APIError):
base_message = "Validation failed"
status_code = 400
class AuthenticationError(APIError):
base_message = "Authentication required"
status_code = 401
class PermissionError(APIError):
base_message = "Insufficient permissions"
status_code = 403
class NotFoundError(APIError):
base_message = "Resource not found"
status_code = 404
class RateLimitError(APIError):
base_message = "Rate limit exceeded"
status_code = 429
def register_error_handlers(app):
@app.errorhandler(APIError)
def handle_api_error(error):
response = {
"error": {
"code": error.error_code,
"message": error.message,
"timestamp": datetime.utcnow().isoformat()
}
}
if error.details:
response["error"]["details"] = error.details
return jsonify(response), error.status_code, error.headers
@app.errorhandler(404)
def handle_not_found(e):
return handle_api_error(NotFoundError())
@app.errorhandler(500)
def handle_internal_error(e):
logger.error(f"Internal server error: {e}")
return handle_api_error(APIError(
"Internal server error",
status_code=500,
error_code="INTERNAL_ERROR"
))
Comprehensive testing ensures error handling works correctly across all scenarios.
def test_authentication_error(client):
response = client.get('/protected')
assert response.status_code == 401
assert response.json['error']['code'] == 'AUTHENTICATION_ERROR'
assert 'WWW-Authenticate' in response.headers
def test_validation_error(client):
response = client.post('/users', json={'email': 'invalid'})
assert response.status_code == 400
assert response.json['error']['code'] == 'VALIDATION_ERROR'
assert 'details' in response.json['error']
def test_rate_limit(client, redis_mock):
for _ in range(101):
response = client.get('/api/data')
assert response.status_code == 429
assert 'Retry-After' in response.headers
def test_database_error(client, db_mock):
db_mock.side_effect = DatabaseError("Connection failed")
response = client.get('/users/1')
assert response.status_code == 503
assert response.json['error']['code'] == 'SERVICE_UNAVAILABLE'
Implement comprehensive monitoring and logging for error tracking and analysis.
class ErrorLogger:
def __init__(self):
self.logger = logging.getLogger('api.errors')
self.metrics = MetricsClient()
def log_error(self, error, request):
error_data = {
'timestamp': datetime.utcnow().isoformat(),
'status_code': getattr(error, 'status_code', 500),
'error_code': getattr(error, 'error_code', 'UNKNOWN'),
'path': request.path,
'method': request.method,
'client_ip': request.remote_addr,
'user_agent': request.headers.get('User-Agent'),
'error_message': str(error)
}
self.logger.error(json.dumps(error_data))
self.metrics.increment('api.errors', tags={
'status_code': error_data['status_code'],
'error_code': error_data['error_code'],
'path': error_data['path']
})
def error_middleware(app):
error_logger = ErrorLogger()
@app.errorhandler(Exception)
def handle_all_errors(error):
error_logger.log_error(error, request)
if isinstance(error, APIError):
response = error_handler(error)
else:
response = error_handler(APIError(
"Internal server error",
status_code=500,
error_code="INTERNAL_ERROR"
))
return response
Maintain consistent error response structure across all endpoints. Use standardized field names and response formats.
Choose the most specific status code for each error condition. Avoid overusing generic codes like 400 or 500 when more specific codes are available.
Avoid exposing sensitive information in error responses. Sanitize error messages and avoid stack traces in production.
Provide actionable error messages and, when appropriate, include guidance on how to resolve the issue or when to retry.
Document all possible error responses in your API documentation, including status codes, error codes, and potential resolutions.
Implement comprehensive monitoring of error rates and patterns. Set up alerts for unusual error patterns or increased error rates.
By following these practices and continuously refining your error handling approach, you'll build more reliable and maintainable APIs that provide excellent developer experiences.