Skip to content

Commit

Permalink
fixup! fixup! KTOR-7139 Fux exception thrown in onCallRespond makes t…
Browse files Browse the repository at this point in the history
…he client wait for response indefinitely
  • Loading branch information
e5l committed Feb 4, 2025
1 parent 0c1cdc8 commit 23cb99e
Show file tree
Hide file tree
Showing 10 changed files with 51 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -341,14 +342,16 @@ 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 {
proceed()
} 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]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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("<html><body><h1>Internal Server Error</h1><h2>Request Information:</h2><pre>")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -213,17 +216,15 @@ class BasicAuthTest {
}
}

assertFailsWith<NotImplementedError> {
handleRequestWithBasic("/", "u", "a")
}
assertEquals(HttpStatusCode.InternalServerError, handleRequestWithBasic("/", "u", "a").status)
}

private suspend fun ApplicationTestBuilder.handleRequestWithBasic(
url: String,
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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -132,11 +133,11 @@ class BearerAuthTest {
authenticate = { throw NotImplementedError() }
)

assertFailsWith<NotImplementedError> {
client.get("/") {
withToken("letmein")
}
val response = client.get("/") {
withToken("letmein")
}

assertEquals(HttpStatusCode.InternalServerError, response.status)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -246,9 +248,7 @@ class StatusPagesTest {
}
}

assertFails {
client.get("/")
}
assertEquals(HttpStatusCode.NotFound, client.get("/").status)
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand Down Expand Up @@ -795,19 +796,22 @@ abstract class SustainabilityTestSuite<TEngine : ApplicationEngine, TConfigurati
val loggerDelegate = LoggerFactory.getLogger("ktor.test")
val logger = object : Logger by loggerDelegate {
override fun error(message: String?, cause: Throwable?) {
println(cause.toString())
exceptions.add(cause!!)
}
}
val phase = EnginePipeline.Before

val server = createServer(log = logger) {
routing {
get("/req") {
call.respond("SUCCESS")
}
}
}
(server.engine as BaseApplicationEngine).pipeline.intercept(phase) {

val phase = PipelinePhase("test")
val pipeline = (server.engine as BaseApplicationEngine).pipeline
pipeline.insertPhaseBefore(EnginePipeline.Before, phase)
pipeline.intercept(EnginePipeline.Before) {
throw IllegalStateException("Failed in engine pipeline")
}
startServer(server)
Expand All @@ -818,7 +822,7 @@ abstract class SustainabilityTestSuite<TEngine : ApplicationEngine, TConfigurati
}
}) {
assertEquals(HttpStatusCode.InternalServerError, status, "Failed in engine pipeline")
assertEquals(exceptions.size, 1, "Failed in phase $phase")
assertEquals(1, exceptions.size, "Failed in phase $phase")
assertEquals("Failed in engine pipeline", exceptions[0].message)
exceptions.clear()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ package io.ktor.server.http

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.testing.*
import kotlinx.coroutines.*
import org.apache.http.HttpStatus
import java.util.concurrent.*
import kotlin.test.*

Expand Down Expand Up @@ -38,9 +40,9 @@ class RespondWriteTest {
}
}

assertFailsWith<IllegalStateException> {
client.get("/")
}
val response = client.get("/")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("", response.bodyAsText())
}

@Test
Expand Down Expand Up @@ -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<IllegalStateException> {
client.get("/")
}
val response = client.get("/")
assertEquals(HttpStatusCode.OK, response.status)
assertEquals("OK", response.bodyAsText())
}
}

0 comments on commit 23cb99e

Please sign in to comment.