Skip to content

fix: guard a11y NPE via uncaught-exception handler path on macOS#645

Merged
rainxchzed merged 2 commits into
OpenHub-Store:mainfrom
nitheeshdr:fix/640-a11y-npe-uncaught-exception-path
May 20, 2026
Merged

fix: guard a11y NPE via uncaught-exception handler path on macOS#645
rainxchzed merged 2 commits into
OpenHub-Store:mainfrom
nitheeshdr:fix/640-a11y-npe-uncaught-exception-path

Conversation

@nitheeshdr
Copy link
Copy Markdown
Contributor

@nitheeshdr nitheeshdr commented May 19, 2026

What

Extends A11yCrashGuard to cover a second propagation path for the Compose MP 1.10.x accessibility NPE on macOS.

The existing FilteringEventQueue only intercepts the NPE when it escapes DispatchedTask.run and propagates up through the AWT event queue chain. When BaseContinuationImpl.resumeWith catches the NPE first (which happens depending on the coroutine call depth), the failure is routed through handleCoroutineException directly to the default uncaught-exception handler — bypassing FilteringEventQueue entirely. CrashReporter then writes a crash dump and the a11y sync loop coroutine dies, leaving the app unresponsive.

Changes:

  • warned and isComposeA11yNpe lifted to A11yCrashGuard object so both paths share them
  • install() wraps the existing uncaught-exception handler (installed by CrashReporter) with an a11y NPE filter; non-a11y exceptions are forwarded to the previous handler unchanged
  • isComposeA11yNpe adds sun.lwawt.macosx.CAccessible as a secondary stack-frame fingerprint (it appears in every reported trace for this bug)
  • Updated log messages and KDoc to reference both Doesnt work on mac at all #330 and Github-Store v1.8.2 -> java.lang.NullPointerException #640

Why

Users on macOS 15 running GitHub Store v1.8.2 see the app crash when navigating from a search result to the details screen. The stack trace is the same Compose MP SemanticsOwnerAccessibility.accessibleParentOf NPE that #330 addressed, but in this trigger path the coroutine machinery intercepts the exception before the FilteringEventQueue sees it.

How to test

  1. macOS only. Reproduce with: open app → Search → enter Happ, select macOS filter → click more details on a result.
  2. Without this fix on v1.8.2: the app freezes or writes a crash-*.log in ~/Library/Logs/GitHub-Store/.
  3. With this fix: the app continues normally; session.log shows one [A11yCrashGuard] Suppressed Compose a11y NPE via uncaught-exception path line and no crash dump is written.

Fixes #640

