Hcs.Server

hcs · API reference

HTTP server runtime.

Use this module to bind sockets and serve a request handler. It supports HTTP/1.1, HTTP/2, TLS configuration, WebSocket upgrade handling, streaming responses, request/idle/read/write timeouts, and bounded request-body buffering.

Most applications call Server.run with a handler produced by Endpoint.to_handler. Lower-level applications can pass a handler directly.

Unified HTTP Server - High-performance server supporting HTTP/1.1, HTTP/2, and WebSocket.

This module consolidates all server functionality into a single, high-performance implementation. Protocol detection overhead is optional and controlled via the protocol configuration.

Protocol Modes

  • Http1_only: Fastest path. No protocol detection, direct HTTP/1.1 handling.
  • Http2_only: HTTP/2 only. h2c (cleartext) or h2 (over TLS).
  • Auto: Auto-detect protocol via connection preface peek (h2c) or ALPN (TLS).
  • Auto_websocket: Auto-detect with WebSocket upgrade support.

Performance Features

  • GC tuning for high-throughput scenarios
  • Cached Date headers (1-second resolution)
  • Pre-built responses for zero-allocation hot paths
  • Zero-copy bigstring responses
  • Streaming response support
  • Multi-domain parallelism via Eio

Example

  (* Fastest HTTP/1.1 server *)
  let handler req = Server.respond "Hello, World!" in
  Server.run ~sw ~net ~clock handler

  (* Multi-protocol with auto-detection *)
  let config = Server.{ default_config with protocol = Auto } in
  Server.run ~sw ~net ~clock ~config handler

Protocol Configuration

type protocol = 
  | Http1_only (* Fastest: No protocol detection, direct HTTP/1.1 handling. With TLS: HTTP/1.1 over TLS (no ALPN negotiation). *)
  | Http2_only (* HTTP/2 only. Without TLS: h2c (HTTP/2 cleartext). With TLS: h2 with ALPN advertising only "h2". *)
  | Auto (* Auto-detect protocol. Without TLS: Peek for "PRI " preface (h2c), fallback to HTTP/1.1. With TLS: ALPN negotiation. *)
  | Auto_websocket (* Auto-detect with WebSocket support. Same as Auto but also handles WebSocket upgrade requests in HTTP/1.1 mode. *)

Protocol mode controls detection overhead and supported protocols.

GC Tuning

module Gc_tune : sig ... end
module Prebuilt = Response.Prebuilt

Types

type config = {
  host : string; (* Bind address. Default: "0.0.0.0" *)
  port : int; (* Listen port. Default: 8080 *)
  backlog : int; (* Listen backlog. Default: 4096 *)
  max_connections : int; (* Max concurrent connections. Default: 100000 *)
  domain_count : int; (* Number of domains (CPUs) to use. Default: 1 *)
  protocol : protocol; (* Protocol mode. Default: Http1_only *)
  read_timeout : float; (* Read timeout in seconds. Default: 0.0, disabled. *)
  write_timeout : float; (* Write timeout in seconds. Default: 0.0, disabled. *)
  idle_timeout : float; (* Idle connection timeout in seconds. Default: 0.0, disabled. *)
  request_timeout : float; (* Request processing timeout in seconds. Default: 0.0, disabled. *)
  max_header_size : int; (* Max size in bytes of the request head (request line + headers). Passed to the codec parser as max_request_head_size (HTTP/1.1) / max_header_list_size (HTTP/2). Default: 32768. *)
  max_body_size : int64 option; (* Max body size. None = unlimited. Default: None *)
  request_body_buffer_limit : int option; (* Cap (in bytes) on how much of a request body is buffered in memory for whole-body access (Request.body). A body larger than this is streamed past without being retained: the handler still sees the exact Request.body_length, but Request.body reads empty. None = buffer the whole body (the default; preserves classic behavior). Set this to bound memory for endpoints that only count/stream large uploads. *)
  tcp_nodelay : bool; (* Set TCP_NODELAY on connections. Default: true *)
  reuse_addr : bool; (* Set SO_REUSEADDR on listener. Default: true *)
  reuse_port : bool; (* Set SO_REUSEPORT on listener. Default: true *)
  tls : Tls_config.Server.t option; (* TLS config. None = plain HTTP *)
  gc_tuning : Gc_tune.config option; (* GC tuning config. Some = apply tuning. Default: Some Gc_tune.default *)
}

