Skip to content

Commit f5bd49d

Browse files
authored
Server. Detect if a request was cancelled by the client. (#5181)
* Server. Detect if a request was cancelled by the client. * Server CIO. Move `onClose` from `Request` to `ServerRequestScope`. * Server Netty. Ensure connection reset detection works with HTTP2.
1 parent 45e7955 commit f5bd49d

File tree

19 files changed

+407
-47
lines changed

19 files changed

+407
-47
lines changed

ktor-http/ktor-http-cio/common/src/io/ktor/http/cio/RequestResponse.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package io.ktor.http.cio
66

77
import io.ktor.http.*
88
import io.ktor.http.cio.internals.*
9+
import io.ktor.utils.io.InternalAPI
910
import io.ktor.utils.io.core.*
1011

1112
/**

ktor-io/api/ktor-io.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,12 @@ public final class io/ktor/utils/io/ConcurrentIOException : java/lang/IllegalSta
226226
public synthetic fun <init> (Ljava/lang/String;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
227227
}
228228

229+
public final class io/ktor/utils/io/ConnectionClosedException : java/io/IOException {
230+
public fun <init> ()V
231+
public fun <init> (Ljava/lang/String;)V
232+
public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
233+
}
234+
229235
public final class io/ktor/utils/io/CountedByteReadChannel : io/ktor/utils/io/ByteReadChannel {
230236
public fun <init> (Lio/ktor/utils/io/ByteReadChannel;)V
231237
public fun awaitContent (ILkotlin/coroutines/Continuation;)Ljava/lang/Object;

ktor-io/api/ktor-io.klib.api

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,10 @@ final class io.ktor.utils.io/ConcurrentIOException : kotlin/IllegalStateExceptio
236236
constructor <init>(kotlin/String, kotlin/Throwable? = ...) // io.ktor.utils.io/ConcurrentIOException.<init>|<init>(kotlin.String;kotlin.Throwable?){}[0]
237237
}
238238

239+
final class io.ktor.utils.io/ConnectionClosedException : kotlinx.io/IOException { // io.ktor.utils.io/ConnectionClosedException|null[0]
240+
constructor <init>(kotlin/String = ...) // io.ktor.utils.io/ConnectionClosedException.<init>|<init>(kotlin.String){}[0]
241+
}
242+
239243
final class io.ktor.utils.io/CountedByteReadChannel : io.ktor.utils.io/ByteReadChannel { // io.ktor.utils.io/CountedByteReadChannel|null[0]
240244
constructor <init>(io.ktor.utils.io/ByteReadChannel) // io.ktor.utils.io/CountedByteReadChannel.<init>|<init>(io.ktor.utils.io.ByteReadChannel){}[0]
241245

ktor-io/common/src/io/ktor/utils/io/Exceptions.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ public class ClosedWriteChannelException(cause: Throwable? = null) : ClosedByteC
2828
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.utils.io.ClosedReadChannelException)
2929
*/
3030
public class ClosedReadChannelException(cause: Throwable? = null) : ClosedByteChannelException(cause)
31+
32+
/**
33+
* Exception thrown when a network connection is closed or reset by peer.
34+
* This exception is used to signal that the underlying connection was terminated.
35+
*
36+
* [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.utils.io.ConnectionClosedException)
37+
*/
38+
public class ConnectionClosedException(message: String = "Connection was closed") : IOException(message)

ktor-server/ktor-server-cio/common/src/io/ktor/server/cio/CIOApplicationEngine.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@ package io.ktor.server.cio
66

77
import io.ktor.events.*
88
import io.ktor.http.*
9+
import io.ktor.http.cio.Request
910
import io.ktor.server.application.*
1011
import io.ktor.server.cio.backend.*
1112
import io.ktor.server.cio.internal.*
1213
import io.ktor.server.engine.*
14+
import io.ktor.server.http.HttpRequestCloseHandlerKey
1315
import io.ktor.server.request.*
1416
import io.ktor.server.response.*
1517
import io.ktor.util.pipeline.*
@@ -169,7 +171,15 @@ public class CIOApplicationEngine(
169171
return transferEncoding != null || (contentLength != null && contentLength > 0)
170172
}
171173

172-
private suspend fun ServerRequestScope.handleRequest(request: io.ktor.http.cio.Request) {
174+
@OptIn(InternalAPI::class)
175+
private fun ServerRequestScope.setCloseHandler(call: CIOApplicationCall) {
176+
onClose = {
177+
val requestCloseHandler = call.attributes.getOrNull(HttpRequestCloseHandlerKey)
178+
requestCloseHandler?.invoke()
179+
}
180+
}
181+
182+
private suspend fun ServerRequestScope.handleRequest(request: Request) {
173183
withContext(userDispatcher) requestContext@{
174184
val call = CIOApplicationCall(
175185
applicationProvider(),
@@ -186,6 +196,7 @@ public class CIOApplicationEngine(
186196

187197
try {
188198
addHandlerForExpectedHeader(output, call)
199+
setCloseHandler(call)
189200
pipeline.execute(call)
190201
} catch (error: Throwable) {
191202
handleFailure(call, error)

ktor-server/ktor-server-cio/common/src/io/ktor/server/cio/backend/ServerPipeline.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,12 @@ public fun CoroutineScope.startServerConnectionPipeline(
5959

6060
val requestContext = RequestHandlerCoroutine + Dispatchers.Unconfined
6161

62+
var handlerScope: ServerRequestScope? = null
6263
try {
6364
while (true) { // parse requests loop
6465
val request = try {
6566
parseRequest(connection.input) ?: break
66-
} catch (cause: TooLongLineException) {
67+
} catch (_: TooLongLineException) {
6768
respondBadRequest(actorChannel)
6869
break // end pipeline loop
6970
} catch (io: IOException) {
@@ -113,7 +114,7 @@ public fun CoroutineScope.startServerConnectionPipeline(
113114
contentType
114115
)
115116
expectedHttpUpgrade = !expectedHttpBody && expectHttpUpgrade(request.method, upgrade, connectionOptions)
116-
} catch (cause: Throwable) {
117+
} catch (_: Throwable) {
117118
request.release()
118119
response.writePacket(BadRequestPacket.copy())
119120
response.close()
@@ -129,7 +130,7 @@ public fun CoroutineScope.startServerConnectionPipeline(
129130
val upgraded = if (expectedHttpUpgrade) CompletableDeferred<Boolean>() else null
130131

131132
launch(requestContext, start = CoroutineStart.UNDISPATCHED) {
132-
val handlerScope = ServerRequestScope(
133+
handlerScope = ServerRequestScope(
133134
coroutineContext,
134135
requestBody,
135136
response,
@@ -181,10 +182,11 @@ public fun CoroutineScope.startServerConnectionPipeline(
181182

182183
if (isLastHttpRequest(version, connectionOptions)) break
183184
}
184-
} catch (cause: IOException) {
185+
} catch (_: IOException) {
185186
// already handled
186187
coroutineContext.cancel()
187188
} finally {
189+
handlerScope?.onClose?.invoke()
188190
actorChannel.close()
189191
}
190192
}

ktor-server/ktor-server-cio/common/src/io/ktor/server/cio/backend/ServerRequestScope.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,6 @@ public class ServerRequestScope internal constructor(
4242
localAddress,
4343
upgraded
4444
)
45+
46+
internal var onClose: (() -> Unit)? = null
4547
}

ktor-server/ktor-server-cio/jvm/test/io/ktor/tests/server/cio/CIOEngineTestJvm.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,3 +88,11 @@ class CIOHooksTest : HooksTestSuite<CIOApplicationEngine, CIOApplicationEngine.C
8888
enableSsl = false
8989
}
9090
}
91+
92+
class CIOHttpRequestLifecycleTest :
93+
HttpRequestLifecycleTest<CIOApplicationEngine, CIOApplicationEngine.Configuration>(CIO) {
94+
init {
95+
enableSsl = false
96+
enableHttp2 = false
97+
}
98+
}

ktor-server/ktor-server-core/api/ktor-server-core.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,16 @@ public final class io/ktor/server/http/HttpDateJvmKt {
782782
public static final fun toHttpDateString (Ljava/time/temporal/Temporal;)Ljava/lang/String;
783783
}
784784

785+
public final class io/ktor/server/http/HttpRequestLifecycleConfig {
786+
public final fun getCancelCallOnClose ()Z
787+
public final fun setCancelCallOnClose (Z)V
788+
}
789+
790+
public final class io/ktor/server/http/HttpRequestLifecycleKt {
791+
public static final fun getHttpRequestCloseHandlerKey ()Lio/ktor/util/AttributeKey;
792+
public static final fun getHttpRequestLifecycle ()Lio/ktor/server/application/RouteScopedPlugin;
793+
}
794+
785795
public final class io/ktor/server/http/LinkHeaderKt {
786796
public static final fun link (Lio/ktor/server/response/ApplicationResponse;Lio/ktor/http/LinkHeader;)V
787797
public static final fun link (Lio/ktor/server/response/ApplicationResponse;Ljava/lang/String;[Ljava/lang/String;)V

ktor-server/ktor-server-core/api/ktor-server-core.klib.api

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,12 @@ final class io.ktor.server.http.content/HttpStatusCodeContent : io.ktor.http.con
671671
final fun toString(): kotlin/String // io.ktor.server.http.content/HttpStatusCodeContent.toString|toString(){}[0]
672672
}
673673

674+
final class io.ktor.server.http/HttpRequestLifecycleConfig { // io.ktor.server.http/HttpRequestLifecycleConfig|null[0]
675+
final var cancelCallOnClose // io.ktor.server.http/HttpRequestLifecycleConfig.cancelCallOnClose|{}cancelCallOnClose[0]
676+
final fun <get-cancelCallOnClose>(): kotlin/Boolean // io.ktor.server.http/HttpRequestLifecycleConfig.cancelCallOnClose.<get-cancelCallOnClose>|<get-cancelCallOnClose>(){}[0]
677+
final fun <set-cancelCallOnClose>(kotlin/Boolean) // io.ktor.server.http/HttpRequestLifecycleConfig.cancelCallOnClose.<set-cancelCallOnClose>|<set-cancelCallOnClose>(kotlin.Boolean){}[0]
678+
}
679+
674680
final class io.ktor.server.plugins/CannotTransformContentToTypeException : io.ktor.server.plugins/ContentTransformationException, kotlinx.coroutines/CopyableThrowable<io.ktor.server.plugins/CannotTransformContentToTypeException> { // io.ktor.server.plugins/CannotTransformContentToTypeException|null[0]
675681
constructor <init>(kotlin.reflect/KType) // io.ktor.server.plugins/CannotTransformContentToTypeException.<init>|<init>(kotlin.reflect.KType){}[0]
676682

@@ -1709,6 +1715,10 @@ final val io.ktor.server.http.content/isCompressionSuppressed // io.ktor.server.
17091715
final fun (io.ktor.server.application/ApplicationCall).<get-isCompressionSuppressed>(): kotlin/Boolean // io.ktor.server.http.content/isCompressionSuppressed.<get-isCompressionSuppressed>|<get-isCompressionSuppressed>@io.ktor.server.application.ApplicationCall(){}[0]
17101716
final val io.ktor.server.http.content/isDecompressionSuppressed // io.ktor.server.http.content/isDecompressionSuppressed|@io.ktor.server.application.ApplicationCall{}isDecompressionSuppressed[0]
17111717
final fun (io.ktor.server.application/ApplicationCall).<get-isDecompressionSuppressed>(): kotlin/Boolean // io.ktor.server.http.content/isDecompressionSuppressed.<get-isDecompressionSuppressed>|<get-isDecompressionSuppressed>@io.ktor.server.application.ApplicationCall(){}[0]
1718+
final val io.ktor.server.http/HttpRequestCloseHandlerKey // io.ktor.server.http/HttpRequestCloseHandlerKey|{}HttpRequestCloseHandlerKey[0]
1719+
final fun <get-HttpRequestCloseHandlerKey>(): io.ktor.util/AttributeKey<kotlin/Function0<kotlin/Unit>> // io.ktor.server.http/HttpRequestCloseHandlerKey.<get-HttpRequestCloseHandlerKey>|<get-HttpRequestCloseHandlerKey>(){}[0]
1720+
final val io.ktor.server.http/HttpRequestLifecycle // io.ktor.server.http/HttpRequestLifecycle|{}HttpRequestLifecycle[0]
1721+
final fun <get-HttpRequestLifecycle>(): io.ktor.server.application/RouteScopedPlugin<io.ktor.server.http/HttpRequestLifecycleConfig> // io.ktor.server.http/HttpRequestLifecycle.<get-HttpRequestLifecycle>|<get-HttpRequestLifecycle>(){}[0]
17121722
final val io.ktor.server.logging/mdcProvider // io.ktor.server.logging/mdcProvider|@io.ktor.server.application.Application{}mdcProvider[0]
17131723
final fun (io.ktor.server.application/Application).<get-mdcProvider>(): io.ktor.server.logging/MDCProvider // io.ktor.server.logging/mdcProvider.<get-mdcProvider>|<get-mdcProvider>@io.ktor.server.application.Application(){}[0]
17141724
final val io.ktor.server.plugins/MutableOriginConnectionPointKey // io.ktor.server.plugins/MutableOriginConnectionPointKey|{}MutableOriginConnectionPointKey[0]

0 commit comments

Comments
 (0)