Error Handling (SDK Layer)#
SDK Layer failures fall into the same three categories as the rest of the stack: retryable, unrecoverable, and OAuth protocol. HaapiManager and OAuthTokenManager surface them with one important addition over the Driver Layer — OAuth protocol errors are returned as ErrorTokenResponse, not thrown.
For the categorization and per-subtype guidance, see Error Handling . For the underlying retryable / unrecoverable mechanics shared with the Driver Layer, see Error Handling (Driver Layer) .
Decision Table#
When an SDK call fails, classify the surface and respond accordingly:
| Surface | What it means | What to do |
|---|---|---|
iOS HaapiError with recoverySuggestion == .retryable(let condition) / Android IdsvrHaapiException.Retryable / RN HaapiError with recovery.kind === 'retryable' | Transient — network blip, fresh DPoP nonce required, 5xx with Retry-After | Retry per the condition (Now or WhenAppForeground) |
iOS recoverySuggestion == .newHaapiFlow / RN recovery.kind === 'newHaapiFlow' | The current flow is no longer usable but a fresh one will succeed | Construct a new manager and start over |
iOS recoverySuggestion == .nonRecoverable(let action) / Android IdsvrHaapiException.Unrecoverable / RN recovery.kind === 'unrecoverable' | Misconfiguration, unsupported platform, or access denied | Surface the cause to the user or developer; do not auto-retry |
TokenResponse with ErrorTokenResponse carrying an OAuth error code | Server understood and explicitly rejected (e.g., invalid_grant, invalid_dpop_proof) | Inspect the error code — see “Common OAuth Error Codes” below |
Retryable and Unrecoverable 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):
// condition is .now (retry immediately with exponential backoff)
// or .whenAppForeground (defer until the app is foregrounded)
break
case .newHaapiFlow:
// start a fresh HAAPI flow
break
case .nonRecoverable(let action):
// action is .modifyConfiguration (developer to fix client settings),
// .invalidPlatform (device cannot satisfy the protocol),
// or .introspectCause (inspect the wrapped cause for follow-up)
break
}
}Specific HaapiError cases — attestationFailure, haapiTokenManagerIsClosed, invalidStatusCode, serverError, dpopKeyCreationFailure, dpopProofFailure — can be matched directly when their context matters:
switch haapiError {
case .attestationFailure(let cause):
// attestation rejected — surface to user; consider DCR fallback
break
case .haapiTokenManagerIsClosed:
// construct a new manager and start over
break
case .dpopKeyCreationFailure, .dpopProofFailure:
// rare; rebuild configuration with .p256 and re-authenticate
break
default:
break
} HaapiManager and OAuthTokenManager throw IdsvrHaapiException. The SDK performs a single automatic retry on transport-level retryable conditions before propagating:
try {
val response = haapiManager.start(coroutineContext)
handleHaapiResponse(response)
} catch (e: IdsvrHaapiException.Retryable) {
// e.condition is Now (retry immediately) or WhenAppForeground (defer)
} catch (e: IdsvrHaapiException.Unrecoverable) {
// e.action is ModifyConfiguration, InvalidPlatform, or 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.
Every SDK failure rejects the Promise with a typed HaapiError. Catch it with try/catch and narrow with isHaapiError(e), then branch on the stable error.code:
import { isHaapiError, HaapiErrorCode } from 'identityserver.haapi.reactnative.sdk'
import type { HaapiError } from 'identityserver.haapi.reactnative.sdk'
try {
const response = await accessor.haapiManager.start()
// …
} catch (e) {
if (!isHaapiError(e)) throw e
switch (e.code) {
case HaapiErrorCode.Networking:
case HaapiErrorCode.DPoP:
// e.recovery.kind === 'retryable' — retry per e.recovery.condition.
break
case HaapiErrorCode.Attestation:
// Attestation rejected — surface to user; consider DCR fallback.
break
case HaapiErrorCode.AccessorAlreadyInitialized:
case HaapiErrorCode.AccessorNotInitialized:
// Lifecycle misuse — close any held accessor and re-initialise.
break
case HaapiErrorCode.Configuration:
// e.recovery.kind === 'unrecoverable' — fix client config.
break
}
}HaapiError also exposes category ('bridge' | 'networking' | 'attestation' | 'dpop' | 'haapi' | 'config' | 'unexpected') and recovery: { kind: 'retryable' | 'newHaapiFlow' | 'unrecoverable', condition?, action? } for the higher-level branching pattern. For HAAPI server errors, e.serverError carries the parsed RFC 7807 payload.
OAuth Protocol Errors#
OAuth protocol errors (invalid_grant, invalid_dpop_proof, and so on) come back as ErrorTokenResponse from TokenResponse rather than as exceptions. Match on the response variant and inspect the parsed error and errorDescription fields:
switch tokenResponse {
case let .successfulToken(success):
// persist tokens
break
case let .errorToken(errorResponse):
// errorResponse.error and errorResponse.errorDescription
break
case let .error(anError):
// framework or transport error — see Retryable / Unrecoverable above
break
} when (tokenResponse) {
is SuccessfulTokenResponse -> {
// persist tokens
}
is ErrorTokenResponse -> {
tokenResponse.error // "invalid_grant", "invalid_dpop_proof", etc.
tokenResponse.errorDescription
}
} if (tokenResponse.responseType === 'success') {
// persist tokens
} else {
tokenResponse.error // "invalid_grant", "invalid_dpop_proof", etc.
tokenResponse.errorDescription
tokenResponse.errorUri
}OAuth-protocol errors resolve the Promise — they don’t throw. The try/catch block above only fires for transport / attestation / DPoP / bridge failures.
invalid_grant clears the bound key pair. When the token endpoint returns invalid_grant, the SDK automatically discards any stored DPoP key pair so the next flow starts cleanly. The caller must treat held tokens as invalid, drop any cached session, and prompt the user to re-authenticate from scratch — re-issuing the same request will not recover.
Common OAuth Error Codes#
The codes below are the ones most apps will encounter. The list is not exhaustive — any RFC 6749 / RFC 9449 error code may surface — but covers the operationally meaningful cases:
error | Typical cause | Recommended response |
|---|---|---|
invalid_grant | Authorization code or refresh token has expired, been revoked, or was bound to a key that no longer exists | SDK clears DPoP keys automatically; prompt re-authentication |
invalid_dpop_proof | Client binding configuration does not match the server’s issue-token-bound-authorization-code setting, or the DPoP proof was missing/malformed on token exchange | Verify client and server binding settings match — see Token Binding |
invalid_request | Token request is malformed (missing parameter, duplicated parameter, unsupported parameter) | Usually a client bug; inspect the outgoing request and reconcile with the OAuth spec |
invalid_client | Client authentication failed (wrong secret, expired certificate, JWT signature mismatch) | Verify the configured client authentication method matches the server’s main HAAPI client — see Client Authentication |
unauthorized_client | The client is authenticated but not permitted to use this grant type or scope | Server-side configuration: check the client’s allowed grant types and scopes on CIS |
use_dpop_nonce | Server requires a fresh DPoP nonce before accepting the request | Surfaces as a retryable error at the Driver Layer; the SDK refreshes the nonce and retries automatically |
How to implement this: Error Handling (concept) · How to Handle Errors · Error Handling (Driver Layer)
Was this helpful?