From c0b575f218c8c3066a85922da2e9184eeb3582c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Kobyli=C5=84ski?= Date: Wed, 29 Apr 2026 14:56:52 +0200 Subject: [PATCH] fix: actionable error message in ExposedConnectionProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When ExposedConnectionProvider.withConnection is called outside an active Exposed transaction, TransactionManager.current() throws a generic Exposed "No transaction in context" IllegalStateException that doesn't tell the caller they are using okapi outside the expected transaction { } scope. Wrap the lookup with TransactionManager.currentOrNull() and throw an IllegalStateException whose message names the okapi method and points at the missing Exposed transaction { } block. Test JdbcConnectionProvider already does this same fail-fast on its ThreadLocal scope; this brings ExposedConnectionProvider to parity. Coverage: * ExposedConnectionProviderTest — unit (FunSpec): - throws IllegalStateException with actionable message when called outside any transaction - supplies the active transaction's connection to the block when called inside transaction(db) { } Follow-up to PR #26 review feedback (code-reviewer Suggestion #6). --- .../exposed/ExposedConnectionProvider.kt | 12 ++++--- .../exposed/ExposedConnectionProviderTest.kt | 34 +++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 okapi-exposed/src/test/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProviderTest.kt diff --git a/okapi-exposed/src/main/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProvider.kt b/okapi-exposed/src/main/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProvider.kt index 0c5419b..cf1851f 100644 --- a/okapi-exposed/src/main/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProvider.kt +++ b/okapi-exposed/src/main/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProvider.kt @@ -13,12 +13,16 @@ import java.sql.Connection * `transaction(database) { }` block completes — so this provider performs no cleanup. * * Use when your application manages transactions via Exposed (e.g. Ktor + Exposed apps). - * Must be called from within an active Exposed transaction; otherwise - * `TransactionManager.current()` throws. + * Must be called from within an active Exposed transaction; otherwise [withConnection] + * throws an [IllegalStateException] pointing the caller at the missing `transaction { }` + * block, instead of letting Exposed's own less specific error surface. */ class ExposedConnectionProvider : ConnectionProvider { override fun withConnection(block: (Connection) -> T): T { - val connection = TransactionManager.current().connection.connection as Connection - return block(connection) + val transaction = TransactionManager.currentOrNull() + ?: throw IllegalStateException( + "ExposedConnectionProvider.withConnection must be called within an Exposed transaction { } block", + ) + return block(transaction.connection.connection as Connection) } } diff --git a/okapi-exposed/src/test/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProviderTest.kt b/okapi-exposed/src/test/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProviderTest.kt new file mode 100644 index 0000000..b75fde7 --- /dev/null +++ b/okapi-exposed/src/test/kotlin/com/softwaremill/okapi/exposed/ExposedConnectionProviderTest.kt @@ -0,0 +1,34 @@ +package com.softwaremill.okapi.exposed + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.string.shouldContain +import org.jetbrains.exposed.v1.jdbc.Database +import org.jetbrains.exposed.v1.jdbc.transactions.transaction + +class ExposedConnectionProviderTest : FunSpec({ + + val provider = ExposedConnectionProvider() + + test("throws IllegalStateException with actionable message when called outside an Exposed transaction") { + val ex = shouldThrow { + provider.withConnection { /* unreachable */ } + } + ex.message shouldContain "ExposedConnectionProvider.withConnection" + ex.message shouldContain "Exposed transaction { } block" + } + + test("supplies the active Exposed transaction's connection to the block") { + val db = Database.connect( + "jdbc:h2:mem:exposed_provider_test_${System.nanoTime()};DB_CLOSE_DELAY=-1", + driver = "org.h2.Driver", + ) + + val connectionWasOpen: Boolean = transaction(db) { + provider.withConnection { conn -> !conn.isClosed } + } + + connectionWasOpen shouldBe true + } +})