-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Server. Detect if a request was cancelled by the client. #5181
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
be2bc95
12d380f
18b8dda
b512714
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| /* | ||
| * 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.http | ||
|
|
||
| import io.ktor.server.application.* | ||
| import io.ktor.server.application.hooks.* | ||
| import io.ktor.util.* | ||
| import io.ktor.utils.io.* | ||
| import kotlinx.coroutines.CancellationException | ||
| import kotlinx.coroutines.cancel | ||
|
|
||
| /** | ||
| * Configuration for the [HttpRequestLifecycle] plugin. | ||
| */ | ||
| public class HttpRequestLifecycleConfig internal constructor() { | ||
| /** | ||
| * When `true`, cancels the call coroutine context if the other peer resets the client connection. | ||
| * When `false` (default), request processing continues even if the connection is closed. | ||
| * | ||
| * **When to use this property: ** | ||
| * - Set to `true` for long-running or resource-intensive requests where you want to stop processing | ||
| * immediately when the client disconnects (e.g., streaming, batch processing, heavy computations) | ||
| * - Keep as `false` (default) for short requests, or when you need to complete processing regardless | ||
| * of client connection status (e.g., important side effects, database transactions) | ||
| * | ||
| * Example: | ||
| * ```kotlin | ||
| * install(HttpRequestLifecycle) { | ||
| * cancelCallOnClose = true | ||
| * } | ||
| * ``` | ||
| */ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not clear from the documentation where the cancellation exception will be thrown. Could you please add an example of handling the exception as well?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is also unclear what the use case for the new property is - when should someone use it? |
||
| public var cancelCallOnClose: Boolean = false | ||
| } | ||
|
|
||
| /** | ||
| * Internal attribute key for storing the connection close handler callback. | ||
| */ | ||
| @InternalAPI | ||
| public val HttpRequestCloseHandlerKey: AttributeKey<() -> Unit> = AttributeKey<() -> Unit>("HttpRequestCloseHandler") | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would make a class to store the config instead of using a callback type. It will have name with clear semantic
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @e5l |
||
|
|
||
| /** | ||
| * A plugin that manages the HTTP request lifecycle, particularly handling client disconnections. | ||
| * | ||
| * The [HttpRequestLifecycle] plugin allows you to detect and respond to client connection closures | ||
| * during request processing. When configured with [HttpRequestLifecycleConfig.cancelCallOnClose] set to `true`, | ||
| * the plugin will automatically cancel the request handling coroutine if the client disconnects, | ||
| * preventing unnecessary processing and freeing up resources. | ||
| * | ||
| * Remember, when the coroutine context is canceled, the next suspension point will throw [CancellationException], but until | ||
| * that moment it doesn't stop any blocking operations, so call `call.coroutineContext.ensureActive` if needed. | ||
| * Plugin only works for CIO and Netty engines. Other implementations fail on closed connection only when trying to write some response. | ||
| * | ||
| * This is particularly useful for: | ||
| * - Long-running requests where the client may disconnect before completion | ||
| * - Streaming responses where detecting disconnection allows early cleanup | ||
| * - Resource-intensive operations that should be canceled when the client is no longer waiting | ||
| * | ||
| * ## Example | ||
| * | ||
| * ```kotlin | ||
| * install(HttpRequestLifecycle) { | ||
| * cancelCallOnClose = true | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this should be the default, and we don't need an option to change it. @bjhham, what do you think?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was the initial idea, but iirc @zibet27 mentioned it broke quite a few tests so it appears it could be quite impactful. There's not many scenarios for keeping the call coroutine alive after the connection is lost, but they do exist. Maybe the best approach would be to log a ticket to enable it by default in 4.0.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @e5l I think the cancellation is needed only for computation-heavy requests because it can cancel a DB insert, which is usually not what you want, even if the client is not waiting for a response.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be pretty uncommon for a client to hang up before receiving a response like this though. Maybe if the DB is under load and the client has a short timeout though... I agree though, it's probably safer to assume that there could be sensitive work being done in the call coroutine. The main use-case for cancellation would be long-polling connections I guess. |
||
| * } | ||
| * | ||
| * routing { | ||
| * get("/long-process") { | ||
| * try { | ||
| * // Long-running operation | ||
| * repeat(100) { | ||
| * // throws an exception if the client disconnects during processing | ||
| * call.coroutineContext.ensureActive() | ||
| * // Process more data... | ||
| * logger.info("Very important work.") | ||
| * } | ||
| * call.respond("Completed") | ||
| * } catch (e: CancellationException) { | ||
| * // Handle client disconnected, clean up resources | ||
| * } | ||
| * } | ||
| * } | ||
| * ``` | ||
| */ | ||
| @OptIn(InternalAPI::class) | ||
| public val HttpRequestLifecycle: RouteScopedPlugin<HttpRequestLifecycleConfig> = createRouteScopedPlugin( | ||
| name = "HttpRequestLifecycle", | ||
| createConfiguration = ::HttpRequestLifecycleConfig | ||
| ) { | ||
| on(CallSetup) { call -> | ||
| if ( | ||
| [email protected] || | ||
| call.attributes.contains(HttpRequestCloseHandlerKey) | ||
| ) { | ||
| return@on | ||
| } | ||
| call.attributes.put(HttpRequestCloseHandlerKey) { | ||
| val cause = CancellationException( | ||
| "Call context was cancelled by `HttpRequestLifecycle` plugin", | ||
| ConnectionClosedException() | ||
| ) | ||
| call.coroutineContext.cancel(cause) | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, could you check if using
invokeOnCompletionfor the job is viable here instead of introducing the field?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey! Sorry, I forgot to mention it here.
invokeOnCompletiondoesn't help in this case.