Server configuration.

val default_config : config

Default configuration optimized for HTTP/1.1 performance.

val auto_config : config

Configuration for auto-detection mode.

val websocket_config : config

Configuration for WebSocket support.

Config Builders

val with_port : int -> config -> config
val with_host : string -> config -> config
val with_backlog : int -> config -> config
val with_max_connections : int -> config -> config
val with_domain_count : int -> config -> config
val with_protocol : protocol -> config -> config
val with_read_timeout : float -> config -> config
val with_write_timeout : float -> config -> config
val with_idle_timeout : float -> config -> config
val with_request_timeout : float -> config -> config
val with_max_header_size : int -> config -> config
val with_max_body_size : int64 -> config -> config
val with_request_body_buffer_limit : int -> config -> config
val with_tcp_nodelay : bool -> config -> config
val with_tls : Tls_config.Server.t -> config -> config
val with_gc_tuning : Gc_tune.config -> config -> config
val without_gc_tuning : config -> config

Request/Response Types

type protocol_version = 
  | HTTP_1_1
  | HTTP_2

Protocol version indicator.

exception Body_too_large
type header_src = 
  | Headers_http of Http_core.Headers.t

Raw request headers, kept in their protocol-native form so the assoc list is only materialized if a handler actually asks for it.

type body_reader = {
  read : unit -> string; (* Read the whole body as a string (memoized; reads on first call). *)
  read_stream : unit -> Cstruct.t option; (* Read the body in chunks; returns None at end of input. *)
  length : unit -> int; (* Body length in octets, without materializing it as a string. *)
  close : unit -> unit; (* Discard the body without reading it. *)
}

On-demand body access. The body is not read from the socket until one of these is called, so a handler that ignores it pays nothing.

type request = {
  meth : Method.t; (* HTTP method (parsed eagerly — cheap). *)
  target : string; (* Request target, path + query (eager). *)
  version : protocol_version; (* Protocol version (eager). *)
  header_src : header_src; (* Raw headers; parsed on demand via Request. *)
  overlay : (string * string) list; (* Headers injected by plugs; checked before header_src. Usually empty. *)
  body_reader : body_reader; (* On-demand body. *)
  assigns : Assigns.t; (* Type-safe application data attached by plugs. *)
}

Request type exposed to handlers. Only meth/target/version are parsed up front; headers, query params, and the body are read on demand through the Request accessors.

val empty_body_reader : body_reader

A body reader for requests with no body (GET/HEAD, or already drained).

val body_reader_of_string : string -> body_reader

Wrap an already-materialized body string as a body_reader. Used where the body was read eagerly (the H1 bodied path, the H2 path, or plugs that transform it). read_stream yields the whole body as a single chunk then None, so streaming consumers (e.g. multipart) still work.

val body_reader_of_bigstring : Http_core.Bigstring.t -> body_reader

Wrap an owned bigstring request body (the http codec delivers bodies as Http_core.Bigstring.t). read_stream yields a zero-copy Cstruct view; read materializes a string only on demand (lazily, once) — so a handler that only needs the length or reads it as a Cstruct never pays the copy.

val request_of : 
  ?meth:Method.t ->
  ?target:string ->
  ?version:protocol_version ->
  ?headers:(string * string) list ->
  ?body:string ->
  unit ->
  request

Build a synthetic request with materialized headers/body. For tests and callers that construct a request by hand (the server builds requests lazily from the wire, not via this).

type response = Response.t
type handler = request -> response
type ws_handler = Websocket.t -> unit
type ws_config = {
  origin_policy : Websocket.origin_policy;
  max_payload_size : int;
}
val default_ws_config : ws_config
val respond : 
  ?status:Status.t ->
  ?headers:(string * string) list ->
  string ->
  Response.t
val respond_bigstring : 
  ?status:Status.t ->
  ?headers:(string * string) list ->
  Bigstringaf.t ->
  Response.t
val respond_prebuilt : Response.Prebuilt.t -> Response.t
val respond_empty : 
  ?status:Status.t ->
  ?headers:(string * string) list ->
  unit ->
  Response.t
val respond_text : ?status:Status.t -> string -> Response.t
val respond_html : ?status:Status.t -> string -> Response.t
module Clen : sig ... end
val respond_json : ?status:Status.t -> string -> Response.t

Internal: Socket Helpers

