Skip to content

SQLErrorCodesFactory.getErrorCodes(DataSource) returns empty error codes if access to transactional connection fails #25681

Closed
@ebarlas

Description

@ebarlas

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.

Screen Shot 2020-09-02 at 8 43 38 PM
Screen Shot 2020-09-02 at 8 43 54 PM
Screen Shot 2020-09-02 at 8 44 11 PM
Screen Shot 2020-09-02 at 8 44 17 PM
Screen Shot 2020-09-02 at 8 44 50 PM
Screen Shot 2020-09-02 at 8 47 48 PM
Screen Shot 2020-09-02 at 8 46 42 PM

Metadata

Metadata

Assignees

Labels

in: dataIssues in data modules (jdbc, orm, oxm, tx)status: backportedAn issue that has been backported to maintenance branchestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions