Server

Server :: # (opaque)

Configure and run an inbound HTTP server.

Server requests deliberately use a different type from outbound http.Request: their bodies are request-scoped streams instead of complete byte lists.

init! = || Ok({
    config: Server.default_config.with_listen({ host: "127.0.0.1", port: 8080 }),
    context: {},
})

respond! = |request, {}| {
    body = request.body().with_limit(64 * 1024).read_all!()?
    response = Response.from_status(200).with_body(body)
    Ok(Server.respond(response))
}

shutdown! = |_, {}| Ok({})

An unhandled semantic error returned by respond! is inspected, logged to stderr with request context, and converted to a 500 response. Unhandled init! and shutdown! errors are logged and produce process exit code 1.

default_request_header_limit_bytes : U32

Default maximum decoded request-header list size: 32 KiB.

Each ordinary field costs its decoded name bytes, value bytes, and the 32-byte per-field overhead defined for HTTP/2 header-list accounting. HTTP/1 uses the same accounting so both protocols expose one contract.

default_request_header_limit_fields : U16

Default maximum number of ordinary request-header fields.

Repeated fields each count once. HTTP/2 pseudo-fields are represented by the request method and target and do not consume this field-count budget.

default_max_file_transfers : U16

Default maximum number of host-managed file responses that may be active concurrently. File transfers do not consume Roc handler capacity.

default_file_chunk_bytes : U32

Default chunk size used while streaming files: 64 KiB. A transfer owns at most one queued chunk and one active read buffer, so memory use does not scale with file size.

no_store : CachePolicy

Do not allow caches to store the response.

revalidate : CachePolicy

Allow storage but require revalidation before reuse. This is the conservative default for declared roots.

private_for : U32 -> CachePolicy

Permit private caches to reuse a response for the given number of seconds.

public_for : U32 -> CachePolicy

Permit shared and private caches to reuse a response for the given number of seconds.

file_root : { id : Str, path : Path } -> FileRoot

Declare a root with the conservative revalidation cache policy. Root identifiers contain 1-64 ASCII letters, digits, '-' or '_'; the complete configuration is validated before listening.

writable_root : { id : Str, path : Path } -> WritableRoot

Declare one writable root. Identifiers use the same 1-64 character syntax as read-only file roots, but live in an independent authority registry.

relative_file : Str -> Try(RelativeFile, [InvalidRelativeFile])

Construct a safe relative child path of at most 4 KiB. Empty components, dot components, dotfiles, separators other than '/', NULs, and Windows drive syntax are rejected so the value has one cross-platform meaning.

inherit_cache : CacheChoice

Inherit the cache policy declared by a file root.

override_cache : CachePolicy -> CacheChoice

Override a root cache policy for one route or response plan.

static_mount : { at : Str, files : FileRoot } -> FileRoute

Declare a public static mount that inherits its root cache policy. Route paths are ASCII absolute URI paths of at most 4 KiB and are validated at startup.

static_mount_with_cache : { at : Str, files : FileRoot, cache : CachePolicy } -> FileRoute

Declare a public static mount with an explicit cache policy.

static_file : { at : Str, files : FileRoot, relative : RelativeFile } -> FileRoute

Declare one exact public file route that inherits its root cache policy.

static_file_with_cache : { at : Str, files : FileRoot, relative : RelativeFile, cache : CachePolicy } -> FileRoute

Declare one exact public file route with an explicit cache policy.

liveness_route : Str -> LivenessRoute

Declare a native liveness route. Paths are validated with all other native routes atomically before the listener is bound.

readiness_route : { at : Str, readiness : Readiness } -> ReadinessRoute

Declare a native readiness route backed by the supplied gate.

no_log_target : LogTarget

Do not include a request target in access logs.

path_without_query : LogTarget

Include the bounded parsed path, never its query string, in access logs.

no_access_log : AccessLog

Disable access logging. This is the default.

json_lines_access_log : { target : LogTarget, max_buffered_events : U16 } -> AccessLog

Write bounded structured request-completion events as JSON Lines to standard error. The buffer capacity must be non-zero.

no_metrics : Metrics

Disable the native metrics exporter. This is the default.

open_metrics : { at : Str } -> Metrics

Expose fixed-cardinality host metrics on one native exact path.

The path must satisfy the same startup validation as native file routes and must not overlap one. GET and HEAD are supported; the response uses the OpenMetrics content type and Cache-Control: no-store.

default_config : Config

Safe defaults: loopback-only; finite connection, handler, handler queue, request-target, request-header, and request-body limits; one buffered 64 KiB body chunk; and bounded graceful shutdown.

