kex docs Standard Library

Net.HTTP

module Net.HTTP

Buffered HTTP clients, responses, and a small declaration-ordered server router. Requests never follow redirects or perform generic retries implicitly.

using Net.HTTP

let response = HTTP.get("https://example.test/").try
response.status.success?   # => true

For connection reuse and statistics, own a client explicitly:

let client = Client.open.try
let response = client.get("https://example.test/").try
client.close.try

record Headers

An insertion-ordered HTTP field collection. Names compare case-insensitively and duplicate fields are preserved.

A list of pairs rather than a {String: String} map, and deliberately so. A map cannot hold the same name twice, and Set-Cookie needs exactly that: RFC 6265 does not define it as a comma-separated list, so two cookies must travel as two fields and cannot be joined into one. A map interface would read as the obvious one right up to the first response that sets two cookies, then silently keep one — the same class of quiet data loss this module's Result-returning builders exist to avoid.

Order is kept for the same reason. RFC 9110 makes order insignificant BETWEEN different names but significant between fields sharing a name, and a map has no order to keep.

Fields

entries
[(String, String)]

Implements Showable, Inspectable.

Methods

add

add(name: String, value: String) -> Result<Headers, NetError>

Appends a field, keeping existing fields of the same name.

Repeats are how Set-Cookie works: it is not a comma-separated list, so two cookies must be two fields. For replace-semantics, remove first.

An invalid name or value is an Error, not a silent drop. Rejecting "a\r\nX: y" is what stops response splitting, but dropping it quietly left the caller holding a valid Headers that simply lacked the field it asked for, and a response with no Content-Type invites MIME sniffing. from and parse already answer with a Result for this same input.

Returns the extended fields, or Parse

Examples

Two cookies on one response

Headers.empty
  .add("Set-Cookie", "session=abc; HttpOnly").try
  .add("Set-Cookie", "theme=dark").try

Replacing a field

headers.remove("Content-Type").add("Content-Type", "application/json").try

remove

remove(name: String) -> Headers

Removes every field matching name case-insensitively.

Examples

Stripping hop-by-hop state before forwarding

let forwarded = incoming.remove("Connection")

get

get(name: String) -> String?

Returns the first matching field value.

Examples

Selecting a response decoder

let contentType = response.headers.get("Content-Type").or("application/octet-stream")

getAll

getAll(name: String) -> [String]

Returns every matching value in insertion order.

Examples

Preserving every Set-Cookie field

let cookies = response.headers.getAll("Set-Cookie")

showValue Showable

showValue : String

Renders fields while replacing authorization and cookie values with ***.

inspectValue Inspectable

inspectValue(colors: Bool) -> String

Structural inspection uses the same credential-safe rendering.

From Showable

record Status

A validated HTTP status code in 100..599.

Fields

Methods

informational?

informational? : Bool

Returns whether the status is in 100..199

success?

success? : Bool

Returns whether the status is in 200..299

redirect?

redirect? : Bool

Returns whether the status is in 300..399

clientError?

clientError? : Bool

Returns whether the status is in 400..499

serverError?

serverError? : Bool

Returns whether the status is in 500..599

record Response<B>

A typed HTTP response envelope whose body representation is explicit.

Fields

status
Status
headers
Headers
body
B

record Request<B>

A typed HTTP request envelope whose body representation is explicit.

Fields

method
String
target
URI
headers
Headers
body
B

record RouteContext

Route captures decoded after path segmentation.

Fields

parameters
Map<String, String> optional

Methods

parameter

parameter(name: String) -> Result<String, NetError>

Returns one decoded named or wildcard route capture.

Parameters
name
the capture name declared in the route

Returns the capture, or Parse when absent

Examples

Reading +:id+ from a +/users/:id+ route

let id = context.parameter("id").try

record Context

Per-request server context.

Fields

type Handler

A buffered HTTP route handler. Handling a request is effectful: routes may read files, query a database, send messages, or perform other application IO.

record Route

One declared route; routers preserve declaration order.

Fields

method
String
path
String
handler
Handler

record Router

An immutable, declaration-ordered HTTP router.

Fields

routes
[Route] optional

Methods

route

route(method: String, path: String, handler: Handler) -> Router

Appends a route; earlier matching declarations win.

Use this for a method without a convenience function. Paths may include named or wildcard captures, which the handler reads from RouteContext.

Examples

Adding a custom method

router.route("PURGE", "/cache/:key", ~purge)

get

get(path: String, handler: Handler) -> Router

Appends a GET route. GET also supplies automatic HEAD fallback.

head

head(path: String, handler: Handler) -> Router