val set_tcp_nodelay : 'a Eio.Resource.t -> unit
val shutdown_flow : 
  [> Eio.Flow.two_way_ty ] Eio.Flow.two_way ->
  Eio.Flow.shutdown_command ->
  unit

Internal: http codec ↔ hcs conversions

The server side runs on the http library codecs (Http_core/Http1/ Http2), which use a typed Http_core.Method.t / Http_core.Status.t / Http_core.Headers.t. The public hcs request/response keep the polymorphic-variant Method.t / Status.t (so routers, plugs and existing handlers are unchanged), so we translate at the boundary.

val meth_to_h1 : Http_core.Method.t -> Method.t
val status_to_core : Status.t -> Http_core.Status.t
val with_deadline : 
  [> float Eio.Time.clock_ty ] Eio.Time.clock ->
  float ->
  (unit -> 'a) ->
  'a

Internal: Protocol Detection

val h2_preface_prefix : string

HTTP/2 connection preface starts with "PRI "

val h2_preface_prefix_len : int
val peek_bytes : 
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  timeout:float ->
  [> Eio.Flow.source_ty ] Eio.Flow.source ->
  int ->
  (string, [> `Eof | `Exn of exn ]) result
val is_h2_preface : string -> bool
module Conn_buffers : sig ... end

Per-connection scratch, reused across connections. buf/cs are the read buffer: the http codecs read from a bigstring (Http_core.Bigstring.t), and Eio reads the socket into a bigarray-backed Cstruct over that buffer, so the socket→codec hop is zero-copy. wscratch is the write scratch: small Bytes iovecs (frame heads, Fixed string bodies) are blitted into it for the vectored write instead of mallocing a fresh Cstruct each. Lock-free Treiber stack via Kcas.

type timeouts = {
  read : float;
  write : float;
  idle : float;
  request : float;
}
val timeouts_of_config : config -> timeouts
val request_timeout_response : Response.t
val run_handler_timed : 
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  request_timeout:float ->
  ('a -> Response.t) ->
  'a ->
  Response.t
val write_iovecs : 
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  write_timeout:float ->
  [> Eio.Flow.sink_ty ] Eio.Flow.sink ->
  Conn_buffers.t ->
  Http_core.Iovec.t list ->
  int
module Driver : sig ... end

Single-loop driver for a sans-I/O http server connection over an Eio flow.

Internal: HTTP/1.1 Connection Handler

module H1_handler : sig ... end
module H2_handler : sig ... end

Internal: TLS Connection Handler

module Tls_handler : sig ... end

Internal: Connection Handler

val handle_connection : 
  sw:'a ->
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  config:config ->
  handler:(request -> Response.t) ->
  ws_handler:(Websocket.t -> unit) ->
  [> Eio.Flow.two_way_ty ] Eio.Flow.sink ->
  unit

Public API

val resolve_bind_addr : 
  net:[> [> `Generic ] Eio.Net.ty ] Eio.Net.t ->
  config ->
  [> `Tcp of string * int ]
val run : 
  sw:Eio.Switch.t ->
  net:[> [> `Generic ] Eio.Net.ty ] Eio.Net.t ->
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  ?config:config ->
  ?ws_handler:(Websocket.t -> unit) ->
  (request -> Response.t) ->
  'a

Run an HTTP server.

parameter sw Switch for resource management parameter net Eio network capability parameter clock Eio (wall) clock, used to enforce the connection timeouts parameter config Server configuration (default: default_config) parameter ws_handler WebSocket handler (required for Auto_websocket mode) parameter handler Request handler

val run_parallel : 
  sw:Eio.Switch.t ->
  net:[> [> `Generic ] Eio.Net.ty ] Eio.Net.t ->
  domain_mgr:[> Eio.Domain_manager.ty ] Eio.Domain_manager.t ->
  clock:[> float Eio.Time.clock_ty ] Eio.Time.clock ->
  ?config:config ->
  ?ws_handler:(Websocket.t -> unit) ->
  (request -> Response.t) ->
  'a

Run an HTTP server with multi-domain parallelism.

parameter sw Switch for resource management parameter net Eio network capability parameter domain_mgr Eio domain manager parameter clock Eio (wall) clock, used to enforce the connection timeouts parameter config Server configuration (default: default_config) parameter ws_handler WebSocket handler (required for Auto_websocket mode) parameter handler Request handler