Architecture

Via is a concurrent streaming I/O proxy.

Each accepted client connection runs in an independent Crystal fiber. Socket waits yield to the Crystal runtime scheduler, and Via does not put requests behind a global lock. Independent client connections can therefore make progress concurrently.

Code boundaries

The implementation is split into bounded modules:

Via::Configuration  YAML model, validation, loading, watching, reload
Via::Routing        immutable route targets and route selection
Via::HTTP           request IDs, error pages, forwarding header policy
Via::Proxy          upstream transport and connection pooling
Via::Runtime        atomic generations and diagnostic state
Via::Static         filesystem containment and file serving
Via::TLS            contexts and reloadable TLS listener

The source tree mirrors these namespaces under src/via/. Via::Server and Via::CLI remain at the root because they compose modules into listener and process lifecycles.

This keeps configuration, routing, transport, and lifecycle decisions independent and directly testable.

Request flow

flowchart TD
    Request["HTTP request"] --> Router
    Router --> Route
    Route --> ProxyTarget["Proxy target"]
    Route --> StaticTarget["Static target"]
    ProxyTarget --> Pool["Upstream connection pool"]
    Pool --> Upstream["HTTP upstream"]
    StaticTarget --> Filesystem["Validated filesystem root"]

The router is independent of the HTTP server and client transports. It only selects a validated route from a hostname and path.

Each active upstream request owns its HTTP::Client; clients are never shared concurrently between fibers. Successfully completed clients return to a bounded idle pool and can reuse their keep-alive connections.

Configuration generations

A validated configuration creates an immutable proxy generation containing its router and upstream connection pools. Hot reload builds the replacement before acquiring the runtime state lock, then swaps one generation reference.

The previous generation is retired rather than closed immediately. It tracks active requests and closes its idle upstream clients once its last request finishes. This keeps reload atomic without interrupting in-flight streams.

For TLS listeners, each accepted TCP connection snapshots the current OpenSSL::SSL::Context::Server before its handshake. Configuration reload creates and validates a replacement context before publishing it with the new proxy generation. Existing TLS connections continue normally; new connections receive the replacement certificate.

Static routes use the same immutable routing generation as proxy routes. Validated static roots are canonical paths. Each request is decoded and resolved again so traversal attempts and symlinks escaping the root cannot bypass configuration-time validation.

Streaming and backpressure

Via passes the incoming request body directly to the upstream HTTP client. It copies the upstream response through a fixed-size buffer instead of loading the entire body into memory.

When either side becomes slower, socket writes suspend the current fiber and backpressure propagates through the stream. Body size therefore does not determine Via's buffering requirement.

Hop-by-hop headers are removed at each proxy boundary. End-to-end request and response headers, methods, paths, queries, and response statuses are forwarded.