Appends an explicit HEAD route, overriding automatic GET fallback.

options

options(path: String, handler: Handler) -> Router

Appends an explicit OPTIONS route, overriding generated OPTIONS.

post

post(path: String, handler: Handler) -> Router

Appends a POST route.

put

put(path: String, handler: Handler) -> Router

Appends a PUT route.

patch

patch(path: String, handler: Handler) -> Router

Appends a PATCH route.

delete

delete(path: String, handler: Handler) -> Router

Appends a DELETE route.

record ShutdownReport

Counts and elapsed time from graceful server shutdown.

Fields

completed
Integer
failed
Integer
forced
Integer
elapsedMilliseconds
Integer

record ServerOptions

Bounded HTTP server resources and default graceful-shutdown duration.

Fields

maximumHandlers
Integer optional
backlog
Integer optional
gracefulShutdown
Duration optional

record PoolOptions

HTTP connection-pool bounds and idle lifetime.

Fields

perOrigin
Integer optional
total
Integer optional
queuedRequests
Integer optional
idleExpiryMilliseconds
Integer optional

record ClientOptions

Options owned by an explicit HTTP client.

Fields

pool
PoolOptions optional

record ClientStatistics

Lifetime request/reuse counters plus current pooled connections.

Fields

openConnections
Integer
requests
Integer
reusedConnections
Integer

record ClientCloseReport

Resources released by Client.close.

Fields

closedConnections
Integer

type Client

The pooled HTTP client. An opaque handle over the connection pool that owns it; Client.open makes one and client.close releases it.

Methods

request

request(method: String, url: String, headers: Headers, body: Binary) -> Result<Response<Binary>, NetError>

Sends a buffered request. Redirects and generic retries are not implicit.

This keeps policy with the caller: inspect a redirect before following it, and retry only methods and failures your application knows are safe.

Examples

Sending JSON with an idempotency key

let headers = Headers.empty
  .add("Content-Type", "application/json").try
  .add("Idempotency-Key", requestId).try
client.request("POST", url, headers, JSON.stringify(order).to(Binary).try)

get

get(url: String) -> Result<Response<Binary>, NetError>

Sends a buffered GET request.

post

