Skip to main content

HTTP/3

socket-http3 implements HTTP/3 (RFC 9114) — including QPACK header compression (RFC 9204), server push (§4.6), and the WebTransport extension (RFC 9220) — on top of socket-quic. It's a separate artifact:

dependencies {
implementation("com.ditchoom:socket-http3:<latest-version>")
}

Like the rest of the stack, everything is scope-based: the connection (or server) lives for the duration of your block and tears down on exit. HTTP/3 inherits QUIC's platform support — JVM/Android, Linux native, and Apple; JS/wasmJs throw UnsupportedOperationException.

Client: Request / Response

withHttp3Connection performs the QUIC handshake with the h3 ALPN, bootstraps the HTTP/3 control and QPACK streams, and hands you an Http3Connection:

import com.ditchoom.buffer.Charset
import com.ditchoom.buffer.freeIfNeeded

val (status, text) = withHttp3Connection("example.com", port = 443) {
val response = request(
Http3Request(method = "GET", authority = "example.com", path = "/"),
)
try {
val body = response.readFullBody() // collect the whole body
val text = body.readString(body.remaining(), Charset.UTF8)
body.freeIfNeeded() // you own the buffer
response.status to text
} finally {
response.close() // release the response reader
}
}

For large or streamed responses, pull DATA frames one at a time with response.nextBodyChunk() (returns null at the end) instead of readFullBody(). Trailing headers, if any, land in response.trailers.

Streaming a request body

To send a body incrementally instead of buffering it, use the lambda form — it hands you an Http3RequestBody you write DATA frames into:

val response = request(
method = "POST",
authority = "example.com",
path = "/upload",
) {
// `this` is an Http3RequestBody — each write() sends one DATA frame, in order.
write(part1)
write(part2)
}

Server

withHttp3Server binds a QUIC server and routes each request to your onRequest handler, which receives an Http3ServerExchange (its request and response):

import com.ditchoom.buffer.BufferFactory
import com.ditchoom.buffer.Charset
import com.ditchoom.buffer.use
import com.ditchoom.socket.quic.network

withHttp3Server(
port = 0, // ephemeral; read it back from `port`
tlsConfig = QuicTlsConfig(certChainPath = "cert.pem", privKeyPath = "key.pem"),
onRequest = {
when (request.path) {
"/hello" -> {
// `send` writes the body but doesn't take ownership — `use { }` frees it after.
// QUIC buffers must be native memory; `BufferFactory.network()` guarantees that.
BufferFactory.network().allocate(64).use { body ->
body.writeString("hello from h3", Charset.UTF8)
body.resetForRead()
response.send(
status = 200,
headers = listOf(QpackHeaderField("content-type", "text/plain")),
body = body,
)
}
}
else -> response.send(404)
}
},
) {
// `this` is the Http3Server; serve until this block is cancelled.
awaitCancellation()
}

response.send(...) is the buffered convenience (headers + optional body + finish). For streamed responses, call sendHeaders(status, headers), then writeBody(chunk) per DATA frame, and finally sendTrailers(...) if needed. Read a request body with request.readFullBody() or request.nextBodyChunk().

Server push (RFC 9114 §4.6)

From inside a handler, push(...) promises and sends an extra response the client didn't ask for (enable it client-side with maxPushId >= 0 on withHttp3Connection, and collect connection.pushes):

onRequest = {
response.send(200, body = indexHtml)
push(path = "/style.css") {
send(200, body = css) // `this` is the pushed Http3ServerResponse
}
}

Next Steps

  • WebTransport — bidirectional sessions, streams, and datagrams over HTTP/3
  • QUIC Overview — the transport HTTP/3 is built on