Samuel Williams Thursday, 03 September 2026

Software becomes difficult at boundaries: between a Ruby object and the memory it exposes, an encoded URL and a filesystem path, an HTTP body and the values an application accepts, or a protocol connection and the service which owns it. When those boundaries remain implicit, unrelated layers begin making assumptions about lifetime, trust, or state.

I spent much of August making those boundaries explicit. This monthly open source progress update follows that work from IO::Buffer inside CRuby through content parsing, Utopia 3, worker-aware HTTP routing, gRPC, and the documentation used to understand those systems.

Giving Raw Memory an Explicit Owner

IO::Buffer gives Ruby programs direct access to native memory. That makes zero-copy I/O and memory mapping possible, but it also means the VM must know exactly which object owns an allocation, how long a pointer remains valid, and whether another operation is still using it.

A slice of a string-backed buffer previously retained the String directly. Freeing the root buffer could therefore leave the slice looking valid even though the lifetime which unlocked the String had ended. Slices now retain their root buffer, including nested slices, so invalidation follows one explicit ownership relationship.

Locking needed the same treatment. A boolean could say that memory was in use, but it could not represent two overlapping users: the first operation to finish could clear the lock while the second still held a native pointer. IO::Buffer locks are now reference counted, and slices share the lock state of their root allocation. The same ownership rule now protects large buffer copies while they release Ruby's Global VM Lock and C callbacks which temporarily expose a Buffer or String as readable or writable memory.

The resulting lifetime is visible from Ruby: nested locks on a root and its slice retain the same allocation, while freeing the root invalidates every view into it.

io-buffer-ownership.rb
source = +"Hello World"
root = IO::Buffer.for(source)
slice = root.slice(0, 5)

root.locked do
	slice.locked do
		# Both locks retain the same root allocation.
	end

	root.locked? # => true; the outer user still holds its lock.
end

root.free

slice.valid? # => false
slice.get_string # Raises IO::Buffer::InvalidatedError.

Several smaller inconsistencies became easier to resolve once that model was clear. Freeing or transferring a buffer now produces one canonical empty state. Resizing mapped buffers and slices to zero no longer enters allocation-specific paths with uninitialized state, while resizing a slice adjusts its view instead of silently copying it into an independent allocation. Empty slices are valid views when their empty range exists, and zero-length native operations avoid null-pointer arithmetic while retaining normal validation.

The public contract describes that state more precisely. Slice validity is defined by whether its recorded range currently exists within its source and distinguished from whether a buffer is empty or null. Calling free leaves a reusable empty buffer rather than a permanently unusable object.

Construction and mutation follow normal Ruby expectations. Constructors reject contradictory ownership and mapping flags; lifecycle operations respect Ruby's frozen-object contract; and IO::Buffer::MAP_ALIGNMENT exposes the platform requirement for non-zero mapping offsets. A read-only mapped buffer also remains read-only when a resize falls back to copying on platforms without mremap.

Turning Buffered I/O into a Composable Primitive

The lifetime work made it possible to simplify how schedulers use those buffers. The experimental buffered I/O hooks previously mixed two responsibilities: performing an I/O operation and deciding whether to wait or retry. Ruby 4.1's scheduler interface version 4 makes each hook perform one transfer of at most the requested length, return short operations directly, and use a consistent (offset, length) buffer range. Higher-level IO methods retain responsibility for waiting and repetition.

IO::Event adopted that interface across its readiness and io_uring backends in one version-aware implementation. The io_uring path then needed to preserve Buffer locks while operations were submitted, interrupted, cancelled, and completed. The corrected hook suspends only after a non-blocking operation would block and keeps completion state alive through cancellation. A follow-up made native build failures fail the build instead of silently falling back to the pure-Ruby selector, and completion records are now recycled only after both an operation and any attached cancellation have been observed.

