fix: propagate CancellationException instead of logging as error (#706)

fixes #242

## How Has This Been Tested?
new and existing tests pass

## Breaking Changes
none 

## Types of changes
- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update

## Checklist
- [x] I have read the [MCP
Documentation](https://modelcontextprotocol.io)
- [x] My code follows the repository's style guidelines
- [x] New and existing tests pass locally
- [x] I have added appropriate error handling
- [x] I have added or updated documentation as needed
This commit is contained in:
Pavel Gorgulov 2026-04-15 14:59:01 +02:00 • committed by GitHub
commit e7b09d84f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 109 additions and 25 deletions

View file

@ -84,6 +84,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio
}
call.receiveText()
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest)
_onError.invoke(e)
@ -92,6 +94,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio
try {
handleMessage(body)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
call.respondText("Error handling message $body: ${e.message}", status = HttpStatusCode.BadRequest)
return
@ -108,6 +112,8 @@ public class SseServerTransport(private val endpoint: String, private val sessio
try {
val parsedMessage = McpJson.decodeFromString<JSONRPCMessage>(message)
_onMessage.invoke(parsedMessage)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_onError.invoke(e)
throw e

View file

@ -28,15 +28,18 @@ import io.modelcontextprotocol.kotlin.sdk.types.RPCError
import io.modelcontextprotocol.kotlin.sdk.types.RPCError.ErrorCode.REQUEST_TIMEOUT
import io.modelcontextprotocol.kotlin.sdk.types.RequestId
import io.modelcontextprotocol.kotlin.sdk.types.SUPPORTED_PROTOCOL_VERSIONS
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.job
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.decodeFromJsonElement
import kotlin.concurrent.atomics.AtomicBoolean
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.coroutines.cancellation.CancellationException
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.uuid.ExperimentalUuidApi
@ -283,17 +286,19 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
}
override suspend fun close() {
streamMutex.withLock {
streamsMapping.values.forEach {
try {
it.session?.close()
} catch (_: Exception) {
withContext(NonCancellable) {
streamMutex.withLock {
streamsMapping.values.forEach {
try {
it.session?.close()
} catch (_: Exception) {
}
}
streamsMapping.clear()
requestToStreamMapping.clear()
requestToResponseMapping.clear()
invokeOnCloseCallback()
}
streamsMapping.clear()
requestToStreamMapping.clear()
requestToResponseMapping.clear()
invokeOnCloseCallback()
}
}
@ -420,6 +425,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
call.coroutineContext.job.invokeOnCompletion { streamsMapping.remove(streamId) }
messages.forEach { message -> _onMessage(message) }
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
call.reject(
HttpStatusCode.BadRequest,
@ -498,12 +505,14 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
val streamId = requestToStreamMapping[requestId] ?: return
val sessionContext = streamsMapping[streamId] ?: return
try {
sessionContext.session?.close()
} catch (e: Exception) {
_onError(e)
} finally {
streamsMapping.remove(streamId)
withContext(NonCancellable) {
try {
sessionContext.session?.close()
} catch (e: Exception) {
_onError(e)
} finally {
streamsMapping.remove(streamId)
}
}
}
@ -554,6 +563,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
id = eventId,
data = McpJson.encodeToString(message),
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_onError(IllegalStateException("Failed to replay event: ${e.message}", e))
}
@ -565,6 +576,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
streamsMapping.remove(streamId)
throwable?.let { _onError(it) }
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_onError(e)
}
@ -658,6 +671,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
private suspend fun flushSse(session: ServerSSESession?) {
try {
session?.send(data = "")
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_onError(e)
}
@ -709,6 +724,9 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
val eventId = configuration.eventStore?.storeEvent(streamId, message)
try {
session?.send(event = "message", id = eventId, data = McpJson.encodeToString(message))
} catch (e: CancellationException) {
streamsMapping.remove(streamId)
throw e
} catch (_: Exception) {
streamsMapping.remove(streamId)
}
@ -733,6 +751,8 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
retry = configuration.retryInterval?.inWholeMilliseconds,
data = "",
)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
_onError(e)
}