
HTTP/3 and QPACK implementation over QUIC, with spec-complete tests, QPACK static literals, server request-head timeout, GOAWAY behavior tweaks, and h3/aioquic interop.
HTTP/3 (RFC 9114) and QPACK (RFC 9204) for Kotlin/Native over com.netonstream:quic. The first version replicates
h3 0.0.8 (and h3-quinn for the QUIC adapter). Artifact com.netonstream:http3, package neton.http.h3. Common
HTTP types (Request, Response, HeaderMap, ...) come from com.netonstream:http.
This repository was split out of http on 2026-09-29, with its history. Specification, deliberate differences from
the reference (marked ⚖️) and the implementation record: SPEC.md.
Release coordinate: com.netonstream:http3:0.3.0, built against com.netonstream:http:0.3.0 and
com.netonstream:quic:0.3.0 (tests: quic-testkit:0.3.0). 0.3.0 adds a request-head time limit for the server
(H3_REQUEST_REJECTED both ways) and moves to http 0.3.0 and quic 0.3.0. 0.2.0 added requests over 0-RTT and RequestStream.isEarlyData(). Kotlin 2.4.20 (the build refuses anything lower), native targets shared by http and quic (no 32-bit Android).
>); the
connection's one GREASE frame on a request stream follows the first head instead of preceding FIN (aioquic needs it).Builder.requestHeaderTimeout, default 10 s, 0 = none): a request
stream whose HEADERS frame has not arrived in time is rejected both ways with H3_REQUEST_REJECTED.--http3 interop, loss and reordering runs over real TLS, performance comparison.HTTP/3 (neton.http.h3) runs on a neton.quic connection whose TLS handshake negotiated ALPN "h3". The TLS
configuration is neton.quic.proto's TLS 1.3 session (OpenSSL): the server gives its certificate chain and key, the
client gives its trust anchors explicitly (there is no system trust store), and both offer ALPN_H3:
// Server
val tls = TlsServerConfig(Certificates.pem(chainPem), PrivateKey.pem(keyPem), alpnProtocols = listOf(ALPN_H3))
val endpoint = Endpoint.create(EndpointConfig.default(), ServerConfig.withCrypto(tls), bindUdp(address))
while (true) {
val quic = endpoint.accept()?.await() ?: break
launch {
val conn = neton.http.h3.server.newConnection(quic.asH3())
while (true) {
val (request, stream) = conn.accept()?.resolveRequest() ?: break
stream.use { // close() releases the QUIC stream, as dropping it does in h3
it.sendResponse(Response.builder().status(200).body(Unit))
it.sendData(Bytes.copyOf("hello".encodeToByteArray()))
it.finish()
}
}
}
}
// Client
val tls = TlsClientConfig(trustAnchors = Certificates.pem(caPem), alpnProtocols = listOf(ALPN_H3))
val quic = clientEndpoint.connectWith(ClientConfig(tls), serverAddress, "example.com").await()
val (driver, sender) = neton.http.h3.client.newClient(quic.asH3())
launch { driver.run() }
val stream = sender.sendRequest(Request.get("https://example.com/").body(Unit))
stream.finish()
val response = stream.recvResponse()
while (true) stream.recvData() ?: breakClose each server RequestStream when done with it (use { }): it is h3's Drop. Until then a request whose body
was not read to its end keeps its QUIC stream, and with it one of the client's stream credits (100 concurrent
bidirectional streams by default), so a server that never closes them stops accepting requests on that connection.
A server certificate that does not chain to the client's trust anchors or does not match the server name fails the
handshake, as does a peer without "h3". The KDoc of neton.http.h3.quic.ALPN_H3 has the details.
./gradlew :http3:macosArm64Test # or linuxX64Test (NETON_IO_DRIVER=epoll|iouring)
Interop peers and scripts (h3 examples, an h3 peer, aioquic, h3spec): http3-interop/ (not published).
HTTP/3 (RFC 9114) and QPACK (RFC 9204) for Kotlin/Native over com.netonstream:quic. The first version replicates
h3 0.0.8 (and h3-quinn for the QUIC adapter). Artifact com.netonstream:http3, package neton.http.h3. Common
HTTP types (Request, Response, HeaderMap, ...) come from com.netonstream:http.
This repository was split out of http on 2026-09-29, with its history. Specification, deliberate differences from
the reference (marked ⚖️) and the implementation record: SPEC.md.
Release coordinate: com.netonstream:http3:0.3.0, built against com.netonstream:http:0.3.0 and
com.netonstream:quic:0.3.0 (tests: quic-testkit:0.3.0). 0.3.0 adds a request-head time limit for the server
(H3_REQUEST_REJECTED both ways) and moves to http 0.3.0 and quic 0.3.0. 0.2.0 added requests over 0-RTT and RequestStream.isEarlyData(). Kotlin 2.4.20 (the build refuses anything lower), native targets shared by http and quic (no 32-bit Android).
>); the
connection's one GREASE frame on a request stream follows the first head instead of preceding FIN (aioquic needs it).Builder.requestHeaderTimeout, default 10 s, 0 = none): a request
stream whose HEADERS frame has not arrived in time is rejected both ways with H3_REQUEST_REJECTED.--http3 interop, loss and reordering runs over real TLS, performance comparison.HTTP/3 (neton.http.h3) runs on a neton.quic connection whose TLS handshake negotiated ALPN "h3". The TLS
configuration is neton.quic.proto's TLS 1.3 session (OpenSSL): the server gives its certificate chain and key, the
client gives its trust anchors explicitly (there is no system trust store), and both offer ALPN_H3:
// Server
val tls = TlsServerConfig(Certificates.pem(chainPem), PrivateKey.pem(keyPem), alpnProtocols = listOf(ALPN_H3))
val endpoint = Endpoint.create(EndpointConfig.default(), ServerConfig.withCrypto(tls), bindUdp(address))
while (true) {
val quic = endpoint.accept()?.await() ?: break
launch {
val conn = neton.http.h3.server.newConnection(quic.asH3())
while (true) {
val (request, stream) = conn.accept()?.resolveRequest() ?: break
stream.use { // close() releases the QUIC stream, as dropping it does in h3
it.sendResponse(Response.builder().status(200).body(Unit))
it.sendData(Bytes.copyOf("hello".encodeToByteArray()))
it.finish()
}
}
}
}
// Client
val tls = TlsClientConfig(trustAnchors = Certificates.pem(caPem), alpnProtocols = listOf(ALPN_H3))
val quic = clientEndpoint.connectWith(ClientConfig(tls), serverAddress, "example.com").await()
val (driver, sender) = neton.http.h3.client.newClient(quic.asH3())
launch { driver.run() }
val stream = sender.sendRequest(Request.get("https://example.com/").body(Unit))
stream.finish()
val response = stream.recvResponse()
while (true) stream.recvData() ?: breakClose each server RequestStream when done with it (use { }): it is h3's Drop. Until then a request whose body
was not read to its end keeps its QUIC stream, and with it one of the client's stream credits (100 concurrent
bidirectional streams by default), so a server that never closes them stops accepting requests on that connection.
A server certificate that does not chain to the client's trust anchors or does not match the server name fails the
handshake, as does a peer without "h3". The KDoc of neton.http.h3.quic.ALPN_H3 has the details.
./gradlew :http3:macosArm64Test # or linuxX64Test (NETON_IO_DRIVER=epoll|iouring)
Interop peers and scripts (h3 examples, an h3 peer, aioquic, h3spec): http3-interop/ (not published).