> ## Documentation Index
> Fetch the complete documentation index at: https://docs.chappiesdk.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Handle ChappieClientError and Auth Errors in Chappie

> Handle ChappieClientError and ChappieAuthError, inspect usage limits, configure rate-limit backoff, and handle stream cancellation gracefully.

Chappie uses two typed error enums for failures you are expected to handle in production code. `ChappieClientError` covers everything that can go wrong during a request — missing credentials, usage caps, tool failures, transport errors, and unexpected server responses. `ChappieAuthError` covers failures during the sign-in flow itself. Both conform to `LocalizedError`, so `error.localizedDescription` always returns a human-readable string suitable for logging, even if you do not exhaustively switch on every case.

## Catching ChappieClientError

All `ChappieClient` methods that communicate with the network are `async throws`. Wrap them in a `do`/`catch` block and match the cases you care about first, then fall through to a generic handler for anything unexpected.

```swift theme={null}
do {
    let reply = try await client.send("Hello")
} catch ChappieClientError.missingCredential {
    // User needs to sign in
} catch ChappieClientError.usageLimitReached(let limit) {
    print(limit.plan?.displayName ?? "Unknown")
    print("Resets:", limit.resetsAt as Any)
} catch ChappieClientError.reauthenticationRequired(let message) {
    // Token expired beyond refresh — show sign-in again
} catch ChappieClientError.httpError(let status, let body) {
    print("HTTP \(status): \(body)")
} catch ChappieClientError.streamingTransportReplayDiverged(let partial) {
    // Streaming reconnect diverged — show error, partial text may be non-empty
} catch ChappieClientError.toolLoopLimitExceeded(let limit) {
    print("Exceeded \(limit) tool call rounds")
} catch {
    print(error.localizedDescription)
}
```

## ChappieClientError Cases

Every case in `ChappieClientError` maps to a distinct failure mode. Expand a case to see what it means and how to respond.

<Accordion title="missingCredential">
  The client attempted a request but found no stored credential in Keychain. The user has never signed in, or credentials were cleared.

  **Response:** Direct the user to sign in with `ChappieSignIn` or by calling `authSession.startDeviceCodeSignIn()`.
</Accordion>

<Accordion title="missingRefreshToken">
  A credential exists in Keychain but it contains no refresh token, so the SDK cannot renew an expired access token.

  **Response:** Ask the user to sign in again. Treat this the same as `missingCredential` in your UI.
</Accordion>

<Accordion title="missingAccountID">
  Stored credentials do not include an account ID. This can happen if account metadata was partially written during an earlier interrupted sign-in.

  **Response:** Sign the user out and prompt them to sign in again to refresh the full credential set.
</Accordion>

<Accordion title="reauthenticationRequired(String)">
  The access token has expired and the SDK could not refresh it — for example, because the refresh token has been revoked on the server. The associated `String` value is a human-readable reason.

  **Response:** Show the reason string to the user if it is informative, then present the sign-in flow again.

  ```swift theme={null}
  catch ChappieClientError.reauthenticationRequired(let message) {
      showSignInAlert(reason: message)
  }
  ```
</Accordion>

<Accordion title="usageLimitReached(ChappieUsageLimit)">
  The ChatGPT usage cap for the user's plan has been reached. The associated `ChappieUsageLimit` value carries plan, reset time, and window data — see [ChappieUsageLimit fields](#chappieusagelimit-fields) below.

  **Response:** Show the user their plan name and when their quota resets. Avoid retrying immediately; the SDK's built-in backoff already attempted retries before surfacing this error.

  ```swift theme={null}
  catch ChappieClientError.usageLimitReached(let limit) {
      let planName = limit.plan?.displayName ?? "your current plan"
      let resetTime = limit.resetsAt.map { $0.formatted(date: .abbreviated, time: .shortened) }
      showUsageBanner(plan: planName, resetsAt: resetTime)
  }
  ```
</Accordion>

<Accordion title="missingToolHandler(String)">
  The model called a tool by name, but no matching `ChappieHostTool` was registered on the client. The associated `String` is the tool name.

  **Response:** Register a handler for every tool declared in your `ChappieHarness`. If the tool is optional, register a handler that returns a graceful "unavailable" result rather than omitting the registration entirely.
</Accordion>

<Accordion title="toolApprovalDenied(String, String)">
  A tool call was blocked — either by your app's approval policy or by an explicit user denial. The first `String` is the tool name; the second is the denial reason.

  **Response:** Log the denial for debugging. In most cases this is expected behaviour when a user declines a permission prompt; no recovery action is needed.
</Accordion>

