Skip to main content

Error Handling

Exceptions, pagination, and response patterns for the Strongly Python SDK.

Exception Hierarchy

All SDK exceptions inherit from a common base, making it easy to catch specific errors or handle broad categories.

StronglyError
|-- APIError
| |-- ValidationError (400)
| |-- AuthenticationError (401)
| |-- PermissionDeniedError (403)
| |-- NotFoundError (404)
| |-- RequestTimeoutError (408)
| |-- ConflictError (409)
| |-- PayloadTooLargeError (413)
| |-- UnsupportedMediaTypeError (415)
| |-- UnprocessableEntityError (422)
| |-- RateLimitError (429)
| |-- InternalServerError (500)
| |-- BadGatewayError (502)
| |-- ServiceUnavailableError (503)
|
|-- TimeoutError
|-- ConnectionError
|-- ConfigurationError

All exceptions are available at the top level:

from strongly import (
StronglyError,
APIError,
ValidationError,
AuthenticationError,
PermissionDeniedError,
NotFoundError,
RequestTimeoutError,
ConflictError,
PayloadTooLargeError,
UnsupportedMediaTypeError,
UnprocessableEntityError,
RateLimitError,
InternalServerError,
BadGatewayError,
ServiceUnavailableError,
TimeoutError,
ConnectionError,
ConfigurationError,
)

Base Exception

StronglyError

The root exception for all SDK errors.

FieldTypeDescription
messagestrHuman-readable error message
from strongly import Strongly, StronglyError

client = Strongly()

try:
client.apps.retrieve("app-abc123")
except StronglyError as e:
print(f"Something went wrong: {e.message}")

API Errors

APIError

Base class for all HTTP errors returned by the Strongly API. Extends StronglyError.

FieldTypeDescription
messagestrHuman-readable error message
status_codeintHTTP status code
error_codeOptional[str]Machine-readable error code from the API
request_idOptional[str]Request ID for support and debugging
bodyOptional[dict]Full response body from the API
from strongly import Strongly, APIError

client = Strongly()

try:
client.apps.retrieve("app-abc123")
except APIError as e:
print(f"HTTP {e.status_code}: {e.message}")
if e.request_id:
print(f"Request ID: {e.request_id}")
if e.body:
print(f"Response body: {e.body}")

HTTP Exceptions

All HTTP exceptions extend APIError and carry the same fields. Each has a default status code and message.

ExceptionStatus CodeDefault MessageExtra Fields
ValidationError400"Validation error"details: List[dict] (defaults to [])
AuthenticationError401"Invalid or missing API key"--
PermissionDeniedError403"Permission denied"--
NotFoundError404"Resource not found"--
RequestTimeoutError408"Request timeout"--
ConflictError409"Resource conflict"--
PayloadTooLargeError413"Payload too large"--
UnsupportedMediaTypeError415"Unsupported media type"--
UnprocessableEntityError422"Unprocessable entity"--
RateLimitError429"Rate limit exceeded"retry_after: Optional[float]
InternalServerError500"Internal server error"--
BadGatewayError502"Bad gateway"--
ServiceUnavailableError503"Service unavailable"--

Transport Errors

Transport errors extend StronglyError directly. They do not have a status_code because the request never received an HTTP response.

ExceptionDescription
TimeoutErrorThe request timed out before the server responded
ConnectionErrorCould not connect to the Strongly API server
ConfigurationErrorMissing credentials, invalid base URL, or other configuration problem
from strongly import Strongly, TimeoutError, ConnectionError, ConfigurationError

try:
client = Strongly(api_key="invalid", base_url="https://bad-url.example.com")
client.apps.list().to_list()
except ConfigurationError as e:
print(f"Configuration problem: {e.message}")
except ConnectionError as e:
print(f"Cannot reach server: {e.message}")
except TimeoutError as e:
print(f"Request timed out: {e.message}")

Comprehensive Error Handling

Catching Specific Errors

Handle each error type with the appropriate recovery strategy:

from strongly import (
Strongly,
NotFoundError,
AuthenticationError,
PermissionDeniedError,
RateLimitError,
ValidationError,
ConflictError,
InternalServerError,
ServiceUnavailableError,
TimeoutError,
ConnectionError,
)

client = Strongly()

