fix: bound request body size in SSE and Streamable HTTP transports (#839)

Caps the size of incoming HTTP POST bodies in both server transports so
an oversized request can no longer exhaust server memory

## How Has This Been Tested?
new tests

## Breaking Changes
binary incompatible for SseServerTransport constuctors 

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

## Checklist
- [x] I have read the [MCP Documentation](https:
- [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-06-30 20:43:45 +02:00 • committed by GitHub
commit 35da063c23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 308 additions and 36 deletions

View file

@ -51,12 +51,12 @@ public final class io/modelcontextprotocol/kotlin/sdk/server/HostValidationKt {
}
public final class io/modelcontextprotocol/kotlin/sdk/server/KtorServerKt {
public static final fun mcp (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun mcp$default (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun mcp (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V
public static final fun mcp (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;)V
public static synthetic fun mcp$default (Lio/ktor/server/application/Application;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static synthetic fun mcp$default (Lio/ktor/server/routing/Route;ZLjava/util/List;Ljava/util/List;JLkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun mcpStatelessStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V
public static synthetic fun mcpStatelessStreamableHttp$default (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V
public static final fun mcpStreamableHttp (Lio/ktor/server/application/Application;Ljava/lang/String;ZLjava/util/List;Ljava/util/List;Lio/modelcontextprotocol/kotlin/sdk/server/EventStore;Lkotlin/jvm/functions/Function1;)V
@ -208,7 +208,8 @@ public class io/modelcontextprotocol/kotlin/sdk/server/ServerSession : io/modelc
}
public final class io/modelcontextprotocol/kotlin/sdk/server/SseServerTransport : io/modelcontextprotocol/kotlin/sdk/shared/AbstractTransport {
public fun <init> (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;)V
public fun <init> (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;J)V
public synthetic fun <init> (Ljava/lang/String;Lio/ktor/server/sse/ServerSSESession;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun close (Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getSessionId ()Ljava/lang/String;
public final fun handleMessage (Ljava/lang/String;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;

View file

@ -44,6 +44,8 @@ private val logger = KotlinLogging.logger {}
* When `null` while the localhost host defaults are in effect (no custom `allowedHosts`),
* the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`.
* With custom `allowedHosts`, `null` skips origin validation.
* @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are
* rejected with `413 Payload Too Large`. Defaults to 4 MiB.
* @param block factory block with access to the [ServerSSESession]
* that creates and returns the [Server] to handle the connection.
* @throws IllegalStateException if the [SSE] plugin is not installed.
@ -55,10 +57,11 @@ public fun Route.mcp(
enableDnsRebindingProtection: Boolean = true,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE,
block: ServerSSESession.() -> Server,
) {
route(path) {
mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, block)
mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block)
}
}
@ -76,15 +79,19 @@ public fun Route.mcp(
* When `null` while the localhost host defaults are in effect (no custom `allowedHosts`),
* the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`.
* With custom `allowedHosts`, `null` skips origin validation.
* @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are
* rejected with `413 Payload Too Large`. Defaults to 4 MiB.
* @param block factory block with access to the [ServerSSESession]
* that creates and returns the [Server] to handle the connection.
* @throws IllegalStateException if the [SSE] plugin is not installed.
*/
@KtorDsl
@Suppress("LongParameterList")
public fun Route.mcp(
enableDnsRebindingProtection: Boolean = true,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE,
block: ServerSSESession.() -> Server,
) {
try {
@ -103,7 +110,7 @@ public fun Route.mcp(
val transportManager = TransportManager<SseServerTransport>()
sse {
mcpSseEndpoint("", transportManager, block)
mcpSseEndpoint("", transportManager, maxRequestBodySize, block)
}
post {
@ -126,21 +133,25 @@ public fun Route.mcp(
* When `null` while the localhost host defaults are in effect (no custom `allowedHosts`),
* the `Origin` header is validated against `localhost`, `127.0.0.1`, `[::1]`.
* With custom `allowedHosts`, `null` skips origin validation.
* @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are
* rejected with `413 Payload Too Large`. Defaults to 4 MiB.
* @param block factory block with access to the [ServerSSESession]
* that creates and returns the [Server] to handle the connection.
*/
@KtorDsl
@Suppress("LongParameterList")
public fun Application.mcp(
enableDnsRebindingProtection: Boolean = true,
allowedHosts: List<String>? = null,
allowedOrigins: List<String>? = null,
maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE,
block: ServerSSESession.() -> Server,
) {
installMcpContentNegotiation()
install(SSE)
routing {
mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, block)
mcp(enableDnsRebindingProtection, allowedHosts, allowedOrigins, maxRequestBodySize, block)
}
}
@ -327,9 +338,10 @@ public fun Application.mcpStatelessStreamableHttp(
private suspend fun ServerSSESession.mcpSseEndpoint(
postEndpoint: String,
transportManager: TransportManager<SseServerTransport>,
maxRequestBodySize: Long,
block: ServerSSESession.() -> Server,
) {
val transport = mcpSseTransport(postEndpoint, transportManager)
val transport = mcpSseTransport(postEndpoint, transportManager, maxRequestBodySize)
val server = block()
@ -348,8 +360,9 @@ private suspend fun ServerSSESession.mcpSseEndpoint(
private fun ServerSSESession.mcpSseTransport(
postEndpoint: String,
transportManager: TransportManager<SseServerTransport>,
maxRequestBodySize: Long,
): SseServerTransport {
val transport = SseServerTransport(postEndpoint, this)
val transport = SseServerTransport(postEndpoint, this, maxRequestBodySize)
transportManager.addTransport(transport.sessionId, transport)
logger.info { "New SSE connection established and stored with sessionId: ${transport.sessionId}" }

View file

@ -0,0 +1,34 @@
package io.modelcontextprotocol.kotlin.sdk.server
import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.receiveChannel
import io.ktor.utils.io.readRemaining
import kotlinx.io.Buffer
import kotlinx.io.IOException
import kotlinx.io.readString
/** Default maximum size (in bytes) for an incoming HTTP request body: 4 MiB. */
internal const val DEFAULT_MAX_REQUEST_BODY_SIZE: Long = 4L * 1024 * 1024
/** Signals that an incoming request body exceeded the maximum size a transport is willing to read. */
internal class RequestBodyTooLargeException(val maxBodySize: Long) :
IOException("Request body exceeds the maximum of $maxBodySize bytes.")
/**
* Reads the request body as a UTF-8 string, rejecting bodies larger than [maxBytes].
*
* At most `maxBytes + 1` bytes are ever pulled from the request channel, so an oversized or unframed
* body cannot exhaust memory regardless of the (untrusted) `Content-Length` header.
*
* @throws RequestBodyTooLargeException if the body exceeds [maxBytes]
*/
internal suspend fun ApplicationCall.receiveTextWithLimit(maxBytes: Long): String {
val buffer = Buffer()
val readLimit = if (maxBytes == Long.MAX_VALUE) maxBytes else maxBytes + 1
val read = receiveChannel().readRemaining(readLimit).transferTo(buffer)
if (read > maxBytes) {
buffer.clear()
throw RequestBodyTooLargeException(maxBytes)
}
return buffer.readString()
}

View file

@ -5,7 +5,6 @@ import io.ktor.http.HttpStatusCode
import io.ktor.http.encodeURLPath
import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.contentType
import io.ktor.server.request.receiveText
import io.ktor.server.response.respondText
import io.ktor.server.sse.ServerSSESession
import io.modelcontextprotocol.kotlin.sdk.shared.AbstractTransport
@ -29,10 +28,19 @@ internal const val SESSION_ID_PARAM = "sessionId"
*
* @param endpoint relative or absolute URL the client will POST messages to
* @param session active SSE session used to deliver server-to-client events
* @param maxRequestBodySize maximum allowed size, in bytes, of an incoming POST body; larger requests are
* rejected with `413 Payload Too Large` without being buffered in full. Defaults to 4 MiB.
*/
@OptIn(ExperimentalAtomicApi::class)
public class SseServerTransport(private val endpoint: String, private val session: ServerSSESession) :
AbstractTransport() {
public class SseServerTransport(
private val endpoint: String,
private val session: ServerSSESession,
private val maxRequestBodySize: Long = DEFAULT_MAX_REQUEST_BODY_SIZE,
) : AbstractTransport() {
init {
require(maxRequestBodySize > 0) { "maxRequestBodySize must be greater than 0" }
}
private val initialized: AtomicBoolean = AtomicBoolean(false)
/** Unique identifier for this transport session, generated randomly on creation. */
@ -77,6 +85,7 @@ public class SseServerTransport(private val endpoint: String, private val sessio
val message = "SSE connection not established"
call.respondText(message, status = HttpStatusCode.InternalServerError)
_onError.invoke(IllegalStateException(message))
return
}
val body = try {
@ -85,9 +94,13 @@ public class SseServerTransport(private val endpoint: String, private val sessio
error("Unsupported content-type: $ct")
}
call.receiveText()
call.receiveTextWithLimit(maxRequestBodySize)
} catch (e: CancellationException) {
throw e
} catch (e: RequestBodyTooLargeException) {
call.respondText(e.message ?: "Request body too large", status = HttpStatusCode.PayloadTooLarge)
_onError.invoke(e)
return
} catch (e: Exception) {
call.respondText("Invalid message: ${e.message}", status = HttpStatusCode.BadRequest)
_onError.invoke(e)

View file

@ -8,7 +8,6 @@ import io.ktor.server.application.ApplicationCall
import io.ktor.server.request.contentType
import io.ktor.server.request.header
import io.ktor.server.request.httpMethod
import io.ktor.server.request.receiveText
import io.ktor.server.response.header
import io.ktor.server.response.respond
import io.ktor.server.response.respondNullable
@ -48,7 +47,6 @@ import kotlin.uuid.Uuid
internal const val MCP_SESSION_ID_HEADER = "mcp-session-id"
private const val MCP_PROTOCOL_VERSION_HEADER = "mcp-protocol-version"
private const val MCP_RESUMPTION_TOKEN_HEADER = "Last-Event-ID"
private const val DEFAULT_MAX_REQUEST_BODY_SIZE: Long = 4L * 1024 * 1024 // 4 MB
private const val MIN_PRIMING_EVENT_PROTOCOL_VERSION = "2025-11-25"
/**
@ -703,23 +701,13 @@ public class StreamableHttpServerTransport(private val configuration: Configurat
}
private suspend fun parseBody(call: ApplicationCall): List<JSONRPCMessage>? {
val maxSize = configuration.maxRequestBodySize
val contentLength = call.request.header(HttpHeaders.ContentLength)?.toLongOrNull() ?: 0L
if (contentLength > maxSize) {
val body = try {
call.receiveTextWithLimit(configuration.maxRequestBodySize)
} catch (e: RequestBodyTooLargeException) {
call.reject(
HttpStatusCode.PayloadTooLarge,
RPCError.ErrorCode.INVALID_REQUEST,
"Invalid Request: message size exceeds maximum of $maxSize bytes",
)
return null
}
val body = call.receiveText()
if (body.length.toLong() > maxSize) {
call.reject(
HttpStatusCode.PayloadTooLarge,
RPCError.ErrorCode.INVALID_REQUEST,
"Invalid Request: message size exceeds maximum of $maxSize bytes",
"Invalid Request: message size exceeds maximum of ${e.maxBodySize} bytes",
)
return null
}

View file

@ -0,0 +1,103 @@
package io.modelcontextprotocol.kotlin.sdk.server
import io.kotest.matchers.shouldBe
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpStatusCode
import io.ktor.server.response.respondText
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import kotlin.test.Test
class RequestBodyTest {
/** Wires a POST endpoint that echoes the body when it fits, or replies 413 when it is too large. */
private fun ApplicationTestBuilder.installEchoEndpoint(maxBytes: Long) {
install(io.ktor.server.sse.SSE)
application {
routing {
post("/echo") {
val text = try {
call.receiveTextWithLimit(maxBytes)
} catch (e: RequestBodyTooLargeException) {
call.respondText(e.message ?: "too large", status = HttpStatusCode.PayloadTooLarge)
return@post
}
call.respondText(text)
}
}
}
}
@Test
fun `body under the limit is returned intact`() = testApplication {
installEchoEndpoint(maxBytes = 1024)
val payload = "hello world"
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.OK
response.bodyAsText() shouldBe payload
}
@Test
fun `body exactly at the limit is accepted`() = testApplication {
val payload = "x".repeat(64)
installEchoEndpoint(maxBytes = payload.length.toLong())
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.OK
response.bodyAsText() shouldBe payload
}
@Test
fun `body exceeding the limit is rejected with 413`() = testApplication {
val payload = "x".repeat(65)
installEchoEndpoint(maxBytes = 64)
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.PayloadTooLarge
}
@Test
fun `large body exceeding the limit is rejected without buffering it whole`() = testApplication {
// 8 MB body against a 4 MB limit: must be rejected.
installEchoEndpoint(maxBytes = 4L * 1024 * 1024)
val payload = "x".repeat(8 * 1024 * 1024)
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.PayloadTooLarge
}
@Test
fun `multibyte UTF-8 body larger than a read chunk decodes intact`() = testApplication {
// Emoji is 4 bytes in UTF-8; a payload larger than the internal read chunk forces the
// accumulated bytes to span multiple chunks. Decoding once at the end must not corrupt
// characters that straddle a chunk boundary.
val unit = "a😀b" // mixes 1- and 4-byte code units so boundaries land mid-character
val payload = unit.repeat(40_000) // well over a 64 KiB read chunk
installEchoEndpoint(maxBytes = payload.encodeToByteArray().size.toLong())
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.OK
response.bodyAsText() shouldBe payload
}
@Test
fun `body is returned intact when the limit is Long MAX_VALUE`() = testApplication {
installEchoEndpoint(maxBytes = Long.MAX_VALUE)
val payload = "hello world"
val response = client.post("/echo") { setBody(payload) }
response.status shouldBe HttpStatusCode.OK
response.bodyAsText() shouldBe payload
}
}

View file

@ -0,0 +1,90 @@
package io.modelcontextprotocol.kotlin.sdk.server
import io.kotest.matchers.shouldBe
import io.ktor.client.request.post
import io.ktor.client.request.prepareGet
import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.ContentType
import io.ktor.http.HttpStatusCode
import io.ktor.http.contentType
import io.ktor.server.application.install
import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import io.ktor.server.sse.SSE
import io.ktor.server.sse.ServerSSESession
import io.ktor.server.testing.testApplication
import io.ktor.utils.io.readLine
import io.mockk.mockk
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.test.Test
class SseServerTransportTest : AbstractKtorExtensionsTest() {
@Test
fun `handlePostMessage on a not-started transport does not deliver the message`() = testApplication {
// A registered onMessage callback opens the delivery gate, so if the not-initialized branch
// falls through it would wrongly hand the message to the application. It must return instead.
val transport = SseServerTransport("/messages", mockk<ServerSSESession>(relaxed = true))
val delivered = AtomicBoolean(false)
transport.onMessage { delivered.set(true) }
application {
routing {
post("/messages") { transport.handlePostMessage(call) }
}
}
val response = client.post("/messages") {
contentType(ContentType.Application.Json)
setBody("""{"jsonrpc":"2.0","id":1,"method":"ping"}""")
}
response.status shouldBe HttpStatusCode.InternalServerError
delivered.get() shouldBe false
}
@Test
fun `SSE POST exceeding maxRequestBodySize is rejected with 413`() = testApplication {
application {
install(SSE)
routing {
mcp(enableDnsRebindingProtection = false, maxRequestBodySize = MAX_BODY) { testServer() }
}
}
client.prepareGet("/").execute { response ->
val sessionId = response.readSessionId()
requireNotNull(sessionId) { "sessionId not found in SSE endpoint event" }
// A body one byte over the configured limit must be rejected before processing.
val oversized = "x".repeat((MAX_BODY + 1).toInt())
val postResponse = client.post("/?sessionId=$sessionId") {
contentType(ContentType.Application.Json)
setBody(oversized)
}
postResponse.status shouldBe HttpStatusCode.PayloadTooLarge
}
}
private suspend fun HttpResponse.readSessionId(): String? {
val channel = bodyAsChannel()
var eventName: String? = null
while (!channel.isClosedForRead) {
val line = channel.readLine() ?: break
when {
line.startsWith("event:") -> eventName = line.substringAfter("event:").trim()
line.startsWith("data:") && eventName == "endpoint" -> {
return line.substringAfter("data:").trim().substringAfter("sessionId=").ifEmpty { null }
}
}
}
return null
}
private companion object {
const val MAX_BODY = 1024L
}
}

View file

@ -24,6 +24,8 @@ import io.ktor.server.routing.post
import io.ktor.server.routing.routing
import io.ktor.server.testing.ApplicationTestBuilder
import io.ktor.server.testing.testApplication
import io.ktor.utils.io.ByteReadChannel
import io.ktor.utils.io.readLine
import io.ktor.utils.io.readUTF8Line
import io.modelcontextprotocol.kotlin.sdk.types.ClientCapabilities
import io.modelcontextprotocol.kotlin.sdk.types.EmptyResult
@ -421,6 +423,34 @@ class StreamableHttpServerTransportTest {
response.status shouldBe HttpStatusCode.PayloadTooLarge
}
@Test
fun `POST with oversized chunked body and no Content-Length returns 413`() = testApplication {
configTestServer()
val client = createTestClient()
val transport = StreamableHttpServerTransport(
StreamableHttpServerTransport.Configuration(enableJsonResponse = true, maxRequestBodySize = 1024),
)
transport.onMessage { message ->
if (message is JSONRPCRequest) {
transport.send(JSONRPCResponse(message.id, EmptyResult()))
}
}
configureTransportEndpoint(transport)
// Stream the body as a channel so the client uses chunked transfer-encoding with no
// Content-Length header: the size limit must hold without trusting the (absent) header.
val oversized = ByteReadChannel("x".repeat(4096).encodeToByteArray())
val response = client.post(path) {
addStreamableHeaders()
setBody(oversized)
}
response.status shouldBe HttpStatusCode.PayloadTooLarge
}
@ParameterizedTest
@MethodSource("maxBodySizeTestCases")
fun `POST with custom max request body size validates payload size`(
@ -507,7 +537,7 @@ class StreamableHttpServerTransportTest {
// New stream is alive
val secondChannel = secondResponse.bodyAsChannel()
val firstLine = secondChannel.readUTF8Line()
val firstLine = secondChannel.readLine()
firstLine.shouldNotBeNull()
secondChannel.isClosedForRead shouldBe false
}
@ -546,7 +576,7 @@ class StreamableHttpServerTransportTest {
header("mcp-protocol-version", LATEST_PROTOCOL_VERSION)
}.execute { response ->
response.status shouldBe HttpStatusCode.OK
response.bodyAsChannel().readUTF8Line()
response.bodyAsChannel().readLine()
}
// Step 3: Immediately reconnect — the transport should close the stale
@ -561,7 +591,7 @@ class StreamableHttpServerTransportTest {
response.headers[MCP_SESSION_ID_HEADER] shouldBe sessionId
val channel = response.bodyAsChannel()
val firstLine = channel.readUTF8Line()
val firstLine = channel.readLine()
firstLine.shouldNotBeNull()
channel.isClosedForRead shouldBe false
}
@ -604,7 +634,7 @@ class StreamableHttpServerTransportTest {
// Verify the stream is alive by reading at least one line (flush event)
val channel = response.bodyAsChannel()
val firstLine = channel.readUTF8Line()
val firstLine = channel.readLine()
firstLine.shouldNotBeNull()
channel.isClosedForRead shouldBe false
}