Advanced
Error Handling

Error Handling

The Intapp REST Client provides structured exceptions for different error scenarios.

Exception Hierarchy

RestClientError (base)
├── HttpError          # HTTP response errors (4xx, 5xx)
├── AuthenticationError # Authentication failures
│   └── TokenExpiredError
├── RequestError       # Network/connection errors
├── ResponseError      # Response parsing errors
├── RetryExhaustedError # All retries failed
└── CircuitBreakerOpenError # Circuit breaker tripped

Basic Error Handling

from intapp_rest_client import (
    RestClient, 
    HttpError, 
    RequestError,
    AuthenticationError
)
 
client = RestClient(base_url="https://api.example.com", api_key="key")
 
try:
    data = client.get("/api/resources")
except HttpError as e:
    print(f"HTTP {e.status_code}: {e.message}")
except RequestError as e:
    print(f"Network error: {e}")
except AuthenticationError as e:
    print(f"Auth failed: {e}")
finally:
    client.close()

HttpError

Raised for HTTP response errors (status codes 4xx and 5xx).

from intapp_rest_client import HttpError
 
try:
    data = client.get("/api/resources")
except HttpError as e:
    print(f"Status: {e.status_code}")
    print(f"Message: {e.message}")
    print(f"Response body: {e.response_body}")
    print(f"Headers: {e.headers}")
    print(f"Request ID: {e.request_id}")
    
    # Special properties
    if e.is_rate_limited:
        print(f"Retry after: {e.retry_after} seconds")
    if e.is_server_error:
        print("Server-side issue")
    if e.is_client_error:
        print("Client-side issue (check your request)")

HttpError Properties

PropertyTypeDescription
status_codeintHTTP status code
messagestrError message
response_bodyAnyResponse body (dict or str)
headersdictResponse headers
request_idstrRequest tracking ID
retry_afterint | NoneSeconds from Retry-After header, if present
is_rate_limitedboolTrue if 429 status
is_server_errorboolTrue if 5xx status
is_client_errorboolTrue if 4xx status
is_retryableboolTrue for typical transient statuses (429, 5xx)

Handling Specific Status Codes

try:
    data = client.get("/api/resources/123")
except HttpError as e:
    if e.status_code == 404:
        print("Resource not found")
    elif e.status_code == 403:
        print("Permission denied")
    elif e.status_code == 429:
        print(f"Rate limited, wait {e.retry_after}s")
    elif e.is_server_error:
        print("Server error, try again later")
    else:
        raise

RequestError

Raised for network and connection issues.

from intapp_rest_client import RequestError
 
try:
    data = client.get("/api/resources")
except RequestError as e:
    print(f"Network error: {e}")
    print(f"Request ID: {e.request_id}")
    # Common causes:
    # - DNS resolution failed
    # - Connection refused
    # - Connection timeout
    # - SSL/TLS errors

AuthenticationError

Raised when authentication fails.

from intapp_rest_client import AuthenticationError, TokenExpiredError
 
try:
    data = client.get("/api/resources")
except TokenExpiredError as e:
    print("Token expired and could not be refreshed")
except AuthenticationError as e:
    print(f"Authentication failed: {e}")
    # Check credentials

RetryExhaustedError

Raised when all retry attempts have been exhausted inside the retry transport.

from intapp_rest_client import RetryExhaustedError
 
try:
    data = client.get("/api/resources")
except RetryExhaustedError as e:
    print(str(e))
    if e.last_error is not None:
        print(f"Last underlying error: {e.last_error}")
đź’ˇ

Use e.last_error for the last low-level exception from the transport. The message on RetryExhaustedError summarizes attempts; there are no attempts or last_status_code attributes.

CircuitBreakerOpenError

Raised only when using RetryTransport / AsyncRetryTransport with a CircuitBreaker. Default RestClient usage does not enable a circuit breaker.

from intapp_rest_client import CircuitBreakerOpenError
 
try:
    ...
except CircuitBreakerOpenError as e:
    print("Circuit breaker is open")
    if e.reset_time is not None:
        print(f"Reset hint: {e.reset_time}s")

Async Error Handling

Error handling works the same in async code:

import asyncio
from intapp_rest_client import RestClient, HttpError, RequestError
 
async def fetch_data():
    async with RestClient(base_url="https://api.example.com", api_key="key") as client:
        try:
            return await client.aget("/api/resources")
        except HttpError as e:
            print(f"HTTP error: {e.status_code}")
            return None
        except RequestError as e:
            print(f"Network error: {e}")
            return None
 
asyncio.run(fetch_data())

Common Patterns

Graceful Degradation

def get_with_fallback(client, primary_endpoint, fallback_endpoint):
    """Try primary endpoint, fall back to secondary."""
    try:
        return client.get(primary_endpoint)
    except HttpError as e:
        if e.status_code >= 500:
            return client.get(fallback_endpoint)
        raise

Retry with Different Parameters

def get_with_reduced_scope(client, endpoint, params):
    """Retry with smaller batch size on failure."""
    try:
        return client.get(endpoint, params=params)
    except HttpError as e:
        if e.status_code == 400 and params.get("limit", 0) > 100:
            # Try with smaller batch
            params["limit"] = 100
            return client.get(endpoint, params=params)
        raise

Logging Errors

import logging
 
logger = logging.getLogger(__name__)
 
def fetch_with_logging(client, endpoint):
    try:
        return client.get(endpoint)
    except HttpError as e:
        logger.error(
            "HTTP error",
            extra={
                "status_code": e.status_code,
                "request_id": e.request_id,
                "endpoint": endpoint
            }
        )
        raise
    except RequestError as e:
        logger.error(
            "Network error",
            extra={
                "request_id": e.request_id,
                "endpoint": endpoint,
                "error": str(e)
            }
        )
        raise

Collecting Errors in Batch Operations

def process_batch(client, items):
    """Process items and collect errors."""
    results = []
    errors = []
    
    for item in items:
        try:
            result = client.post("/api/items", json=item)
            results.append(result)
        except HttpError as e:
            errors.append({
                "item": item,
                "status": e.status_code,
                "message": e.message
            })
    
    return results, errors
 
results, errors = process_batch(client, items)
print(f"Processed {len(results)}, failed {len(errors)}")

Error Response Bodies

Many APIs return structured error responses:

try:
    client.post("/api/resources", json=invalid_data)
except HttpError as e:
    if isinstance(e.response_body, dict):
        error_code = e.response_body.get("errorCode")
        error_details = e.response_body.get("details", [])
        print(f"Error {error_code}: {error_details}")
    else:
        print(f"Error: {e.response_body}")

Best Practices

  1. Catch specific exceptions: Don't catch RestClientError blindly
  2. Log request IDs: Essential for debugging with support
  3. Handle rate limits gracefully: Check is_rate_limited and retry_after
  4. Distinguish client vs server errors: Client errors (4xx) won't succeed on retry
  5. Use structured logging: Include status codes and request IDs

Next Steps