Skip to content

Commit 3526740

Browse files
committed
KTOR-8528 Fix race condition when Netty removes headers for some responses
1 parent b1600be commit 3526740

File tree

2 files changed

+107
-3
lines changed

2 files changed

+107
-3
lines changed

ktor-server/ktor-server-netty/jvm/src/io/ktor/server/netty/NettyApplicationResponse.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,15 +72,18 @@ public abstract class NettyApplicationResponse(
7272

7373
internal fun isInfoOrNoContentStatus(): Boolean {
7474
val status = status()
75-
if (status == null) return false
76-
77-
return status == HttpStatusCode.NoContent || (status.value >= 100 && status.value < 200)
75+
return (status != null) && (status == HttpStatusCode.NoContent || (status.value >= 100 && status.value < 200))
7876
}
7977

8078
override suspend fun responseChannel(): ByteWriteChannel {
8179
val channel = ByteChannel()
8280
val chunked = headers[HttpHeaders.TransferEncoding] == "chunked"
8381
sendResponse(chunked, content = channel)
82+
83+
if (isInfoOrNoContentStatus()) {
84+
sendCompleted.await()
85+
}
86+
8487
return channel
8588
}
8689

ktor-server/ktor-server-netty/jvm/test/io/ktor/tests/server/netty/NettySpecificTest.kt

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,27 @@
44

55
package io.ktor.tests.server.netty
66

7+
import io.ktor.client.HttpClient
8+
import io.ktor.client.engine.cio.CIO
9+
import io.ktor.client.plugins.DefaultRequest
10+
import io.ktor.client.request.get
11+
import io.ktor.http.HttpHeaders
12+
import io.ktor.http.HttpStatusCode
13+
import io.ktor.http.content.OutgoingContent
714
import io.ktor.server.application.*
15+
import io.ktor.server.application.hooks.ResponseSent
816
import io.ktor.server.engine.*
917
import io.ktor.server.netty.*
18+
import io.ktor.server.response.respond
19+
import io.ktor.server.response.respondBytesWriter
20+
import io.ktor.server.routing.get
21+
import io.ktor.server.routing.routing
22+
import io.ktor.utils.io.ByteReadChannel
23+
import kotlinx.coroutines.CompletableDeferred
24+
import kotlinx.coroutines.CoroutineScope
25+
import kotlinx.coroutines.Dispatchers
26+
import kotlinx.coroutines.launch
27+
import kotlinx.coroutines.test.runTest
1028
import java.net.*
1129
import java.util.concurrent.*
1230
import kotlin.test.*
@@ -68,4 +86,87 @@ class NettySpecificTest {
6886
assertTrue(server.engine.bootstraps.all { (it.config().group() as ExecutorService).isTerminated })
6987
}
7088
}
89+
90+
@Test
91+
fun contentLengthAndTransferEncodingAreSafelyRemoved() = runTest {
92+
val appStarted = CompletableDeferred<Application>()
93+
val testScope = CoroutineScope(coroutineContext)
94+
val earlyHints = HttpStatusCode(103, "Early Hints")
95+
96+
val serverJob = launch(Dispatchers.IO) {
97+
val server = embeddedServer(Netty, port = 0) {
98+
install(
99+
createApplicationPlugin("CallLogging") {
100+
on(ResponseSent) { call ->
101+
testScope.launch {
102+
val headers = call.response.headers.allValues()
103+
assertNull(headers[HttpHeaders.ContentLength])
104+
assertNull(headers[HttpHeaders.TransferEncoding])
105+
}
106+
}
107+
},
108+
)
109+
110+
routing {
111+
get("/no-content") {
112+
call.respond(HttpStatusCode.NoContent)
113+
}
114+
115+
get("no-content-channel-writer") {
116+
call.respondBytesWriter(status = HttpStatusCode.NoContent) {}
117+
}
118+
119+
get("no-content-read-channel") {
120+
call.respond(object : OutgoingContent.ReadChannelContent() {
121+
override val status: HttpStatusCode = HttpStatusCode.NoContent
122+
override fun readFrom(): ByteReadChannel = ByteReadChannel.Empty
123+
})
124+
}
125+
126+
get("/info") {
127+
call.respond(earlyHints)
128+
}
129+
130+
get("info-channel-writer") {
131+
call.respondBytesWriter(status = earlyHints) {}
132+
}
133+
134+
get("info-read-channel") {
135+
call.respond(object : OutgoingContent.ReadChannelContent() {
136+
override val status: HttpStatusCode = earlyHints
137+
override fun readFrom(): ByteReadChannel = ByteReadChannel.Empty
138+
})
139+
}
140+
}
141+
}
142+
143+
server.monitor.subscribe(ApplicationStarted) { app ->
144+
appStarted.complete(app)
145+
}
146+
147+
server.start(wait = true)
148+
}
149+
150+
try {
151+
val serverApp = appStarted.await()
152+
val connector = serverApp.engine.resolvedConnectors()[0]
153+
val host = connector.host
154+
val port = connector.port
155+
156+
HttpClient(CIO) {
157+
install(DefaultRequest) {
158+
url("http://$host:$port/")
159+
}
160+
}.use { client ->
161+
assertEquals(HttpStatusCode.NoContent, client.get("/no-content").status)
162+
assertEquals(HttpStatusCode.NoContent, client.get("/no-content-channel-writer").status)
163+
assertEquals(HttpStatusCode.NoContent, client.get("/no-content-read-channel").status)
164+
assertEquals(earlyHints, client.get("/info").status)
165+
assertEquals(earlyHints, client.get("/info-channel-writer").status)
166+
assertEquals(earlyHints, client.get("/info-read-channel").status)
167+
}
168+
} finally {
169+
serverJob.cancel()
170+
}
171+
}
71172
}

0 commit comments

Comments
 (0)