Skip to content

Commit e8e7c52

Browse files
committed
fix hang caused by StreamableHttpClientTransport (#226)
1 parent 6bae987 commit e8e7c52

File tree

2 files changed

+52
-2
lines changed

2 files changed

+52
-2
lines changed

kotlin-sdk-client/src/commonMain/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransport.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,10 @@ public class StreamableHttpClientTransport(
142142
ContentType.Application.Json -> response.bodyAsText().takeIf { it.isNotEmpty() }?.let { json ->
143143
runCatching { McpJson.decodeFromString<JSONRPCMessage>(json) }
144144
.onSuccess { _onMessage(it) }
145-
.onFailure(_onError)
145+
.onFailure {
146+
_onError(it)
147+
throw it
148+
}
146149
}
147150

148151
ContentType.Text.EventStream -> handleInlineSse(
@@ -313,7 +316,10 @@ public class StreamableHttpClientTransport(
313316
_onMessage(msg)
314317
}
315318
}
316-
.onFailure(_onError)
319+
.onFailure {
320+
_onError(it)
321+
throw it
322+
}
317323
}
318324
// reset
319325
id = null

kotlin-sdk-client/src/commonTest/kotlin/io/modelcontextprotocol/kotlin/sdk/client/StreamableHttpClientTransportTest.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,16 @@ import io.ktor.http.HttpStatusCode
1212
import io.ktor.http.content.TextContent
1313
import io.ktor.http.headersOf
1414
import io.ktor.utils.io.ByteReadChannel
15+
import io.modelcontextprotocol.kotlin.sdk.Implementation
1516
import io.modelcontextprotocol.kotlin.sdk.JSONRPCMessage
1617
import io.modelcontextprotocol.kotlin.sdk.JSONRPCNotification
1718
import io.modelcontextprotocol.kotlin.sdk.JSONRPCRequest
1819
import io.modelcontextprotocol.kotlin.sdk.RequestId
1920
import io.modelcontextprotocol.kotlin.sdk.shared.McpJson
21+
import kotlinx.coroutines.TimeoutCancellationException
2022
import kotlinx.coroutines.delay
2123
import kotlinx.coroutines.test.runTest
24+
import kotlinx.coroutines.withTimeout
2225
import kotlinx.serialization.json.JsonObject
2326
import kotlinx.serialization.json.JsonPrimitive
2427
import kotlinx.serialization.json.buildJsonObject
@@ -27,6 +30,7 @@ import kotlin.test.Test
2730
import kotlin.test.assertEquals
2831
import kotlin.test.assertNull
2932
import kotlin.test.assertTrue
33+
import kotlin.test.fail
3034
import kotlin.time.Duration.Companion.seconds
3135

3236
class StreamableHttpClientTransportTest {
@@ -380,4 +384,44 @@ class StreamableHttpClientTransportTest {
380384
assertEquals("resume-100", resumptionTokenReceived)
381385
transport.close()
382386
}
387+
388+
@Test
389+
fun testClientConnectWithInvalidJson() = runTest {
390+
// Transport under test: respond with invalid JSON for the initialize request
391+
val transport = createTransport { _ ->
392+
respond(
393+
"this is not valid json",
394+
status = HttpStatusCode.OK,
395+
headers = headersOf(HttpHeaders.ContentType, ContentType.Application.Json.toString()),
396+
)
397+
}
398+
399+
val client = Client(
400+
clientInfo = Implementation(
401+
name = "test-client",
402+
version = "1.0",
403+
),
404+
)
405+
406+
runCatching {
407+
// Real time-keeping is needed; otherwise Protocol will always throw TimeoutCancellationException in tests
408+
withTimeout(5.seconds) {
409+
client.connect(transport)
410+
}
411+
412+
}.onSuccess {
413+
fail("Expected client.connect to fail on invalid JSON response")
414+
}.onFailure { e ->
415+
when (e) {
416+
is TimeoutCancellationException -> fail("Client connect caused a hang", e)
417+
is IllegalStateException -> {
418+
// Expected behavior: connect finishes and fails with an exception.
419+
}
420+
421+
else -> fail("Unexpected exception during client.connect", e)
422+
}
423+
}.also {
424+
transport.close()
425+
}
426+
}
383427
}

0 commit comments

Comments
 (0)