Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions ktor-client/ktor-client-netty/api/ktor-client-netty.api
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;
}

49 changes: 49 additions & 0 deletions ktor-client/ktor-client-netty/build.gradle.kts
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java

*
* 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
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

java

Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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
}
}
Loading