Interrupted process waits received the same explicit contract: IO::Event returns false when cancellation wins, while preserving a real status if the process completes first. Async forwards Ruby 4.1's corrected buffered I/O signatures, and Ruby 4.1 now requires schedulers to implement fiber_interrupt rather than retaining a compatibility path which cannot provide the same interruption behavior.

The result is a clearer division of responsibility: Buffer owns memory lifetime, a scheduler performs one native transfer, and higher-level I/O decides when an operation should wait or continue.

Parsing Untrusted Content Without Losing Its Shape

Web input is often presented as a String, but its structure matters. A plus sign means a space in URL-encoded form data, a slash may be an encoded filename character rather than a path separator, and a multipart upload should remain a stream rather than becoming an unexpectedly large in-memory value.

Bounded Form and Multipart Parsing

Protocol::URL now decodes the form-specific + and percent-encoding rules and parses URL-encoded bodies incrementally with explicit size, pair-count, and nesting-depth limits. Applications can supply a small result object instead of accepting one fixed data representation, and limit failures have a dedicated exception type rather than being indistinguishable from malformed input.

Multipart parsing has historically accumulated permissive compatibility behavior around headers, boundaries, and nested form names. The new Protocol::Multipart::Header parser accepts the defined token, quoted-value, escape, and UTF-8 filename forms while rejecting duplicates, controls, obsolete folding, and malformed syntax. The streaming parser applies finite limits to preambles, headers, header counts, and part counts before unbounded data can be buffered.

On top of that lower-level parser, FormData separates bounded ordinary fields from streaming file uploads. The configurable parser operates directly on a readable body and an explicit boundary, while multipart limit failures remain distinguishable from syntax errors. Uploads gained readable, copy, discard, and secure-save operations, so an application can consume accepted content incrementally without giving up enforcement of the remaining size budget. Faraday's adapter reads the resulting structured Content-Disposition field rather than reparsing the raw header.

Protocol::Content composes the JSON, URL-encoded, and multipart parsers behind explicit media-type dispatch. It does not take ownership of request metadata, body memoization, or upload storage. Instead, operation-specific parameter declarations convert and validate only the fields an application expects, enumerations constrain accepted values without losing mappings to false or nil, and upload declarations constrain media type and size before handing a bounded stream to application code.

A parameter model can express those expectations independently from the transport or the application's persistence model:

content-parameters.rb
require "protocol/content"

parameters = Protocol::Content::Parameters.build do
	nested "user", required: true do
		field "name", String, required: true
		field "age", Integer
		field "status", enumeration("draft", "published")

		upload "avatar",
			accept: ["image/jpeg", "image/png"],
			size_limit: 5 * 1024 * 1024
	end
end

result = parameters.parse(media_type, input)

Negotiating Representations Explicitly

The media types used for parsing and responses also became easier to compile into application policy. Protocol::Media::Set represents accepted media ranges, while maps associate concrete types with handlers. The final collection API is mutable while being configured and immutable after freeze, with lazily compiled lookup indexes shared by maps and sets. Matching now treats * as a wildcard only when it occupies an entire type or subtype component.

HTTP preference headers preserve information which is easy to discard during sorting. Accept#preferred_media_ranges orders by quality while preserving source order for equal values, and the same stable rule now applies to Accept-Charset, Accept-Encoding, Accept-Language, and TE. Utopia's responder uses those ordered ranges with Protocol::Media maps rather than maintaining separate negotiation logic.

Several adjacent HTTP values became structured too: Range headers can resolve bounded, open-ended, and suffix byte ranges against a representation size; Cookie fields are parsed as request cookie pairs rather than as Set-Cookie attributes, with Utopia using that shared parser instead of maintaining its own split-and-unescape path; and one canonical status-description table replaces the duplicate HTTP/1 table through Protocol::HTTP1's shared lookup.

Bounded parsing must continue below the header layer. Fixed and chunked HTTP/1 bodies now return at most 64 KiB from each read, letting callers enforce representation limits without first accepting an adversarially large transfer chunk. Truncated chunked trailers preserve the original end-of-file failure, and readable HTTP bodies can be adapted to fresh read-only IO streams for consumers which expect an IO interface.

