Error Handling (Driver Layer)#
Driver Layer failures fall into the same three categories as the rest of the stack: retryable, unrecoverable, and OAuth protocol. For the categorization and per-subtype guidance, see Error Handling.
The two platforms surface errors through different idioms — iOS returns Error values via completion handlers; Android throws IdsvrHaapiException. Casting or matching gives you the typed metadata.
Handling Errors#
Completion handlers surface Error values. Cast to HaapiError for the enum, then consult recoverySuggestion for retry guidance:
private func handleError(_ error: Error) {
guard let haapiError = error as? HaapiError else { return }
switch haapiError.recoverySuggestion {
case .retryable(let condition):
// schedule a retry based on the condition
break
case .newHaapiFlow:
// start a fresh HAAPI flow
break
case .nonRecoverable(let action):
// surface the cause to the user; no retry possible
break
}
}A refused HTTP redirect — a 301, 302, 303, 307 or 308 with a Location, answered to any HaapiClient or HaapiTokenManager request — is reported as invalidURLResponse with the error code haapi_redirect_refused, and its cause is a HaapiRedirectRefusal carrying the status code. It is unrecoverable: fix the redirect on the server or reverse proxy rather than retrying. See Upgrade — Redirects Are Refused.
if case .invalidURLResponse(_, _, let cause) = haapiError,
let refusal = cause as? HaapiRedirectRefusal
{
// refusal.statusCode: the 3xx status the server answered with
}Specific HaapiError cases — attestationFailure, haapiTokenManagerIsClosed, invalidStatusCode, serverError, and others — can be matched directly when their context matters:
switch haapiError {
case .attestationFailure(let cause):
// attestation rejected — surface to user, do not auto-retry
break
case .haapiTokenManagerIsClosed:
// construct a new manager and start over
break
case .invalidStatusCode(let statusCode):
// server returned an unexpected status
break
default:
break
}An attestationFailure can also mean that the device reached the App Attest key limit. Its error is then attestation_limited and its cause a HaapiAttestationLimitRefusal. Retry no earlier than the cause’s nextAttemptAfter: the recoverySuggestion, .retryable(condition: .whenAppInForeground), carries no time. See App Attest Key Limit (iOS).
The driver throws IdsvrHaapiException, split into two main categories:
try {
val result = throwableOperation()
} catch (e: IdsvrHaapiException.Retryable) {
// transient — schedule retry per e.condition (Now or WhenAppForeground)
} catch (e: IdsvrHaapiException.Unrecoverable) {
// permanent — handle per e.action (ModifyConfiguration, InvalidPlatform, IntrospectCause)
}Retryable subtypes include HttpClientRetryableException.SocketStreamInterruptionException, HostConnectionException, HttpRetryException (408, 429, 5xx with Retry-After), and HaapiRetryableException.UseDpopNonceException. Unrecoverable subtypes include AccessDeniedException, UnsupportedDeviceException, and UnsupportedConfigurationException.
When HttpClient.Response.Failure has a response code of 0, the failure is internal (parsing, attestation, key generation) rather than a server response.
A refused HTTP redirect — a 301, 302, 303, 307 or 308 with a non-empty Location, answered to a HaapiTokenManager attestation or token request or to a HAAPI request through an OkHttp client built with addHaapiInterceptor — is an IdsvrHaapiException.Unrecoverable with the error code haapi_redirect_refused, and its cause is a HaapiRedirectRefusal carrying the status code. Through OkHttp, the call fails with an IOException whose cause is that exception. It is unrecoverable: fix the redirect on the server or reverse proxy rather than retrying. See Upgrade — Redirects Are Refused.
val refusal = (exception as? IdsvrHaapiException)?.cause as? HaapiRedirectRefusal
// refusal?.statusCode: the 3xx status the server answered withaddHaapiInterceptor turns redirect following off for the whole OkHttp client. Keep it off: turned back on, a redirected HAAPI request still fails with the refusal, but only after the hop has happened and the request has reached the redirect target.
OAuth Protocol Errors#
OAuth protocol errors are returned as part of the response payload from the token endpoint, not as exceptions at the Driver Layer. They surface at the SDK Layer through OAuthTokenManager as ErrorTokenResponse. See OAuthTokenManager for the OAuth-specific handling.
Storage Errors#
The driver persists credentials through the Storage protocol. The built-in keychain-backed store is built with StorageBuilder.Keychain() — optionally with a shared access group and a data-protection level:
let storage = StorageBuilder.Keychain()
.accessGroup("group.example.shared") // optional — share with same-team targets
.accessibility(.whenUnlockedThisDeviceOnly) // optional — default: .afterFirstUnlockThisDeviceOnly
.build()Higher layers accept this store via HaapiConfiguration(storage:) and HaapiUIKitConfigurationBuilder.setStorage(_:); see Configuration (UI Layer).
When the keychain is unavailable (device locked, or an unusable access group), the thrown StorageError.readError / .writeError / .deleteError carries a typed, inspectable cause of StorageError.unavailable(operation:category:code:) — non-sensitive diagnostic context only, never the key or value. During a DPoP operation this still surfaces as HaapiError with recoverySuggestion == .retryable(.whenAppInForeground), so the retryable-error handling shown above already covers it.
A misconfigured access group is flagged early by a non-throwing probe that logs a warning; the store does not silently fall back to app-private storage, so this typed error remains the single authoritative signal on the first real keychain operation.
The driver persists DPoP state through the Storage interface. The built-in stores — StorageBuilder.encrypted(context) (the default) and StorageBuilder.shared(context) — encrypt values at rest under a non-exportable Android Keystore key, so a value can become unreadable if the entry is corrupted or the Keystore key is lost.
Read operations handle that differently:
- Bulk reads (
Storage.getAll) omit entries whose values no longer decrypt instead of failing the whole read. Each omission is logged under theHAAPI_DRIVER_STORAGEtag — the record carries the entry’s storage key, never the value. - Single-key reads (
Storage.get) propagate the failure to the caller, so an unreadable entry is distinguishable from an absent one.
How to implement this: Error Handling (concept) · How to Handle Errors · OAuthTokenManager