<Accordion title="toolExecutionFailed(String, String)">
  A registered tool handler threw an error during execution. The first `String` is the tool name; the second is the error message from the handler.

  **Response:** Investigate the handler implementation. You can surface a user-facing error message using the tool name, but avoid exposing raw internal error strings to users.
</Accordion>

<Accordion title="toolLoopLimitExceeded(Int)">
  The model kept calling tools beyond the SDK's configured maximum number of tool-call rounds. The associated `Int` is the limit that was exceeded.

  **Response:** Show a generic "could not complete" message. If this happens regularly, review whether your tool definitions create loops, or increase the loop limit in client configuration.

  ```swift theme={null}
  catch ChappieClientError.toolLoopLimitExceeded(let limit) {
      print("Model exceeded \(limit) tool call rounds — possible tool loop")
  }
  ```
</Accordion>

<Accordion title="responseTerminalFailure(ChappieResponse)">
  The response stream ended with a terminal error status rather than a successful completion. The associated `ChappieResponse` contains the raw status and any error payload.

  **Response:** Log the response for debugging. Show a generic retry prompt to the user; the failure is likely transient.
</Accordion>

<Accordion title="streamingTransportReplayDiverged(String)">
  After a WebSocket disconnect and reconnect, the replayed text did not match what was already delivered to your app. Chappie surfaces this error instead of emitting corrupted output. The associated `String` contains any partial text that was emitted before the divergence.

  **Response:** Show an error and give the user the option to retry. You can use the partial text string to reconstruct a best-effort display if needed.

  ```swift theme={null}
  catch ChappieClientError.streamingTransportReplayDiverged(let partial) {
      if partial.isEmpty {
          showRetryPrompt()
      } else {
          showPartialResultWithRetry(partial)
      }
  }
  ```
</Accordion>

<Accordion title="httpError(Int, String)">
  The server returned an HTTP error response that the SDK could not map to a more specific error. The `Int` is the HTTP status code; the `String` is the response body.

  **Response:** Log the status and body. For `5xx` errors, show a "service unavailable" message. For `4xx` errors (other than `401` and `429`, which the SDK handles automatically), check whether the request was well-formed.

  ```swift theme={null}
  catch ChappieClientError.httpError(let status, let body) {
      logger.error("HTTP \(status): \(body)")
      showGenericNetworkError()
  }
  ```
</Accordion>

<Accordion title="invalidResponse">
  The server returned a response the SDK could not parse. This is distinct from an HTTP error — the HTTP status was likely successful, but the body was malformed or had an unexpected shape.

  **Response:** Show a generic error. File a bug with Chappie support if this occurs consistently.
</Accordion>

## ChappieUsageLimit Fields

When you catch `usageLimitReached`, the associated `ChappieUsageLimit` value gives you structured data to build a meaningful error UI.

<ParamField path="type" type="String">
  The error type string from the server. Typically `"usage_limit_reached"`.
</ParamField>

<ParamField path="message" type="String?">
  A human-readable description of the limit that was hit. Safe to display directly to users when present.
</ParamField>

<ParamField path="plan" type="ChappiePlan?">
  The user's current ChatGPT plan at the time the limit was reached. Use `plan?.displayName` for a user-facing plan name.
</ParamField>

<ParamField path="resetsAt" type="Date?">
  The date and time when the quota resets. Format this with `Date.FormatStyle` before showing it in your UI.
</ParamField>

<ParamField path="resetsInSeconds" type="TimeInterval?">
  Seconds until the quota resets, as reported by the server at response time. Useful for countdown timers; note that it is a point-in-time value, not a live countdown.
</ParamField>

<ParamField path="rateLimit" type="ChappieUsageLimitSnapshot?">
  Detailed window data with primary and secondary usage windows. Each window exposes `usedPercent`, `windowMinutes`, and `resetsAt`. Useful for building progress-bar or usage-meter UIs.
</ParamField>

## Auth Errors (ChappieAuthError)

`ChappieAuthError` is thrown by auth-session methods such as `startDeviceCodeSignIn()` and `refreshAuth()`. Handle it wherever you initiate or observe the sign-in flow.

<Accordion title="expiredDeviceCode">
  The device code shown to the user was not approved before it expired.

  **Response:** Restart the sign-in flow. The expiry window is set by the authorization server, not the SDK.
</Accordion>

<Accordion title="cancelledSignIn">
  The user cancelled the sign-in flow, or a `CancellationError` was thrown during signing in.

  **Response:** Return to the signed-out state silently. No error UI is needed unless the cancellation was unexpected.
</Accordion>

<Accordion title="networkFailure(String)">
  A `URLError` occurred during an auth network call. The associated `String` is the localized description from `URLError`.

  **Response:** Show a network-error message and offer a retry.
</Accordion>

