-
Notifications
You must be signed in to change notification settings - Fork 1.2k
[Experimental] Netty Client Engine #5209
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
93f47d7
25ba655
ba17db4
3d4d153
d0cebb8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| public final class io/ktor/client/engine/netty/Netty : io/ktor/client/engine/HttpClientEngineFactory { | ||
| public static final field INSTANCE Lio/ktor/client/engine/netty/Netty; | ||
| public fun create (Lkotlin/jvm/functions/Function1;)Lio/ktor/client/engine/HttpClientEngine; | ||
| public fun equals (Ljava/lang/Object;)Z | ||
| public fun hashCode ()I | ||
| public fun toString ()Ljava/lang/String; | ||
| } | ||
|
|
||
| public final class io/ktor/client/engine/netty/NettyHttpConfig : io/ktor/client/engine/HttpClientEngineConfig { | ||
| public fun <init> ()V | ||
| public final fun bootstrap (Lkotlin/jvm/functions/Function1;)V | ||
| public final fun getMaxConnectionsPerRoute ()I | ||
| public final fun getMaxConnectionsTotal ()I | ||
| public final fun getMaxDecompressorAllocation ()I | ||
| public final fun getProtocolVersion ()Ljava/net/http/HttpClient$Version; | ||
| public final fun getSslContext ()Ljavax/net/ssl/SSLContext; | ||
| public final fun setMaxConnectionsPerRoute (I)V | ||
| public final fun setMaxConnectionsTotal (I)V | ||
| public final fun setMaxDecompressorAllocation (I)V | ||
| public final fun setProtocolVersion (Ljava/net/http/HttpClient$Version;)V | ||
| public final fun setSslContext (Ljavax/net/ssl/SSLContext;)V | ||
| public final fun sslContext (Ljavax/net/ssl/SSLContext;)V | ||
| } | ||
|
|
||
| public final class io/ktor/client/engine/netty/NettyHttpEngine : io/ktor/client/engine/HttpClientEngineBase { | ||
| public fun <init> (Lio/ktor/client/engine/netty/NettyHttpConfig;)V | ||
| public fun execute (Lio/ktor/client/request/HttpRequestData;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; | ||
| public synthetic fun getConfig ()Lio/ktor/client/engine/HttpClientEngineConfig; | ||
| public fun getConfig ()Lio/ktor/client/engine/netty/NettyHttpConfig; | ||
| public fun getSupportedCapabilities ()Ljava/util/Set; | ||
| } | ||
|
|
||
| public final class io/ktor/client/engine/netty/NettyHttpEngineContainer : io/ktor/client/HttpClientEngineContainer { | ||
| public fun <init> ()V | ||
| public fun getFactory ()Lio/ktor/client/engine/HttpClientEngineFactory; | ||
| public fun toString ()Ljava/lang/String; | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| /* | ||
| * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| plugins { | ||
| id("ktorbuild.project.library") | ||
| id("test-server") | ||
| } | ||
|
|
||
|
|
||
| val enableAlpnProp = project.hasProperty("enableAlpn") | ||
| val osName = System.getProperty("os.name").lowercase() | ||
| val nativeClassifier: String? = if (enableAlpnProp) { | ||
| when { | ||
| osName.contains("win") -> "windows-x86_64" | ||
| osName.contains("linux") -> "linux-x86_64" | ||
| osName.contains("mac") -> "osx-x86_64" | ||
| else -> throw InvalidUserDataException("Unsupported os family $osName") | ||
| } | ||
| } else { | ||
| null | ||
| } | ||
| kotlin { | ||
| // Package java.net.http was introduced in Java 11 | ||
| jvmToolchain(11) | ||
|
|
||
| sourceSets { | ||
| jvmMain.dependencies { | ||
| api(projects.ktorClientCore) | ||
|
|
||
| api(libs.netty.codec.http2) | ||
| api(libs.jetty.alpn.api) | ||
|
|
||
| api(libs.netty.transport.native.kqueue) | ||
| api(libs.netty.transport.native.epoll) | ||
| if (nativeClassifier != null) { | ||
| api(libs.netty.tcnative.boringssl.static) | ||
| } | ||
| } | ||
|
|
||
| jvmTest.dependencies { | ||
| api(projects.ktorClientTests) | ||
|
|
||
| api(libs.netty.tcnative) | ||
| api(libs.netty.tcnative.boringssl.static) | ||
| api(libs.mockk) | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| io.ktor.client.engine.netty.NettyHttpEngineContainer |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| /* | ||
| * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package io.ktor.client.engine.netty | ||
|
|
||
| import io.ktor.client.* | ||
| import io.ktor.client.engine.* | ||
|
|
||
| /** | ||
| * A JVM client engine that uses the Java HTTP Client introduced in Java 11. | ||
| * | ||
| * To create the client with this engine, pass it to the `HttpClient` constructor: | ||
| * ```kotlin | ||
| * val client = HttpClient(Netty) | ||
| * ``` | ||
| * To configure the engine, pass settings exposed by [NettyHttpConfig] to the `engine` method: | ||
| * ```kotlin | ||
| * val client = HttpClient(Netty) { | ||
| * engine { | ||
| * // this: JavaHttpConfig | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. java |
||
| * } | ||
| * } | ||
| * ``` | ||
| * | ||
| * You can learn more about client engines from [Engines](https://ktor.io/docs/http-client-engines.html). | ||
| * | ||
| * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.java.Java) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. java
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can re-generate these links before release |
||
| */ | ||
| public data object Netty : HttpClientEngineFactory<NettyHttpConfig> { | ||
| override fun create(block: NettyHttpConfig.() -> Unit): HttpClientEngine = | ||
| NettyHttpEngine(NettyHttpConfig().apply(block)) | ||
| } | ||
|
|
||
| public class NettyHttpEngineContainer : HttpClientEngineContainer { | ||
| override val factory: HttpClientEngineFactory<*> = Netty | ||
|
|
||
| override fun toString(): String = "Netty" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /* | ||
| * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package io.ktor.client.engine.netty | ||
|
|
||
| import io.ktor.util.date.* | ||
| import io.netty.channel.* | ||
| import io.netty.channel.socket.* | ||
| import io.netty.handler.codec.http.* | ||
| import io.netty.handler.ssl.* | ||
| import kotlin.coroutines.* | ||
|
|
||
| /** | ||
| * Channel initializer for Netty HTTP client. | ||
| * Configures the pipeline with SSL/TLS, HTTP codec, and response handler. | ||
| * | ||
| * @param sslCtx SSL context for secure connections, null for plain HTTP | ||
| * @param host Target host (used for SNI) | ||
| * @param port Target port | ||
| * @param callContext Coroutine context for the request | ||
| * @param requestData Request data from Ktor | ||
| * @param requestTime Request timestamp | ||
| * @param useHttp2 Whether HTTP/2 is requested (currently falls back to HTTP/1.1) | ||
| */ | ||
| internal class NettyClientInitializer( | ||
| private val sslCtx: SslContext?, | ||
| private val host: String, | ||
| private val port: Int, | ||
| private val callContext: CoroutineContext, | ||
| private val requestTime: GMTDate, | ||
| private val useHttp2: Boolean = false | ||
| ) : ChannelInitializer<SocketChannel>() { | ||
|
|
||
| override fun initChannel(ch: SocketChannel) { | ||
| val pipeline = ch.pipeline() | ||
|
|
||
| if (sslCtx != null) { | ||
| configureSslHandler(pipeline) | ||
|
|
||
| // TODO: Implement HTTP/2 support | ||
| // When useHttp2 is true and ALPN negotiates HTTP/2, we should configure | ||
| // an HTTP/2 pipeline. Currently, we always fall back to HTTP/1.1. | ||
| // See HTTP2_INVESTIGATION.md for details on the implementation challenge. | ||
| if (useHttp2) { | ||
| // ALPN is configured in SslContext (NettyHttpEngine.kt:117-129) | ||
| // Future: Add ApplicationProtocolNegotiationHandler here | ||
| // For now, fall back to HTTP/1.1 | ||
| configureHttp1Pipeline(pipeline) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. let's throw an exception when it is not implemented |
||
| } else { | ||
| configureHttp1Pipeline(pipeline) | ||
| } | ||
| } else { | ||
| // Plain HTTP | ||
| configureHttp1Pipeline(pipeline) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Configures SSL/TLS handler with hostname verification. | ||
| */ | ||
| private fun configureSslHandler(pipeline: ChannelPipeline) { | ||
| val sslHandler = sslCtx!!.newHandler(pipeline.channel().alloc(), host, port) | ||
|
|
||
| // Enable hostname verification for security | ||
| val sslEngine = sslHandler.engine() | ||
| val sslParams = sslEngine.sslParameters | ||
| sslParams.endpointIdentificationAlgorithm = "HTTPS" | ||
| sslEngine.sslParameters = sslParams | ||
|
|
||
| pipeline.addLast("ssl", sslHandler) | ||
| } | ||
|
|
||
| /** | ||
| * Configures HTTP/1.1 pipeline with codec, decompressor, and response handler. | ||
| */ | ||
| private fun configureHttp1Pipeline(pipeline: ChannelPipeline) { | ||
| // HTTP/1.1 codec for encoding requests and decoding responses | ||
| pipeline.addLast("http-codec", HttpClientCodec()) | ||
|
|
||
| // HTTP content decompressor for gzip, deflate | ||
| pipeline.addLast("decompressor", HttpContentDecompressor()) | ||
|
|
||
| // Custom handler for processing HTTP responses and streaming content | ||
| val handler = NettyHttpHandler(callContext, requestTime) | ||
| NettyHttpHandler.set(pipeline.channel(), handler) | ||
| pipeline.addLast("handler", handler) | ||
| } | ||
|
|
||
| // TODO: Future HTTP/2 Implementation | ||
| // When adding HTTP/2 support, implement: | ||
| // 1. ApplicationProtocolNegotiationHandler to detect negotiated protocol | ||
| // 2. configureHttp2Pipeline() method with proper stream management | ||
| // 3. HttpToHttp2ConnectionHandler or Http2FrameCodec based approach | ||
| // See HTTP2_INVESTIGATION.md for detailed analysis and recommendations | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| /* | ||
| * Copyright 2014-2021 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. | ||
| */ | ||
|
|
||
| package io.ktor.client.engine.netty | ||
|
|
||
| import io.ktor.client.engine.* | ||
| import io.netty.bootstrap.Bootstrap | ||
| import javax.net.ssl.SSLContext | ||
|
|
||
| /** | ||
| * A configuration for the [Netty] client engine. | ||
| * | ||
| * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.netty.NettyHttpConfig) | ||
| */ | ||
| public class NettyHttpConfig : HttpClientEngineConfig() { | ||
|
|
||
| /** | ||
| * An HTTP version to use. | ||
| * | ||
| * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.netty.NettyHttpConfig.protocolVersion) | ||
| */ | ||
| public var protocolVersion: java.net.http.HttpClient.Version = java.net.http.HttpClient.Version.HTTP_1_1 | ||
|
|
||
| /** | ||
| * Maximum number of connections per route. | ||
| */ | ||
| public var maxConnectionsPerRoute: Int = 100 | ||
|
|
||
| /** | ||
| * Maximum total connections. | ||
| */ | ||
| public var maxConnectionsTotal: Int = 1000 | ||
|
|
||
| /** | ||
| * Specifies the maximum amount of allocation allowed for a decompressor in the Netty engine. | ||
| * | ||
| * This value is used as a configuration parameter to manage memory usage when handling compressed data. | ||
| */ | ||
| public var maxDecompressorAllocation: Int = 1_048_576 * 4 | ||
|
|
||
| /** | ||
| * Custom SSL context to use for HTTPS connections. | ||
| */ | ||
| public var sslContext: SSLContext? = null | ||
|
|
||
| internal var bootstrapConfig: Bootstrap.() -> Unit = {} | ||
|
|
||
| /** | ||
| * Configure Netty [Bootstrap]. | ||
| * | ||
| * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.netty.NettyHttpConfig.bootstrap) | ||
| */ | ||
| public fun bootstrap(block: Bootstrap.() -> Unit) { | ||
| val oldConfig = bootstrapConfig | ||
| bootstrapConfig = { | ||
| oldConfig() | ||
| block() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Configure SSL context for HTTPS connections. | ||
| * | ||
| * [Report a problem](https://ktor.io/feedback/?fqname=io.ktor.client.engine.netty.NettyHttpConfig.sslContext) | ||
| */ | ||
| public fun sslContext(context: SSLContext) { | ||
| sslContext = context | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
java