try:
app = client.apps.retrieve("app-abc123")
except NotFoundError as e:
print(f"Not found: {e.message}")
except AuthenticationError:
print("Check your API key")
except PermissionDeniedError:
print("Your key does not have permission for this operation")
except ValidationError as e:
print(f"Invalid input: {e.message}")
for detail in e.details:
print(f" Field: {detail.get('field')}{detail.get('message')}")
except ConflictError as e:
print(f"Conflict: {e.message}")
except RateLimitError as e:
print(f"Rate limited — retry after {e.retry_after}s")
except InternalServerError as e:
print(f"Server error (request_id={e.request_id}): {e.message}")
except ServiceUnavailableError:
print("Service is temporarily unavailable, try again later")
except TimeoutError:
print("Request timed out — consider increasing the timeout")
except ConnectionError:
print("Cannot reach the API — check your network")

Broad Catch with APIError

If you want a single handler for all HTTP errors:

from strongly import Strongly, APIError

client = Strongly()

try:
client.workflows.execute("wf-abc123")
except APIError as e:
print(f"API error {e.status_code}: {e.message}")
if e.request_id:
print(f"Include this in support requests: {e.request_id}")

Broadest Catch with StronglyError

Catch everything (HTTP, transport, and configuration errors):

from strongly import Strongly, StronglyError

client = Strongly()

try:
client.workflows.execute("wf-abc123")
except StronglyError as e:
print(f"Error: {e.message}")

Retry Patterns

Manual Retry with Rate Limiting

import time
from strongly import Strongly, RateLimitError, APIError

client = Strongly()

def call_with_retry(fn, max_retries=3):
for attempt in range(max_retries):
try:
return fn()
except RateLimitError as e:
wait = e.retry_after or (2 ** attempt)
print(f"Rate limited. Waiting {wait}s (attempt {attempt + 1}/{max_retries})")
time.sleep(wait)
except APIError as e:
if e.status_code >= 500 and attempt < max_retries - 1:
wait = 2 ** attempt
print(f"Server error {e.status_code}. Retrying in {wait}s...")
time.sleep(wait)
else:
raise
raise RuntimeError("Max retries exceeded")

# Usage
app = call_with_retry(lambda: client.apps.retrieve("app-abc123"))
print(f"App: {app.name}")

Exponential Backoff for Server Errors

import time
from strongly import (
Strongly,
RateLimitError,
InternalServerError,
BadGatewayError,
ServiceUnavailableError,
TimeoutError,
)

RETRYABLE = (RateLimitError, InternalServerError, BadGatewayError, ServiceUnavailableError, TimeoutError)

client = Strongly()

def robust_call(fn, max_retries=5, base_delay=1.0):
for attempt in range(max_retries):
try:
return fn()
except RateLimitError as e:
delay = e.retry_after or base_delay * (2 ** attempt)
print(f"Rate limited. Waiting {delay:.1f}s...")
time.sleep(delay)
except RETRYABLE:
if attempt == max_retries - 1:
raise
delay = base_delay * (2 ** attempt)
print(f"Retryable error. Waiting {delay:.1f}s...")
time.sleep(delay)

# Usage
result = robust_call(lambda: client.workflows.execute("wf-abc123"))
print(f"Execution started: {result['executionId']}")

Built-in Retries

The client itself retries on server errors (5xx) automatically:

from strongly import Strongly

# Default: 3 retries with exponential backoff
client = Strongly()

# Custom retry count
client = Strongly(max_retries=5)

# Disable retries entirely
client = Strongly(max_retries=0)

Built-in retries apply to transient server errors (408, 429, 500, 502, 503) and connection timeouts. They do not retry other client errors (4xx). Retry delays use exponential backoff with jitter.


Pagination

List methods return auto-paginating iterators that fetch results in batches automatically. The SDK provides SyncPaginator[T] for synchronous code and AsyncPaginator[T] for async.

SyncPaginator

SyncPaginator[T](
*,
fetch_page, # Callable that fetches a page of results
model, # The model class for deserialization
params=None, # Optional query parameters
page_size=50, # Number of items per page
)
Method / PropertyReturn TypeDescription
__iter__()Iterator[T]Iterate over all items across all pages
__next__()TGet the next item
to_list()List[T]Eagerly fetch all pages and return a flat list
first()Optional[T]Return the first item, or None if empty
totalOptional[int]Total item count (available after the first page loads)

AsyncPaginator

Same interface but with async methods:

Method / PropertyReturn TypeDescription
__aiter__()AsyncIterator[T]Async iterate over all items
__anext__()TGet the next item asynchronously
async to_list()List[T]Eagerly fetch all pages asynchronously
async first()Optional[T]Return the first item, or None if empty
totalOptional[int]Total item count (available after the first page loads)

Iterating with a For Loop

The most common pattern -- iterate through all results automatically:

from strongly import Strongly

client = Strongly()

for app in client.apps.list():
print(f"{app.name}{app.status}")

The paginator fetches pages behind the scenes as you iterate. You never need to manage page tokens or offsets.

Getting All Results as a List

