Description
The SQLErrorCodes getErrorCodes(DataSource dataSource)
method of the org.springframework.jdbc.support.SQLErrorCodesFactory
class is responsible for producing a SQLErrorCodes
instance with a repository of vendor-specific SQLException
error codes that are neatly categorized for Spring JDBC exception mapping.
For example, duplicateKeyCodes
contains codes that will be mapped to DuplicateKeyException
.
This is a great convenience for applications since they can work with a comprehensive and intuitive hierarchy of exceptions rather than vendor-specific error codes.
Unfortunately, getErrorCodes
has fallback logic that can have dramatic and damaging side effects.
If a SQLErrorCodes
instance is not yet cached for a particular DataSource
then getErrorCodes
creates one. It does so by first obtaining the "database product name" for the DataSource
. Next it looks in a repository of error codes, keyed by database product name, for the target SQLErrorCodes
instance. Lastly, that result is added to the cache by DataSource
.
Unfortunately, obtaining the database product name is an operation on a JDBC connection and has many failure modes. For example, the thread-bound JDBC connection might have been closed for some reason.
Bizarrely, getErrorCodes
responds to such exceptions by simply producing an empty SQLErrorCodes
with no mapping at all. That's the best it can do if it must return an SQLErrorCodes
instance since it doesn't know the database engine. Unfortunately, this changes the expected behavior of upstream components like JdbcTemplate
, which caches the SQLErrorCodes
in an SQLErrorCodeSQLExceptionTranslator
. In other words, the exception corrupts the JdbcTemplate
exception translator and the conveniences mentioned above go out the window!
Consider the following code snippet in a long-running application:
try {
jdbcTemplate.update(...);
} catch (DuplicateKeyException e) {
// handle unique constraint violation
}
Suppose a SQLException
occurs due to a unique constraint violation with MySQL vendor code 1062
. The execution path above executes, resulting in a SQLErrorCodes
instance that has code 1062
in the duplicateKeyCodes
bucket. Spring JDBC translates that to a DuplicateKeyException
and all is well!
However, suppose instead that the error path above occurs resulting in an empty SQLErrorCodes
. Now code 1062
isn't represented at all and the exception translator has to employ fallback logic that results in a DataIntegrityViolationException
. The exception is not handled by the catch block and mayhem ensues.
This unpredictable behavior strikes me as extremely dangerous behavior. In my case, it surfed in the production environment and had quite a noticeable impact.
Fortunately, a quick workaround is available by simply setting the exception translator and database product name up front. However, I didn't realize that until it was too late.
The issue can be trivially reproduced using a debug session in which the connection is closed. See attached screenshots.