The parser stack now preserves both shape and ownership: protocol libraries recognize syntax and enforce resource limits, while the application declares which values and uploads it is prepared to accept.

Keeping URL Paths Separate from Filesystem Paths

An encoded slash is data inside a URL component; a literal slash separates components. Decoding a path into one String too early loses that distinction and can turn URL manipulation into a filesystem traversal problem.

Protocol::URL::Path stores encoded paths as structured components. It exposes decoded components without treating %2F as a separator, and converts to local paths only after rejecting decoded filesystem separators. Paths can be assigned through value coercion, URL components can be replaced without rebuilding an entire reference, and normalization decodes only unreserved characters while preserving reserved-character distinctions. When code compares a path with a String, equality uses the exact encoded representation without silently normalizing it.

Relative references required their own careful boundary. A root-relative URL can now be expressed relative to another URL or path while preserving queries, fragments, and encoded segments. Current-directory, shared-final-segment, and colon-prefixed cases follow URI reference semantics. When a relative URL appears inside a mixed grammar such as an import map, to_s(explicit: true) adds ./ where the value would otherwise look like a package name or scheme.

Downstream consumers moved to the structured value instead of comparing decoded strings. Async::REST compares resource paths as Protocol::URL::Path instances, and Utopia carries an explicit normalized URL alongside the decoded application path. Controllers, localization, redirection, sessions, static files, and import maps can therefore choose the encoded or decoded representation appropriate to their layer. Content nodes now resolve local paths through the same lexical containment check used by static serving, preventing an application path from escaping its configured content root.

Import maps demonstrate why serialization context matters. Utopia rebases component URLs structurally, then serializes ambiguous relative addresses explicitly. A browser sees ./component.js as a URL rather than confusing component.js with a bare module specifier.

Rebuilding Utopia Around Protocol Interfaces

Utopia historically sat on Rack's environment and response-array interface. That was useful for compatibility, but it forced controllers, middleware, and content rendering to repeatedly translate between Rack-shaped hashes and the richer protocol objects used by Falcon and Async.

Utopia 3 replaces its Rack-centric application boundary with Utopia::Application, a Protocol::HTTP middleware which accepts request objects and returns response objects. Rack compatibility remains a separate server concern: falcon serve prefers a protocol-native config/serve.rb, but can still fall back to Rack's config.ru for existing applications. Generated project documentation uses the same protocol middleware interface, allowing the application and its in-process tests to share one request model.

The distinction becomes clearer in the shape of a minimal application:

utopia-application.rb
require "protocol/http/middleware"
require "protocol/http/response"
require "utopia/application"

# Rack passes an environment hash and receives a response array:
rack_application = lambda do |environment|
	[200, {"content-type" => "text/plain"}, [environment["PATH_INFO"]]]
end

# Utopia uses Protocol::HTTP request and response objects directly:
Application = Utopia::Application.build do
	run Protocol::HTTP::Middleware.for do |request|
		Protocol::HTTP::Response[
			200,
			{"content-type" => "text/plain"},
			[request.path],
		]
	end
end

That protocol boundary can also cross an execution context without flattening requests back into Rack. The new Protocol::HTTP::Executor reconstructs requests and responses on either side of a thread or Ractor, preserving metadata, interim responses, errors, protocol upgrades, and trailers. A full-duplex body channel streams request and response data independently with backpressure. Isolation therefore becomes a deployment choice around the same application interface rather than a second application API.

Middleware became explicit composition rather than a collection of special cases. Redirection rules are configured through one ordered middleware, while error-document rewriting remains a separate response-phase operation. Internal error documents bypass client-facing redirects by construction, and freezing middleware also freezes its owned configuration. Application and mailer exception handling now catch application script failures such as SyntaxError without swallowing process-level exceptions such as Interrupt.