Use to_list() to eagerly consume all pages into a Python list:

from strongly import Strongly

client = Strongly()

all_workflows = client.workflows.list(status="active").to_list()
print(f"Found {len(all_workflows)} active workflows")

Getting the First Item

Use first() to grab just the first matching result without fetching everything:

from strongly import Strongly

client = Strongly()

latest = client.executions.list(workflow_id="wf-abc123", status="completed").first()
if latest:
print(f"Last execution: {latest.id}{latest.ended_at}")
else:
print("No completed executions found")

Checking the Total Count

The total property is available after the first page has been fetched:

from strongly import Strongly

client = Strongly()

paginator = client.addons.list(type="postgresql")
next(paginator) # fetch the first page
print(f"Total PostgreSQL addons: {paginator.total}")

Controlling Batch Size

Pass limit to control how many items are fetched per page:

from strongly import Strongly

client = Strongly()

# Fetch in batches of 10
for addon in client.addons.list(limit=10):
print(addon.label)

Async Iteration

With AsyncStrongly, use async for and await:

import asyncio
from strongly import AsyncStrongly

async def main():
async with AsyncStrongly() as client:
# Async for loop
async for wf in client.workflows.list(status="active"):
print(wf.name)

# Async to_list
all_apps = await client.apps.list().to_list()
print(f"Total apps: {len(all_apps)}")

# Async first
latest = await client.executions.list(status="running").first()
if latest:
print(f"Running: {latest.id}")

asyncio.run(main())

Response Patterns

Dictionary Responses

Some methods return plain dict objects (create, delete, deploy, and action methods):

from strongly import Strongly

client = Strongly()

result = client.workflows.execute("wf-abc123")
execution_id = result["executionId"]
print(f"Execution started: {execution_id}")

Model Responses

Retrieve and update methods return typed model objects with attribute access:

from strongly import Strongly

client = Strongly()

app = client.apps.retrieve("app-abc123")
print(app.name) # Attribute access
print(app.status)
print(app.created_at)

Combining Patterns

A typical flow mixing both response types:

from strongly import Strongly, NotFoundError, RateLimitError
import time

client = Strongly()

# Create returns a model
app = client.apps.create({
"name": "my-api",
"image": "my-org/my-api:latest",
"port": 8080,
})

# Deploy returns a dict
client.apps.deploy(app.id)

# Poll status (returns a model)
while True:
try:
status = client.apps.status(app.id)
if status.ready_replicas and status.ready_replicas > 0:
print(f"Live at {status.url}")
break
print(f"State: {status.state}...")
time.sleep(5)
except RateLimitError as e:
time.sleep(e.retry_after or 5)

Complete Example

import time
from strongly import (
Strongly,
NotFoundError,
AuthenticationError,
RateLimitError,
ValidationError,
APIError,
StronglyError,
)

def main():
# --- Initialize client ---
try:
client = Strongly()
me = client.auth.whoami()
print(f"Authenticated as {me.email}")
except AuthenticationError:
print("Invalid API key. Check STRONGLY_API_KEY.")
return

# --- List with pagination ---
print("\nActive workflows:")
paginator = client.workflows.list(status="active")
all_workflows = paginator.to_list()
print(f"Found {len(all_workflows)} active workflows")

for wf in all_workflows[:5]:
print(f" {wf.name} (ID: {wf.id})")

# --- Handle specific errors ---
print("\nLooking up a workflow...")
try:
wf = client.workflows.retrieve("wf-does-not-exist")
except NotFoundError as e:
print(f"Not found: {e.message}")

# --- Handle validation errors ---
print("\nCreating an invalid workflow...")
try:
client.workflows.create({}) # missing required fields
except ValidationError as e:
print(f"Validation failed: {e.message}")
for detail in e.details:
print(f" {detail}")

# --- Retry on rate limiting ---
print("\nExecuting workflows with retry...")
if all_workflows:
wf_id = all_workflows[0].id
for attempt in range(3):
try:
result = client.workflows.execute(wf_id)
print(f"Execution started: {result['executionId']}")
break
except RateLimitError as e:
wait = e.retry_after or 2
print(f"Rate limited. Waiting {wait}s...")
time.sleep(wait)

# --- Pagination patterns ---
print("\nPagination examples:")

# for loop
count = 0
for app in client.apps.list(limit=5):
count += 1
print(f" Iterated over {count} apps")

# first()
first_addon = client.addons.list().first()
if first_addon:
print(f" First addon: {first_addon.label}")

# total count
pg = client.apps.list()
next(pg)
print(f" Total apps: {pg.total}")

# --- Catch-all ---
print("\nDone.")

if __name__ == "__main__":
main()