Skip to content

Conversation

@kpavlov
Copy link
Contributor

@kpavlov kpavlov commented Nov 20, 2025

Subsystem
Client, ktor-client-netty

Motivation
There is no client engine built on top of Netty for JVM. Netty is a top JVM HttpClient, so it would be beneficial to have a Ktor wrapper around it.

Solution
I've sketched a näive and extremely sub-optimal implementation. It lacks HTTP/2 support, but other tests, copied from the Java client, were green on my machine 🟢 😇

Add initial sub-optimal `ktor-client-netty` implementation **without HTTP/2 support**
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 20, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@kpavlov kpavlov changed the title Experimental Netty Client Engine [Experimental] Netty Client Engine Nov 20, 2025
@e5l e5l requested review from bjhham, e5l, marychatte and osipxd and removed request for bjhham, marychatte and osipxd November 21, 2025 08:19
Copy link
Member

@e5l e5l left a comment

Choose a reason for hiding this comment

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

Hey @kpavlov, thank you for the PR. It's a good start, please check the comments

*
* 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

* ```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

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

// 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


// Determine if HTTP/2 is requested
// Note: HTTP/2 support is not yet implemented; requests will fall back to HTTP/1.1
// See HTTP2_INVESTIGATION.md for details
Copy link
Member

Choose a reason for hiding this comment

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

HTTP2_INVESTIGATION can't see it

): HttpResponseData {
// For now, delegate to Java HttpClient for WebSocket support
// TODO: Implement native Netty WebSocket support
val javaClient = java.net.http.HttpClient.newBuilder()
Copy link
Member

Choose a reason for hiding this comment

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

it's better to not use java http client for web sockets and throw error instead

Copy link
Contributor

@bjhham bjhham left a comment

Choose a reason for hiding this comment

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

Very interesting, I'll create a ticket for tracking.

Is this motivated by a desire for a more performant client engine, or are there some other advantages for introducing this engine?

Comment on lines +182 to +185
} catch (cause: io.netty.channel.ConnectTimeoutException) {
throw ConnectTimeoutException(requestData, cause)
} catch (cause: ConnectTimeoutException) {
throw cause
Copy link
Contributor

Choose a reason for hiding this comment

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

These are catching the same exception class. Consider introducing a typealias to avoid the fully-qualified name confusion.

Comment on lines +233 to +238
val bodyChannel = ByteChannel()
launch {
body.writeTo(bodyChannel)
bodyChannel.close()
}
writeChannelContent(channel, bodyChannel)
Copy link
Contributor

Choose a reason for hiding this comment

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

Would probably be best to use the call context here:

val bodyWriter = writer(callContext) {
    body.writeTo(this.channel)
}
writeChannelContent(channel, bodyWriter.channel)

Comment on lines +264 to +280
private suspend fun writeChannelContent(channel: io.netty.channel.Channel, content: ByteReadChannel) {
try {
val buffer = ByteArray(8192)
while (!content.isClosedForRead) {
val bytesRead = content.readAvailable(buffer)
if (bytesRead > 0) {
val byteBuf = Unpooled.wrappedBuffer(buffer, 0, bytesRead)
val httpContent = DefaultHttpContent(byteBuf)
channel.writeAndFlush(httpContent).awaitNetty()
}
}
// Send last content to signal end of request
channel.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT).awaitNetty()
} finally {
content.cancel()
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

We could eliminate some unnecessary transferring, and clean things up a little, with a netty channel implementation of ByteWriteBuffer

import io.ktor.utils.io.*
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
import io.netty.handler.codec.http.DefaultHttpContent
import kotlinx.io.Buffer
import kotlinx.io.Sink
import kotlinx.io.UnsafeIoApi
import kotlinx.io.unsafe.UnsafeBufferOperations

internal class NettyByteWriteChannel(val channel: io.netty.channel.Channel): ByteWriteChannel {
    private val buffer = Buffer()

    override val isClosedForWrite: Boolean
        get() = !channel.isWritable
    override val closedCause: Throwable?
        get() = channel.closeFuture().cause()

    @InternalAPI
    override val writeBuffer: Sink = buffer

    @OptIn(UnsafeIoApi::class)
    override suspend fun flush() {
        var bytes: ByteBuf? = null
        UnsafeBufferOperations.readFromHead(buffer) { array, start, endExclusive ->
            bytes = Unpooled.wrappedBuffer(array, start, endExclusive - start)
            endExclusive - start
        }
        bytes?.let {
            channel.writeAndFlush(DefaultHttpContent(bytes))
        }?.awaitNetty()
    }

    override suspend fun flushAndClose() {
        flush()
        channel.close()
    }

    override fun cancel(cause: Throwable?) {
        // TODO
    }
}

@bjhham
Copy link
Contributor

bjhham commented Nov 21, 2025

Here's a YouTrack ticket: KTOR-9117 Introduce Netty client engine

Please feel free to include any relevant information.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants