HTTP¶
fib.http supplies common values and helpers, fib.http.server a Ring-style HTTP/1.1 server and fib.http.client a Hato-style client. Both are
native: one message layer (RFC 9112) over a Transport seam, plain TCP built on fib.os (fib.os.net: sockets, poll, getaddrinfo).
There is no libcurl and no native library to link; a program that uses them builds with fibc build (and --static). TLS is fib.tls (lib/fib/tls/README.md, UNAUDITED): (tls/https-options options provider trust) registers it;
until a TLS Transport factory is registered an https URL is a typed error (TlsUnsupported), never a plain-text request. Native execution is
tested on Linux LP64; the Darwin backend of fib.os is checked separately and not run. The architecture, limits and what is deferred are in
docs/design/http.md.
Running the examples¶
fibc build examples/http-server.fib -I lib -o /tmp/http-server
/tmp/http-server
# In another terminal:
fibc build examples/http-client.fib -I lib -o /tmp/http-client
/tmp/http-client
FIB_HTTP_PORT selects the example server's port; FIB_HTTP_URL the client's destination (http only).
Values and headers¶
| Type | Fields |
|---|---|
h/Response |
status: i64, headers: (Map str (Vec str)), body: (Array i8) |
h/Request |
request-method: str, uri: str, query-string: (Option str), headers, body, trailers, version: str, remote-addr: str, server-port: i64 |
h/ClientRequest |
method: str, url: str, headers, body |
Methods retain their case; use uppercase HTTP method names. uri and
query-string retain their encoded text. Header names are normalized to lowercase
by the helpers. Values retain their order within each field; Set-Cookie values
are never combined into one string. Received header values preserve wire bytes
as Latin-1 characters. Outgoing string values are written as their UTF-8 bytes.
h/header returns the first value as an Option, and h/header-values returns all
values. h/header-set replaces a field; h/header-add appends another value.
h/validate-headers rejects invalid names, control characters, and CRLF injection.
Manual header maps can use mixed case; lookup and transport setup normalize them.
Bodies are binary arrays. h/text-response status text and h/response text
encode UTF-8; the latter uses status 200. h/with-header response name value
returns an updated response. h/body-text response and h/decode-text bytes
return (Result str h/Error), rejecting invalid UTF-8 without trapping.
Query and form parameters are (Vec (Pair str str)), preserving duplicate keys
and their order. h/query-string, h/with-query, and h/form-body encode them.
h/form-decode and h/parse-query decode percent escapes and UTF-8, returning
Result values. Plus signs mean spaces in form encoding. parse-query gives an
empty value to a parameter without =, and skips empty components.
Server¶
(defun handler (request: h/Request) -> h/Response
(cond (= (. request uri) "/") (h/response "hello\n")
(= (. request uri) "/echo") (h/Response 200 {} (. request body))
:else (h/text-response 404 "not found\n")))
(server/run-server
(server/wrap-header handler "x-app" "fibber")
(with (server/default-options) (port 8080) (workers 4)))
run-server returns (Result server/Server h/Error) immediately after binding
and starting a fixed worker pool. Handlers must be sendable; the ownership checker
rejects captures of ordinary cells. Each worker owns one connection at a time,
and its buffered reader remains local to that worker.
server/port reports the bound port, including the assigned port when options
specify zero. server/stop-server stops acceptance, cancels reads and writes,
joins workers, and closes the listener. Calling it again is harmless.
server/wait-server joins workers without initiating shutdown. Both return
Result values. Keep the server handle and call stop-server on normal exit.
Shutdown waits for an executing handler to return; a trapping handler follows
Fibber's normal process-abort semantics.
| Server option | Default |
|---|---|
host |
"127.0.0.1" (numeric IPv4 or IPv6 address) |
port |
8080 |
workers (most connections at once, one task each) |
64 |
backlog |
128 |
request-timeout-ms |
10,000 |
max-body-bytes |
8,388,608 |
max-header-bytes |
65,536 |
max-line-bytes |
8,192 |
max-requests-per-connection |
100 |
max-headers |
100 |
header-timeout-ms (whole head, from its first byte) |
10,000 |
idle-timeout-ms (wait for the next request) |
10,000 |
drain-timeout-ms (graceful stop waits for requests in flight) |
5,000 |
Options are immutable structures, updated with with. Request and response I/O
have deadlines; the request deadline covers the complete head and body, including
fragmented reads. Headers and trailers are limited to 100 fields, and chunked
bodies to 4,096 nonempty chunks. Idle keep-alive connections occupy workers until
their request deadline or shutdown. Workers are bounded; excess connections wait
in the socket backlog.
The server accepts HTTP/1.0 and HTTP/1.1 origin-form requests, fixed and chunked bodies, 100-continue, keep-alive, and pipelining. It derives response framing, supplies Date, suppresses bodies for HEAD and 204/205/304, and rejects duplicate Host/Content-Length, simultaneous Content-Length/Transfer-Encoding, obsolete folding, forbidden trailers, and malformed lines. Invalid response headers produce a 500 and close the connection. Chunked transfer encoding is refused on HTTP/1.0.
Client¶
(client/get "http://example.com")
(client/get "http://example.com" (with (client/default-options) (timeout-ms 5000)))
(client/post "http://localhost:8080/echo" "hello")
(client/request
(h/ClientRequest "PUT" "http://localhost:8080/echo"
{"content-type" ["application/octet-stream"]} bytes)
(client/default-options))
request and the convenience functions return (Result h/Response h/Error); get and post have an optional Options argument, head, put,
patch and delete take Options explicitly. The client derives Host, Content-Length and the body framing; passing Content-Length or
Transfer-Encoding yourself is an InvalidRequest. Userinfo in the URL becomes a Basic Authorization header.
client/build-http-client options makes a reusable client (a connection pool); client/request-with client request sends on it;
client/request-stream client request returns a client/StreamResponse (status, headers, a fib.http.body/Body to pull chunks from, abandon);
client/pool-stats gives connections opened, reused and idle; client/close-http-client closes the pool (twice is harmless; requests after close
return h/Closed). A client is local to its thread; client/request-async request options returns a (Task (Result h/Response h/Error)) and owns
its client. client/form-request, client/basic-auth, client/bearer-auth and client/wrap-header are as before.
| Client option | Default |
|---|---|
connect-timeout-ms (one connect) |
10,000 |
timeout-ms (whole request, redirects included) |
30,000 |
read-timeout-ms (the wait for any one piece of the response) |
0 (none) |
follow-redirects |
false |
max-redirects |
5 |
decompress (use the registered decoders) |
true |
throw-exceptions (status 400 and above is h/StatusError) |
true |
ca-file (passed to the TLS factory; unused by TCP) |
nil |
max-response-bytes |
8,388,608 |
max-header-bytes |
65,536 |
max-headers |
100 |
max-line-bytes |
8,192 |
max-idle-per-host (0: no reuse) |
4 |
idle-timeout-ms (pooled connections) |
30,000 |
factories (scheme to Transport factory) |
{"http" (tcp-factory)} |
decoders (Content-Encoding) |
none |
Timeout zero means none (for connect-timeout-ms, the rest of timeout-ms). (client/with-factory options "https" factory) registers the TLS
driver's factory; (client/with-decoder options (client/Decoder "gzip" inflate)) registers a decoder (DEFLATE is a driver, not here).
Errors are h/Error values: InvalidRequest, ProtocolError, LimitError (too large), TransportError, StatusError response, Closed, Cancelled,
DnsError, ConnectError, Timeout phase ("connect", "read", "total"), TlsUnsupported, RedirectLoop, RedirectRefused.
Redirects (off by default): 301/302/303/307/308; 303 becomes GET, 301 and 302 turn POST into GET, 307 and 308 keep method and body;
credentials are dropped on a change of origin; https to http is refused; loops and max-redirects are RedirectLoop.
Transport¶
fib.http.transport/Transport is the seam: read, write, close, set-deadline, peer; Factory makes one from a Target. fib.http.tcp has
TCP, pipes and connect; fib.http.transport/scripted an in-memory stream; fib.http.testing/loopback-factory a client connection served by the
native server over pipes. See the design record for how a TLS driver plugs in.
Scope and checks¶
Not provided: HTTP/2, TLS (next package), cookie jars, proxies, a native DNS resolver, WebSockets, multipart builders, streaming request bodies, and automatic JSON or Transit coercion.
fibc test specs/http-message-spec.fib specs/http-uri-spec.fib specs/http-client-spec.fib specs/http-server-spec.fib specs/http-fuzz-spec.fib -I lib
HTTP_FIBC=fibc scripts/test-http.sh # the cases and the Python differential suite
FIBC=fibc scripts/http-diff.sh # curl against python3 and the native server
FIBC=fibc scripts/mutant-http.sh # 18 planted faults, each must be killed
HTTP-2 additions¶
;; names: the native resolver is the default (docs/design/dns.md); getaddrinfo is opt-in
(with-system-resolver options) ; hosts whose names live in NSS only (mDNS, LDAP)
(with-resolver options (CustomResolver (fn (host) (Ok ["127.0.0.1"]))))
;; proxies: explicit; nothing reads the environment unless asked
(with-proxy options (proxy-from-env)) ; http_proxy, https_proxy, no_proxy (then HTTPS_PROXY, NO_PROXY; HTTP_PROXY outside CGI)
(with-proxy options (ProxySettings (some p) (some p) ["internal.example" ".corp.test"])) ; p from (parse-proxy-url "http://user:pass@proxy:3128")
;; plain http: absolute-form to the proxy; https: CONNECT host:port, then the TLS factory over the tunnel; Basic proxy credentials never appear in an error
;; cookies: opt-in (fib.http.cookies; no public-suffix list: see docs/design/http.md)
(with-cookies options (new-jar))
;; decoders: the seam passes the output cap; fib-zlib (its own repo) is the gzip and deflate driver
(with-decoder options (Decoder "gzip" (fn (bytes max-output) ...))) ; or (zh/with-zlib options) from fib.zlib.http
;; streaming bodies
(request-upload client (StreamingRequest "PUT" url {} next (some length))) ; next: (fn () (Option (Array i8))); length nil = chunked
(run-server-stream (fn (request body) reply) options) ; body: fib.http.body/next-chunk until the empty array
Target gained dial (how a factory reaches the network with the client's resolver and proxy); a TLS factory calls ((. target dial) host port ms) instead of fib.http.tcp/connect.
A pooled connection is probed for liveness before reuse. At stop the server answers connections it never accepted with a 503.