Skip to content

v0.11.0

Choose a tag to compare

@ehsanmok ehsanmok released this 30 Sep 20:25
· 314 commits to main since this release

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, mojodoc and mozz are 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.
  • json no longer depends on MAX for CPU use (max-core is only a version constraint for its GPU path), and the simdjson wrapper includes <iterator> before simdjson.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.13 as 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-Length must be a header line of its own with 1 to 18 digits. Transfer-Encoding is read across all lines, must end in chunked, and cannot appear with Content-Length. Any coding before chunked is a 501.
  • A bare LF ending the header block, a field line with no colon, an empty field name, and a second Host field.
  • 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 and get_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 in Domain or Path. SameSite=None also emits Secure.
  • The client cookie jar follows RFC 6265 scoping: host-only unless Domain matches, Path matched, Secure only 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 Cookie and Proxy-Authorization headers, as Authorization was 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_ms now also applies to a connection that has not sent its first byte.
  • request_timeout_ms is enforced. A client that trickles bytes just inside the idle timeout gets a 408.
  • WsUpgrade.offload defaults to True. A WebSocket used to stall its whole worker by default.
  • A handshake with Sec-WebSocket-Version other 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 over http:// and https://. 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_chunked takes body_size to send Content-Length instead of chunked framing, and a cancel token that is checked between chunks. get_streaming_tls() is folded into get_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 against content-length).
  • HttpClient.pool_stats(), and a user agent derived from FLARE_VERSION.
  • WebSocket and HTTP on one port. Set ServerConfig.ws and upgrade requests go to the WebSocket handler while everything else goes to the Handler.
  • 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-continue is 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; RateLimit no 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_SIZE applies 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_notify reads 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-message is 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. WsConnection has 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_ms is 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