<Accordion title="pollingTimeout">
  The SDK polled the authorization server for device-code approval until the maximum polling duration was reached without a response.

  **Response:** Show a timeout message and offer the user a fresh sign-in attempt.
</Accordion>

<Accordion title="callbackTimeout">
  The SDK timed out waiting for the ChatGPT sign-in callback to arrive after the user approved the device code in their browser.

  **Response:** Show a timeout message and offer the user a fresh sign-in attempt.
</Accordion>

<Accordion title="missingRefreshToken">
  The token exchange succeeded but the response did not include a refresh token.

  **Response:** Prompt the user to sign in again from scratch.
</Accordion>

<Accordion title="tokenExchangeFailed(statusCode:body:)">
  The OAuth token-exchange request returned an error HTTP status. `statusCode` is the HTTP status; `body` is the raw response body.

  **Response:** Log the status and body. For `400`/`401`/`403` responses, the `requiresReauthentication` property on the error is `true` — treat them as a sign-out and re-sign-in scenario.
</Accordion>

<Accordion title="authorizationDenied(String)">
  The user explicitly denied the authorization request. The associated `String` is the denial message.

  **Response:** Respect the user's decision. Show a message explaining that the app needs ChatGPT access to function, but do not re-prompt without a deliberate user action.
</Accordion>

<Accordion title="secureRandomFailed(statusCode:)">
  Secure random data generation failed at the OS level. The associated `statusCode` is the `Int32` status returned by the system call.

  **Response:** Show a generic sign-in error. This is extremely rare; log the status code and file a bug if it recurs.
</Accordion>

<Accordion title="invalidResponse">
  An auth response body was malformed or had an unexpected structure.

  **Response:** Show a generic sign-in error. File a bug if this occurs consistently.
</Accordion>

<Accordion title="unknown(String)">
  An unexpected error occurred that the SDK could not map to a more specific case. The associated `String` is the localized description of the underlying error.

  **Response:** Log the message for debugging. Show a generic sign-in error to the user.
</Accordion>

## Rate Limiting and Automatic Backoff

Chappie automatically retries `429` responses before surfacing `usageLimitReached` or `httpError`. The default policy attempts **2 retries** with an initial delay of **1 second**, a maximum delay of **30 seconds**, and honours the `Retry-After` header when the server provides one.

To tighten or relax that behaviour, pass a custom `ChappieRateLimitBackoffPolicy` when creating the client:

```swift theme={null}
let client = Chappie.client(
    rateLimitBackoff: ChappieRateLimitBackoffPolicy(
        maxRetries: 3,
        initialDelay: 1,
        maximumDelay: 15
    )
)
```

To disable automatic retries entirely and receive the error on the first `429`, use the `.disabled` preset — which is equivalent to `ChappieRateLimitBackoffPolicy(maxRetries: 0)`:

```swift theme={null}
let noBackoffClient = Chappie.client(rateLimitBackoff: .disabled)
```

<Tip>
  Disabling backoff is useful in test environments where you want deterministic error behaviour. Keep the default or a custom policy in production so transient rate-limit blips do not surface as errors to users.
</Tip>

`ChappieRateLimitBackoffPolicy` fields:

<ParamField path="maxRetries" type="Int" default="2">
  Number of retry attempts before the SDK surfaces the error. Set to `0` for `.disabled` behaviour.
</ParamField>

<ParamField path="initialDelay" type="TimeInterval" default="1">
  Seconds to wait before the first retry. Subsequent retries use bounded exponential backoff up to `maximumDelay`.
</ParamField>

<ParamField path="maximumDelay" type="TimeInterval" default="30">
  Upper bound on the backoff interval in seconds, regardless of how many retries have been attempted.
</ParamField>

<ParamField path="respectsRetryAfterHeader" type="Bool" default="true">
  When `true`, the SDK uses the `Retry-After` value from the server response instead of computing its own delay. Set to `false` if you want purely client-side backoff timing.
</ParamField>

## Handling Stream Cancellation

When you cancel a streaming task — by calling `handle.cancel()` or cancelling a Swift `Task` — the stream throws either `CancellationError` or a `URLError` with code `.cancelled`. Both are normal and expected; catch them and take no action.

```swift theme={null}
do {
    for try await event in handle.events {
        // Handle streaming events
    }
} catch is CancellationError {
    // Normal stream cancellation — no action needed
} catch let error as URLError where error.code == .cancelled {
    // Also normal — treat the same as CancellationError
} catch {
    // Unexpected error — log and surface to the user
    logger.error("Stream failed: \(error.localizedDescription)")
    showStreamError(error)
}
```

<Note>
  Do not surface `CancellationError` or `URLError.cancelled` to users. They indicate the stream was stopped deliberately by your code, not an unexpected failure.
</Note>
