whitecap
High-performance Erlang HTTP/1.1 server.
Whitecap optimizes the hot path of bidder-style traffic: short-lived requests, high concurrency, sub-millisecond targets. It is not a fully RFC 7230 conformant server — see Non-goals.
Install
%% rebar.config
{deps, [
{whitecap, "0.1.0"}
]}.Usage
A handler is any module exporting handle/2:
-module(my_handler).
-export([handle/2]).
handle(_Req, _Opts) ->
{ok, {200, [{<<"Content-Type">>, <<"text/plain">>}], <<"hello">>}}.
The return shape is {ok, {Status, Headers, Body}} where:
Statusis an integer status code (e.g.200) or{Code, ReasonBinary}for codes whitecap doesn't know about.Headersis a list of{Key, Value}iodata pairs.Content-Lengthis added automatically (except for 204).Bodyis iodata.
Then start one or more listeners (defaults to four SO_REUSEPORT acceptors):
{ok, _} = application:ensure_all_started(whitecap),
ok = whitecap:start_listeners(#{
handler => my_handler,
handler_opts => #{},
port => 8080,
ip => {0, 0, 0, 0}
}).Configuration
Set via sys.config or application:set_env/3 before application:start(whitecap):
| Key | Default | Meaning |
|---|---|---|
max_keepalive | 10000 | Requests served per connection before close. |
receive_timeout | infinity | gen_tcp:recv timeout in ms. Set finite for slowloris protection. |
Telemetry
Events emitted under the [whitecap, connections, ...] prefix:
| Event | Measurements | Metadata |
|---|---|---|
[whitecap, connections, accept] | #{} | #{} |
[whitecap, connections, accept_error] | #{} | #{reason => term()} |
[whitecap, connections, close] | #{} | #{} |
[whitecap, connections, stats] | #{duration => microseconds, keep_alive => integer()} | #{} |
[whitecap, connections, timeout] | #{} | #{} |
[whitecap, connections, max_keepalive] | #{} | #{} |
duration is microseconds (from os:system_time/0 deltas converted via erlang:convert_time_unit/3).
Non-goals
Whitecap intentionally trades HTTP/1.1 conformance for throughput. Clients are expected to be cooperative (the bidder use case). Known and deliberate deviations:
- Case-sensitive header matching.
Content-LengthandTransfer-Encodingmust use the exact casingContent-Length:/content-length:/Transfer-Encoding:/transfer-encoding:. Any other casing is silently ignored. Use canonical casing. - Strict OWS in header values. Exactly one space is allowed between the colon and the value.
- No HTTP/1.0. Status lines must end with
HTTP/1.1. 1.0 requests get501 Not Implemented. - No
Transfer-Encoding: chunked. Returns501. - Limited verbs. GET, HEAD, POST, PUT only.
- No size limits. Request lines, header sections, and bodies are not bounded. Run whitecap only with trusted clients or a fronting proxy.
- Acceptor does not transfer socket ownership (
gen_tcp:controlling_process/2is skipped). Works because connection workers use{active, false}+ synchronousrecv. Don't switch to active mode without revisiting this.
Development
make compile # debug_info + warn-everything
make eunit # eunit + cover
make xref # cross-reference checks
make dialyzer # success-typing
make test # xref + eunit + dialyzer
make profile # fprofx → cachegrindLicense
MIT.