Skip to content

colibri 0.9.0: routes, queries, one set of HTTP names, and TLS alerts

Choose a tag to compare

@c4milo c4milo released this 29 Oct 07:51
· 271 commits to main since this release

colibri 0.9.0 gives the server's router a program's functions and middleware, gives a request its query, names each HTTP concept once in the server and the client, and lets a TLS record session send a fatal alert the program names. The client's connection pool opens TCP connections on demand. It pins chapulin 1f3a99cb and stdx 5fd03aa.

Breaking changes

  • One name for each HTTP concept in server and client (decision 126):
    • server.Body is RequestBody, server.Content is ResponseBody, and the client's Request.content is body.
    • Fields, Field and FieldSection are Headers and Header, and fields is headers. Trailers holds headers: Headers, Wanted is WantedHeader, and NeverIndexed.fields is headers.
    • The server's done and cancelled events are one finished event with an outcome, on both sides. RequestId names a request on both sides.
    • Versions is Protocols, Deadlines and Deadline are Timeouts and Timeout, and the close reason deadline is timeout.
    • client.Channel is client.ConnectionPool, and ChannelOf is ConnectionPoolOf. client.Https is HttpsRecord, client.Values is Destination, and set_values is set_destination. client.Options folds into Config: memory_bytes(config) and init(memory, config, destination).
    • Names say bytes, not octets: StreamOctets is StreamBytes. Received is ReceiveResult, and accept(Security) takes tls: bool.
    • Decision 126 holds the whole table.
  • The client's connection pool opens TCP connections on demand and holds its connections and requests in memory the program places (decision 125). Each event and call about a connection names a ConnectionHandle, and a send event names a TCP connection that owes bytes. The counts say requests: Config.requests, constants.requests_max and pool_requests_max.
  • A request's query is apart from its path (decision 127): Request.path holds no query, and Request.query holds the raw query or null. http.QueryParameters reads its name=value pairs, from the WHATWG URL Standard. The router percent-decodes each path segment and takes %. A segment that encodes /, \ or NUL gets not_found.
  • server.router(Context, table) replaces router(Value, table) (decision 127). The table is .{ .error_handler, .middleware, .routes }. A route holds a function, which takes typed path and query structs, or a struct with on_request, on_body, on_writable, on_finished and a State. Router.Of(options) sizes the router's pool, and handle(context, endpoint, event) drives it. New names: server.ResponseWriter, server.Handled and server.middleware.Next. match gives a Route tag.
  • A route table's walk backs up past a leaf that does not serve the method (decision 128), so a table such as /v1/charges/search beside /v1/charges/:charge compiles, and route_hidden is gone. A 405's Allow lists the methods of every leaf the path reaches, so its value can grow. A table that names more than server.constants.route_methods_max (10) methods does not compile.
  • chapulin 50d7d17b answers a KeyUpdate with its next record of data. tls_provider.Content.new_session_ticket and .key_update are .without_data, and h2's and h11's Decrypted.owes_handshake is gone. More than 16 KeyUpdates in a run close the connection: OpenError.KeyUpdates, h2.Limit.key_updates and server.Limit.key_updates are new, and tls.constants.key_update_replies_max is removed. A program with its own tls_provider follows the new contract.
  • stdx's platform.Cpu holds sha256, sha512, sha3, ssse3 and sse4_1, with no default. A program that writes a platform.Cpu literal states them. One that calls platform.probe() changes nothing.

What's new

  • tls.record.Client.fail and tls.record.Server.fail send the fatal alert the program names, for an error it found itself, such as a record longer than RFC 9846 §5.2 allows. They take the error alerts of RFC 9846 §6.2 alone, on a connected session, and fail the session. A refused call returns a tls.record.FailError and leaves the session as it was (#114).
  • Middleware may wrap a body's reader and writer: State, wrap_body, wrap_response and finish_response.
  • Each route names its own parameters, so two routes may name one position differently (decision 129).
  • On arm64, a session runs the SHA instructions where the probe has them and the timing is stated as data-independent.

Fixes

  • chapulin 1f3a99cb sends record_overflow before it decrypts a record longer than RFC 9846 §5.1 or §5.4 allows, or longer than the record_size_limit this side sent (RFC 8449 §4). Each TLS session over TCP grows 8 bytes.
  • A server sends record_size_limit only to a client that offered it (chapulin a6703587, RFC 9846 §4.3). Clients on rustls refused it before.
  • h11's write_response returns FramingInvalid for a bad Content-Length, where it panicked.

Speed

No API change. Against the competitors' run of 2026-10-20, per request or connection, kernel and user together, on the arm64 judge:

  • A TLS handshake costs about half: h11-tls-one, h2-tls-one and h3-one fall 48%, 47% and 59%.
  • h2-many falls 7%, h11-many 11%, and h11-tls-many 20%.
  • The router matches a path in one pass: h2-routes-many, which routes each request over the GitHub API's GET routes, falls 4% (ratio 0.960 in two judge runs).

Checked

GitHub's main workflow passed on bf4fe21f (run 40828900211).