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

@ -24,11 +24,12 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.serialization.SerializationException
import kotlin.concurrent.atomics.ExperimentalAtomicApi
import kotlin.time.Duration
@ -171,13 +172,15 @@ public class SseClientTransport(
}
override suspend fun closeResources() {
job?.cancelAndJoin()
try {
if (::session.isInitialized) session.cancel()
if (::scope.isInitialized) scope.cancel()
endpoint.cancel()
} catch (e: Throwable) {
_onError(e)
withContext(NonCancellable) {
job?.cancel()
try {
if (::session.isInitialized) session.cancel()
if (::scope.isInitialized) scope.cancel()
endpoint.cancel()
} catch (e: Throwable) {
_onError(e)
}
}
}
}

View file

@ -21,6 +21,7 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.cancelAndJoin
@ -31,6 +32,7 @@ import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.flow.channelFlow
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.yield
import kotlinx.io.Buffer
import kotlinx.io.IOException
@ -241,8 +243,10 @@ public class StdioClientTransport @JvmOverloads public constructor(
}
override suspend fun closeResources() {
scope.stopProcessing("Closed")
scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete
withContext(NonCancellable) {
scope.stopProcessing("Closed")
scope.coroutineContext[Job]?.join() // Wait for all coroutines to complete
}
}
private fun sendOutboundMessage(message: JSONRPCMessage, sink: Sink, mainScope: CoroutineScope) {
@ -264,6 +268,8 @@ public class StdioClientTransport @JvmOverloads public constructor(
private suspend fun handleJSONRPCMessage(msg: JSONRPCMessage) {
try {
_onMessage.invoke(msg)
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
logger.error(e) { "Error processing message." }
runCatching { _onError.invoke(e) }