Error Handling
The Saner Python SDK provides a structured exception handling system designed for reliable automation, predictable integrations, and faster debugging. Instead of returning ambiguous failures, the SDK raises clear, categorized exceptions that help developers immediately identify the cause of an issue.
Errors typically fall into five categories:
- validation errors (client-side)
- authentication errors
- API response errors
- retry and request failures, including rate limiting
- client lifecycle errors (using a client after it has been closed)
All SDK exceptions inherit from a single base exception class, making them easy to manage in both simple scripts and enterprise workflows. Every exception also carries structured context (status code, response body, method, URL, attempt count) so you can branch on it programmatically instead of parsing the message string.
Note: Error handling behaves identically across all three client classes (
SanerClient,PlatformClient,CvemClient); the examples below use flat resource access (client.Organization...,client.Patch...) as ifclientis aPlatformClient/CvemClientscoped to that resource's product, since that's the shortest form. On aSanerClientinstance, prefix with the namespace instead:client.platform.Organization...,client.cvem.Patch.... Everything else about the exception shown is unchanged. See the "Client Types" guide for the full comparison.
Exception Hierarchy
The SDK defines a clean exception structure:
SanerError
├── SanerAuthError
├── SanerRequestError
│ └── SanerRateLimitError
├── SanerResponseError
└── SanerClientClosedErrorThis allows developers to:
- catch all SDK errors together
- handle authentication failures separately
- detect rate limiting precisely, distinct from other network issues
- detect retry/network issues precisely
- identify malformed API responses quickly
- catch reuse of a closed client separately from a genuine request failure
Example:
from saner.exceptions import SanerError
try:
response = client.Organization.get(organization="Example")
except SanerError as e:
print(e)Structured Exception Attributes
Every SanerError (and its subclasses) carries the following attributes, populated whenever the information is available:
| Attribute | Type | Description |
|---|---|---|
message | str | Human-readable description of the failure |
status_code | int | None | HTTP status code, if a response was received |
response_body | str | None | Response body, redacted the same way logs are and truncated for very large bodies |
method | str | None | HTTP method or RPC method name involved |
url | str | None | Fully-qualified request URL |
attempts | int | None | Number of attempts actually made before the error was raised |
Note:
SanerClientClosedErroris normally raised before any request is attempted, so itsstatus_code,response_body,method,url, andattemptsare allNone. The exception is a call that was already retrying when another thread closed the client: thenmethod,urlandattemptsdescribe that call (see "Client Lifecycle Errors" below). Every other exception populates whichever of them apply.
Because response_body goes through the same redaction as the SDK's own logs, it's safe to forward a caught exception's fields into your own logging or APM stack (Sentry, Datadog, etc.) without re-checking it for credentials first. A clean body is passed through byte-for-byte, so this doesn't affect debugging output that never had a secret in it.
This means production error handlers can act on structured data instead of parsing text:
from saner.exceptions import SanerRequestError
try:
response = client.Patch.getRemediationJobStatus(accountid="Default", name="job1")
except SanerRequestError as e:
print("status:", e.status_code)
print("attempts:", e.attempts)
print("endpoint:", e.method, e.url)Validation Errors (Client-Side)
Before sending a request, the SDK validates parameters locally using its validation engine.
Validation errors occur when:
- required parameters are missing
- incorrect parameter types are provided
- enum values are invalid
- formats (email, IP, MAC, date) are incorrect
- password strength requirements fail
- list/dict structures are invalid
- a parameter group requiring at least one of several alternatives has none supplied
Example:
response = client.Organization.get(organization=123)Error:
TypeError: organization must be string; got 123 (int)Where a validation error can name the value it rejected, it does, so the correction never has to be re-derived. Credentials and email addresses are never echoed, and length rules report the length rather than the value. See the "Parameter Validation System" guide for the full rules.
These errors stop execution immediately and prevent invalid requests from reaching the server.
Client Lifecycle Errors
Closing a client is final. Once close() has been called, or once a with block has exited, any further request raises SanerClientClosedError rather than transparently opening a new connection:
from saner import SanerClient, SanerClientClosedError
client = SanerClient(api_key="YOUR_API_KEY", accountid="YOUR_ACCOUNT")
client.close()
try:
client.platform.Organization.get(organization="Example")
except SanerClientClosedError as e:
print(e)Error:
SanerClient has been closed; create a new client instead of reusing a closed oneThis is normally raised before any network activity, so nothing reached the server.
One case is different. If a client is shared between threads and one thread calls close() while another thread's call is waiting to retry, that call stops and raises SanerClientClosedError instead of retrying. Its earlier attempts were sent and may have been applied, so the error says so in its message, attempts counts them, method and url name the call, and __cause__ holds the failure that triggered the retry (for example the SanerRequestError for an HTTP 503). Check attempts before re-issuing a write on a new client:
try:
response = client.Patch.createRemediationJob(...)
except SanerClientClosedError as e:
if e.attempts:
# The call was interrupted mid-retry and may already have been applied.
print(f"{e.method} was sent {e.attempts} time(s); cause: {e.__cause__!r}")Use client.closed to check the state without triggering the error:
if not client.closed:
response = client.platform.Organization.get(organization="Example")The most common way to encounter this is a client reference that outlives its with block. Because the error is explicit, that mistake surfaces the first time it happens rather than working by accident and failing later under load.
Authentication Errors
Authentication failures occur when API credentials are invalid or permissions are insufficient.
Raised exception:
SanerAuthErrorTriggered by:
- HTTP 401 (Unauthorized)
- HTTP 403 (Forbidden)
Example:
from saner.exceptions import SanerAuthError
try:
response = client.Organization.get(organization="Example")
except SanerAuthError as e:
print("Authentication failed:", e)Authentication errors are never retried automatically because credentials must be corrected manually.
Rate Limit Errors
Rate limit failures occur when the Saner API has throttled your account due to too many requests.
Raised exception:
SanerRateLimitErrorTriggered by:
- HTTP 429 (Too Many Requests)
Unlike other failures, HTTP 429 responses are automatically retried by the SDK before this exception is ever raised, up to max_retries times, honoring the server's Retry-After header when present (capped at 60 seconds per attempt). SanerRateLimitError is only raised once retries are exhausted.
It carries one extra attribute beyond the base exception:
| Attribute | Type | Description |
|---|---|---|
retry_after | float | None | Seconds the server asked you to wait, parsed from Retry-After, if provided |
Example:
from saner.exceptions import SanerRateLimitError
try:
response = client.Vulnerability.getAssets()
except SanerRateLimitError as e:
print("Rate limited. Retry after:", e.retry_after, "seconds")SanerRateLimitError is a subclass of SanerRequestError, so existing code that catches SanerRequestError will continue to catch rate-limit failures without any changes.
See the "Retry and Timeout Behaviour" guide for the full retry policy.
API Response Errors
Response errors occur when the server returns a response that cannot be parsed correctly.
Raised exception:
SanerResponseErrorTypical causes:
- invalid JSON returned by server
- unexpected response structure
- corrupted response payload
- the response body exceeded the client's
max_response_byteslimit (see the "Client Configuration" guide), raised before or immediately after the oversized body would have been fully read, so it never sits in memory
Example:
from saner.exceptions import SanerResponseError
try:
response = client.Organization.get(organization="Example")
except SanerResponseError as e:
print("Invalid response received:", e)These errors indicate a server formatting issue rather than a connectivity failure.
Retry Failures and Request Errors
Request-level failures occur when communication with the server fails even after automatic retries.
Raised exception:
SanerRequestErrorTriggered by:
- connection failures
- request timeouts
- SSL verification failures
- retry exhaustion after HTTP 500 / 502 / 503 / 504 responses
- unexpected transport-level errors
Example:
from saner.exceptions import SanerRequestError
try:
response = client.Organization.get(organization="Example")
except SanerRequestError as e:
print("Request failed:", e)Note: Not every failing request is retried the same number of times. Read-only and other idempotent calls are always retried on a transient failure; calls that create or modify data are only retried when they are known not to have reached the server, or when the client is configured with
retry_on_write=True. See the "Retry and Timeout Behaviour" guide for the full policy. This affects how many attempts you'll see beforeSanerRequestErroris raised.
Example: Basic Error Handling Pattern
For lightweight scripts:
try:
response = client.Organization.get(organization="Example")
except Exception as e:
print(e)This captures all SDK errors and validation failures.
Example: Production-Grade Error Handling Pattern
For automation pipelines and integrations at scale:
from saner.exceptions import (
SanerAuthError,
SanerRateLimitError,
SanerRequestError,
SanerResponseError,
)
try:
response = client.Organization.get(organization="Example")
except SanerAuthError:
print("Check API credentials or permissions")
except SanerRateLimitError as e:
print(f"Rate limited, back off for {e.retry_after or 'a while'} seconds")
except SanerRequestError as e:
print(f"Network issue or retry limit reached (status={e.status_code}, attempts={e.attempts})")
except SanerResponseError:
print("Unexpected server response format")Note:
SanerRateLimitErrormust be caught beforeSanerRequestErrorin anexceptchain, since it is a subclass of it. Python matches the first applicableexceptclause in order.
This enables smarter recovery strategies depending on failure type.
Example Error Messages Returned by the SDK
The SDK produces clear, human-readable error messages such as:
organization must be string; got 123 (int)
organization cannot be empty
email must be a valid email
password must contain a special character
ScanDeviceModel requires at least one of: hostname, ip, mac. ...
SanerClient has been closed; create a new client instead of reusing a closed one
Authentication failed (HTTP 401)
Rate limited (HTTP 429) after 4 attempts: ...
HTTP error 503 after 4 attempts
Timeout after 4 attempts
SSL error: certificate verify failedNote: The attempt count in these messages reflects attempts actually made, not always
max_retries + 1. A write operation that isn't eligible for retry (see the "Retry and Timeout Behaviour" guide) can fail after a single attempt even withmax_retries=3configured.
These messages help developers quickly identify integration mistakes without inspecting raw HTTP responses.
Benefits for Automation and Production Workflows
The structured exception system helps developers:
- detect failures instantly
- distinguish validation vs authentication vs rate-limiting vs network issues
- catch use of a closed client instead of silently reconnecting
- implement retry-aware automation logic
- reduce debugging time
- build resilient multi-environment integrations
- maintain predictable SDK behavior at scale
This makes the Saner Python SDK well-suited for both simple scripts and enterprise automation pipelines.
Updated 6 days ago