Request metadata limits are exact and checked before native route selection or Roc. Target overflow returns 414 and header byte/count overflow returns 431 whenever the protocol parser can safely construct a response. HTTP/1 closes after a parser-level overflow. An initial HTTP/2 request beyond the advertised hard decoding envelope receives a header-only 431; an overflow that cannot safely receive a response resets only the affected stream. Zero values, target limits above 65,534 bytes, header limits above 1 MiB, and field limits above 1,024 fail startup before the listener is bound.

Exceeding the drain deadline forces process exit without running the shutdown hook, because a request handler may still be using the application context.

with_listen : Config, { host : Str, port : U16 } -> Config

Set the listener host and port. The default is loopback-only on port 8000.

with_limits : Config, { max_connections : U32, max_handlers : U16, max_queued_handlers : U16 } -> Config

Set connection, active-handler, and queued-handler capacity together. Saturated handler queues receive 503 responses.

with_timeouts : Config, { header_ms : U64, body_idle_ms : U64, keep_alive_idle_ms : U64, handler_queue_ms : U64, response_idle_ms : U64 } -> Config

Set the complete inbound and outbound transport timeout policy. Every value is milliseconds in the inclusive range 1 through 86_400_000; zero is invalid and startup fails before listening.

header_ms is an idle deadline while completing a request head and resets whenever head bytes arrive. body_idle_ms begins when the host waits for the next body frame and resets only after non-empty body data arrives. keep_alive_idle_ms begins after a response has completed and bounds the gap before the next request. handler_queue_ms begins after a request takes a queue slot and ends before Roc execution begins. response_idle_ms begins when response transmission starts and resets whenever the socket or HTTP/2 stream flow-control window makes progress.

with_request_metadata_limits : Config, { max_target_bytes : U32, max_header_bytes : U32, max_header_fields : U16 } -> Config

Set the exact, finite request-target and decoded request-header budgets. Invalid values fail startup atomically before the listener is bound.

with_graceful_shutdown : Config, { drain_timeout_ms : U64, hook_timeout_ms : U64 } -> Config

Set the request-drain deadline and final shutdown-hook deadline.

with_file_roots : Config, List(FileRoot) -> Config

Replace the complete set of startup-declared file roots.

with_writable_roots : Config, List(WritableRoot) -> Config

Replace the complete set of startup-declared writable roots. Writable authority is never inferred from read-only file roots.

with_native_routes : Config, NativeRoutes -> Config

Replace the complete immutable host-native route table.

with_file_transfer_limits : Config, { max_concurrent : U16, chunk_bytes : U32 } -> Config

Set the active-transfer bound and streaming chunk size for host-managed file responses. Saturation returns 503 without queueing. Each transfer owns at most one queued chunk and one active read buffer.

with_body_sink_limits : Config, { max_concurrent : U16, timeout_ms : U64 } -> Config

Set the active staging-file bound and whole-operation deadline for host-managed request-body sinks. Saturation is typed and does not queue.

with_access_log : Config, AccessLog -> Config

Configure host-owned structured request-completion logging.

with_metrics : Config, Metrics -> Config

Configure the host-owned fixed-cardinality metrics exporter.

respond : Response -> Outcome

Return a response and keep serving requests.

file_response : { files : FileRoot, relative : RelativeFile } -> Outcome

Ask the host to stream one authorized file inline, inheriting the root's cache policy. The plan can only name a startup-declared root and a validated relative child path.

file_response_with : {
    files : FileRoot,
    relative : RelativeFile,
    disposition : [Inline, Attachment(Str)],
    cache : CacheChoice,
} -> Outcome

Ask the host to stream one authorized file with explicit disposition/cache options.

inline : [Inline, Attachment(Str)]

Render a host-managed file inline.

attachment : Str -> [Inline, Attachment(Str)]

Render a host-managed file as an attachment. The host safely encodes the supplied filename and prevents response-header injection.

stop_after : Response -> Outcome

Return a final response, then begin graceful shutdown with exit code 0.

stop_after_with_code : Response, I64 -> Outcome

Return a final response, then begin graceful shutdown with the given exit code.

CachePolicy

:= [NoStore, Revalidate, PrivateFor(U32), PublicFor(U32)]

A small, typed cache policy for host-managed file responses.

to_host : CachePolicy -> { tag : U8, max_age_seconds : U32 }

Platform ABI conversion hook; not an application API.

FileRoot

:= [
    FileRoot({ id : Str, path : Path, cache : CachePolicy }),
]