Static and generated content gained clearer ownership too. Utopia replaced the full mime-types object graph with Protocol::Media::Registry's compiled perfect-hash index, reducing the measured incremental resident memory for Utopia's static lookups by about 3.5 MiB when the native registry is available. A new relative content namespace resolves logical page overrides independently from the physical template location. Exercising that contract more completely repaired nested ancestor fallback, restored localized link enumeration, and corrected unbalanced-markup diagnostics. The middleware also exposes its link resolver directly, leaving indexed lookup to the content nodes which need it.

Static defaults serve JavaScript modules and WebAssembly modules, and component installation can select only the package files required at runtime. Utopia then removed the legacy component-directory fallback so browser packages consistently come from node_modules. The selected-file support reduced Utopia Project's vendored Mermaid distribution from about 66 MiB to 3.8 MiB in the corresponding dependency update.

Protecting Application State at Its Edges

Encrypted session cookies need authenticity as well as confidentiality. Utopia's earlier AES-CBC payload hid the contents but did not authenticate the ciphertext or the context in which it was used. Sessions now use a versioned AES-256-GCM payload which authenticates the cookie name and format version. Existing or modified cookies are intentionally rejected, and encoded size limits apply to both incoming and outgoing payloads.

Expected invalid client cookies are reported at debug level without hiding unexpected implementation failures. The lazy session object now exposes an explicit persistence lifecycle instead of its mutable backing hash, and uses one controllable clock for loading, expiry, and persistence.

Diagnostics can also cross a trust boundary. Exception email previously risked including headers, query values, session state, controller variables, bodies, and large environment attachments. Sensitive fields are now redacted by default, request bodies are opt-in, and attachments are bounded. Static range responses use the structured Range parser to return 416 Range Not Satisfiable with the required representation size.

Optional tracing follows the same pattern. Content and static instrumentation moved into explicitly loaded trace providers, with stable operation-oriented names rather than middleware implementation names.

Utopia 3 is becoming a protocol-native application framework whose boundaries—request paths, content types, session state, static files, exceptions, and instrumentation—are explicit enough to test and secure independently.

Making Runtime Protocol State Observable

Connections and services have similar hidden state: a TLS policy may be tied to one transport, a peer may disappear during a write, a pooled resource may stop accepting new work while active streams remain, or an Envoy control plane may own only part of the configuration it serves.

Transport-Neutral TLS and Connection Failure

IO::Endpoint now represents certificate chains, trust stores, peer verification, and local identity independently from OpenSSL. An explicitly loaded adapter compiles that policy for today's TLS transport, while the representation can also support a future QUIC implementation. The endpoint integration uses OpenSSL 3.3's forwarding support and removes redundant compatibility methods, and Async::HTTP::Endpoint accepts the transport-neutral configuration without leaking it into TCP options.

HTTP/1 now maps connection-level EPIPE and ECONNRESET failures to Protocol::HTTP::RemoteError while retaining the socket exception as its cause. The server treats that typed failure as a normal remote disconnect without hiding unrelated application errors. For HTTP/2, a request assigned to a connection which closes before any bytes are written becomes a refused request, allowing even a rewindable non-idempotent request to be retried safely.

Pool state also needs to distinguish “cannot accept another stream” from “can be destroyed now.” Active multiplexed resources are removed from availability but retained until their final user releases them, and available resources use identity-based ordered membership so equal-looking connections cannot collapse into one entry. A related dependency-order problem appeared in Faraday proxy shutdown: tunnelled clients now close before the proxy clients whose active CONNECT requests they own.

Dispatching Requests to Ready Workers

Aggregate load metrics help a proxy choose among workers, but they remain observations of recent behavior. A worker can become busy between measurements, while another may become ready before its next report. Fantail introduces a worker-aware HTTP proxy with a global admission queue: registered backends advertise a processing slot only when they can accept another request, and the proxy acquires the next available slot instead of predicting capacity from a periodic sample.

