Skip to content

Commit 09b599e

Browse files
authored
KTOR-6734 Upgrade Jetty to 12 (#4887)
* KTOR-6734 Upgrade Jetty to 12 * KTOR-6734 Fix upgrade for Jetty servlets * KTOR-6734 Fix empty query parameters; tests * KTOR-6734 Reduce allocations for Jetty engine * KTOR-6734 Cleanup for Jetty 12 upgrade
1 parent c36929c commit 09b599e

File tree

55 files changed

+911
-623
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+911
-623
lines changed

gradle/libs.versions.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ netty = "4.2.4.Final"
1919
netty-tcnative = "2.0.72.Final"
2020

2121
jetty = "9.4.58.v20250814"
22-
jetty-jakarta = "11.0.26"
22+
jetty-jakarta = "12.0.25"
2323
jetty-alpn-api = "1.1.3.v20160715"
2424
jetty-alpn-openjdk8 = "9.4.58.v20250814"
2525

@@ -162,12 +162,13 @@ jetty-alpn-openjdk8-server = { module = "org.eclipse.jetty:jetty-alpn-openjdk8-s
162162
jetty-alpn-openjdk8-client = { module = "org.eclipse.jetty:jetty-alpn-openjdk8-client", version.ref = "jetty-alpn-openjdk8" }
163163

164164
jetty-client-jakarta = { module = "org.eclipse.jetty:jetty-client", version.ref = "jetty-jakarta" }
165-
jetty-http2-server-jakarta = { module = "org.eclipse.jetty.http2:http2-server", version.ref = "jetty-jakarta" }
166-
jetty-http2-client-jakarta = { module = "org.eclipse.jetty.http2:http2-client", version.ref = "jetty-jakarta" }
165+
jetty-http2-server-jakarta = { module = "org.eclipse.jetty.http2:jetty-http2-server", version.ref = "jetty-jakarta" }
166+
jetty-http2-client-jakarta = { module = "org.eclipse.jetty.http2:jetty-http2-client", version.ref = "jetty-jakarta" }
167167
jetty-http2-client-transport-jakarta = { module = "org.eclipse.jetty.http2:http2-http-client-transport", version.ref = "jetty-jakarta" }
168168
jetty-server-jakarta = { module = "org.eclipse.jetty:jetty-server", version.ref = "jetty-jakarta" }
169-
jetty-servlets-jakarta = { module = "org.eclipse.jetty:jetty-servlets", version.ref = "jetty-jakarta" }
170-
jetty-servlet-jakarta = { module = "org.eclipse.jetty:jetty-servlet", version.ref = "jetty-jakarta" }
169+
jetty-servlets-jakarta = { module = "org.eclipse.jetty.ee10:jetty-ee10-servlets", version.ref = "jetty-jakarta" }
170+
jetty-servlet-jakarta = { module = "org.eclipse.jetty.ee10:jetty-ee10-servlet", version.ref = "jetty-jakarta" }
171+
jetty-servlet-websocket-jakarta = { module = "org.eclipse.jetty.ee10.websocket:jetty-ee10-websocket-jakarta-server", version.ref = "jetty-jakarta" }
171172
jetty-alpn-server-jakarta = { module = "org.eclipse.jetty:jetty-alpn-server", version.ref = "jetty-jakarta" }
172173
jetty-alpn-java-server-jakarta = { module = "org.eclipse.jetty:jetty-alpn-java-server", version.ref = "jetty-jakarta" }
173174
jetty-alpn-java-client-jakarta = { module = "org.eclipse.jetty:jetty-alpn-java-client", version.ref = "jetty-jakarta" }

ktor-client/ktor-client-jetty-jakarta/build.gradle.kts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,15 @@ plugins {
99
}
1010

1111
kotlin {
12-
// The minimal JVM version required for Jetty 10+
13-
jvmToolchain(11)
12+
// The minimal JVM version required for Jetty 12+
13+
jvmToolchain(17)
1414

1515
sourceSets {
1616
jvmMain.dependencies {
1717
api(projects.ktorClientCore)
1818

1919
api(libs.jetty.http2.client.jakarta)
20-
api(libs.jetty.alpn.openjdk8.client)
21-
api(libs.jetty.alpn.java.client)
20+
api(libs.jetty.alpn.java.client.jakarta)
2221
}
2322
commonTest.dependencies {
2423
api(projects.ktorClientTests)

ktor-client/ktor-client-jetty-jakarta/jvm/src/io/ktor/client/engine/jetty/jakarta/JettyEngineConfig.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
package io.ktor.client.engine.jetty.jakarta
66

77
import io.ktor.client.engine.*
8-
import org.eclipse.jetty.http2.client.*
9-
import org.eclipse.jetty.util.ssl.*
8+
import org.eclipse.jetty.http2.client.HTTP2Client
9+
import org.eclipse.jetty.util.ssl.SslContextFactory
1010

1111
/**
1212
* A configuration for the [Jetty] client engine.

ktor-client/ktor-client-jetty-jakarta/jvm/src/io/ktor/client/engine/jetty/jakarta/JettyHttpRequest.kt

Lines changed: 23 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,18 @@ import io.ktor.util.cio.*
1313
import io.ktor.util.date.*
1414
import io.ktor.utils.io.*
1515
import io.ktor.utils.io.pool.*
16-
import kotlinx.coroutines.DelicateCoroutinesApi
17-
import kotlinx.coroutines.GlobalScope
18-
import kotlinx.coroutines.Job
19-
import kotlinx.coroutines.launch
20-
import org.eclipse.jetty.http.HostPortHttpField
21-
import org.eclipse.jetty.http.HttpFields
22-
import org.eclipse.jetty.http.HttpVersion
23-
import org.eclipse.jetty.http.MetaData
24-
import org.eclipse.jetty.http2.api.Session
25-
import org.eclipse.jetty.http2.client.HTTP2Client
26-
import org.eclipse.jetty.http2.client.HTTP2ClientSession
27-
import org.eclipse.jetty.http2.frames.HeadersFrame
28-
import org.eclipse.jetty.http2.frames.SettingsFrame
29-
import org.eclipse.jetty.util.Callback
30-
import java.net.InetSocketAddress
31-
import java.nio.ByteBuffer
32-
import kotlin.coroutines.CoroutineContext
16+
import kotlinx.coroutines.*
17+
import org.eclipse.jetty.http.*
18+
import org.eclipse.jetty.http2.api.*
19+
import org.eclipse.jetty.http2.client.*
20+
import org.eclipse.jetty.http2.client.internal.HTTP2ClientSession
21+
import org.eclipse.jetty.http2.frames.*
22+
import org.eclipse.jetty.io.Transport
23+
import org.eclipse.jetty.util.*
24+
import org.eclipse.jetty.util.ssl.SslContextFactory
25+
import java.net.*
26+
import java.nio.*
27+
import kotlin.coroutines.*
3328

3429
internal suspend fun HttpRequestData.executeRequest(
3530
client: HTTP2Client,
@@ -65,12 +60,20 @@ internal suspend fun HttpRequestData.executeRequest(
6560
)
6661
}
6762

63+
internal val NoopListener = object : Session.Listener {}
64+
6865
internal suspend fun HTTP2Client.connect(
6966
url: Url,
7067
config: JettyEngineConfig
71-
): Session = withPromise { promise ->
72-
val factory = if (url.protocol.isSecure()) config.sslContextFactory else null
73-
connect(factory, InetSocketAddress(url.host, url.port), Session.Listener.Adapter(), promise)
68+
): Session = withPromise { promise: Promise<Session> ->
69+
connect(
70+
Transport.TCP_IP,
71+
config.sslContextFactory as SslContextFactory.Client?,
72+
InetSocketAddress(url.host, url.port),
73+
NoopListener,
74+
promise,
75+
mutableMapOf<String, Object>() as Map<String, Object>
76+
)
7477
}
7578

7679
@OptIn(InternalAPI::class)

ktor-client/ktor-client-jetty-jakarta/jvm/src/io/ktor/client/engine/jetty/jakarta/JettyResponseListener.kt

Lines changed: 53 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,35 @@ package io.ktor.client.engine.jetty.jakarta
66

77
import io.ktor.client.request.*
88
import io.ktor.http.*
9-
import io.ktor.http.HttpMethod
109
import io.ktor.utils.io.*
11-
import kotlinx.coroutines.*
10+
import kotlinx.coroutines.CompletableDeferred
11+
import kotlinx.coroutines.CoroutineScope
12+
import kotlinx.coroutines.DelicateCoroutinesApi
13+
import kotlinx.coroutines.GlobalScope
1214
import kotlinx.coroutines.channels.Channel
13-
import org.eclipse.jetty.http.*
14-
import org.eclipse.jetty.http2.*
15-
import org.eclipse.jetty.http2.api.*
16-
import org.eclipse.jetty.http2.client.*
17-
import org.eclipse.jetty.http2.frames.*
18-
import org.eclipse.jetty.util.*
19-
import java.io.*
20-
import java.nio.*
21-
import java.nio.channels.*
22-
import kotlin.coroutines.*
15+
import kotlinx.coroutines.launch
16+
import org.eclipse.jetty.http.MetaData
17+
import org.eclipse.jetty.http2.ErrorCode
18+
import org.eclipse.jetty.http2.HTTP2Session
19+
import org.eclipse.jetty.http2.api.Stream
20+
import org.eclipse.jetty.http2.frames.HeadersFrame
21+
import org.eclipse.jetty.http2.frames.PushPromiseFrame
22+
import org.eclipse.jetty.http2.frames.ResetFrame
23+
import org.eclipse.jetty.util.Callback
24+
import org.eclipse.jetty.util.Promise
25+
import java.io.IOException
26+
import java.nio.ByteBuffer
27+
import java.nio.channels.ClosedChannelException
28+
import java.util.concurrent.TimeoutException
29+
import kotlin.coroutines.CoroutineContext
2330

2431
internal data class StatusWithHeaders(val statusCode: HttpStatusCode, val headers: Headers)
2532

26-
private data class JettyResponseChunk(val buffer: ByteBuffer, val callback: Callback)
33+
private data class JettyResponseChunk(val data: Stream.Data)
2734

2835
internal class JettyResponseListener(
2936
private val request: HttpRequestData,
30-
private val session: HTTP2ClientSession,
37+
private val session: HTTP2Session,
3138
private val channel: ByteWriteChannel,
3239
private val callContext: CoroutineContext
3340
) : Stream.Listener {
@@ -41,40 +48,48 @@ internal class JettyResponseListener(
4148

4249
override fun onPush(stream: Stream, frame: PushPromiseFrame): Stream.Listener {
4350
stream.reset(ResetFrame(frame.promisedStreamId, ErrorCode.CANCEL_STREAM_ERROR.code), Callback.NOOP)
44-
return Ignore
51+
return Stream.Listener.AUTO_DISCARD
4552
}
4653

47-
override fun onIdleTimeout(stream: Stream, cause: Throwable): Boolean {
48-
channel.close(cause)
49-
return true
54+
override fun onIdleTimeout(
55+
stream: Stream?,
56+
x: TimeoutException?,
57+
promise: Promise<Boolean?>?
58+
) {
59+
channel.close(x)
60+
backendChannel.close()
61+
promise?.succeeded(true)
5062
}
5163

52-
override fun onReset(stream: Stream, frame: ResetFrame) {
53-
val error = when (frame.error) {
64+
override fun onReset(
65+
stream: Stream?,
66+
frame: ResetFrame?,
67+
callback: Callback?
68+
) {
69+
val cause = when (val code = frame?.error ?: 0) {
5470
0 -> null
5571
ErrorCode.CANCEL_STREAM_ERROR.code -> ClosedChannelException()
5672
else -> {
57-
val code = ErrorCode.from(frame.error)
58-
IOException("Connection reset ${code?.name ?: "with unknown error code ${frame.error}"}")
73+
val ec = ErrorCode.from(code)
74+
IOException("Connection reset ${ec?.name ?: "with unknown error code $code"}")
5975
}
6076
}
61-
62-
error?.let { backendChannel.close(it) }
63-
77+
backendChannel.close(cause)
6478
onHeadersReceived.complete(null)
79+
callback?.succeeded()
6580
}
6681

67-
override fun onData(stream: Stream, frame: DataFrame, callback: Callback) {
68-
val data = frame.data!!
82+
override fun onDataAvailable(stream: Stream?) {
83+
val streamData = stream?.readData() ?: return
84+
val frame = streamData.frame()
6985
try {
70-
if (!backendChannel.trySend(JettyResponseChunk(data, callback)).isSuccess) {
71-
throw IOException("backendChannel.offer() failed")
86+
if (!backendChannel.trySend(JettyResponseChunk(streamData)).isSuccess) {
87+
throw IOException("Failed to send response data to processing channel - channel may be full or closed")
7288
}
7389

7490
if (frame.isEndStream) backendChannel.close()
7591
} catch (cause: Throwable) {
7692
backendChannel.close(cause)
77-
callback.failed(cause)
7893
}
7994
}
8095

@@ -93,7 +108,7 @@ internal class JettyResponseListener(
93108
}
94109

95110
override fun onHeaders(stream: Stream, frame: HeadersFrame) {
96-
frame.metaData.fields.forEach { field ->
111+
frame.metaData.httpFields.forEach { field ->
97112
headersBuilder.append(field.name, field.value)
98113
}
99114

@@ -115,33 +130,25 @@ internal class JettyResponseListener(
115130
}
116131

117132
@OptIn(DelicateCoroutinesApi::class)
118-
private fun runResponseProcessing() = GlobalScope.launch(callContext) {
133+
private fun runResponseProcessing() = CoroutineScope(callContext).launch {
119134
while (true) {
120-
val (buffer, callback) = backendChannel.receiveCatching().getOrNull() ?: break
135+
val (data) = backendChannel.receiveCatching().getOrNull() ?: break
136+
val buffer = data.frame().byteBuffer
121137
try {
122-
if (buffer.remaining() > 0) channel.writeFully(buffer)
123-
callback.succeeded()
138+
if (buffer.remaining() > 0) {
139+
channel.writeFully(buffer)
140+
}
141+
data.release()
124142
} catch (cause: ClosedWriteChannelException) {
125-
callback.failed(cause)
126143
session.endPoint.close()
127144
break
128145
} catch (cause: Throwable) {
129-
callback.failed(cause)
130146
session.endPoint.close()
131147
throw cause
132148
}
133149
}
134150
}.invokeOnCompletion { cause ->
135151
channel.close(cause)
136152
backendChannel.close()
137-
GlobalScope.launch {
138-
for ((_, callback) in backendChannel) {
139-
callback.succeeded()
140-
}
141-
}
142-
}
143-
144-
companion object {
145-
private val Ignore = Stream.Listener.Adapter()
146153
}
147154
}

ktor-http/api/ktor-http.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -874,6 +874,7 @@ public final class io/ktor/http/ParametersSingleImpl : io/ktor/util/StringValues
874874
public final class io/ktor/http/QueryKt {
875875
public static final fun parseQueryString (Ljava/lang/String;IIZ)Lio/ktor/http/Parameters;
876876
public static synthetic fun parseQueryString$default (Ljava/lang/String;IIZILjava/lang/Object;)Lio/ktor/http/Parameters;
877+
public static final fun withEmptyStringForValuelessKeys (Lio/ktor/http/Parameters;)Lio/ktor/http/Parameters;
877878
}
878879

879880
public final class io/ktor/http/RangeUnits : java/lang/Enum {

ktor-http/api/ktor-http.klib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1735,6 +1735,7 @@ final fun (io.ktor.http/HttpStatusCode).io.ktor.http/isSuccess(): kotlin/Boolean
17351735
final fun (io.ktor.http/Parameters).io.ktor.http/formUrlEncode(): kotlin/String // io.ktor.http/formUrlEncode|formUrlEncode@io.ktor.http.Parameters(){}[0]
17361736
final fun (io.ktor.http/Parameters).io.ktor.http/formUrlEncodeTo(kotlin.text/Appendable) // io.ktor.http/formUrlEncodeTo|formUrlEncodeTo@io.ktor.http.Parameters(kotlin.text.Appendable){}[0]
17371737
final fun (io.ktor.http/Parameters).io.ktor.http/plus(io.ktor.http/Parameters): io.ktor.http/Parameters // io.ktor.http/plus|plus@io.ktor.http.Parameters(io.ktor.http.Parameters){}[0]
1738+
final fun (io.ktor.http/Parameters).io.ktor.http/withEmptyStringForValuelessKeys(): io.ktor.http/Parameters // io.ktor.http/withEmptyStringForValuelessKeys|withEmptyStringForValuelessKeys@io.ktor.http.Parameters(){}[0]
17381739
final fun (io.ktor.http/URLBuilder).io.ktor.http/appendEncodedPathSegments(kotlin.collections/List<kotlin/String>): io.ktor.http/URLBuilder // io.ktor.http/appendEncodedPathSegments|appendEncodedPathSegments@io.ktor.http.URLBuilder(kotlin.collections.List<kotlin.String>){}[0]
17391740
final fun (io.ktor.http/URLBuilder).io.ktor.http/appendEncodedPathSegments(kotlin/Array<out kotlin/String>...): io.ktor.http/URLBuilder // io.ktor.http/appendEncodedPathSegments|appendEncodedPathSegments@io.ktor.http.URLBuilder(kotlin.Array<out|kotlin.String>...){}[0]
17401741
final fun (io.ktor.http/URLBuilder).io.ktor.http/appendPathSegments(kotlin.collections/List<kotlin/String>, kotlin/Boolean = ...): io.ktor.http/URLBuilder // io.ktor.http/appendPathSegments|appendPathSegments@io.ktor.http.URLBuilder(kotlin.collections.List<kotlin.String>;kotlin.Boolean){}[0]

ktor-http/common/src/io/ktor/http/Query.kt

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

55
package io.ktor.http
66

7+
import io.ktor.utils.io.InternalAPI
8+
79
/**
810
* Parse query string withing starting at the specified [startIndex] but up to [limit] pairs
911
*
@@ -94,3 +96,28 @@ private fun trimStart(start: Int, end: Int, query: CharSequence): Int {
9496
while (spaceIndex < end && query[spaceIndex].isWhitespace()) spaceIndex++
9597
return spaceIndex
9698
}
99+
100+
/**
101+
* Converts parameters to query parameters by fixing the [Parameters.get] method
102+
* to make it return an empty string for the parameters without value (e.g., `?empty`)
103+
*/
104+
@InternalAPI
105+
public fun Parameters.withEmptyStringForValuelessKeys(): Parameters =
106+
if (entries().none { it.value.isEmpty() }) {
107+
this
108+
} else {
109+
let { parameters ->
110+
object : Parameters {
111+
override fun get(name: String): String? {
112+
val values = getAll(name) ?: return null
113+
return if (values.isEmpty()) "" else values.first()
114+
}
115+
override val caseInsensitiveName: Boolean
116+
get() = parameters.caseInsensitiveName
117+
override fun getAll(name: String): List<String>? = parameters.getAll(name)
118+
override fun names(): Set<String> = parameters.names()
119+
override fun entries(): Set<Map.Entry<String, List<String>>> = parameters.entries()
120+
override fun isEmpty(): Boolean = parameters.isEmpty()
121+
}
122+
}
123+
}

ktor-io/api/ktor-io.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ public final class io/ktor/utils/io/ByteWriteChannelOperationsKt {
159159
public static final fun join (Lio/ktor/utils/io/ChannelJob;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
160160
public static final fun write (Lio/ktor/utils/io/ByteWriteChannel;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
161161
public static synthetic fun write$default (Lio/ktor/utils/io/ByteWriteChannel;ILkotlin/jvm/functions/Function3;Lkotlin/coroutines/Continuation;ILjava/lang/Object;)Ljava/lang/Object;
162+
public static final fun writeBuffer (Lio/ktor/utils/io/ByteWriteChannel;Lkotlinx/io/RawSource;JLkotlin/coroutines/Continuation;)Ljava/lang/Object;
162163
public static final fun writeBuffer (Lio/ktor/utils/io/ByteWriteChannel;Lkotlinx/io/RawSource;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
163164
public static final fun writeByte (Lio/ktor/utils/io/ByteWriteChannel;BLkotlin/coroutines/Continuation;)Ljava/lang/Object;
164165
public static final fun writeByteArray (Lio/ktor/utils/io/ByteWriteChannel;[BLkotlin/coroutines/Continuation;)Ljava/lang/Object;

ktor-io/api/ktor-io.klib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,7 @@ final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/awaitFree
499499
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/flushIfNeeded() // io.ktor.utils.io/flushIfNeeded|flushIfNeeded@io.ktor.utils.io.ByteWriteChannel(){}[0]
500500
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/write(kotlin/Int = ..., kotlin/Function3<kotlin/ByteArray, kotlin/Int, kotlin/Int, kotlin/Int>): kotlin/Int // io.ktor.utils.io/write|write@io.ktor.utils.io.ByteWriteChannel(kotlin.Int;kotlin.Function3<kotlin.ByteArray,kotlin.Int,kotlin.Int,kotlin.Int>){}[0]
501501
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/writeBuffer(kotlinx.io/RawSource) // io.ktor.utils.io/writeBuffer|writeBuffer@io.ktor.utils.io.ByteWriteChannel(kotlinx.io.RawSource){}[0]
502+
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/writeBuffer(kotlinx.io/RawSource, kotlin/Long) // io.ktor.utils.io/writeBuffer|writeBuffer@io.ktor.utils.io.ByteWriteChannel(kotlinx.io.RawSource;kotlin.Long){}[0]
502503
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/writeByte(kotlin/Byte) // io.ktor.utils.io/writeByte|writeByte@io.ktor.utils.io.ByteWriteChannel(kotlin.Byte){}[0]
503504
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/writeByteArray(kotlin/ByteArray) // io.ktor.utils.io/writeByteArray|writeByteArray@io.ktor.utils.io.ByteWriteChannel(kotlin.ByteArray){}[0]
504505
final suspend fun (io.ktor.utils.io/ByteWriteChannel).io.ktor.utils.io/writeDouble(kotlin/Double) // io.ktor.utils.io/writeDouble|writeDouble@io.ktor.utils.io.ByteWriteChannel(kotlin.Double){}[0]

0 commit comments

Comments
 (0)