Skip to content

Guard against all uncaught exceptions in the serial executor block#997

Open
pmathew92 wants to merge 1 commit into
v4_developmentfrom
port/970-executor-guard
Open

Guard against all uncaught exceptions in the serial executor block#997
pmathew92 wants to merge 1 commit into
v4_developmentfrom
port/970-executor-guard

Conversation

@pmathew92

@pmathew92 pmathew92 commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the serial-executor exception guard (#970) onto the v4_development branch as its own feature.

  • Adds runCatchingOnExecutor helper to BaseCredentialsManager, wrapping each serialExecutor.execute { } body in a catch (Throwable) -> onFailure(UNKNOWN_ERROR) safety net so unexpected errors surface as a failure callback instead of being swallowed on the executor thread.
  • Wraps all 3 CredentialsManager and all 3 SecureCredentialsManager execute blocks; removes the now-redundant trailing catch (RuntimeException) blocks while keeping the domain-specific catches (AuthenticationException, CredentialsManagerException, IncompatibleDeviceException, CryptoException).
  • Brings SecureCredentialsManager.continueGetApiCredentials to parity with the other two blocks by mapping a save failure to STORE_FAILED (rather than letting it fall through to the new UNKNOWN_ERROR net).

This is the v4 port of the change merged to main in #970; reconciled onto v4's restructured credentials managers and test imports (org.mockito.kotlin.*).

Test plan

  • ./gradlew :auth0:compileDebugKotlin
  • ./gradlew :auth0:testDebugUnitTest — full suite green
  • 5 new tests in CredentialsManagerTest (98 total), 4 new in SecureCredentialsManagerTest (131 total), 0 failures
  • Verified wrap/catch parity against fix : Guard against all uncaught exception in the serial executor block #970 (3+3 execute blocks wrapped, STORE_FAILED 3=3, domain catches match)

Summary by CodeRabbit

  • Bug Fixes

    • Improved credential retrieval so unexpected errors are now handled consistently and returned as a standard error response instead of causing unhandled failures.
    • Applied the same safer error handling across sign-in, SSO, and API credential renewal flows.
    • Preserved existing behavior for authentication-specific errors while making runtime failures more predictable.
  • Tests

    • Added coverage for unexpected storage and renewal failures to verify they surface the expected error response.

@pmathew92 pmathew92 requested a review from a team as a code owner June 30, 2026 16:40
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@NandanPrabhu, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 3 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 3a5604fa-4ebe-40cf-9679-2412f679cca0

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6dc69 and b97a0a5.

📒 Files selected for processing (5)
  • auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt
  • auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
  • auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt
  • auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt
  • auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt
📝 Walkthrough

Walkthrough

Adds a runCatchingOnExecutor helper to BaseCredentialsManager and refactors getSsoCredentials, getCredentials, and getApiCredentials in both CredentialsManager and SecureCredentialsManager to route unexpected exceptions through it, replacing local RuntimeException catch blocks. New tests validate the resulting UNKNOWN_ERROR behavior.

Changes

runCatchingOnExecutor refactor

Layer / File(s) Summary
Shared error-handling helper
auth0/.../storage/BaseCredentialsManager.kt
Adds internal inline fun runCatchingOnExecutor that catches Throwable in a block and forwards CredentialsManagerException(UNKNOWN_ERROR) to the callback.
CredentialsManager flows wrapped
auth0/.../storage/CredentialsManager.kt
getSsoCredentials, getCredentials, getApiCredentials now execute inside runCatchingOnExecutor(callback), removing method-local RuntimeException handling while preserving AuthenticationException mapping and existing renewal/persistence logic.
SecureCredentialsManager flows wrapped
auth0/.../storage/SecureCredentialsManager.kt
getSsoCredentials, continueGetCredentials, continueGetApiCredentials now run inside runCatchingOnExecutor(callback), removing broad RuntimeException catches while retaining explicit AuthenticationException, CryptoException, and MFA-required handling.
Unexpected exception test coverage
auth0/src/test/.../CredentialsManagerTest.kt, auth0/src/test/.../SecureCredentialsManagerTest.kt
New tests simulate storage/request.execute() throwing unexpected exceptions and assert callbacks receive CredentialsManagerException.UNKNOWN_ERROR with the original exception as cause.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Caller
  participant CredentialsManager
  participant runCatchingOnExecutor
  participant Storage
  participant Callback

  Caller->>CredentialsManager: getCredentials(...)
  CredentialsManager->>runCatchingOnExecutor: execute block
  runCatchingOnExecutor->>Storage: retrieveString(...)
  alt storage throws unexpected exception
    Storage-->>runCatchingOnExecutor: Throwable
    runCatchingOnExecutor->>Callback: onFailure(UNKNOWN_ERROR, cause)
  else success
    Storage-->>runCatchingOnExecutor: stored value
    runCatchingOnExecutor-->>Callback: onSuccess(credentials)
  end
Loading

Possibly related PRs

  • auth0/Auth0.Android#983: Refactors the same getSsoCredentials, getCredentials, and getApiCredentials methods in CredentialsManager/SecureCredentialsManager, overlapping in the same control/error paths.

Suggested reviewers: kishore7snehil

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: adding a guard for uncaught exceptions in the serial executor path.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch port/970-executor-guard

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

tokenType.orEmpty(),
refreshToken,
Date(expiresAt),
storedScope
val expiresAt = fresh.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
val expiresAt = newCredentials.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
val expiresAt = fresh.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
val expiresAt = newCredentials.expiresAt.time
val willAccessTokenExpire = willExpire(expiresAt, minTtl.toLong())
if (willAccessTokenExpire) {
val tokenLifetime = (expiresAt - currentTimeInMillis - minTtl * 1000) / -1000
@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@NandanPrabhu

Copy link
Copy Markdown
Contributor

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt (1)

4215-4239: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing test for the STORE_FAILED alignment mentioned in the PR objectives.

These new tests only cover the outer runCatchingOnExecutor safety net (UNKNOWN_ERROR). The PR description states continueGetApiCredentials was changed to map save failures to STORE_FAILED instead of falling through to UNKNOWN_ERROR, but no test in this diff exercises that specific path (e.g., crypto.encrypt/storage save throwing during API credential renewal).

Consider adding a test that triggers a save failure after a successful request.execute() in continueGetApiCredentials and asserts CredentialsManagerException.Code.STORE_FAILED.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt`
around lines 4215 - 4239, The new test in SecureCredentialsManagerTest only
validates the outer runCatchingOnExecutor fallback to UNKNOWN_ERROR, but it does
not cover the STORE_FAILED mapping added in continueGetApiCredentials. Add a
test around continueGetApiCredentials that makes request.execute() succeed and
then forces the credential persistence step to fail (for example via
crypto.encrypt or the storage save path), and assert that the callback receives
CredentialsManagerException.Code.STORE_FAILED rather than UNKNOWN_ERROR. Use the
existing shouldCatchUnexpectedExceptionDuringApiCredentialRenewal setup style
and the apiCredentialsCallback/exceptionCaptor assertions to keep it aligned
with the surrounding tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt`:
- Around line 335-339: The catch block in BaseCredentialsManager’s executor
handling is swallowing fatal JVM errors by treating every Throwable as an
UNKNOWN_ERROR callback failure. Update the Throwable handling in this block to
rethrow VirtualMachineError, ThreadDeath, and LinkageError before the Log.e and
callback.onFailure path, so only non-fatal exceptions are mapped to
CredentialsManagerException.Code.UNKNOWN_ERROR.

In
`@auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt`:
- Line 757: The public getCredentials flow still performs a storage pre-check
via hasValidCredentials(minTtl.toLong()) outside the runCatchingOnExecutor
protection, so storage failures can bypass UNKNOWN_ERROR. Update
SecureCredentialsManager.getCredentials so that the pre-check is wrapped in the
same guarded error handling as continueGetCredentials, or move the check under
that wrapper, while keeping the biometric prompt behavior unchanged.

---

Nitpick comments:
In
`@auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt`:
- Around line 4215-4239: The new test in SecureCredentialsManagerTest only
validates the outer runCatchingOnExecutor fallback to UNKNOWN_ERROR, but it does
not cover the STORE_FAILED mapping added in continueGetApiCredentials. Add a
test around continueGetApiCredentials that makes request.execute() succeed and
then forces the credential persistence step to fail (for example via
crypto.encrypt or the storage save path), and assert that the callback receives
CredentialsManagerException.Code.STORE_FAILED rather than UNKNOWN_ERROR. Use the
existing shouldCatchUnexpectedExceptionDuringApiCredentialRenewal setup style
and the apiCredentialsCallback/exceptionCaptor assertions to keep it aligned
with the surrounding tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c55bef49-7298-4726-ab4e-5867e3f972e8

📥 Commits

Reviewing files that changed from the base of the PR and between 6a6dc69 and b97a0a5.

📒 Files selected for processing (5)
  • auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt
  • auth0/src/main/java/com/auth0/android/authentication/storage/CredentialsManager.kt
  • auth0/src/main/java/com/auth0/android/authentication/storage/SecureCredentialsManager.kt
  • auth0/src/test/java/com/auth0/android/authentication/storage/CredentialsManagerTest.kt
  • auth0/src/test/java/com/auth0/android/authentication/storage/SecureCredentialsManagerTest.kt

Comment on lines +335 to +339
} catch (t: Throwable) {
Log.e("BaseCredentialsManager", "Unexpected error in executor block", t)
callback.onFailure(
CredentialsManagerException(CredentialsManagerException.Code.UNKNOWN_ERROR, t)
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant file and inspect the target region.
ast-grep outline auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt --view expanded || true
wc -l auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt
sed -n '300,360p' auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt

Repository: auth0/Auth0.Android

Length of output: 3597


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether similar Throwable handling elsewhere in the repo already excludes fatal errors.
rg -n "catch \(t: Throwable\)|catch \([a-zA-Z_]+: Throwable\)|VirtualMachineError|ThreadDeath|LinkageError" auth0/src/main/java/com/auth0/android -S

Repository: auth0/Auth0.Android

Length of output: 281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Probe Kotlin/JVM semantics for catching fatal errors is not needed; instead inspect API contract/docs if any mention this executor helper.
rg -n "runCatchingOnExecutor|Unexpected error in executor block|UNKNOWN_ERROR|CredentialsManagerException.Code.UNKNOWN_ERROR" auth0/src/main/java/com/auth0/android auth0/README.md auth0/EXAMPLES.md -S

Repository: auth0/Auth0.Android

Length of output: 5250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the exact helper and nearby implementations to see whether fatal errors are intentionally converted into callback failures.
sed -n '320,350p' auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt
printf '\n---\n'
rg -n "catch \(t: Throwable\)|catch \(e: Exception\)|runCatchingOnExecutor|UNKNOWN_ERROR" auth0/src/main/java/com/auth0/android/authentication/storage -S

Repository: auth0/Auth0.Android

Length of output: 2792


Rethrow fatal JVM errors before mapping Throwable to UNKNOWN_ERROR. VirtualMachineError, ThreadDeath, and LinkageError should escape here instead of being converted into a recoverable callback failure.

Suggested guard
-        } catch (t: Throwable) {
+        } catch (t: Throwable) {
+            if (t is VirtualMachineError || t is ThreadDeath || t is LinkageError) {
+                throw t
+            }
             Log.e("BaseCredentialsManager", "Unexpected error in executor block", t)
             callback.onFailure(
                 CredentialsManagerException(CredentialsManagerException.Code.UNKNOWN_ERROR, t)
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (t: Throwable) {
Log.e("BaseCredentialsManager", "Unexpected error in executor block", t)
callback.onFailure(
CredentialsManagerException(CredentialsManagerException.Code.UNKNOWN_ERROR, t)
)
} catch (t: Throwable) {
if (t is VirtualMachineError || t is ThreadDeath || t is LinkageError) {
throw t
}
Log.e("BaseCredentialsManager", "Unexpected error in executor block", t)
callback.onFailure(
CredentialsManagerException(CredentialsManagerException.Code.UNKNOWN_ERROR, t)
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@auth0/src/main/java/com/auth0/android/authentication/storage/BaseCredentialsManager.kt`
around lines 335 - 339, The catch block in BaseCredentialsManager’s executor
handling is swallowing fatal JVM errors by treating every Throwable as an
UNKNOWN_ERROR callback failure. Update the Throwable handling in this block to
rethrow VirtualMachineError, ThreadDeath, and LinkageError before the Log.e and
callback.onFailure path, so only non-fatal exceptions are mapped to
CredentialsManagerException.Code.UNKNOWN_ERROR.

validateDPoPState(tokenType)?.let { dpopError ->
callback.onFailure(dpopError)
return@execute
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapping the whole block means the fast-path callback.onSuccess(...) now runs inside the catch (Throwable). If a consumer's onSuccess throws, it gets caught and turned into a second onFailure(UNKNOWN_ERROR) — a double callback. On awaitCredentials, that's a resume after resume → IllegalStateException: Already resumed. Might be cleaner to call onSuccess outside the guarded block. Minor/edge-case, and it's inherited from #970

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants