Liking cljdoc? Tell your friends :D

Changelog

1.0.0-alpha85 — 2026-10-12

Changes since 1.0.0-alpha56. This release reworks the server core. HTTP/1.1, HTTP/2 and HTTP/3 now share one request and response policy and one timeout model, options are validated, WebSocket support is much more complete, and cleartext HTTP/2, Ring async handlers and event hooks are new. Many options were renamed, so read the breaking changes first.

Breaking changes

Options

  • Options are validated. An unknown key throws ex-info with a "did you mean" suggestion. A wrong type or invalid value throws ex-info whose ex-data holds :type, :option and :value. Previously unknown keys were ignored and invalid values threw IllegalArgumentException, so code catching that from run-server must catch ExceptionInfo instead. A nil value means the default.
  • Naming: durations end in -timeout and are in milliseconds, sizes end in -bytes, TLS options start with :ssl-. See the table below for renames.
  • New timeout model: :handshake-timeout, :header-timeout, :read-timeout, :write-timeout, :idle-timeout and :handler-timeout, each with one meaning on every protocol (doc/options.md).
    • :request-timeout is removed. On HTTP/1.1 it bounded reading the whole request; that is now :header-timeout (10 s for the head, from its first byte) plus :read-timeout (30 s without body progress). On HTTP/2 and HTTP/3 it bounded the handler and answered 408. Handlers now run unbounded by default, and :handler-timeout (off by default) answers 503.
    • :idle-timeout changes meaning and default. It was a per-read socket timeout (30 s). It is now how long a connection may sit with no request in progress (75 s): HTTP/1.1 between requests, HTTP/2 and HTTP/3 with no open stream, a WebSocket with no frame from the client.
    • :keep-alive-timeout is removed, folded into :idle-timeout.
    • :http3-max-idle-timeout is removed. QUIC's max_idle_timeout is :idle-timeout, so its default goes from 30 s to 75 s.
  • One request-head limit on every protocol. :http2-max-header-list-size (8192) and :http3-max-field-section-size are removed, and :max-header-bytes (65536) applies to HTTP/2 and HTTP/3 too: 431 per request, and the connection only ends above 4× that (at least 64 KiB).
  • Removed without replacement: :request-buffer-size, :max-inline-body, :coalesce-high-water, :chunk-buffer-size, :max-drain-bytes (HTTP/1.1 buffer tuning), :http3-qpack-max-table-capacity, :http3-qpack-blocked-streams (QPACK uses the static table only) and :worker-executor (handlers always run on virtual threads).
  • Changed defaults and rules:
    • :http3-initial-max-data-bytes: 1 GiB → 512 KiB. It is now the window a connection starts with (it grows up to twice :http3-max-window-bytes as :max-buffered-bytes pays, with the release builds' libquiche) and caps the request body bytes buffered per connection. It is charged to :max-buffered-bytes while the connection reads a request body. HTTP/3 per-stream windows default to half of it (256 KiB), so one stream whose handler doesn't read can't stall the connection's other requests.
    • :http2-initial-window-bytes: 1 MiB → 256 KiB, now where windows start. A stream window doubles up to :http2-max-window-bytes (8 MiB) while its handler keeps up, and the connection window (4× the initial one, 1 MiB) up to twice that, as far as :max-buffered-bytes pays.
    • :http2-initial-window-bytes must be at least 65535.
    • :max-request-body-bytes no longer applies to WebSocket frames; use :ws-max-message-bytes.
    • :ssl-alpn-protocols defaults to ["http/1.1"] without :http2. It used to be the JVM default.
  • Checked at startup: :ssl-alpn-protocols, :ssl-cipher-suites, :ssl-protocols and client auth need TLS, and offering "h2" needs :http2. :http3-port and :advertise-alt-svc true need :http3. :http3 with :port 0 needs :http3-port (or :advertise-alt-svc false). :server-header must be a valid field value. :http2 needs TLS; cleartext HTTP/2 is :http2c.
alpha56 optionNow
:idle-timeout (30000, per read):idle-timeout (75000, connection idle); per-read bounds are :read-timeout and :write-timeout
:request-timeoutremoved: :header-timeout + :read-timeout (HTTP/1.1), :handler-timeout (HTTP/2, HTTP/3; 503, off by default)
:keep-alive-timeoutremoved (:idle-timeout)
:so-rcv-buf:so-rcv-buf-bytes
:so-snd-buf:so-snd-buf-bytes
:alpn-protocols:ssl-alpn-protocols
:enabled-cipher-suites:ssl-cipher-suites
:enabled-tls-protocols:ssl-protocols
:http2-initial-window-size:http2-initial-window-bytes
:http2-max-frame-size:http2-max-frame-bytes
:http2-max-header-list-sizeremoved (:max-header-bytes)
:http3-max-idle-timeoutremoved (:idle-timeout)
:http3-initial-max-data (1 GiB):http3-initial-max-data-bytes (512 KiB)
:http3-max-udp-payload-size:http3-max-udp-payload-bytes
:http3-initial-max-stream-data-bidi-local:http3-initial-max-stream-data-bidi-local-bytes
:http3-initial-max-stream-data-bidi-remote:http3-initial-max-stream-data-bidi-remote-bytes
:http3-initial-max-stream-data-uni:http3-initial-max-stream-data-uni-bytes
:http3-max-field-section-sizeremoved (:max-header-bytes)
:http3-qpack-max-table-capacityremoved
:http3-qpack-blocked-streamsremoved
:request-buffer-size, :max-inline-body, :coalesce-high-water, :chunk-buffer-size, :max-drain-bytesremoved
:worker-executorremoved

Unchanged: :port :host :backlog :shutdown-timeout :max-header-bytes :max-request-body-bytes :max-keep-alive-requests :so-nodelay :so-reuse-addr :so-linger :ssl-context :ssl-context-provider :ssl-need-client-auth :ssl-want-client-auth :ssl-session-cache-size :http2 :http2-max-concurrent-streams :http2-stream-reset-limit :http2-continuation-limit :http3 :http3-port :http3-cert-path :http3-key-path :http3-initial-max-streams-bidi :http3-initial-max-streams-uni :http3-stateless-retry :http3-ack-delay-exponent :http3-max-ack-delay :http3-active-connection-id-limit :advertise-alt-svc :alt-svc-max-age :server-header :error-handler.

Java API

  • WebSocketListener and WebSocketSocket moved from com.s_exp.enso.websocket to com.s_exp.enso.api. WebSocketSocket gained sendTextAsync, sendBinaryAsync and SendCallback. New api.WebSocketException, with code().
  • The supported API is the s-exp.enso namespace, EnsoServer and the com.s_exp.enso.api package. Every other package is internal.
  • Config.Builder setters follow the option names (soRcvBuf → soRcvBufBytes, alpnProtocols → sslAlpnProtocols, http2InitialWindowSize → http2InitialWindowBytes, http3InitialMaxData → http3InitialMaxDataBytes, ...), and setters of removed options are gone. build() throws Config.InvalidOptionException, an IllegalArgumentException with option() and value().
  • EnsoServer constructors no longer throw IOException; start() does. register and unregister are removed.
  • ChunkedWriter.write declares IOException, since it may flush. closeInternal() is removed; charset(Charset) and bytesWritten() are added.
  • RingErrorHandler.respond takes a fourth argument, expected: whether the failure is expected (I/O, or the interrupt that follows the server giving up on the exchange), which is logged at FINE instead of WARNING.

Request map

  • :body is nil when the request has no body, on every protocol. HTTP/2 and HTTP/3 used to always pass an InputStream.
  • :scheme is :https on TLS connections. It was always :http.
  • :server-name without a Host header is the local address. It was "localhost".
  • :remote-addr writes IPv6 in the RFC 5952 short form.

Responses

These apply to every protocol. A violation is a 500, or :error-handler.

  • :status must be an integer in 200-599, or 101 for a WebSocket upgrade. A :status that is present but nil or not an integer is a handler error. A missing :status still means 200.
  • A response that isn't a map is a handler error naming its class.
  • Header names must be RFC 9110 tokens. Values may not contain control characters (HTAB is allowed) or characters above U+00FF. alpha56 only rejected CR, LF and NUL.
  • A handler Transfer-Encoding is dropped, since the server frames the body. A handler Content-Length is dropped when the server knows the length. Two different Content-Length values are an error.
  • String bodies are encoded with the Content-Type charset, UTF-8 when there is none. An unknown charset is a 500. alpha56 always used UTF-8.

New options

  • :max-connections (10000) and :max-connections-per-ip (0): connection caps, TCP and QUIC together.
  • :handshake-timeout (10000), :header-timeout (10000), :read-timeout (30000), :write-timeout (30000), :handler-timeout (0).
  • :max-header-fields (100): request header fields per head, 431 above.
  • :max-buffered-bytes (0, meaning a quarter of the heap): a server-wide budget for bytes buffered on behalf of peers and the flow-control credit promised to them, enforced by back-pressure with a fair share per connection.
  • :min-data-rate-bytes (240) and :min-data-rate-grace (5000): the slowest request body accepted while a handler waits for it (Kestrel's defaults). Slower bodies fail with 408 ("min-data-rate").
  • :http2c (false): cleartext HTTP/2 with prior knowledge, next to HTTP/1.1.
  • :http2-max-window-bytes (8388608): the largest an HTTP/2 stream's receive window autotunes to. The connection window grows to twice this.
  • :http3-max-window-bytes (8388608): the same for HTTP/3. Needs the release builds' patched libquiche; ignored with a stock one.
  • :http3-cert-reload-interval (10000): HTTP/3 certificate rotation.
  • :http3-retry-threshold (256), :http3-max-half-open (1024) and :http3-stream-reset-limit (400): HTTP/3 flood protection.
  • :http3-event-loops (0, meaning one per core on Linux and one elsewhere), :http3-so-rcv-buf-bytes and :http3-so-snd-buf-bytes (4194304).
  • :ws-max-message-bytes (1048576), :ws-max-queued-bytes (1048576), :ws-close-timeout (5000), :ws-compression (false), :ws-ping-interval (0) and :ws-allowed-origins (same origin).
  • :async (false): Ring asynchronous handlers.
  • :server-events (nil): connection, request and protocol-error callbacks, delivered in order from their own thread, never a server thread. A listener that falls 8192 events behind loses the next ones, counted by EnsoServer.droppedEvents().

Features and behaviour changes

  • Ring async handlers with :async true. raise goes to :error-handler, and :handler-timeout applies. A handler that throws before respond or raise has raised; one that throws after is logged and its response stands.
  • The stream a StreamableResponseBody writes to refuses writes once closed.
  • Cleartext HTTP/2 (:http2c) with prior knowledge on the plain listener, next to HTTP/1.1. No Upgrade: h2c. Reported as protocol "h2c".
  • HTTP/2 receive windows autotune, as quiche's and quic-go's do. A window doubles when its handler reads half of it within two round trips (measured from the SETTINGS acknowledgement, refreshed by a PING at most once a second while uploading), from :http2-initial-window-bytes up to :http2-max-window-bytes, and the connection's up to twice that. Every growth of the connection window is paid for from :max-buffered-bytes first, nothing grows while the connection is throttled, the connection window returns to its baseline once no request body is open, and no credit is granted past what bodies with a Content-Length have left to send.
  • HTTP/2 request bodies allocate nothing per burst. Past 8 KiB they are buffered in pooled 64 KiB segments, returned as the handler reads them.
  • HTTP/3 receive windows autotune with quiche's own rule, from the initial windows up to :http3-max-window-bytes, and the connection's up to twice that. Every doubling of the connection window is paid for from :max-buffered-bytes first and skipped while the connection is throttled. Release shims link libquiche with patches that add receive-window control on a live connection and bound the out-of-order fragments a stream buffers (native/enso_quiche/patches/). With a stock libquiche (development builds) windows keep their initial sizes and fragments are unbounded.
  • HTTP/3 admission no longer consults :max-buffered-bytes. An idle QUIC connection buffers no request body, so an exhausted budget no longer drops new Initials; :max-connections, :max-connections-per-ip and :http3-max-half-open bound how many connections are accepted.
  • HTTP/3 refuses over an admission limit instead of dropping the Initial. When :max-connections, :max-connections-per-ip or :http3-max-half-open turns a client away, its Initial is answered with a stateless CONNECTION_CLOSE carrying CONNECTION_REFUSED at the Initial level, so the client falls back at once instead of waiting out a handshake timeout.
  • HTTP/3 certificate rotation: changed PEM files are loaded for new connections without a restart.
  • HTTP/3 uses several cores: connections are sharded over event loops (Linux SO_REUSEPORT with BPF steering), and a failed loop is restarted.
  • :error-handler works on HTTP/2 and HTTP/3. It was HTTP/1.1 only.
  • Expect: 100-continue is supported on every protocol, answered on the handler's first body read. Any other expectation gets 417 without running the handler.
  • The same limits on every protocol: 413 for a declared body over :max-request-body-bytes without running the handler, 431 for heads over :max-header-bytes or :max-header-fields.
  • The same error responses on every protocol: the 400, 408, 413, 417, 431, 500, 501 and 503 the server produces carry a text/plain; charset=utf-8 reason phrase.
  • Request body errors are the client's. A broken, oversized or stalled body is answered 400, 413 or 408 whatever the handler does with the exception, and :error-handler isn't called.
  • Header values: nil values are skipped, and a seq sends one field per non-nil element.
  • write! and seq bodies encode Strings with the response charset.
  • Lingering close on HTTP/1.1, HTTP/2 and WebSocket: output is shut down and input read for up to 2 s, so clients read the last response instead of a TCP reset.
  • Graceful shutdown: :shutdown-timeout is one deadline for the whole server, HTTP/2 and HTTP/3 drain in two GOAWAYs, and streams in flight when a peer sends GOAWAY finish.
  • WebSocket: ring.websocket.protocols support (Listener, PingListener, Socket, AsyncSocket), asynchronous sends with back-pressure, permessage-deflate, server pings, idle close with 1001, CLOSE 1001 on server stop. A listener exception closes with 1011 and its message stays on the server. Handler :headers are sent on the 101. A WebSocket response on HTTP/2 or HTTP/3 is answered 501.
  • Observability: api.ServerEvents (connectionOpened, connectionClosed, requestCompleted, protocolError) for HTTP/1.1, h2, h2c, h3, WebSocket, TCP and TLS, and the JFR events com.s_exp.enso.Connection, Request and ProtocolError on every protocol.
  • Logging: client-caused failures at FINE, server faults at WARNING, rate-limited to one record per second per call site. A logging setup that throws never reaches a server thread. The same rule holds for handlers and streamed response bodies on every protocol. A handler the server interrupted because it gave up on the request (stream reset, connection gone, :handler-timeout) logs the resulting InterruptedException at FINE. A streamed body that fails for another reason logs at WARNING on HTTP/1.1 and HTTP/3 too.
  • :max-buffered-bytes limits growth, never progress. Each connection keeps a floor it can always use (HTTP/2's initial 65535-octet window, 64 KiB of buffered body per HTTP/3 connection, 64 KiB of message buffer per WebSocket), so an upload on a connection under its floor goes on while the budget is exhausted. WebSocket messages are delivered whole, so one WebSocket at a time reads past the limit until its message is delivered, and the others take turns. Messages that together, or alone, need more than the budget no longer stall until their deadline.
  • :read-timeout and :min-data-rate-bytes only count time the client could have sent. While the server holds a request body back (an HTTP/2 client out of connection credit the server withholds, an HTTP/3 stream the server doesn't read, an HTTP/3 client out of connection credit because unread streams fill its window) they stop, so the client no longer gets a 408 for a stall the server caused. A WebSocket message's :read-timeout stops the same way while the server doesn't read it for the budget, so no false 1008.
  • Supervised server threads. The timer survives failing tasks and restarts its loop, the acceptor survives any failure of one iteration, and an HTTP/3 event loop restart can't be undone by a failure while reporting it. EnsoServer.isHealthy() tells whether they all run.
  • Options taking a function accept a var (#'handler).
  • Stricter request parsing. Content-Length must be digits and appear once, even with equal values. An HTTP/1.1 request without Host is 400. The request target and Host authority are validated. HTTP/2 and HTTP/3 heads follow the same rules. A well-formed CONNECT gets 501. On HTTP/1.1 a request line over :max-header-bytes is 414 ("uri-too-long"), and a bare LF line ending is 400 at once instead of waiting out :header-timeout.
  • HTTP/1.1 response framing. A body of unknown length is chunked on every HTTP/1.1 response, even when the connection closes after it, so a body cut short is visible. On HTTP/1.0, where the end of the connection is the only framing, a body that fails ends it with a reset (over TLS without close_notify). When the server closes, a handler's Connection field without "close" is left out. A handler throwing HttpError with a status outside 4xx and 5xx gets 500.
  • HEAD with a nil body (Ring's wrap-head) carries no Content-Length, on every protocol, since nil says nothing about GET's length.
  • requestCompleted fires for every HTTP/1.1 response, including a failed write or a 101 flush. The 101 after a keep-alive response reports its own body bytes (none).

Security fixes

HTTP/1.1:

  • A chunk size overflowing a long is rejected. Control characters in chunk extensions are rejected and chunk-size lines are capped.
  • The trailer size cap holds after the buffer compacts, and trailer lines are validated.
  • Building the header map stays linear with many distinct names.
  • Over TLS, socket timeouts restarted with every ciphertext byte and a read returned only whole records, so a record trickled a byte at a time held a read open. The idle wait, :header-timeout and the :min-data-rate-bytes allowance are now wall clock over TLS too.
  • The post-response drain of an unread body is held to 2 s. A drain can no longer close a connection whose request just started arriving. Closing (a TLS close_notify, the lingering close's shutdown) is bounded by :write-timeout.
  • The :handler-timeout interrupt never lands in a socket read, where it closed the socket and lost the lingering close that protects the 503 from a reset. A late expiry can't claim the next request on the connection.

HTTP/2:

  • Closed-stream bookkeeping is bounded, with constant-time lookup.
  • The reset limit (CVE-2023-44487) is a rate refilling every 30 s, and counts reset streams while their handler runs. Resets caused by peer frames share that budget (CVE-2025-8671).
  • Header blocks hit a hard ceiling before buffering. HPACK integer overflow is rejected. A header block can't be starved by control frames.
  • Empty DATA floods, and control-frame floods from a peer that doesn't read, are cut off. Sparse empty DATA on closed streams is no longer mistaken for a flood.
  • A shut flow-control window is bounded by :write-timeout. Tiny connection WINDOW_UPDATEs no longer reschedule streams waiting on their own window.
  • Padding is credited on a length mismatch. HEADERS on a forgotten stream id is a connection error. Trailers are validated. Repeated SETTINGS are applied in one pass. A SETTINGS ACK or PING off stream 0 is a PROTOCOL_ERROR. The SETTINGS ACK precedes any header block using a new HPACK table size.
  • Request and response buffers count against :max-buffered-bytes. Connection credit beyond the initial 65535 octets is paid for from it before it is granted, and the connection window only grows once body bytes arrive, shared fairly between connections under pressure. Budget wake-ups run off the releasing thread, since an inline wake-up could deadlock two connections. The default stream window is 256 KiB (1 MiB per connection, as in Go and Kestrel).
  • A graceful close waits for responses still held by flow control after their handler returned; they used to be cut short. A writer failure ends the connection instead of leaving it unable to write.
  • END_STREAM on the DATA frame crossing :max-request-body-bytes ends the stream. :idle-timeout counts from the end of the last response.
  • A TLS 1.3 KeyUpdate no longer makes the framer wait for a blocked writer.
  • Small ByteArrayInputStream bodies cost what a byte array does (about 21 KB less per response), and streamed bodies use pooled, right-sized rings.

All protocols:

  • Request bodies trickled below :min-data-rate-bytes fail with 408, so one byte every 29 s no longer holds a request forever.
  • HTTP/3 write progress needs 16 KiB per :write-timeout, as on HTTP/2.

HTTP/3 and QUIC:

  • Retry tokens are bound to the address family and the Retry connection id. Retry is required automatically past :http3-retry-threshold, and half-open connections are capped. With :max-connections-per-ip set, clients prove their address with a Retry before taking a per-address slot.
  • Initials with a destination connection id under 8 bytes are dropped.
  • Rapid-reset protection counts peer resets and the server's own stream resets after a handler was dispatched (a malformed trailer or oversized body), so induced resets can't churn the server past :http3-stream-reset-limit. Unread request bodies are bounded per connection.
  • Trailers reject connection-specific fields and a te other than trailers (H3_MESSAGE_ERROR), the same rule HTTP/2 applies.
  • Control-stream rules (CANCEL_PUSH, MAX_PUSH_ID, SETTINGS first, critical streams) are enforced. An oversized control frame is refused (H3_EXCESSIVE_LOAD) before any of it is buffered. Unknown unidirectional stream types no longer leak state.
  • QPACK checks Huffman lengths before allocating, caps decoded sections and rejects overlong integers.
  • Partial header sections share a per-connection budget and a timeout. Streams the server stopped reading don't count against :header-timeout.
  • QUIC connections count against :max-connections.
  • The JNI shim bounds-checks its arguments, and connection teardown is idempotent.
  • An event loop restarts on fresh state after a failure.
  • HTTP/3 response bytes waiting for the peer count against :max-buffered-bytes.

QUIC interoperability:

  • A long header with an unknown version gets Version Negotiation whatever its connection id lengths (RFC 8999), as the quic-interop-runner's readiness probe needs.
  • Initial and 0-RTT packets are spread over the event loops by the kernel's 4-tuple hash.

WebSocket:

  • Same-origin check by default (cross-site WebSocket hijacking); the default compares the Origin scheme too. A configured :ws-allowed-origins is a hard gate: a missing or opaque Origin is refused even when the list is ["*"].
  • Pongs to a ping flood are coalesced.
  • A claimed frame length isn't allocated up front, deflate bombs are bounded, and a message must complete within :read-timeout.
  • Message buffers count against :max-buffered-bytes by capacity, a buffer kept for the next message included. Compressed messages count as they inflate, not as their compressed bytes arrive.
  • A second protocol failure while waiting for the client's CLOSE still drains the client's input, so the first CLOSE isn't lost to a reset.
  • Over TLS the closing handshake ends with close_notify before TCP is closed.
  • Upgrade is read as a token list, like Connection (Upgrade: h2c, websocket).
  • Clients that end messages with final deflate blocks keep their window across messages. Back-references past the previous message failed with 1007.
  • An asynchronous binary send of a buffer without an accessible array (a direct buffer) takes the bytes remaining at the call, as for heap buffers. Moving its position before the write failed the connection.
  • Pongs to pings that arrived together share a write, empty pings and pongs allocate nothing, and a pending pong takes a new payload without allocating.
  • An idle WebSocket gives back the HTTP/1.1 output buffer that a burst of replies grew.

Core and TLS:

  • Global and per-address connection limits, a write watchdog for peers that stop reading, and a TLS handshake timeout that bounds both directions, so a peer that stops reading mid-handshake can't wedge the server's write flight. :handshake-timeout and :idle-timeout may not both be 0 (that would leave a half-open connection unbounded); the server refuses to start.
  • TLS 1.2 client renegotiation is refused.
  • A TLS read timeout no longer replays plaintext.

Native loader:

  • The development library probe only looks next to a classes directory, never relative to the working directory.
  • On musl the glibc shim is never tried; musl is detected from the JVM's own mapped libc.
  • The Java side checks the shim's ABI version and its record layout, field by field.

Performance

Current numbers from the separate-process harness (clojure -M:perf, Apple M1 Pro laptop, 8 CPUs, JDK 25, median of three runs; see doc/performance.md):

Scenarioreq/sAllocated per request (whole server JVM)
HTTP/1.1 GET, 64 connections123,830393 B
HTTP/1.1 pipelined, depth 161,384,895235 B
HTTP/2 GET over TLS (h2load -c 8 -m 32)745,1271,090 B
HTTP/3 GET, 4 connections × 32 in flight73,221801 B
WebSocket echo, 128-byte messages1,023,481333 B

Measured during this rework, each against the code before the change:

  • HTTP/1.1 GET: 497 → 392 bytes per request, pipelined 339 → 235, at the same throughput. Over TLS 1.3, 3510 → 3280 bytes per request.
  • Heap per idle HTTP/1.1 keep-alive connection: 32.4 → 9.7 KiB plain, 98.6 → 16.1 KiB over TLS 1.3. Every protocol gives back grown buffers after a second idle (idle HTTP/2: about 11 KB cleartext and 23 KB over TLS; WebSocket 16 and 25 KiB; HTTP/3 about 3 KB).
  • HTTP/2 GET on the in-process test path: about 760 bytes per request, down from 3108.
  • HTTP/1.1 on the in-process test path: a browser-like 14-field GET went from 1728 to 329 bytes (header maps stay array maps up to 32 fields, and unknown header names and values are reused per connection), and reading :remote-addr, :server-name and :server-port from 376 to 240 (derived once per connection).
  • HTTP/2 and HTTP/3 request heads keep array maps up to 32 fields too, as HTTP/1.1 does. A 14-field browser GET allocates about 1.1 KB instead of 1.6 KB on HTTP/2; small heads are unchanged.

Build and distribution

  • Artifacts: the core jar and five classifier jars (darwin-arm64, linux-amd64, linux-arm64, linux-musl-amd64, linux-musl-arm64) on Clojars. The all-platform jar is a release-run artifact only.
  • Linux glibc shims are built on AlmaLinux 8 (glibc 2.28 or later); the macOS shim needs macOS 11 or later. libquiche 0.29.3, its crate lockfile and the Rust toolchain are pinned.
  • Every release shim is checked (exported symbols, RELRO, BIND_NOW, non-executable stack, glibc symbol ceiling, macOS minimum, a JVM load test) and built with hardening flags.
  • Jars carry Automatic-Module-Name: com.s_exp.enso, LICENSE, NOTICE and THIRD-PARTY-NOTICES.
  • JDK 21+ for every protocol, tested on JDK 21 and 25. On JDK 24+ add --enable-native-access=ALL-UNNAMED for HTTP/3.
  • The shim extraction directory can be set with enso.quiche.tmpdir or ENSO_QUICHE_TMPDIR, for noexec temp directories.
  • Releases are gated on the full test workflow: every namespace on JDK 21 and 25 (x86-64) and JDK 25 (arm64), HTTP/3 under ASan/UBSan, h2spec (TLS and h2c), h3spec and Autobahn. A nightly job runs the soak test, deep property tests, Jazzer fuzzing (HPACK, QPACK, HTTP/1.1, HTTP/2, HTTP/3 streams, WebSocket frames), QUIC interop and the allocation-per-request gate.

Can you improve this documentation?Edit on GitHub

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close