From a79afc63ccc31b7ad4f6bd31f1f061db4c9220cd Mon Sep 17 00:00:00 2001 From: "leonid.stashevsky" Date: Thu, 16 Jan 2025 15:13:50 +0100 Subject: [PATCH 1/4] KTOR-7139 Fux exception thrown in onCallRespond makes the client wait for response indefinitely --- .../src/io/ktor/utils/io/core/Closeable.kt | 3 +- .../server/engine/BaseApplicationEngine.kt | 4 +- .../server/engine/BaseApplicationResponse.kt | 35 +++++++++- .../engine/internal/ExceptionPageContent.kt | 42 +++++++++++ .../netty/NettyApplicationCallHandler.kt | 2 +- .../server/test/base/EngineTestBaseJvm.kt | 2 - .../testing/suites/SustainabilityTestSuite.kt | 69 +++++++++++++++++-- 7 files changed, 145 insertions(+), 12 deletions(-) create mode 100644 ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt diff --git a/ktor-io/common/src/io/ktor/utils/io/core/Closeable.kt b/ktor-io/common/src/io/ktor/utils/io/core/Closeable.kt index 043fab49be2..e9c82d9f034 100644 --- a/ktor-io/common/src/io/ktor/utils/io/core/Closeable.kt +++ b/ktor-io/common/src/io/ktor/utils/io/core/Closeable.kt @@ -8,5 +8,6 @@ import kotlin.use as stdlibUse public expect interface Closeable : AutoCloseable -@Deprecated("Use stdlib implementation instead. Remove import of this function", ReplaceWith("stdlibUse(block)")) +@Suppress("DeprecatedCallableAddReplaceWith") +@Deprecated("Use stdlib implementation instead. Remove import of this function") public inline fun T.use(block: (T) -> R): R = stdlibUse(block) diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt index 30220aa3fbe..4286e88828b 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt @@ -51,6 +51,7 @@ public abstract class BaseApplicationEngine( val pipeline = pipeline BaseApplicationResponse.setupSendPipeline(pipeline.sendPipeline) + BaseApplicationResponse.setupFallbackResponse(pipeline) monitor.subscribe(ApplicationStarting) { if (!info.isFirstLoading) { @@ -63,6 +64,7 @@ public abstract class BaseApplicationEngine( it.installDefaultInterceptors() it.installDefaultTransformationChecker() } + monitor.subscribe(ApplicationStarted) { val finishedAt = getTimeMillis() val elapsedTimeInSeconds = (finishedAt - info.initializedStartAt) / 1_000.0 @@ -115,7 +117,7 @@ private fun Application.installDefaultTransformationChecker() { intercept(ApplicationCallPipeline.Plugins) { try { proceed() - } catch (e: CannotTransformContentToTypeException) { + } catch (_: CannotTransformContentToTypeException) { call.respond(HttpStatusCode.UnsupportedMediaType) } } diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt index 66a397d223d..c5bbeb4616d 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt @@ -14,7 +14,14 @@ import io.ktor.util.* import io.ktor.util.cio.* import io.ktor.util.internal.* import io.ktor.utils.io.* -import kotlinx.coroutines.* +import kotlinx.coroutines.CopyableThrowable +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.withContext + +private val ERROR_CONTENT = object : OutgoingContent.NoContent() { + override val status: HttpStatusCode = HttpStatusCode.InternalServerError +} public abstract class BaseApplicationResponse( final override val call: PipelineCall @@ -80,10 +87,12 @@ public abstract class BaseApplicationResponse( // TODO: What should we do if TransferEncoding was set and length is present? headers.append(HttpHeaders.ContentLength, contentLength.toStringFast(), safeOnly = false) } + !transferEncodingSet -> { when (content) { is OutgoingContent.ProtocolUpgrade -> { } + is OutgoingContent.NoContent -> headers.append(HttpHeaders.ContentLength, "0", safeOnly = false) else -> headers.append(HttpHeaders.TransferEncoding, "chunked", safeOnly = false) } @@ -331,5 +340,29 @@ public abstract class BaseApplicationResponse( response.respondOutgoingContent(body) } } + + public fun setupFallbackResponse(application: EnginePipeline) { + val inDevMode = application.developmentMode + application.intercept(EnginePipeline.Before) { + try { + proceed() + } catch (cause: Throwable) { + if (call.isHandled) return@intercept + + val response = call.response as? BaseApplicationResponse + ?: call.attributes[EngineResponseAttributeKey] + + val content = if (inDevMode) { + ExceptionPageContent(call, cause) + } else { + object : OutgoingContent.NoContent() { + override val status: HttpStatusCode = HttpStatusCode.InternalServerError + } + } + + response.respondOutgoingContent(content) + } + } + } } } diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt new file mode 100644 index 00000000000..b7dd751a015 --- /dev/null +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.server.engine.internal + +import io.ktor.http.content.OutgoingContent +import io.ktor.server.application.ApplicationCall +import io.ktor.server.plugins.origin +import io.ktor.server.request.httpMethod +import io.ktor.server.request.path +import io.ktor.utils.io.ByteReadChannel + +internal class ExceptionPageContent(call: ApplicationCall, cause: Throwable) : OutgoingContent.ReadChannelContent() { + + private val responsePage: String = buildString { + val request = call.request + append("

Internal Server Error

Request Information:

")
+        append("Method: ${request.httpMethod}\n")
+        append("Path: ${request.path()}\n")
+        append("Parameters: ${request.rawQueryParameters}\n")
+        append("From origin: ${request.origin}\n")
+        append("

Stack Trace:

")
+
+        val stackTrace = cause.stackTraceToString().lines()
+        stackTrace.forEach { element ->
+            append("$element
") + } + var currentCause = cause.cause + while (currentCause != null) { + append("
Caused by:
") + val causeStack = currentCause.stackTraceToString().lines() + causeStack.forEach { element -> + append("$element
") + } + currentCause = currentCause.cause + } + append("
") + } + + override fun readFrom(): ByteReadChannel = ByteReadChannel(responsePage) +} diff --git a/ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationCallHandler.kt b/ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationCallHandler.kt index e38b5aed89e..1e6eb77f15e 100644 --- a/ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationCallHandler.kt +++ b/ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationCallHandler.kt @@ -47,7 +47,7 @@ internal class NettyApplicationCallHandler( else -> try { enginePipeline.execute(call) - } catch (error: Exception) { + } catch (error: Throwable) { handleFailure(call, error) } } diff --git a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt index 40d326bd666..085fa81f926 100644 --- a/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt +++ b/ktor-server/ktor-server-test-base/jvm/src/io/ktor/server/test/base/EngineTestBaseJvm.kt @@ -334,8 +334,6 @@ actual abstract class EngineTestBase< followRedirects = false expectSuccess = false - - install(HttpRequestRetry) } } diff --git a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt index c939d333416..c9359afc302 100644 --- a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt +++ b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt @@ -27,15 +27,21 @@ import io.ktor.utils.io.core.* import io.ktor.utils.io.jvm.javaio.* import io.ktor.utils.io.streams.* import kotlinx.coroutines.* -import kotlinx.coroutines.debug.* -import org.slf4j.* -import java.io.* -import java.net.* +import kotlinx.coroutines.debug.DebugProbes +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import org.slf4j.event.Level +import org.slf4j.helpers.AbstractLogger +import java.io.File +import java.io.IOException +import java.net.HttpURLConnection +import java.net.Proxy +import java.net.URL import java.util.* import java.util.concurrent.* -import java.util.concurrent.atomic.* +import java.util.concurrent.atomic.AtomicInteger import kotlin.test.* -import kotlin.use abstract class SustainabilityTestSuite( hostFactory: ApplicationEngineFactory @@ -918,6 +924,57 @@ abstract class SustainabilityTestSuite(failCause) } + + @Test + fun testOnCallRespondException() = runTest { + var loggedException: Throwable? = null + val log = object : AbstractLogger() { + override fun isTraceEnabled(): Boolean = false + override fun isTraceEnabled(marker: Marker?): Boolean = false + override fun isDebugEnabled(): Boolean = false + override fun isDebugEnabled(marker: Marker?): Boolean = false + override fun isInfoEnabled(): Boolean = false + override fun isInfoEnabled(marker: Marker?): Boolean = false + override fun isWarnEnabled(): Boolean = false + override fun isWarnEnabled(marker: Marker?): Boolean = false + + override fun isErrorEnabled(): Boolean = true + + override fun isErrorEnabled(marker: Marker?): Boolean = true + + override fun getFullyQualifiedCallerName(): String = "TEST" + + override fun handleNormalizedLoggingCall( + level: Level?, + marker: Marker?, + messagePattern: String?, + arguments: Array?, + throwable: Throwable? + ) { + loggedException = throwable + } + } + + createAndStartServer(log = log) { + application.install( + createApplicationPlugin("MyPlugin") { + onCallRespond { _ -> + error("oh nooooo") + } + } + ) + + get { + call.respondText("hello world") + } + } + + withUrl("") { + assertEquals(HttpStatusCode.InternalServerError, status) + assertNotNull(loggedException) + loggedException = null + } + } } internal inline fun assertFails(block: () -> Unit) { From 0c1cdc88e9bc8328713efcf3668af744bc055068 Mon Sep 17 00:00:00 2001 From: "leonid.stashevsky" Date: Fri, 24 Jan 2025 11:02:06 +0100 Subject: [PATCH 2/4] fixup! KTOR-7139 Fux exception thrown in onCallRespond makes the client wait for response indefinitely --- ktor-server/ktor-server-core/api/ktor-server-core.api | 1 + ktor-server/ktor-server-core/api/ktor-server-core.klib.api | 1 + 2 files changed, 2 insertions(+) diff --git a/ktor-server/ktor-server-core/api/ktor-server-core.api b/ktor-server/ktor-server-core/api/ktor-server-core.api index 4cce6b30df3..1fc1838b04f 100644 --- a/ktor-server/ktor-server-core/api/ktor-server-core.api +++ b/ktor-server/ktor-server-core/api/ktor-server-core.api @@ -499,6 +499,7 @@ public final class io/ktor/server/engine/BaseApplicationResponse$BodyLengthIsToo public final class io/ktor/server/engine/BaseApplicationResponse$Companion { public final fun getEngineResponseAttributeKey ()Lio/ktor/util/AttributeKey; + public final fun setupFallbackResponse (Lio/ktor/server/engine/EnginePipeline;)V public final fun setupSendPipeline (Lio/ktor/server/response/ApplicationSendPipeline;)V } diff --git a/ktor-server/ktor-server-core/api/ktor-server-core.klib.api b/ktor-server/ktor-server-core/api/ktor-server-core.klib.api index 2fc2e279451..bba0e2efffe 100644 --- a/ktor-server/ktor-server-core/api/ktor-server-core.klib.api +++ b/ktor-server/ktor-server-core/api/ktor-server-core.klib.api @@ -370,6 +370,7 @@ abstract class io.ktor.server.engine/BaseApplicationResponse : io.ktor.server.re final val EngineResponseAttributeKey // io.ktor.server.engine/BaseApplicationResponse.Companion.EngineResponseAttributeKey|{}EngineResponseAttributeKey[0] final fun (): io.ktor.util/AttributeKey // io.ktor.server.engine/BaseApplicationResponse.Companion.EngineResponseAttributeKey.|(){}[0] + final fun setupFallbackResponse(io.ktor.server.engine/EnginePipeline) // io.ktor.server.engine/BaseApplicationResponse.Companion.setupFallbackResponse|setupFallbackResponse(io.ktor.server.engine.EnginePipeline){}[0] final fun setupSendPipeline(io.ktor.server.response/ApplicationSendPipeline) // io.ktor.server.engine/BaseApplicationResponse.Companion.setupSendPipeline|setupSendPipeline(io.ktor.server.response.ApplicationSendPipeline){}[0] } } From 23cb99e1093acb15233d00921c5d3e813ead4f38 Mon Sep 17 00:00:00 2001 From: "leonid.stashevsky" Date: Tue, 4 Feb 2025 13:30:56 +0100 Subject: [PATCH 3/4] fixup! fixup! KTOR-7139 Fux exception thrown in onCallRespond makes the client wait for response indefinitely --- .../ktor/server/engine/BaseApplicationEngine.kt | 2 +- .../server/engine/BaseApplicationResponse.kt | 5 ++++- .../ktor/server/engine/DefaultEnginePipeline.kt | 16 +++++++--------- .../engine/internal/ExceptionPageContent.kt | 4 ++++ .../test/io/ktor/tests/auth/BasicAuthTest.kt | 11 ++++++----- .../test/io/ktor/tests/auth/BearerAuthTest.kt | 11 ++++++----- .../plugins/statuspages/StatusPagesTest.kt | 8 ++++---- .../server/testing/TestApplicationEngine.kt | 2 +- .../testing/suites/SustainabilityTestSuite.kt | 12 ++++++++---- .../io/ktor/server/http/RespondWriteTest.kt | 17 ++++++++++------- 10 files changed, 51 insertions(+), 37 deletions(-) diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt index 4286e88828b..0ad4a1e2fa7 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationEngine.kt @@ -51,7 +51,7 @@ public abstract class BaseApplicationEngine( val pipeline = pipeline BaseApplicationResponse.setupSendPipeline(pipeline.sendPipeline) - BaseApplicationResponse.setupFallbackResponse(pipeline) + BaseApplicationResponse.setupFallbackResponse(pipeline, environment.log) monitor.subscribe(ApplicationStarting) { if (!info.isFirstLoading) { diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt index c5bbeb4616d..4b2efe575ac 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/BaseApplicationResponse.kt @@ -13,6 +13,7 @@ import io.ktor.server.response.* import io.ktor.util.* import io.ktor.util.cio.* import io.ktor.util.internal.* +import io.ktor.util.logging.Logger import io.ktor.utils.io.* import kotlinx.coroutines.CopyableThrowable import kotlinx.coroutines.Dispatchers @@ -341,7 +342,7 @@ public abstract class BaseApplicationResponse( } } - public fun setupFallbackResponse(application: EnginePipeline) { + public fun setupFallbackResponse(application: EnginePipeline, logger: Logger) { val inDevMode = application.developmentMode application.intercept(EnginePipeline.Before) { try { @@ -349,6 +350,8 @@ public abstract class BaseApplicationResponse( } catch (cause: Throwable) { if (call.isHandled) return@intercept + logger.error("Unhandled server error: \"${cause.message}\"", cause) + val response = call.response as? BaseApplicationResponse ?: call.attributes[EngineResponseAttributeKey] diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/DefaultEnginePipeline.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/DefaultEnginePipeline.kt index dd0476dafc6..102b5797176 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/DefaultEnginePipeline.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/DefaultEnginePipeline.kt @@ -76,15 +76,13 @@ public suspend fun logError(call: ApplicationCall, error: Throwable) { * * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.server.engine.defaultExceptionStatusCode) */ -public fun defaultExceptionStatusCode(cause: Throwable): HttpStatusCode? { - return when (cause) { - is BadRequestException -> HttpStatusCode.BadRequest - is NotFoundException -> HttpStatusCode.NotFound - is UnsupportedMediaTypeException -> HttpStatusCode.UnsupportedMediaType - is PayloadTooLargeException -> HttpStatusCode.PayloadTooLarge - is TimeoutException, is TimeoutCancellationException -> HttpStatusCode.GatewayTimeout - else -> null - } +public fun defaultExceptionStatusCode(cause: Throwable): HttpStatusCode? = when (cause) { + is BadRequestException -> HttpStatusCode.BadRequest + is NotFoundException -> HttpStatusCode.NotFound + is UnsupportedMediaTypeException -> HttpStatusCode.UnsupportedMediaType + is PayloadTooLargeException -> HttpStatusCode.PayloadTooLarge + is TimeoutException, is TimeoutCancellationException -> HttpStatusCode.GatewayTimeout + else -> null } private suspend fun tryRespondError(call: ApplicationCall, statusCode: HttpStatusCode) { diff --git a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt index b7dd751a015..cafe993f1ad 100644 --- a/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt +++ b/ktor-server/ktor-server-core/common/src/io/ktor/server/engine/internal/ExceptionPageContent.kt @@ -4,6 +4,7 @@ package io.ktor.server.engine.internal +import io.ktor.http.HttpStatusCode import io.ktor.http.content.OutgoingContent import io.ktor.server.application.ApplicationCall import io.ktor.server.plugins.origin @@ -13,6 +14,9 @@ import io.ktor.utils.io.ByteReadChannel internal class ExceptionPageContent(call: ApplicationCall, cause: Throwable) : OutgoingContent.ReadChannelContent() { + override val status: HttpStatusCode? + get() = HttpStatusCode.InternalServerError + private val responsePage: String = buildString { val request = call.request append("

Internal Server Error

Request Information:

")
diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt
index 087a3278a9a..0ca34cb5e18 100644
--- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt
+++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BasicAuthTest.kt
@@ -17,7 +17,10 @@ import io.ktor.server.testing.*
 import io.ktor.util.*
 import io.ktor.utils.io.charsets.*
 import io.ktor.utils.io.core.*
-import kotlin.test.*
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
 
 class BasicAuthTest {
     @Test
@@ -213,9 +216,7 @@ class BasicAuthTest {
             }
         }
 
-        assertFailsWith {
-            handleRequestWithBasic("/", "u", "a")
-        }
+        assertEquals(HttpStatusCode.InternalServerError, handleRequestWithBasic("/", "u", "a").status)
     }
 
     private suspend fun ApplicationTestBuilder.handleRequestWithBasic(
@@ -223,7 +224,7 @@ class BasicAuthTest {
         user: String,
         pass: String,
         charset: Charset = Charsets.ISO_8859_1
-    ) = client.get(url) {
+    ): HttpResponse = client.get(url) {
         val up = "$user:$pass"
         val encoded = up.toByteArray(charset).encodeBase64()
         header(HttpHeaders.Authorization, "Basic $encoded")
diff --git a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BearerAuthTest.kt b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BearerAuthTest.kt
index 1db4b94f288..e831cf7ade7 100644
--- a/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BearerAuthTest.kt
+++ b/ktor-server/ktor-server-plugins/ktor-server-auth/common/test/io/ktor/tests/auth/BearerAuthTest.kt
@@ -12,7 +12,8 @@ import io.ktor.server.auth.*
 import io.ktor.server.response.*
 import io.ktor.server.routing.*
 import io.ktor.server.testing.*
-import kotlin.test.*
+import kotlin.test.Test
+import kotlin.test.assertEquals
 
 class BearerAuthTest {
 
@@ -132,11 +133,11 @@ class BearerAuthTest {
             authenticate = { throw NotImplementedError() }
         )
 
-        assertFailsWith {
-            client.get("/") {
-                withToken("letmein")
-            }
+        val response = client.get("/") {
+            withToken("letmein")
         }
+
+        assertEquals(HttpStatusCode.InternalServerError, response.status)
     }
 
     @Test
diff --git a/ktor-server/ktor-server-plugins/ktor-server-status-pages/common/test/io/ktor/server/plugins/statuspages/StatusPagesTest.kt b/ktor-server/ktor-server-plugins/ktor-server-status-pages/common/test/io/ktor/server/plugins/statuspages/StatusPagesTest.kt
index c084edeace9..df7155d8ba3 100644
--- a/ktor-server/ktor-server-plugins/ktor-server-status-pages/common/test/io/ktor/server/plugins/statuspages/StatusPagesTest.kt
+++ b/ktor-server/ktor-server-plugins/ktor-server-status-pages/common/test/io/ktor/server/plugins/statuspages/StatusPagesTest.kt
@@ -17,8 +17,10 @@ import io.ktor.server.routing.*
 import io.ktor.server.testing.*
 import io.ktor.utils.io.*
 import io.ktor.utils.io.charsets.*
-import kotlinx.coroutines.*
 import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.async
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
 import kotlin.test.*
 
 class StatusPagesTest {
@@ -246,9 +248,7 @@ class StatusPagesTest {
             }
         }
 
-        assertFails {
-            client.get("/")
-        }
+        assertEquals(HttpStatusCode.NotFound, client.get("/").status)
     }
 
     @Test
diff --git a/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplicationEngine.kt b/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplicationEngine.kt
index d6345875349..26d91d40929 100644
--- a/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplicationEngine.kt
+++ b/ktor-server/ktor-server-test-host/common/src/io/ktor/server/testing/TestApplicationEngine.kt
@@ -118,7 +118,7 @@ public class TestApplicationEngine(
 
         val throwOnException = environment.config
             .propertyOrNull(CONFIG_KEY_THROW_ON_EXCEPTION)
-            ?.getString()?.toBoolean() ?: true
+            ?.getString()?.toBoolean() != false
         tryRespondError(
             defaultExceptionStatusCode(cause)
                 ?: if (throwOnException) throw cause else HttpStatusCode.InternalServerError
diff --git a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
index c9359afc302..f15779679bb 100644
--- a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
+++ b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
@@ -22,6 +22,7 @@ import io.ktor.server.routing.*
 import io.ktor.server.test.base.*
 import io.ktor.server.testing.*
 import io.ktor.util.cio.*
+import io.ktor.util.pipeline.PipelinePhase
 import io.ktor.utils.io.*
 import io.ktor.utils.io.core.*
 import io.ktor.utils.io.jvm.javaio.*
@@ -795,11 +796,10 @@ abstract class SustainabilityTestSuite {
-            client.get("/")
-        }
+        val response = client.get("/")
+        assertEquals(HttpStatusCode.OK, response.status)
+        assertEquals("", response.bodyAsText())
     }
 
     @Test
@@ -69,18 +71,19 @@ class RespondWriteTest {
         routing {
             get("/") {
                 call.respondTextWriter {
+                    write("OK")
                     close() // after that point the main pipeline is going to continue since the channel is closed
                     // so we can't simply bypass an exception
                     // the workaround is to hold pipeline on channel pipe until commit rather than just close
 
-                    Thread.sleep(1000)
+                    delay(1000)
                     throw IllegalStateException("expected")
                 }
             }
         }
 
-        assertFailsWith {
-            client.get("/")
-        }
+        val response = client.get("/")
+        assertEquals(HttpStatusCode.OK, response.status)
+        assertEquals("OK", response.bodyAsText())
     }
 }

From 1bb7d913e0e3f000544e786d53427c688cf74f0b Mon Sep 17 00:00:00 2001
From: "leonid.stashevsky" 
Date: Tue, 4 Feb 2025 13:32:03 +0100
Subject: [PATCH 4/4] fixup! fixup! fixup! KTOR-7139 Fux exception thrown in
 onCallRespond makes the client wait for response indefinitely

---
 .../io/ktor/server/testing/suites/SustainabilityTestSuite.kt | 5 ++---
 1 file changed, 2 insertions(+), 3 deletions(-)

diff --git a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
index f15779679bb..dfd4615aea1 100644
--- a/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
+++ b/ktor-server/ktor-server-test-suites/jvm/src/io/ktor/server/testing/suites/SustainabilityTestSuite.kt
@@ -808,10 +808,9 @@ abstract class SustainabilityTestSuite