post(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends a buffered binary POST request.

put

put(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends a buffered binary PUT request.

patch

patch(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends a buffered binary PATCH request.

delete

delete(url: String) -> Result<Response<Binary>, NetError>

Sends a DELETE request with an empty body.

head

head(url: String) -> Result<Response<Binary>, NetError>

Sends a HEAD request; the returned body is empty.

options

options(url: String) -> Result<Response<Binary>, NetError>

Sends an OPTIONS request.

statistics

statistics : ClientStatistics

Reports current pool occupancy and lifetime request counters.

Returns current pool and lifetime request counters

Examples

Emitting client-pool diagnostics

let stats = client.statistics
IO.printLine("HTTP reuse: ${stats.reusedConnections}/${stats.requests}")

close

close : Result<ClientCloseReport, NetError>

Idempotently closes the client and every idle pooled connection.

Further requests fail with Closed; a second close is harmless.

module Net.HTTP.Headers

Construction and parsing of validated HTTP header collections.

empty constant

empty : Headers

Returns a field collection with no entries.

Examples

Building request headers immutably

Headers.empty
  .add("Accept", "application/json").try
  .add("User-Agent", "inventory-sync/1.0").try

from

from(entries: [(String, String)]) -> Result<Headers, NetError>

Validates header names and values without folding duplicates.

Duplicate fields stay in their original order. Invalid names and values containing line breaks are rejected instead of creating a malformed or injectable HTTP message.

Returns validated fields, or Parse

Examples

Forwarding an explicitly selected set of request headers

Headers.from([
  ("Accept", "application/json"),
  ("X-Request-ID", requestId)
]).try

parse

parse(text: String) -> Result<Headers, NetError>

Parses CRLF- or LF-separated header fields.

Use this at a protocol boundary when headers arrive as text. Application code normally builds them with from, add, and set.

Returns fields in source order, or Parse

Examples

Parsing headers captured from a diagnostic fixture

Headers.parse("Content-Type: text/plain\r\nX-Trace: abc\r\n").try

module Net.HTTP.Status

Validation for numeric HTTP status codes.

from

from(code: Integer) -> Result<Status, NetError>

Validates an HTTP status code.

Parameters
code
a status in 100..599

Returns the status, or Parse

Examples

Validating a configurable health-check success code

let code = ENV.get("HEALTH_STATUS").flatMap { |text| text.to(Integer) }.or(204)
let expected = Status.from(code).try

module Net.HTTP.Response

Buffered response constructors for route handlers.

binary

binary(status: Integer, body: Binary, headers: Headers) -> Response<Binary>

Builds a buffered binary response with validated headers.

Examples

Returning a downloaded file without decoding it as text

Response.binary(200, archive, Headers.empty.add("Content-Type", "application/zip").try)

text

text(status: Integer, body: String) -> Response<Binary>

Builds a UTF-8 text response with an explicit text/plain content type.

Examples

A small health endpoint

Response.text(200, "ok")

empty

empty(status: Integer) -> Response<Binary>

Builds a response with an empty body.

Examples

A successful DELETE endpoint

Response.empty(204)

module Net.HTTP.Router

The empty starting point for an immutable route declaration chain.

build constant

Examples

+Router.build.get("/health", { |request, context| Response.text(200, "ok") })+

module Net.HTTP.Server

Starting, observing, and gracefully stopping HTTP servers.

start

start(endpoint: Net.Socket.TCP.Endpoint, router: Router, options: ServerOptions) -> Result<Running, NetError>

Starts a server with conservative defaults and returns immediately.

The returned handle owns the listener and active handlers. Use start when the process has other work to do; use serve for a foreground server whose main job is handling HTTP.

Examples
let router = Router.build.get("/health", do |request, context|
  Response.text(200, "ok")
end)
let endpoint = Net.Socket.TCP.Endpoint.loopback(Net.Port.from(0).try)
let server = Server.start(endpoint, router).try
Server.stop(server).try

serve

serve(endpoint: Net.Socket.TCP.Endpoint, router: Router, options: ServerOptions) -> Result<Void, NetError>

Starts with defaults and blocks until the server stops.

stop

stop(server: Running, grace: Duration) -> Result<ShutdownReport, NetError>

Gracefully stops using the duration captured at start.

New requests stop being accepted while in-flight handlers get their grace period to finish. The report says how much work completed or was forced down during shutdown.

Examples

Shutting down from an application lifecycle hook

let report = Server.stop(server).try
IO.printLine("closed after ${report.completed} requests")

join

join(server: Running) -> Result<Void, NetError>

Waits until the server owner exits.

running?

running?(server: Running) -> Bool

Returns whether the server owner is alive

localAddress

localAddress(server: Running) -> Net.Socket.TCP.Endpoint

Returns the bound address, including an operating-system-assigned port.

Returns the bound address

Examples

Publishing the actual address of a test server

let endpoint = Server.localAddress(server)
IO.printLine("test server: http://${endpoint.host}:${endpoint.port.string}")

type Running

An opaque asynchronous HTTP server handle.

module Net.HTTP.Client

Constructors for explicitly owned, connection-pooling HTTP clients.

open

open : Result<Client, NetError>
open(options: ClientOptions) -> Result<Client, NetError>

Opens an explicit pooled client with conservative defaults.

Reuse one client for related requests so keep-alive connections and DNS work can be reused. Close it when the owning service shuts down.

Examples

Fetching several pages through one connection pool

let client = Client.open.try
let first = client.get("https://api.example.com/items?page=1").try
let second = client.get("https://api.example.com/items?page=2").try
client.close.try

module Net.HTTP.HTTP

Stateless HTTP convenience calls for scripts and occasional requests.

request

request : String -> String -> Headers -> Binary -> Result<Response<Binary>, NetError>

Sends one stateless buffered request with no redirect or hidden retry.

Each call owns a short-lived client. This is convenient for scripts and occasional requests; use Client for a service making repeated calls.

Examples

A one-off authenticated request in a command-line tool

let headers = Headers.empty.add("Authorization", "Bearer ${token}").try
HTTP.request("GET", url, headers).try

get

get(url: String) -> Result<Response<Binary>, NetError>

Sends one stateless buffered GET.

Examples

+HTTP.get("https://example.test/").try+

delete

delete(url: String) -> Result<Response<Binary>, NetError>

Sends one stateless DELETE with an empty body.

head

head(url: String) -> Result<Response<Binary>, NetError>

Sends one stateless HEAD and returns an empty response body.

options

options(url: String) -> Result<Response<Binary>, NetError>

Sends one stateless OPTIONS request.

post

post(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends one stateless buffered binary POST.

put

put(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends one stateless buffered binary PUT.

patch

patch(url: String, body: Binary) -> Result<Response<Binary>, NetError>

Sends one stateless buffered binary PATCH.

Submodules

  • Net.HTTP.WebSocket High-level RFC 6455 client messages. The runtime handles fragmentation and ping/pong frames; reconnect and heartbeat policies remain application-owned.