Fantail separates request processing from the lifetime of the response exchange. A backend releases its processing slot as soon as response headers arrive, allowing it to begin another request while the earlier body is still streaming. The response remains counted until its body closes, so a configurable exchange limit prevents slow consumers from accumulating unbounded streams. Endpoint monitors publish complete state after connecting and incremental changes thereafter; removed or replaced backends stop receiving requests but drain existing responses before their clients close.

Separating gRPC Values and Control-Plane Ownership

Protocol::GRPC::Call exposes the client's original timeout, constructs runtime deadlines through one factory, and provides the resulting nullable response status. Timeout formatting moved to the typed header which owns its wire representation.

Routes have a parser and builder which enforce protobuf service and method identifiers, while application metadata has its own build and extraction interface. Async's dispatcher and client adopted those values in deadline construction and routing and metadata handling. The dispatcher also documents invoke_service as the supported boundary for middleware and instrumentation.

That separation allowed the old catch-all Methods compatibility namespace to be removed, along with a cancellation flag which never drove protocol behavior. Compression uses scoped one-shot zlib operations; the coverage work around that path also rejects unsupported encodings and truncated messages explicitly. In Async's streaming layer, blockless bidirectional calls close their request stream while preserving the returned response stream.

Envoy's xDS APIs have an ownership question of their own. The supervisor knows about live Falcon workers, but it should not need to claim the single Aggregated Discovery Service used by another control plane. Dedicated Cluster and Endpoint Discovery Services now publish only their implied resource types. The supervisor can serve CDS and EDS together or publish only live endpoint assignments, leaving cluster, listener, and route ownership elsewhere. Falcon's example now consumes those dedicated streams.

Health policy became composable rather than fixed. Generated clusters can carry explicit Envoy HTTP health checks, which the supervisor attaches to each worker cluster. Status tasks expose the clusters and flattened endpoints being published.

Load reporting has a similarly shared foundation. Processor utilization is normalized in core units where 1.0 means one fully occupied CPU core. Async::Utilization::SegmentStore owns reusable, growable shared-memory segments, and the supervisor consumes that reusable store instead of maintaining its own allocator. The control-plane integration exercises live ORCA utilization reporting through Envoy.

Making Documentation Part of the Interface

An explicit API is only useful if readers can find and follow it. Utopia Project's generated documentation moved from breadcrumb-only pages to section and page-relative navigation, then added a Pagefind index scoped to the main documentation content. Search assets resolve relative to their module so project sites work beneath a GitHub Pages repository prefix, and the cached Pagefind executable makes repeat builds skip the Rust and WebAssembly toolchain on cache hits.

Async::HTTP's guides were reorganized around decisions developers actually need to make: which interface owns the response and connection lifecycle, when to use the shared Internet interface, an explicit client, middleware, Async::REST, or Faraday, and how request concurrency differs from HTTP/1 and HTTP/2 pool limits.

The reference pages explain relationships rather than showing isolated definitions. Decode preserves absolute Ruby constants and included and extended mixin references. Utopia Project uses that information to link superclass definitions, list inherited methods and module relationships, summarize nested definitions, and show where inherited behavior comes from.

Inline code can carry language and semantic links all the way to the browser. The CommonMark binding preserves language prefixes on inline code, Markly exposes that info string through its Ruby API, and Utopia Project turns language-prefixed Ruby references into typed code before rendering them as linked HTML.

The JavaScript highlighter keeps source visible while languages load, exposes a completion state for layout consumers, and preserves authored links and other semantic markup during highlighting. Presently waits for that completion and checks whether highlighting succeeded before measuring code-focus regions. Link styling then uses the surrounding token color without hiding focus or hover state.

Looking Forward

Explicit boundaries are not an end in themselves. They let each layer make the decision for which it has enough information: a Buffer protects an allocation, a parser limits untrusted structure, an application selects accepted values, a URL preserves encoded components, and a control plane publishes only the resources it owns.