# Changelog

## 0.2.0

### Changed

- The acceptor and connection loop now use the `socket` NIF API
  (`socket:accept/recv/send/close`) instead of `gen_tcp`. Accepted
  sockets run with `{otp, select_read}` enabled: every successful
  `recv` re-arms the read select inside the same NIF call, so the
  steady state is one `$socket` message plus one `recv` per request
  instead of recv/EAGAIN/select/recv. Keep-alive throughput improves
  ~7% (107.5k -> 115.1k req/s, 64 connections, M2 Pro).
- The raw `SO_REUSEPORT` options are replaced with the portable
  `{socket, reuseport}` option, and the listener's `send_timeout`
  behaviour is emulated with `socket:send/3` plus close-on-error.
- Minimum supported OTP is now 27.3 (required by `select_read`).

### Known issues

- Connection-per-request workloads drop ~6% on stock OTP because
  `socket:close` finalizes on a dirty scheduler; a fix is pending
  upstream.

## 0.1.7

### Added

- `#whitecap_req{}` carries a `received` field: `os:system_time()` in
  native units, stamped when the first byte of the request arrived. A
  handler with a deadline (e.g. an RTB `tmax`) can measure the time
  already spent on the wire and shed accordingly.
- `handler_timeout` config (default `infinity`). A finite value runs
  the handler in a monitored process and, if it overruns, kills it and
  answers `504`, emitting a `[whitecap, handler, timeout]` telemetry
  event. `infinity` keeps the handler inline with no per-request
  process — the right choice when the handler enforces its own
  deadline.

### Changed

- The single `receive_timeout` is split into `keepalive_timeout` (idle
  wait for the next request on a keep-alive connection) and
  `request_timeout` (per-read wait once a request has started
  arriving). This allows a short in-request deadline for slowloris
  protection without dropping healthy idle keep-alive connections.
  `receive_timeout`, if set, remains the back-compat default for both;
  all three still default to `infinity`, so behaviour is unchanged
  unless a finite `request_timeout` is configured.
- A body with a known `Content-Length` is now read with an exact-length
  `gen_tcp:recv`, so it lands in one read and one concatenation instead
  of repeatedly re-buffering partial reads (previously O(n²) on a body
  fragmented across TCP segments).
- Per-connection config (`handler_timeout`, `keepalive_timeout`,
  `max_keepalive`, `request_timeout`) is read once when the connection
  starts rather than on every request/read.

## 0.1.6

### Changed

- Connection workers now proactively garbage-collect after sending a
  response when their heap grows past ~64K words (half of ERTS's
  `ERTS_POTENTIALLY_LONG_GC_HSIZE`). Above that threshold ERTS
  schedules a process's collection on a dirty CPU scheduler; with
  thousands of connections that offloading could starve the dirty
  schedulers, so the collection is forced inline instead.

## 0.1.5

### Fixed

- A non-integer, empty, or negative `Content-Length` no longer
  crashes the connection worker. `binary_to_integer/1` threw `badarg`
  on a non-integer value and a negative length caused a later
  badmatch; both propagated to the acceptor and, repeated within the
  supervisor restart window, took the whole application down. Such
  values now return `400 Bad Request`.
- A crashing connection worker can no longer take the acceptor (and
  its sibling connections) down with it. Workers are spawned unlinked
  with `proc_lib:spawn/3` and own their socket via
  `gen_tcp:controlling_process/2`, so an abnormal exit closes only
  that worker's socket.
- `whitecap_protocol:headers/1` returns `{error, invalid_headers}`
  for a header value that violates the single-space rule instead of
  raising `case_clause`.
- The keep-alive limit served `max_keepalive + 1` requests; it now
  serves exactly `max_keepalive`. The forced `Connection: close` also
  overwrites a handler-set `Connection` header of any casing or
  iodata form instead of emitting a duplicate.

### Changed