An immutable descriptor for one startup-declared filesystem root. Its identifier is the only value sent in a response plan; the host rejects plans whose identifier was not activated by the returned Config.

to_host : FileRoot -> {
    id : Str,
    path_tag : U8,
    path_utf8 : Str,
    path_unix_bytes : List(U8),
    path_windows_u16s : List(U16),
    cache_tag : U8,
    cache_max_age_seconds : U32,
}

Platform ABI conversion hook; not an application API.

WritableRoot

:= [
    WritableRoot({ id : Str, path : Path }),
]

A startup-declared writable filesystem authority. This is deliberately a different type from FileRoot: permission to serve files never grants permission to create them.

to_host : WritableRoot -> {
    id : Str,
    path_tag : U8,
    path_utf8 : Str,
    path_unix_bytes : List(U8),
    path_windows_u16s : List(U16),
}

Platform ABI conversion hook; not an application API.

RelativeFile

:= [RelativeFile(Str)]

A validated relative child path used by exact routes and authorized file plans. The host repeats this validation at the ABI boundary.

to_host : RelativeFile -> Str

Platform ABI conversion hook; not an application API.

CacheChoice

:= [Inherit, Override(CachePolicy)]

Override a root's cache policy for one native route or response plan, or inherit the root policy.

to_host : CacheChoice -> { override : Bool, tag : U8, max_age_seconds : U32 }

Platform ABI conversion hook; not an application API.

FileRoute

:= [
    FileRoute(
        {
            at : Str,
            files : FileRoot,
            kind : U8,
            relative : Str,
            cache : CacheChoice,
        },
    ),
]

One startup-declared host-native file route. Static mounts own an exact prefix on segment boundaries. Static files own one exact URI path.

to_host : FileRoute -> {
    at : Str,
    root_id : Str,
    kind : U8,
    relative : Str,
    cache_override : Bool,
    cache_tag : U8,
    cache_max_age_seconds : U32,
}

Platform ABI conversion hook; not an application API.

ReadinessState : [NotReady, Ready]

The complete readiness state. There are deliberately no names, reasons, dependency callbacks, or intermediate states.

Readiness

:= { host : Readiness }

A bounded host-owned readiness gate. It is safe to retain in immutable context and update from concurrent handlers. Final Roc ARC release closes the capability; graceful drain permanently changes it to NotReady before shutdown! runs.

create! : ReadinessState => Try(Readiness, [ReadinessCapacityExhausted])

Create one readiness gate with an explicit initial state. The host has finite capacity and reports exhaustion instead of growing a registry.

set! : Readiness, ReadinessState => Try({  }, [InvalidReadiness, StaleReadiness, ServerStopping])

Atomically replace the readiness state. Once graceful drain begins, every update returns ServerStopping and the state remains NotReady.

to_inspect : Readiness -> Str

Render without exposing the host lifecycle token.

to_host : Readiness -> Readiness

Platform ABI conversion hook; not an application API.

LivenessRoute

:= [LivenessRoute(Str)]

One native exact route whose response proves only that the listener and HTTP machinery can serve it. init! and complete route validation finish before the listener is bound, so this route also serves as a deployment startup probe once it becomes reachable; there is no separate mutable startup state.

to_host : LivenessRoute -> Str

Platform ABI conversion hook; not an application API.

ReadinessRoute

:= [ReadinessRoute({ at : Str, readiness : Readiness })]

One native exact route backed by a typed readiness gate.

to_host : ReadinessRoute -> { at : Str, readiness : Readiness }

Platform ABI conversion hook; not an application API.

NativeRoutes : {
    files : List(FileRoute),
    liveness : List(LivenessRoute),
    readiness : List(ReadinessRoute),
}

The immutable startup route topology. Exact route duplicates are rejected; exact routes take precedence over more general file prefixes.

LogTarget

:= [NoTarget, PathWithoutQuery]

Privacy policy for the request target in structured access logs.

Query strings are never logged. PathWithoutQuery records only the parsed URI path, truncated by the host to a finite byte limit.

to_host : LogTarget -> U8

Platform ABI conversion hook; not an application API.

AccessLog

:= [
    AccessLogOff,
    JsonLines({ target : LogTarget, max_buffered_events : U16 }),
]

Host-owned access logging configuration.

JSON Lines are written to standard error by a dedicated host thread. Request transport never waits for that thread: terminal events enter a finite queue, and overflow is counted by the metrics exporter. Shutdown gives the queue one second to drain; a blocked standard-error sink is detached so it cannot defeat the server's finite shutdown deadlines.

