Repository navigation
v0.11.0
flare v0.11.0 is a large release. It collapses the server API onto four names, adds a streaming HTTP client for every wire, and fixes a long list of protocol and security bugs found in a review of the HTTP/1, HTTP/2, HTTP/3, QUIC, TLS, WebSocket and gRPC code. Several of those fixes change behavior that used to be accepted, so the breaking changes come first. 200 commits since v0.10.0.
Toolchain
- The Mojo pin is
>=1.1.0,<2.0.0. The build runs warning-free on 1.1.0: 843 uses of deprecated APIs were migrated, then the remaining 71. json,mojodocandmozzare pinned to release tags (v0.4.1,v0.1.1,v0.2.1), and the recipe builds json from the matching commit. A nightly fuzz failure can no longer come from a dependency that moved underneath flare.jsonno longer depends on MAX for CPU use (max-coreis only a version constraint for its GPU path), and the simdjson wrapper includes<iterator>beforesimdjson.h. That fixes the clean-install failure with simdjson 4.6 and libc++ 23 reported in #33.- The build backend is
pixi-build-rattler-build >=0.3.13as a range.
Breaking changes
The old spellings of the server API stay as shims and are removed in v0.12.
Server API: nineteen entry points became four names.
| Before | Now |
|---|---|
bind(addr, config, h2_config) |
bind(addr, config); HTTP/2 settings live at config.h2 |
bind_many(addrs, config) |
bind(addrs, config) |
serve(handler, ws_handler) |
attach_ws_h2(ws_handler), then serve(handler) |
serve_tls(h) |
serve(h) on a bind_tls server |
serve_ws_upgrade(h, ws_fn, ...) |
ServerConfig.ws = WsUpgrade(ws_fn, offload), then serve(h) |
serve_static_multicore(resp, n) |
serve_static(resp, n) |
serve_comptime[handler, config]() |
ServerConfig.check[config]() |
serve_cancellable, serve_view and serve_static now raise when a TLS context or extra listeners are bound. They used to ignore both. Http2Config moved to flare.http.proto, and the protocol configs nest inside ServerConfig.
Stricter HTTP/1.1 request parsing. These were accepted before and are now answered 400 (or 501), because a proxy in front of flare could read the same bytes as a different request:
Content-Lengthmust be a header line of its own with 1 to 18 digits.Transfer-Encodingis read across all lines, must end inchunked, and cannot appear withContent-Length. Any coding beforechunkedis a 501.- A bare LF ending the header block, a field line with no colon, an empty field name, and a second
Hostfield. - A method that is not a token, a request target with bytes outside visible ASCII, and a control byte other than HTAB in a field value.
- Repeated fields are all kept.
headers.get(name)returns the first value andget_all(name)returns every one. The last line used to win.
Middleware. A layer binds its inner handler as Handler & Copyable and no longer needs Defaultable. The no-argument form is gone: write Logger(MyHandler()), not Logger[MyHandler](). Preconditions on unsafe methods are checked before the handler runs, not after the write has happened.
Sessions and cookies.
- Every session store needs a key of at least 16 bytes and raises at construction without one. A short key used to be kept as an empty marker that could not sign but did verify, so a store built from an unset environment variable accepted forged cookies.
Cookie.to_set_cookie_header()raises on a name that is not a token, on values with;or other characters outside RFC 6265, and on control bytes inDomainorPath.SameSite=Nonealso emitsSecure.- The client cookie jar follows RFC 6265 scoping: host-only unless
Domainmatches,Pathmatched,Secureonly over https. It used to send every cookie to every origin.cookie_header(url)takes the URL to match against. - A cross-origin redirect drops the caller's
CookieandProxy-Authorizationheaders, asAuthorizationwas already dropped.
Shared cache. Responses marked private are never stored. A response to a request with Authorization is stored only with public, s-maxage or must-revalidate, and one to a request with Cookie only with public or a Vary that names Cookie. These used to be stored and served to other clients.
Timeouts and WebSocket.
idle_timeout_msnow also applies to a connection that has not sent its first byte.request_timeout_msis enforced. A client that trickles bytes just inside the idle timeout gets a 408.WsUpgrade.offloaddefaults toTrue. A WebSocket used to stall its whole worker by default.- A handshake with
Sec-WebSocket-Versionother than 13 gets a 426.
Prelude and constants. The frame-mux codec left the root surface, the status-code constants are namespaced, and the prelude exports the types that the exported API already hands back. tests/test_prelude_surface.mojo checks that the prelude and the root package stay in step.
New
- Streaming client.
HttpClient.get_streaming(url)streams a download overhttp://andhttps://. ALPN picks h2 or HTTP/1.1 on TLS and the matching reader is used, so an HTTP/2 response no longer falls back to buffering.with_h2c(prior_knowledge, upgrade)reaches cleartext HTTP/2.send_chunkedtakesbody_sizeto sendContent-Lengthinstead of chunked framing, and acanceltoken that is checked between chunks.get_streaming_tls()is folded intoget_streaming(). - Incremental readers for HTTP/1 (strict framing, 1xx heads consumed, trailers in their own
HeaderMap), HTTP/2 (credit returned as the body is drained, so a large body stays bounded) and HTTP/3 (head before body, final size checked againstcontent-length). HttpClient.pool_stats(), and a user agent derived fromFLARE_VERSION.- WebSocket and HTTP on one port. Set
ServerConfig.wsand upgrade requests go to the WebSocket handler while everything else goes to theHandler. - QUIC loss recovery. RFC 9002 loss detection, PTO and CUBIC now run on the server as well as the client.
- One CSPRNG for every random byte flare needs. QUIC keys, connection IDs and tokens come from it.
- Opt-in AsyncRT thread engine behind
-D FLARE_ASYNCRT. The whole test suite runs on it in CI and there is a benchmark against the default pthread engine. It is experimental. serve_static(num_workers=N)now uses all N workers. It ignored N before.
Hardening
About 120 fixes came out of the review. The main groups:
- HTTP/1 server: request smuggling shapes listed above;
Expect: 100-continueis answered; pipelined requests already in the buffer are served; a complete request is answered before a half-close is honored; no body is sent for HEAD, 1xx, 204 and 304; caller text cannot reach the status line; SSE field injection is closed; static files are served by path, not path plus query;RateLimitno longer loses refills. - HTTP client: pooled and unpooled responses are framed by method, status and chunk structure with one set of rules; truncated compressed bodies fail; only requests that cannot repeat an effect are replayed; credentials are stripped and cookies scoped across origins; the Alt-Svc endpoint the origin advertised is the one dialed.
- HTTP/2: HPACK strings stay bytes and Huffman decoding is constant-time per code; header blocks and CONTINUATION are size-bounded and every block is decoded even on a refused stream; the peer's
HEADER_TABLE_SIZEapplies to our encoder; flow control tracks our window and the peer's separately and every response body goes through it; closed streams are pruned and parked bodies freed on reset; pseudo-headers and field values are validated; nothing is processed after GOAWAY. - HTTP/3 and QUIC: separate packet-number spaces with duplicate drop; ACKs for unsent packets rejected; the anti-amplification limit and 1200-byte Initial enforced; our own connection ID instead of the client's; CRYPTO reassembly bounded; stream limits and credit;
active_connection_id_limit; path validation only for authenticated packets; connection slots and CIDs reclaimed; request streams reassembled by offset; a failing request is contained to its stream. - TLS: resumed sessions are bound to their host; hostname-check failure fails the connect; a missing
close_notifyreads as an unclean EOF; a closed stream raises instead of crashing; SIGPIPE no longer kills the process; certificate reload validates the new pair first. - WebSocket: client frames use random masks; frame and message sizes are capped (16 MiB by default, close code 1009); bytes after the first frame are kept; permessage-deflate inflates and deflates each message in one pass.
- gRPC: decompressed bytes are capped per call, and
grpc-messageis percent-encoded and no longer echoes exception text. - Runtime and network: every socket is opened close-on-exec; ports outside 0-65535 are rejected; Unix sockets unlink only a stale or their own file; FFI libraries load from absolute paths only; timer wheels start at the real clock; a late watchdog fire cannot cancel the next request; a worker bind error fails scheduler start; the DNS cache is bounded and matches names case-insensitively.
- Crypto: non-canonical base64 and empty HMAC keys are rejected.
CI, conformance and testing
- The h2spec gate now gates. It was not failing the job when h2spec failed. Autobahn runs against the WebSocket echo server with a recorded baseline.
- All 63 fuzz harnesses run in the nightly workflow. Two QUIC harnesses had unused-value warnings and are fixed.
- Tests compile once per area instead of once per file, run once in CI, and the examples are built and run.
- The Mojo compile cache persists between CI runs.
Known limits
- Server-side WebSocket is not fully RFC 6455 conformant. 63 of about 450 Autobahn cases fail.
WsConnectionhas no fragment reassembly, reserved opcodes and bits reach the handler, and invalid UTF-8 or reserved close codes are not rejected with the required status. The baseline is recorded case by case. handler_timeout_msis not enforced because handlers run synchronously on the worker.- QUIC send pacing is not wired. The congestion window limits burst size only.
- Offloaded WebSocket threads have no cap.
- The deprecated server spellings are removed in v0.12.
Full inventory in docs/features.md.
Full Changelog: v0.10.0...v0.11.0