Summary by CodeRabbit

  • Bug Fixes
    • Prevented macOS crashes triggered by accessibility interactions; app no longer crashes when accessibility features are enabled.
    • Reduced repetitive error logging so only the first occurrence per failure path is reported.
  • Documentation
    • Clarified diagnostic messages and logs (references to issues #330 and #640) and expanded notes on behavior and trade-offs.

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 19, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1ef2d0e-a931-412b-94c5-6a66fd5f2bd8

📥 Commits

Reviewing files that changed from the base of the PR and between b8a5bcf and f937a70.

📒 Files selected for processing (1)
  • composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt
🚧 Files skipped from review as they are similar to previous changes (1)
  • composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt

Walkthrough

A11yCrashGuard centralizes Compose accessibility NPE detection and suppresses matching NPEs on macOS via two guarded paths: the AWT EventQueue (FilteringEventQueue) and the JVM uncaught-exception handler, with per-path one-time logging and an extracted isComposeA11yNpe classifier.

Changes

macOS Compose A11y NPE Suppression with Dual Propagation Paths

Layer / File(s) Summary
Docs and one-time warning flags
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt
Module documentation now describes two guarded propagation paths and trade-offs; two object-level AtomicBoolean flags (warnedEdt, warnedUncaught) were added to gate one-time suppression logging.
install() wiring and uncaught-exception wrapper
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt
install() (macOS only) sets FilteringEventQueue and wraps the JVM default UncaughtExceptionHandler to intercept and suppress matching Compose a11y NullPointerExceptions; non-matching exceptions are forwarded to the previous handler or printed. Also adds isComposeA11yNpe helper.
FilteringEventQueue dispatch guard
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt
FilteringEventQueue.dispatchEvent uses isComposeA11yNpe to suppress only matching NPEs, uses the shared warnedEdt for a one-time #640 log, and rethrows other NullPointerExceptions.

Sequence Diagram(s)

sequenceDiagram
  participant Installer as A11yCrashGuard.install()
  participant SysEQ as System EventQueue
  participant FEQ as FilteringEventQueue
  participant Uncaught as JVM UncaughtExceptionHandler
  participant Original as PreviousHandler

  rect rgba(100,150,200,0.5)
  Installer->>FEQ: set as system EventQueue
  Installer->>Uncaught: wrap default handler (save previous)
  Uncaught->>Original: delegate non-matching exceptions
  end

  rect rgba(150,100,200,0.5)
  SysEQ->>FEQ: dispatchEvent(Event)
  FEQ->>FEQ: isComposeA11yNpe(Throwable)?
  alt match
    FEQ-->>FEQ: suppress NPE, log once (warnedEdt)
  else no match
    FEQ->>SysEQ: rethrow NPE
  end
  end

  rect rgba(200,150,100,0.5)
  Uncaught->>Uncaught: onUncaught(Throwable)
  Uncaught->>Uncaught: isComposeA11yNpe(Throwable)?
  alt match
    Uncaught-->>Uncaught: suppress NPE, log once (warnedUncaught)
  else no match
    Uncaught->>Original: forward throwable
  end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped in code where crashes crept,
Two paths I now watch while users slept,
EventQueue and handlers hold the line,
One helper checks the traceback sign,
NPEs hush—I'm nibbling bugs divine.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 directly and specifically describes the primary change: guarding accessibility NPE via the uncaught-exception handler path on macOS, which is the main objective of this PR.
Linked Issues check ✅ Passed The code changes fully address issue #640 by implementing protection against Compose accessibility NPE via both the event-queue and uncaught-exception handler paths, with shared NPE detection logic and appropriate logging.
Out of Scope Changes check ✅ Passed All changes are scoped to the A11yCrashGuard.kt file and directly address the NPE suppression objective; no unrelated modifications are present.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 and usage tips.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 19, 2026

Greptile Summary

This PR extends A11yCrashGuard to intercept the Compose MP 1.10.x accessibility NPE on a second propagation path — the coroutine-failure route through handleCoroutineException — which bypasses the existing FilteringEventQueue guard. It also addresses all three concerns raised in prior review rounds: separate per-path warned flags, a previous == null fallback with documented installation-order contract, and a tighter stack-frame fingerprint (CAccessible$AXChangeNotifier instead of CAccessible).

  • install() now captures and wraps the existing uncaught-exception handler so non-a11y exceptions are forwarded unchanged; the installation-order dependency on CrashReporter is documented in both a comment and KDoc.
  • warnedEdt and warnedUncaught are separate AtomicBoolean flags so each propagation path emits its own first-occurrence diagnostic line in session.log, preserving the observable proof the fix was exercised on each path.
  • isComposeA11yNpe is lifted to the object level and adds sun.lwawt.macosx.CAccessible$AXChangeNotifier as a secondary fingerprint, scoped to the specific inner class present in all known traces rather than the broader CAccessible type.

Confidence Score: 5/5

Safe to merge — the change is a narrowly scoped exception filter that explicitly forwards all non-a11y exceptions and includes a documented fallback for edge cases.

All three concerns from the prior review round have been resolved: separate per-path flags, a non-null fallback with documented installation-order contract, and a tighter stack fingerprint. The single changed file is a targeted workaround with no new business logic, and the installation order in DesktopApp.kt (CrashReporter first, A11yCrashGuard second) is correct and stable.

No files require special attention. DesktopApp.kt preserves the required installation order and was not changed.

Important Files Changed

Filename Overview
composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt Adds a second guard path via the default uncaught-exception handler; lifts shared helpers to object level; separates per-path warned flags and tightens the CAccessible fingerprint. All three previously raised concerns are addressed.

Sequence Diagram

sequenceDiagram
    participant Compose as Compose a11y loop
    participant Coroutine as BaseContinuationImpl
    participant EDT as AWT EventQueue
    participant FEQ as FilteringEventQueue
    participant UCH as UncaughtExceptionHandler (A11yCrashGuard)
    participant CR as CrashReporter handler

    Note over Compose: accessibleParentOf NPE

    alt EDT path (Path 1)
        Compose->>EDT: NPE propagates up dispatch chain
        EDT->>FEQ: dispatchEvent(event) throws NPE
        FEQ->>FEQ: isComposeA11yNpe? true
        FEQ-->>FEQ: suppress + log warnedEdt (once)
    else Coroutine-failure path (Path 2)
        Compose->>Coroutine: resumeWith catches NPE
        Coroutine->>UCH: handleCoroutineException to defaultUncaughtExceptionHandler
        UCH->>UCH: isComposeA11yNpe? true
        UCH-->>UCH: suppress + log warnedUncaught (once)
    end

    Note over UCH,CR: Non-a11y exceptions forwarded to CrashReporter unchanged
Loading

Reviews (3): Last reviewed commit: "fix: address Greptile review — split war..." | Re-trigger Greptile

Comment thread composeApp/src/jvmMain/kotlin/zed/rainxch/githubstore/A11yCrashGuard.kt Outdated
@nitheeshdr
Copy link
Copy Markdown
Contributor Author

Addressed all three P2 concerns in the follow-up commit (f937a70):

Shared warned flag — split into warnedEdt (path 1) and warnedUncaught (path 2). Each path now logs its first suppression independently, so the new uncaught-exception path message will appear even if the EDT path fires first.

Implicit ordering dependency — added a comment on install() documenting that it must be called after CrashReporter.install(). Also hardened the previous == null case: instead of silently dropping non-a11y exceptions, we now fall back to throwable.printStackTrace(System.err) which mirrors JVM default behavior if previous is somehow null.

CAccessible fingerprint too broad — narrowed from sun.lwawt.macosx.CAccessible to sun.lwawt.macosx.CAccessible$AXChangeNotifier, the specific inner class present in every known trace for this bug.

@nitheeshdr
Copy link
Copy Markdown
Contributor Author

@greptile-apps re-review

@rainxchzed
Copy link
Copy Markdown
Member

@nitheeshdr Thanks for contribution 🩵

@rainxchzed rainxchzed merged commit fe342c0 into OpenHub-Store:main May 20, 2026
1 check passed
@nitheeshdr nitheeshdr deleted the fix/640-a11y-npe-uncaught-exception-path branch May 20, 2026 05:09
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.

Github-Store v1.8.2 -> java.lang.NullPointerException

2 participants