to_host : AccessLog -> { enabled : Bool, target : U8, buffer_events : U16 }

Platform ABI conversion hook; not an application API.

Metrics

:= [MetricsOff, OpenMetrics({ at : Str })]

Host-owned fixed-cardinality metrics export configuration.

to_host : Metrics -> { enabled : Bool, path : Str }

Platform ABI conversion hook; not an application API.

Config

:= [
    Config(
        {
            listen : { host : Str, port : U16 },
            limits : {
                max_connections : U32,
                max_handlers : U16,
                max_queued_handlers : U16,
            },
            request_bodies : {
                max_bytes : U64,
                chunk_bytes : U32,
                buffered_chunks : U16,
            },
            timeouts : {
                header_ms : U64,
                body_idle_ms : U64,
                keep_alive_idle_ms : U64,
                handler_queue_ms : U64,
                response_idle_ms : U64,
            },
            request_metadata : {
                max_target_bytes : U32,
                max_header_bytes : U32,
                max_header_fields : U16,
            },
            graceful_shutdown : {
                drain_timeout_ms : U64,
                hook_timeout_ms : U64,
            },
            file_roots : List(FileRoot),
            writable_roots : List(WritableRoot),
            native_routes : NativeRoutes,
            file_transfers : {
                max_concurrent : U16,
                chunk_bytes : U32,
            },
            body_sinks : {
                max_concurrent : U16,
                timeout_ms : U64,
            },
            operations : {
                access_log : AccessLog,
                metrics : Metrics,
            },
        },
    ),
]

Opaque runtime configuration returned from the application's init! function. Use the builders below so future server settings can be added without invalidating application record construction.

to_host : Config -> {
    host : Str,
    port : U16,
    body_max_bytes : U64,
    body_chunk_bytes : U32,
    body_buffered_chunks : U16,
    header_timeout_ms : U64,
    body_idle_timeout_ms : U64,
    keep_alive_idle_timeout_ms : U64,
    handler_queue_timeout_ms : U64,
    response_idle_timeout_ms : U64,
    request_target_max_bytes : U32,
    request_header_max_bytes : U32,
    request_header_max_fields : U16,
    drain_timeout_ms : U64,
    hook_timeout_ms : U64,
    max_connections : U32,
    max_handlers : U16,
    max_queued_handlers : U16,
    file_roots : List(
        {
            id : Str,
            path_tag : U8,
            path_utf8 : Str,
            path_unix_bytes : List(U8),
            path_windows_u16s : List(U16),
            cache_tag : U8,
            cache_max_age_seconds : U32,
        },
    ),
    writable_roots : List(
        {
            id : Str,
            path_tag : U8,
            path_utf8 : Str,
            path_unix_bytes : List(U8),
            path_windows_u16s : List(U16),
        },
    ),
    native_file_routes : List(
        {
            at : Str,
            root_id : Str,
            kind : U8,
            relative : Str,
            cache_override : Bool,
            cache_tag : U8,
            cache_max_age_seconds : U32,
        },
    ),
    liveness_routes : List(Str),
    readiness_routes : List({ at : Str, readiness : Readiness }),
    file_max_concurrent : U16,
    file_chunk_bytes : U32,
    body_sink_max_concurrent : U16,
    body_sink_timeout_ms : U64,
    access_log_enabled : Bool,
    access_log_target : U8,
    access_log_buffer_events : U16,
    metrics_enabled : Bool,
    metrics_path : Str,
}

Platform ABI conversion hook; not an application API. Applications should use default_config and the with_* builders.

Body

:= [
    Body(
        {
            host : RequestBody,
            limit_bytes : U64,
            content_length : [Unknown, Known(U64)],
        },
    ),
]

A request-scoped inbound body. The host expires this capability when the request handler returns, and permits only one active reader at a time.

to_inspect : Body -> Str

Render a body without exposing its request-scoped host identifier.

from_host : RequestBody, U64, [Unknown, Known(U64)] -> Body

Platform ABI conversion hook; not an application API.

limit : Body -> U64

The maximum number of bytes this request body may deliver.

content_length : Body -> [Unknown, Known(U64)]

The declared Content-Length, when the request supplied one. The stream still enforces its byte limit independently of this untrusted value.

with_limit : Body, U64 -> Body

Return the same request stream with a stricter total byte limit. Limits may only be narrowed, never widened beyond the server configuration.

read! : Body => Try(Read, [RequestBodyErr(Err)])

Read the next bounded chunk. End is stable and may be observed more than once. Concurrent reads of one body return ConcurrentRead.