- `bin_patterns` is stored in `foil` rather than `persistent_term`,
  the same mechanism as the rest of the config. foil's
  compiled-module lookup is faster and carries the compiled patterns
  through its constant pool.
- `whitecap:events/0` and the README telemetry table now list the
  `[whitecap, connections, send_error]` event, which has been emitted
  since 0.1.4.

## 0.1.4

### Changed

- Listen sockets now set `sndbuf=256KB`, `send_timeout=50ms`, and
  `send_timeout_close=true` so accepted sockets inherit them. The
  default kernel sndbuf was too small for multi-segment writes
  under tight RTB timing, where ~20KB responses could be silently
  dropped.

### Fixed

- `gen_tcp:send` return values are now checked. On error the
  connection is closed instead of looping back into
  `parse_requests` against a dead socket, and a
  `[whitecap, connections, send_error]` telemetry event is emitted
  with the response size and reason.

## 0.1.3

### Added

- `whitecap:events/0` — returns the list of `telemetry:event_name()`
  values whitecap emits. Consumers can iterate this list to attach
  handlers programmatically instead of hardcoding event names that
  drift across releases:

  ```erlang
  [telemetry:attach(handler_id_for(E), E, fun handler/4, [])
      || E <- whitecap:events()].
  ```

- README gained a "Pipelining and backpressure" section. The
  per-connection request loop has no concurrent in-flight cap and
  relies on TCP send buffer pushback to throttle a slow client,
  which is appropriate for HTTP/1.1 pipelining semantics. The
  section spells out the contract so callers don't reach for the
  wrong tool.

## 0.1.2

### Fixed

- `whitecap:start_listeners/2` is now safe to call more than once per
  node. Listener processes are registered under names that include the
  configured port, so a second call with a different `port` no longer
  collides on `whitecap_listener_<N>` with the first call. Before this
  fix, the second `start_listeners` call always failed with
  `{error, {already_started, _}}` as soon as its countdown reached an
  `N` value already used by the first call (e.g. one listener on
  metrics port 9091 plus sixteen on the main port would crash on
  `whitecap_listener_1`).

## 0.1.1

Dep bumps + CI bootstrap. No source changes.

### Changed

- `foil` bumped from `0.1.3` to `0.1.4` (tighter `error/0` type;
  internal DRY refactor; behaviour unchanged).
- `metal` bumped from `0.1.1` to `0.1.2` (infrastructure refresh).
- `telemetry` bumped from `1.2.1` to `1.4.2`.
- Test profile: `buoy` switched from git ref `0.2.4` to the hex
  release `0.2.6` -- buoy itself bumped through to 0.2.6 with the
  same dep-modernization pass; the hex release transitively pulls
  in `shackle 0.7.1` (which replaced granderl with knot, fixing
  the OTP 27+ build break).
- Test profile: `timing` git ref pinned from `master` to tag `0.1.3`.
- Test profile: `fprofx` moved from `ransomr/fprofx` to
  `lpgauth/fprofx` (`otp_19` branch).

### Added

- GitHub Actions CI workflow. Matrix covers OTP 25, 26, 27, 28.
  (whitecap shipped 0.1.0 without any CI configured.)

### Removed

- `edoc` profile (was still configured to use `edown`; whitecap
  already uses `rebar3_ex_doc` for hex docs, so the edown path
  was unreachable).

## 0.1.0

Initial release.

### Features

- HTTP/1.1 GET, HEAD, POST, PUT.
- Multi-acceptor model using `SO_REUSEPORT` on Linux and Darwin.
- Configurable handler module via `whitecap:start_listeners/1,2`.
- Keep-alive with a configurable per-connection request cap (`max_keepalive`).
- Telemetry events under `[whitecap, connections, ...]`.

### Notes

- `[whitecap, connections, stats]` reports `duration` in **microseconds** (not seconds).
- See README "Non-goals" for deliberate departures from RFC 7230.
