Error Handling
Request Forge provides a comprehensive exception hierarchy for robust error handling.
Exception Hierarchy
All HTTP Client exceptions inherit from HttpClientException:
HttpClientException (base)
├── MaxRetryException # Maximum retries exceeded
├── TimeoutException # Request timeout
├── ConnectionException # Network/connection errors
├── SSLException # SSL/TLS errors
├── ResponseParseException # JSON/response parsing errors
├── AuthenticationException # Authentication failures
│ └── TokenRefreshException # Token refresh failures
└── HttpStatusException # HTTP error status codes
├── BadRequestException # 400 Bad Request
├── UnauthorizedException # 401 Unauthorized
├── ForbiddenException # 403 Forbidden
├── NotFoundException # 404 Not Found
└── ServerErrorException # 5xx Server Errors
Base Exception
HttpClientException
All exceptions inherit from this base class:
from requestforge import HttpClientException
try:
response = client.get('/users/1')
except HttpClientException as e:
print(f"Error: {e.message}")
print(f"Original exception: {e.original_exception}")
print(f"Context: {e.context}")
Attributes:
message(str): Human-readable error messageoriginal_exception(Exception | None): Original underlying exceptioncontext(dict): Additional context about the error
Network Errors
TimeoutException
Raised when a request times out:
from requestforge import TimeoutException
try:
response = client.get('/slow-endpoint', timeout=5.0)
except TimeoutException as e:
print(f"Request timed out after 5 seconds")
print(f"URL: {e.context.get('request').url}")
When raised:
Request exceeds configured timeout
Connection timeout
Read timeout
Best practices:
# Handle timeout with retry
from requestforge import TimeoutException
import time
max_attempts = 3
for attempt in range(max_attempts):
try:
response = client.get('/data', timeout=10.0)
break
except TimeoutException:
if attempt < max_attempts - 1:
time.sleep(2 ** attempt) # Exponential backoff
continue
raise
ConnectionException
Raised on network connection failures:
from requestforge import ConnectionException
try:
response = client.get('/users')
except ConnectionException as e:
print(f"Network error: {e.message}")
# Check if server is unreachable
if "Connection refused" in str(e.original_exception):
print("Server is not running")
Common causes:
Server is down or unreachable
DNS resolution failure
Network connectivity issues
Firewall blocking connection
Handling:
from requestforge import ConnectionException
import logging
try:
response = client.get('/api/data')
except ConnectionException as e:
logging.error(f"Cannot connect to API: {e}")
# Fallback to cached data or show error to user
return get_cached_data()
SSLException
Raised on SSL/TLS errors:
from requestforge import SSLException
try:
response = client.get('https://self-signed.badssl.com')
except SSLException as e:
print(f"SSL error: {e.message}")
print(f"Original: {e.original_exception}")
Common causes:
Invalid SSL certificate
Self-signed certificate
Certificate expired
Hostname mismatch
Disabling SSL verification (not recommended for production):
config = (
HttpClientConfigBuilder()
.with_base_url('https://api.example.com')
.with_verify_ssl(False) # ⚠️ Security risk!
.build()
)
HTTP Status Errors
HttpStatusException
Base class for HTTP status code errors:
from requestforge import HttpStatusException
try:
response = client.get('/users/999')
except HttpStatusException as e:
print(f"HTTP {e.status_code}: {e.message}")
print(f"Response body: {e.response_body}")
Attributes:
status_code(int): HTTP status coderesponse_body(str | None): Response body contentmessage(str): Error messageoriginal_exception(Exception | None): Original exceptioncontext(dict): Request context
BadRequestException (400)
Raised for 400 Bad Request responses:
from requestforge import BadRequestException
try:
response = client.post('/users', json_data={
'email': 'invalid-email' # Invalid data
})
except BadRequestException as e:
print(f"Invalid request: {e.response_body}")
# Parse error details
error_data = json.loads(e.response_body)
for field, errors in error_data.get('errors', {}).items():
print(f"{field}: {errors}")
ForbiddenException (403)
Raised for 403 Forbidden responses:
from requestforge import ForbiddenException
try:
response = client.delete('/admin/users/1')
except ForbiddenException:
print("Insufficient permissions")
# Show error to user or request elevated access
NotFoundException (404)
Raised for 404 Not Found responses:
from requestforge import NotFoundException
try:
response = client.get('/users/nonexistent')
except NotFoundException:
print("Resource not found")
return None # Or show 404 page
ServerErrorException (5xx)
Raised for 5xx server errors:
from requestforge import ServerErrorException
try:
response = client.get('/api/data')
except ServerErrorException as e:
print(f"Server error {e.status_code}")
if e.status_code == 503:
print("Service temporarily unavailable")
elif e.status_code == 500:
print("Internal server error")
Retry Errors
MaxRetryException
Raised when maximum retry attempts are exceeded:
from requestforge import MaxRetryException
try:
response = client.get('/unstable-endpoint')
except MaxRetryException as e:
print(f"Failed after {e.attempts} attempts")
print(f"Last error: {e.original_exception}")
print(f"Request: {e.context.get('request')}")
Attributes:
attempts(int): Total number of attempts madeoriginal_exception(Exception | None): Last exception that caused failuremessage(str): Error messagecontext(dict): Request context
Example with custom handling:
from requestforge import MaxRetryException, TimeoutException
try:
response = client.get('/api/data')
except MaxRetryException as e:
if isinstance(e.original_exception, TimeoutException):
print("Service is responding too slowly")
# Maybe increase timeout or reduce load
else:
print(f"Service failed: {e.original_exception}")
# Log to monitoring system
Authentication Errors
AuthenticationException
Raised on authentication failures:
from requestforge import AuthenticationException
try:
response = client.get('/protected')
except AuthenticationException as e:
print(f"Auth failed for service: {e.service_name}")
print(f"Reason: {e.message}")
# Clear cached credentials or redirect to login
Attributes:
service_name(str | None): Name of the service that failedmessage(str): Error messageoriginal_exception(Exception | None): Original exceptioncontext(dict): Additional context
TokenRefreshException
Raised when token refresh fails:
from requestforge.token_manager import TokenRefreshException
try:
token = token_manager.get_token()
except TokenRefreshException as e:
print(f"Failed to refresh token: {e}")
# Clear session and redirect to login
Parsing Errors
ResponseParseException
Raised when response parsing fails:
from requestforge import ResponseParseException
try:
response = client.get('/data')
data = response.json() # Expects JSON
except ResponseParseException as e:
print(f"Invalid JSON response: {e.message}")
print(f"Content preview: {e.context.get('content_preview')}")
# Use response.text instead or handle as plain text
Common causes:
Server returns HTML instead of JSON
Malformed JSON
Empty response when JSON expected
Safe parsing:
response = client.get('/data')
# Option 1: Use json_or_none()
data = response.json_or_none()
if data is None:
print("Response is not JSON")
# Option 2: Try-except
try:
data = response.json()
except ResponseParseException:
# Fallback to text
data = response.text
Error Handling Patterns
Specific to General
Handle specific exceptions first, then general:
from requestforge import (
NotFoundException,
UnauthorizedException,
HttpStatusException,
TimeoutException,
HttpClientException
)
try:
response = client.get('/users/1')
user = response.json()
# Most specific first
except NotFoundException:
print("User not found")
return None
except UnauthorizedException:
print("Please login")
redirect_to_login()
# More general HTTP errors
except HttpStatusException as e:
print(f"HTTP error {e.status_code}")
# Network errors
except TimeoutException:
print("Request timed out")
# Catch-all for HTTP client errors
except HttpClientException as e:
print(f"Request failed: {e}")
logger.error(f"Error details: {e.context}")
Retry Pattern
Implement custom retry with specific error handling:
from requestforge import TimeoutException, ServerErrorException
import time
def fetch_with_retry(url, max_retries=3):
last_error = None
for attempt in range(max_retries):
try:
response = client.get(url)
return response.json()
except TimeoutException as e:
print(f"Timeout on attempt {attempt + 1}")
last_error = e
time.sleep(2 ** attempt) # Exponential backoff
except ServerErrorException as e:
if e.status_code == 503: # Service Unavailable
print(f"Service unavailable, attempt {attempt + 1}")
last_error = e
time.sleep(5)
else:
raise # Don't retry other server errors
except HttpClientException as e:
# Don't retry other errors
raise
# Max retries exceeded
raise MaxRetryException(
message=f"Failed after {max_retries} attempts",
attempts=max_retries,
original_exception=last_error
)
Fallback Pattern
Provide fallback data on errors:
from requestforge import HttpClientException
def get_user_data(user_id):
try:
response = client.get(f'/users/{user_id}')
return response.json()
except NotFoundException:
return None
except HttpClientException as e:
logger.error(f"Failed to fetch user {user_id}: {e}")
# Return cached data if available
return get_cached_user_data(user_id)
Circuit Breaker Pattern
Stop making requests after consecutive failures:
from requestforge import HttpClientException
import time
class CircuitBreaker:
def __init__(self, failure_threshold=5, timeout=60):
self.failure_count = 0
self.failure_threshold = failure_threshold
self.timeout = timeout
self.last_failure_time = None
self.state = 'closed' # closed, open, half_open
def call(self, func):
if self.state == 'open':
if time.time() - self.last_failure_time > self.timeout:
self.state = 'half_open'
else:
raise Exception("Circuit breaker is OPEN")
try:
result = func()
self.on_success()
return result
except HttpClientException as e:
self.on_failure()
raise
def on_success(self):
self.failure_count = 0
self.state = 'closed'
def on_failure(self):
self.failure_count += 1
self.last_failure_time = time.time()
if self.failure_count >= self.failure_threshold:
self.state = 'open'
# Usage
breaker = CircuitBreaker()
try:
response = breaker.call(lambda: client.get('/api/data'))
except Exception as e:
print(f"Request failed or circuit open: {e}")
Context Managers
Ensure cleanup even on errors:
from requestforge import http_client, HttpClientException
try:
with http_client('https://api.example.com') as client:
response = client.get('/users')
users = response.json()
# Process users
except HttpClientException as e:
print(f"API error: {e}")
# Client automatically closed
Logging Errors
Basic Logging
import logging
from requestforge import HttpClientException
logger = logging.getLogger(__name__)
try:
response = client.get('/api/data')
except HttpClientException as e:
logger.error(
f"API request failed: {e.message}",
exc_info=True, # Include stack trace
extra={
'url': e.context.get('request', {}).get('url'),
'exception_type': type(e).__name__
}
)
Structured Logging
import structlog
from requestforge import HttpClientException, HttpStatusException
logger = structlog.get_logger()
try:
response = client.get('/users/1')
except HttpStatusException as e:
logger.error(
"http_request_failed",
status_code=e.status_code,
url=e.context.get('request', {}).get('url'),
response_body=e.response_body[:200],
exception_type=type(e).__name__
)
except HttpClientException as e:
logger.error(
"http_request_error",
error=str(e),
exception_type=type(e).__name__,
context=e.context
)
Error Hooks
Use error hooks for centralized logging:
from requestforge.interfaces import ErrorHookInterface
import logging
class ErrorLoggingHook(ErrorHookInterface):
def __init__(self):
self.logger = logging.getLogger('requestforge')
def on_error(self, exception, context):
self.logger.error(
f"Request failed: {type(exception).__name__}",
extra={
'exception': str(exception),
'url': context.request.url,
'method': context.request.method.value,
'attempt': context.attempt,
'max_retries': context.max_retries
}
)
config = builder.with_error_hook(ErrorLoggingHook()).build()
Error Monitoring
Sentry Integration
import sentry_sdk
from requestforge import HttpClientException
from requestforge.interfaces import ErrorHookInterface
class SentryErrorHook(ErrorHookInterface):
def on_error(self, exception, context):
with sentry_sdk.push_scope() as scope:
# Add tags
scope.set_tag("http_method", context.request.method.value)
scope.set_tag("http_url", context.request.url)
# Add context
scope.set_context("request", {
"url": context.request.url,
"method": context.request.method.value,
"attempt": context.attempt,
"max_retries": context.max_retries
})
# Capture exception
sentry_sdk.capture_exception(exception)
config = builder.with_error_hook(SentryErrorHook()).build()
Datadog Integration
from datadog import statsd
from requestforge.interfaces import ErrorHookInterface
class DatadogErrorHook(ErrorHookInterface):
def on_error(self, exception, context):
# Increment error counter
statsd.increment(
'requestforge.error',
tags=[
f'exception_type:{type(exception).__name__}',
f'url:{context.request.url}',
f'method:{context.request.method.value}'
]
)
Best Practices
Handle Specific Exceptions
# Good ✅ - Specific handling try: response = client.get('/users/1') except NotFoundException: return None except UnauthorizedException: redirect_to_login() # Avoid ❌ - Too broad try: response = client.get('/users/1') except Exception: print("Something went wrong")
Don’t Silence Errors
# Good ✅ - Log and re-raise or handle try: response = client.get('/data') except HttpClientException as e: logger.error(f"Request failed: {e}") raise # Avoid ❌ - Silent failure try: response = client.get('/data') except: pass
Provide Context
# Good ✅ - Rich error information try: response = client.get(f'/users/{user_id}') except HttpClientException as e: raise ValueError( f"Failed to fetch user {user_id}: {e.message}" ) from e
Use Appropriate Retry
# Good ✅ - Retry on transient errors config = builder.with_retry(max_retries=3).build() # Transient errors automatically retried: # - TimeoutException # - ConnectionException # - 5xx ServerErrorException
Clean Up Resources
# Good ✅ - Context manager ensures cleanup with http_client('https://api.example.com') as client: response = client.get('/data') # Or explicit cleanup client = create_client('https://api.example.com') try: response = client.get('/data') finally: client.close()
Testing Error Handling
Mock Exceptions
import pytest
from unittest.mock import patch, Mock
from requestforge import TimeoutException, NotFoundException
def test_timeout_handling():
with patch('requestforge.HttpClient.get') as mock_get:
mock_get.side_effect = TimeoutException("Timeout")
# Your code that handles timeout
result = fetch_user_with_timeout_handling(1)
assert result is None
def test_not_found_handling():
with patch('requestforge.HttpClient.get') as mock_get:
mock_get.side_effect = NotFoundException("Not found")
result = get_user(999)
assert result is None
Mock HTTP Responses
import responses
from requestforge import HttpClient, NotFoundException
@responses.activate
def test_404_handling():
responses.add(
responses.GET,
'https://api.example.com/users/999',
status=404,
json={'error': 'User not found'}
)
client = create_client('https://api.example.com')
with pytest.raises(NotFoundException):
client.get('/users/999')
Common Scenarios
API Rate Limiting
from requestforge import HttpStatusException
import time
def api_call_with_rate_limit(url):
while True:
try:
response = client.get(url)
return response.json()
except HttpStatusException as e:
if e.status_code == 429: # Too Many Requests
retry_after = int(e.response.headers.get('Retry-After', 60))
print(f"Rate limited, waiting {retry_after}s")
time.sleep(retry_after)
else:
raise
Partial Failures
from requestforge import HttpClientException
def fetch_multiple_users(user_ids):
results = []
errors = []
for user_id in user_ids:
try:
response = client.get(f'/users/{user_id}')
results.append(response.json())
except HttpClientException as e:
errors.append({
'user_id': user_id,
'error': str(e)
})
return results, errors
Graceful Degradation
from requestforge import HttpClientException
def get_recommendations(user_id):
try:
# Try personalized recommendations
response = client.get(f'/recommendations/{user_id}')
return response.json()
except HttpClientException:
# Fall back to popular items
try:
response = client.get('/recommendations/popular')
return response.json()
except HttpClientException:
# Final fallback to cached data
return get_default_recommendations()
Next Steps
Learn about Concurrent Requests for parallel error handling
Explore Hooks for centralized error handling
Check Custom Retry Examples for advanced error recovery