fold_chunks! : Body, state, (state, List(U8) => Try(state, err)) => Try(state, [ChunkReadErr({ err : Err, state : state }), ChunkStepErr(err)])

Read every remaining chunk sequentially and thread request-local state through an effectful step function. The first step error stops reading; returning from the handler then cancels any unread request bytes.

read_all! : Body => Try(List(U8), [RequestBodyErr(Err)])

Read all remaining bytes while enforcing this body's current limit. Prefer read! for large or incrementally processed payloads.

write_file! : Body, {
    root : WritableRoot,
    relative : RelativeFile,
    digest : Digest,
} => Try(WriteFileSuccess, [BodySinkErr(WriteFileErr)])

Stream all remaining body bytes into a securely created staging file, then publish it atomically at a validated child path. Parent directories must already exist. Publication is always CreateNew and never overwrites an existing destination. Atomic publication describes name visibility, not crash durability or fsync.

Authority

:= [Authority({ host : Str, port : [Absent, Present(U16)] })]

A validated HTTP authority. host is an ASCII URI host: registered names and IPv4 addresses are unbracketed, while IPv6 and IPvFuture literals keep their brackets. port is parsed as a U16, so applications never need to split an IPv6 authority on :.

Authority values remain untrusted client input. They are not a canonical public origin and do not interpret Forwarded or X-Forwarded-*.

host : Authority -> Str

Return the validated URI host.

port : Authority -> [Absent, Present(U16)]

Return the explicit port, if the client supplied one.

from_host : Str, Bool, U16 -> Authority

Platform ABI conversion hook; not an application API.

Target

:= [
    Resource(
        {
            raw_path : Str,
            raw_query : [Absent, Present(Str)],
        },
    ),
    Authority(Authority),
    Asterisk,
]

A protocol-neutral parsed request target.

Resource paths and queries remain percent-encoded. Absent query differs from Present(""), preserving the distinction between /path and /path?. Origin-form and absolute-form requests produce the same Resource shape. CONNECT authority-form and OPTIONS * remain distinct, so neither can accidentally enter ordinary resource routing.

Request

:= {
    method : Method,
    headers : List(Header),
    target : Target,
    authority : [Absent, Present(Authority)],
    body : Body,
}

An inbound server request. Its body is always streaming; use Body.read_all! only when a bounded complete body is appropriate.

method : Request -> Method

Return the request method.

headers : Request -> List(Header)

Return the request headers in received order.

target : Request -> Target

Return the validated, protocol-neutral request target.

authority : Request -> [Absent, Present(Authority)]

Return the effective request authority, when present.

HTTP/1.1 requires exactly one valid Host field. Absolute-form and CONNECT authority-form take precedence over that field. HTTP/2 uses :authority, falls back to Host when absent, and rejects disagreement when both are present. A present but empty Host field represents Absent, as required when the target authority is undefined.

body : Request -> Body

Return the request-scoped streaming body capability.

from_host : Method, List(Header), Target, [Absent, Present(Authority)], Body -> Request

Platform ABI conversion hook; not an application API.

Outcome

:= [
    Respond(Response),
    ServeFile(
        {
            files : FileRoot,
            relative : RelativeFile,
            disposition : [Inline, Attachment(Str)],
            cache : CacheChoice,
        },
    ),
    StopAfter({ response : Response, exit_code : I64 }),
]

A successful request outcome. Ordinary responses have one host-owned framing contract across HTTP/1.1 and HTTP/2. Header names and values must be valid; connection-specific fields and transfer coding are rejected. Content-Length is optional, but when present every value must agree with the complete returned body before host content coding. The host emits one canonical length for the bytes it will transmit.

HEAD transmits no body but reports the returned representation's length. 204 and 304 require an empty body and no Content-Length; 205 also requires an empty body. Informational responses and successful CONNECT responses are unsupported. An invalid response is logged and replaced with a bounded 500 before any part of it is transmitted.

StopAfter applies the same validation, sends the resulting response while beginning graceful shutdown, and preserves the first shutdown cause.

to_host : Outcome -> {
    kind : U8,
    response : Response,
    stop : Bool,
    exit_code : I64,
    file_root_id : Str,
    file_relative : Str,
    file_disposition : U8,
    file_download_name : Str,
    file_cache_override : Bool,
    file_cache_tag : U8,
    file_cache_max_age_seconds : U32,
}

Platform ABI conversion hook; not an application API.

ShutdownReason : [
    ApplicationRequested,
    Interrupt,
    Terminate,
    StartupFailed(Str),
    RuntimeFailed(Str),
]

Why the server is invoking the application's final shutdown hook.