ACheckerConnectionFactory swallows exceptions and drops the cause chain, masking H2 startup failures
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Problem Statement
Exception handling in ACheckerConnectionFactory drops the original failure in two different ways: getConn() catches the exception, prints it to stderr, and returns null; getConnection(), closeConnection(), and getDbVersion() rethrow with new SQLException(e.toString()) which discards the cause chain. As a result, when the AChecker H2 pool fails to initialize, the real stack trace never reaches the logs, and downstream code NPEs on a null Connection producing a secondary error that masks the actual root cause.
Actual Behavior
The real H2 failure (in our reproduction case: NullPointerException: Cannot invoke "Object.hashCode()" because "key" is null at DROP TABLE IF EXISTS AC_check_examples [50000-224]) is printed via e.printStackTrace() to stderr only — it does not go through the application logger and in many deployments is not captured in the log aggregator at all.
getConn() returns null after the catch block.
getConnection() assigns that null to its static connection field and returns it.
The caller in DAOImpl.execute() calls conn.createStatement() on the null reference.
The only exception that surfaces in logs is a NullPointerException from DAOImpl.java:58, several frames away from the real fault. There is no causal link back to the original H2 error.
The defective code is in dotCMS/src/enterprise/java/com/dotcms/enterprise/achecker/dao/ACheckerConnectionFactory.java:
// lines 54–80 — getConn() swallows and returns null
private static Connection getConn() throws Exception {
if ( pool == null ) {
try {
...
return pool.getConnection();
} catch (Exception e) {
e.printStackTrace(); // line 71
System.err.println("Unable to start db properly : " + e.getMessage()); // line 72
}
} else {
return pool.getConnection();
}
return null; // line 79
}
// line 45 — getConnection() strips the cause
throw new SQLException(e.toString());
// line 94 — closeConnection() strips the cause
throw new SQLException(e.toString());
// line 107 — getDbVersion() strips the cause
throw new Exception(e.toString());
And in dotCMS/src/enterprise/java/com/dotcms/enterprise/achecker/dao/DAOImpl.java:57-58:
Connection conn = ACheckerConnectionFactory.getConnection(); // can be null
st = conn.createStatement(); // → NPE, masking the real failure
This also violates the project rule in CLAUDE.md: "Config/Logger only: Never System.out, System.getProperty, or System.getenv."
Steps to Reproduce
- Put the AChecker H2 pool into a state where JdbcConnectionPool.create(...) or the initial pool.getConnection() will throw — for example on RHEL 8.10 with SELinux enforcing on an XFS filesystem (the environment in Freshdesk #36298), or by pointing achecker.sql at an unreadable file, or by placing an incompatible .mv.db file in dotsecure/h2db/1/.
- Start dotCMS.
- Open a content type that uses the TinyMCE editor (this triggers ACheckerAPIImpl.getAccessibilityGuidelineList → GuidelinesDAO.getOpenGuidelines → ACheckerConnectionFactory.getConnection).
- Inspect the application logs.
Acceptance Criteria
Scenario 1 — pool creation fails
Given H2 pool creation throws (e.g. unreadable achecker.sql)
When any caller invokes ACheckerConnectionFactory.getConnection()
Then a SQLException is thrown whose getCause() is the original H2 exception
And the real stack trace appears in the dotCMS application log via Logger.error(...)
And no NullPointerException surfaces from DAOImpl.execute
Scenario 2 — pool created but pool.getConnection() fails
Given the pool exists but a transient JDBC failure occurs
When a caller invokes getConnection()
Then a SQLException is thrown with the original cause attached
And the log line names ACheckerConnectionFactory and includes the full stack trace
Scenario 3 — close failure
Given connection.close() throws
When closeConnection() is called
Then a SQLException is thrown whose getCause() is the original exception
Scenario 4 — happy path
Given H2 starts successfully
When a user opens the TinyMCE editor
Then AChecker loads normally with no behaviour change and no new log noise
Scenario 5 — no stderr output
Given any failure path in ACheckerConnectionFactory
When the error is handled
Then no System.err / System.out / e.printStackTrace() calls execute — all output goes through com.dotmarketing.util.Logger
dotCMS Version
core-26.04.11-01
Severity
Medium - Some functionality impacted
Links
https://dotcms.freshdesk.com/a/tickets/36435
https://dotcms.freshdesk.com/a/tickets/36298
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with dotCMS/src/enterprise/java/com/dotcms/enterprise/achecker/dao/ACheckerConnectionFactory.java, especially getConn(), getConnection(), closeConnection(), and getDbVersion(), then inspect DAOImpl.java:57-58. Trace the pool creation and connection failure paths and the project's com.dotmarketing.util.Logger usage. Done means original causes remain attached, failures use application logging without stderr or printStackTrace(), and DAOImpl no longer receives a null connection.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- java
- Domain
- backend, databases
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 58/100