Repository navigation
v0.12.0
flare v0.12.0 removes the server shims that v0.11 deprecated and fixes 138 bugs found by formalizing the HTTP/1, HTTP/2, HTTP/3, QUIC, TLS, WebSocket, gRPC and runtime code in Lean 4. Each bug has its own commit, a regression test, a docs update, a matching change to the Lean model and a Mojo repro that now reports resolved. Several fixes make flare refuse input it used to accept, so the breaking changes come first. 156 commits since v0.11.0.
By severity the findings are 6 High, 50 Medium, 81 Low and 1 Info. The six High findings were an out-of-bounds read when a UDP datagram came from an IPv6 sender, a use after free in drain, a WebSocket upgrade answered in plaintext on a TLS connection, a connection-level flow-control window that HTTP/2 never checked, a QPACK reference outside the Required Insert Count, and a gRPC length field that wrapped its bounds check.
Breaking changes
Removed server shims
v0.11 announced these for removal, and they are gone.
| Removed | Use instead |
|---|---|
HttpServer.bind_many(addrs, config) |
HttpServer.bind(addrs, config) |
serve_tls(h) and serve_tls(h, n) |
serve(h) and serve(h, n) on a bind_tls server |
serve_ws_upgrade(h, ws_fn, ...) |
config.ws = WsUpgrade(ws_fn, offload), then serve(h) |
serve_static_multicore(resp, n) |
serve_static(resp, n) |
serve_comptime[handler, config]() |
ServerConfig.check[config]() |
tests/http/test_server_serve_comptime.mojo is now test_server_config_check.mojo.
Sessions expire by default
CookieSessionStore, InMemorySessionStore and BackedSessionStore take ttl_s and default it to DEFAULT_SESSION_TTL_S (86400 seconds). Pass ttl_s=0 for the old behavior. A signed cookie now carries an absolute expiry inside the MAC, and load refuses a validly signed cookie that has none, so cookies issued by v0.11 sign their holders out once. BackedSessionStore used to default to a TTL of 0. The stores gained encode_at, load_at and insert_at forms that take the clock, and InMemorySessionStore.sweep(now_s).
TLS session tickets are opt-in
TlsServerConfig.enable_session_tickets defaults to False, and False now really sends no tickets and keeps no server-side session cache. TlsAcceptor.reload() builds a new context with a fresh ticket key, so a ticket issued before a reload does not resume after it. HttpServer.bind_tls takes no TlsServerConfig and is unaffected.
WebSocket
WsFrame.decode_oneraisesWsProtocolErroron a reserved opcode (0x3 to 0x7, 0xB to 0xF). The server, client and HTTP/2 receive paths all decode through it, so none of them hands such a frame to the application any more.WsConnectiontakes part in the closing handshake.recv()answers a received CLOSE before returning it, with the echoed code, an empty CLOSE for an empty body, or 1002 for a 1-byte body, a code that may not appear on the wire or a reason that is not UTF-8.close()sends one CLOSE and does nothing if one already went out. After a CLOSE,send_text,send_binaryandsend_frameraiseNetworkError, andsend_frametakesmut self.- A final TEXT frame that is not valid UTF-8 closes the connection with 1007. An unmasked client frame closes it with 1002. Before, the socket was dropped with no status.
WsClient.recv()andrecv_message()raise on a masked frame from the server.WsClient.connectraisesWsHandshakeErrorunless the 101 carriesUpgrade: websocketand aConnectionfield with theupgradetoken, and it refuses a response that names a subprotocol or extension the client never offered.- The standalone
WsServerand the shared-listener upgrade check the whole opening handshake (RFC 6455 section 4.2.1): a GET over HTTP/1.1,UpgradeandConnectionmatched as token lists (Connection: noupgradeno longer counts), exactly one key that is base64 of 16 bytes, and exactly one version 13. A refused handshake gets 426 or 400 before the close. ServerConfig.wsis ignored on a TLS-terminated listener. The upgrade path writes on the raw socket, so on abind_tlslistener it used to answer the handshake in cleartext on the TLS connection. Awss://request is now served as ordinary HTTP/1.1 inside TLS. Use a standaloneWsServerforwss://.
HTTP/1.1 client
- A chunked response that ends before the last chunk and the trailing empty line raises
NetworkError, on TCP and on TLS. It used to return the bytes read so far as the whole body. get_streamingover HTTPS raises when a close-delimited body ends withoutclose_notify.- A response head with a bare LF or an empty line before its end, and a status code of more than three digits, are refused.
HTTP/1.1 2041 OKused to become a bodyless 204. - A connection is pooled only after an
HTTP/1.1response that did not sayConnection: close. AnHTTP/1.0response closes it. RedirectPolicy.same_origin_only()compares hosts case-insensitively, andLocationis resolved per RFC 3986 section 5.2.
HTTP/1.1 server
- A header value that is not valid UTF-8 is a 400, including one that arrives through an obs-fold line. A bare LF inside a chunk-size or trailer line is a 400. A
Name : valueline is framed the way the lenient parser reads it. drain(timeout_ms)on a single worker waits out the full timeout before it stops the reactor. It used to return early.drain(0)is still a hard stop.- Errors from a handler that raises are logged to stderr with the request id (
rid=, or-when there is none). The message is still never sent to the client. - A response to HEAD carries no body, including the reactor's own error responses and the static fast path.
HTTP/2
The server and client enforce rules they used to skip, so a peer that depended on the laxness gets a connection or stream error. The first frame after the client preface must be SETTINGS. PUSH_PROMISE is a connection error. Field names with non-ASCII or colon bytes, and malformed or conflicting content-length, are rejected. DATA before the response HEADERS is a stream error. The connection-level receive window is enforced.
Fixed
The whole list, with commit hashes, is in formal/report/Fixed.md. By area:
Memory safety and crashes:
- UDP
recvfromgave the kernel an IPv4-sized address buffer, so an IPv6 sender's address was truncated and the decoder read eight bytes past the buffer. It now reports the right sender. drainfreed the stop flag while a detached worker could still read it. It also leaked the joined workers' listeners, closed the shared listener before the workers joined, and left per-worker listeners open whenScheduler.startrolled back.sem_openwas called with the wrong ABI on macOS arm64, so the blocking-pool cap was never enforced there. A NULL result on Linux was dereferenced. The pool now fails closed when the semaphore cannot be opened.- A length of 2^63-1 on the wire wrapped the bounds check in the gRPC proto reader, which then aborted on an out-of-range index.
ByteReaderhad the same overflow in its own check. - A QPACK field section with a Required Insert Count of 0 could still resolve dynamic entry 0 through wrapped indices, and the Sign byte of its prefix was never bounds-checked.
- The idle
io_uringworker now wakes on a timeout and sees the stop flag, and the wakeup read is re-armed after the submission queue is flushed.
HTTP/1.1, URLs and middleware:
- The URL authority now ends at
?and the fragment at the first#.Url.parsestrips userinfo through the last@.urldecoderejects ill-formed UTF-8. Compressleaves partial content alone and sendsVary: Accept-Encodingon identity responses.negotiate_encodinghonors the*weight.- The CORS allowlist no longer depends on entry order, and every response carries
Vary: Origin. RateLimitno longer wraps after a long idle period.CircuitBreakercounts its cooldown from the failure and lets one probe through while half-open.ComptimeRoutertreats a*that is not the last segment as matching nothing. TheConnectionheader is read as an option list on every line.- An h2c upgrade whose 101 flushes on a later writable edge now migrates, and a partly written
100 Continueis completed.
HTTP/2 and HPACK:
- Closed, refused, idle and half-closed streams follow RFC 9113 in both roles, including connection credit returned for DATA that a reset discarded.
- HPACK strings are stored byte for byte, the decode budget counts the last field, and the decoder stays at 4096 until the peer's size update.
- A client answers an oversized frame with GOAWAY instead of raising, and a GOAWAY shorter than 8 octets is a FRAME_SIZE_ERROR.
QUIC and HTTP/3:
- Unknown frame types, stream limits above 2^60, ACK ranges below packet number 0 and frames on streams that cannot exist are rejected.
- The server validates the client's transport parameters, and the client tells an absent connection ID parameter from a zero-length one.
- The server sends CONNECTION_CLOSE and holds a closing period, sends nothing while draining, and follows RFC 9000 section 10.1 for its idle timer. The client now has an idle timeout too.
- The two halves of a stream reset independently, and STOP_SENDING is answered with RESET_STREAM.
- The HTTP/3 server opens its own control stream and sends SETTINGS. An undecodable field section closes the connection with
QPACK_DECOMPRESSION_FAILED, and HTTP/2-reserved frame types and settings are refused.
Runtime and networking:
IpAddr.is_multicastis exactlyff00::/8, the civil date conversion no longer puts dates before year 0 one day off, andDnsCachesaturates a huge TTL.UnixListener.bindremoves only a socket whose probe is refused. Non-ASCII socket paths round-trip.write_allandwritev_buf_allraise when the kernel makes no progress instead of spinning.TimerWheel.next_fire_msis capped at the overflow promotion boundary,BufferPool.releasedrops handles smaller than their class, and the watchdog deadline is clamped to a valid range.
Known limits
- The server side of WebSocket has no fragment reassembly. 42 of about 450 Autobahn cases still fail, down from 63 in v0.11, and all 42 come from that gap. They are listed one by one in
tests/tools/conformance/autobahn-known-fail.txt, and the 21 that now pass are off the list, so CI would catch them failing again. - QUIC send pacing is built but not wired, and the congestion window does not gate the HTTP/3 DATA pump.
- The QPACK dynamic table is dormant. The encoder references no dynamic entry and refuses an insert that would evict one.
- HTTP/3 serves single-worker only, and the h3 client rejects a request body larger than one packet.
- The Lean model and its repros live in
formal/and are not part of CI.
Verification
All five CI jobs pass on the tagged commit: tests on Ubuntu and macOS, the ASan build, the lints, and conformance with h2spec and Autobahn. pixi run formal-repros reports 138 resolved and none open. The per-feature details are in docs/features.md at this tag.
Full Changelog: v0.11.0...v0.12.0