Skip to main content
Browse the documentation

SDKs

Errors and retries

How the SDKs turn problem documents into typed errors, which failures they retry, and how idempotency keys make writes safe to repeat.

Problem documents

When the API refuses a request, it answers with an RFC 9457 problem document (application/problem+json). This is what a request without a key gets back:

{
  "type": "about:blank",
  "title": "Unauthorized",
  "status": 401,
  "detail": "Missing Authorization header, token query parameter, or uarp_session cookie",
  "correlation_id": "<request_id>",
  "code": "AUTH_ERROR"
}
MemberWhat it is for
status, titleThe HTTP status and its reason phrase.
detailThe sentence a person reads. Its wording can change, so do not match on it.
codeThe stable value to switch on, such as SESSION_NOT_ACTIVE or QUOTA_EXCEEDED.
errorsField-level validation failures (field, message), present on 422 responses.
correlation_idThe request id, the same as the X-Request-Id header. Quote it to support.

correlationId is a deprecated spelling of the same value. Every SDK parses all of this into one error object, and keeps any other member the server sent.

Error types per language

SDKAPI refusalNever reached the server
TypeScriptAPIError and subclasses: NotFoundError, UnprocessableEntityError, RateLimitError, …APIConnectionError, APITimeoutError
PythonAPIError and subclasses per statusAPIConnectionError, APITimeoutError
RustError::Api, matched by ApiErrorKindError::Timeout and other variants
KotlinApiException, matched by ApiErrorKindTimeoutException (ai.snaga.uarp)
SwiftUARPError.api, matched by error.kindUARPError.timeout
AdaUARP.Errors.API_ErrorUARP.Errors.Transport_Error

Everything the TypeScript and Python SDKs raise derives from UarpError; in Kotlin, from UarpException. Python adds DecodeError, EncodeError and ConfigError; Ada adds Decoding_Error and Configuration_Error.

Handling an error

import { APIError, RateLimitError, UnprocessableEntityError } from 'uarp-sdk';

try {
  await client.agents.get(id);
} catch (error) {
  if (error instanceof UnprocessableEntityError) {
    console.error(error.validationErrors);      // [{ field, message }]
  } else if (error instanceof RateLimitError) {
    console.error(error.retryAfterSeconds, error.remaining, error.reset);
  } else if (error instanceof APIError && error.code === 'SESSION_NOT_ACTIVE') {
    console.error('the session ended:', error.problem.session_status);
  } else if (error instanceof APIError) {
    console.error(error.status, error.code, error.correlationId, error.problem.detail);
  }
}
from uarp_sdk import APIError, APITimeoutError, NotFoundError, RateLimitError, UnprocessableEntityError

try:
    client.agents.get("missing")
except NotFoundError:
    print("no such agent")
except UnprocessableEntityError as error:
    print(error.validation_errors)
except RateLimitError as error:
    print("retry after", error.retry_after_seconds)
except APIError as error:
    print(error.status, error.code, error.reason, error.correlation_id)
except APITimeoutError:
    print("timed out")
use uarp_sdk::{ApiErrorKind, Error};

match client.agents().get("missing").await {
    Ok(agent) => println!("{}", agent.name),
    Err(Error::Api(api)) => match api.kind() {
        ApiErrorKind::NotFound => println!("no such agent"),
        ApiErrorKind::UnprocessableEntity => println!("{:?}", api.problem.errors),
        ApiErrorKind::RateLimit => println!("retry after {:?}", api.retry_after_seconds()),
        _ => println!("{api}"),
    },
    Err(Error::Timeout) => println!("timed out"),
    Err(other) => println!("{other}"),
}

The Kotlin, Swift and Ada READMEs show the same pattern with their own types.

Retries

The SDKs retry transient failures for you: statuses 408, 409, 429, 500, 502, 503 and 504, and dropped connections. They wait with jittered exponential backoff that starts near 500 ms and is capped at 8 s, and honour the server's Retry-After header when it sends one. If a server or a proxy in between answers with X-Should-Retry: false, that response is not retried — the SDKs honour the header when it is sent; the Snaga API itself does not send it today. The default budget is two retries; change it per client (maxRetries) or per call.

Reads always retry. Writes retry only when they carry an idempotency key, because repeating a write without one could do the work twice.

Two calls never retry: streamPost / stream_post, which would run and bill a model twice, and anything you send with retries set to zero.

Idempotency keys

Every mutating /api/v1/* call the SDKs make sends an Idempotency-Key header, generated once per call and reused across its retries. That is what makes an automatic retry of a write safe. On the server side:

  • The key is bound to the request it was first sent with: method, path with query, and body.
  • The same request again within 24 hours returns the cached response, marked X-Idempotency-Replayed: true, instead of running again.
  • While the first request is still in flight, a repeat gets 202 with Retry-After.
  • The same key on a different request is refused with 422 and code: IDEMPOTENCY_KEY_REUSED.
  • A key is 1–128 characters of letters, digits, _, . and -.

Pass your own key when you want to repeat a create on purpose, for example when your own process restarts and does not know whether its last call landed:

await client.agents.create(body, { idempotencyKey: 'order-4711' });
client.agents.create(body, request_options=RequestOptions(idempotency_key="order-4711"))
client.with_idempotency_key("order-4711").agents().create(&body).await?;

Anonymous operations, such as sign-in exchanges and /public/* calls, do not take the header. For the same rules over plain HTTP, see Requests and errors.