CHANGELOG.md
Rama develop-3.x · réplica de lectura del repositorio original.
# Ferron 3 change log
## Ferron UNRELEASED
**Not yet released**
### Changed
#### Reverse proxy
- **Extended CONNECT support for HTTP/2 backends**: the reverse proxy now "upgrades" HTTP/2 requests via extended CONNECT, if the request specifies an HTTP upgrade.
### Fixed
#### HTTP server core
- **Many HTTP correctness improvements**: improved overall HTTP protocol correctness (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-0416)).
- **Proper extended CONNECT support**: previously, extended CONNECT requests for HTTP/2 and HTTP/3 were not handled correctly, which could lead to connection stalls or dropped connections. This has been fixed.
## Ferron 3.0.0-rc.11
**Released in November 8, 2026**
### Added
#### Reverse proxy
- **Custom CA trust anchors**: added support for custom CA bundle paths (`ca`) in reverse proxy configurations for verifying TLS certificates not trusted by standard CA stores (which are often used in internal or private networks). ([GitHub issue](https://github.com/ferronweb/ferron/issues/978))
#### TLS
- **Support for unauthenticated requests for mTLS**: added `client_auth_required` subdirective for `tls` block, which can allow unauthenticated connections even if mTLS is enabled.
#### Automatic TLS
- **Control over TLS certificate key algorithms**: added support for `key_algo` and `key_size` configuration options in ACME automatic TLS configurations. They can be used to configure TLS certificate key (for example, it could be set to RSA with 2048-bit keys or ECDSA). ([GitHub issue](https://github.com/ferronweb/ferron/issues/976))
#### Configuration
- **Strict host definitions**: added the global `strict_hosts` directive. When it's turned on, a host defined by host blocks in more than one configuration file fails validation instead of being merged. This is useful when each site lives in its own included file. ([thank you, @SridharRG](https://github.com/ferronweb/ferron/pull/1004))
### Changed
#### Reverse proxy
- **Improved keep-alive handling for intercepted responses**: when HTTP response status code interception is enabled, the backend server connection is now reused.
#### Automatic TLS
- **Certificate orders survive reloads**: a configuration reload no longer cancels an ACME order in progress. The order finishes (for up to 5 minutes), and with a file-based cache the reloaded configuration uses its certificate, so frequent reloads no longer stall certificate issuance. ([thank you, @neurekadev](https://github.com/ferronweb/ferron/pull/994))
- **Concurrent certificate provisioning**: the ACME background task now obtains and renews up to 4 certificates at once instead of one after another, so servers with many hosts get their certificates much sooner. ([thank you, @neurekadev](https://github.com/ferronweb/ferron/pull/992))
- **Faster DNS-01 challenges**: instead of always waiting 60 seconds after creating each DNS-01 record, Ferron now asks the CA to validate as soon as the zone's authoritative nameservers serve the record when queried from Ferron, still waiting at most 60 seconds. With providers that publish records quickly, each challenge now takes a few seconds. ([thank you, @neurekadev](https://github.com/ferronweb/ferron/pull/990))
### Fixed
#### HTTP server core
- **Many HTTP correctness improvements**: improved HTTP/3 correctness, as well as fixed HTTP/2 graceful shutdown stall bug, and more (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-0415)).
#### Automatic TLS
- **Fixed TLS parameter support for ACME**: the server now accepts TLS cryptographic parameters and client certificate authentication settings for ACME TLS provider. ([GitHub issue](https://github.com/ferronweb/ferron/issues/986))
- **Orphaned provisioning locks after reloads**: when a configuration reload cancelled an ACME order in progress, its `lock_certificate_*` lockfile stayed in the cache, and the domain got no certificate until the 5-minute lock lease expired. The lockfile is now removed when the order is cancelled. ([thank you, @neurekadev](https://github.com/ferronweb/ferron/pull/988))
#### OCSP stapling
- **Android and old Firefox compatibility**: for improved compatibility with Android and old Firefox versions, OCSP stapling functionality now uses SHA-1 issuer name and key hashes instead of SHA-256 by default, if possible.
#### Forward proxy
- **IP blocklist hardening**: IP addresses are now canonicalized before checking against the deny list, ensuring consistent matching across IPv4 and IPv6. Also, "current network" server addresses are now blocked by default.
## Ferron 3.0.0-rc.10
**Released in October 23, 2026**
### Breaking changes
#### Observability
- **`EventTraceContext` API change**: the `EventTraceContext` struct for `ferron_observability` has been updated to include `tracestate`. If your custom Ferron modules rely on the `EventTraceContext` struct, you may need to update it to include `tracestate` (you can use `tracestate: None`).
### Added
#### Runtime
- **Support for pinning into specific CPU cores**: Ferron now supports pinning worker threads to specific CPU cores (for asymmetric CPU cores and NUMA). This can be configured via the `cpu_affinity` directive in the global configuration block.
### Changed
#### HTTP server core
- **HTTP body buffering**: added support for disabling request and response body buffering using `Incremental: ?1` or `X-Accel-Buffering: no` headers.
#### Observability
- **`http.route` is now a route template**: previously, the `ferron.request` span reported `http.route` as the requested hostname (or `"*"`). It now reports the matched route template built from the configured `location` block names, for example `/api/users/:id`, and is omitted when no `location` block matched.
- **`ferron.host` added to request spans**: the requested virtual host is now reported as `ferron.host` on the `ferron.request` span. It uses the same attribute name as the TLS metrics, so traces and metrics join on the same key. `server.address` is unchanged and still reports the bound socket address.
- **Sampling probability reported in `tracestate`**: when Ferron starts a trace itself and `trace_sampling` uses a ratio, the request's trace context now carries the OpenTelemetry `ot` `tracestate` entry (for example `ot=0.1`), on the propagated headers and on the exported span. Backends that extrapolate sampled traces can scale counts back up; previously a ratio sampler silently reported only the sampled fraction of traffic. No `ot` entry is written for `always_on`, `always_off`, `parentbased_always_on`, a ratio of `1.0` or `0.0`, or `attribute_based` sampling. An incoming `tracestate` is passed through unchanged.
- **Active health checking metric attributes**: the metrics related to active health checking now have resolved IP address attributes when `metrics_resolved_ip` is enabled (similarly to circuit breaker metrics).
- **Aggregate active health checking partial success metrics**: added a `ferron.proxy.health.partial_success` metric that reports health check probe results that succeeded only for some backend servers (percentage of successes in 0.0-1.0 scale).
- **Circuit breaker and active health check status metrics for multi-address upstreams**: improved overall metric clarity and reduced ambiguity when `metrics_resolved_ip` is disabled.
- **TLS and HTTP protocol error log improvement**: when server is logging to text-format application logs (not JSON structured ones), it now logs client IP address and port for TLS and HTTP protocol error logs.
#### HTTP server core
- **`request.uri.path` improvement**: the `request.uri.path` variable is now resolving to URL-decoded value for more consistent matching. `request.uri` and `request.uri.query` are documented as not necessarily URL-decoded.
### Fixed
#### HTTP server core
- **`trace.id` and `span.id` resolution fix**: previously, `trace.id` and `span.id` variable interpolations resolve to a doubly hex-encoded IDs, not proper single hex-encoded values.
- **`HEAD` request response body discard**: When sending a response for an HTTP `HEAD` request, the response body is now discarded instead of being sent to the client, improving HTTP spec compliance (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-0412)).
- **Conditional configuration validation**: the server is now reporting (via `ferron validate`) invalid forms of `if`, `if_not`, `location` and `handle_error` directives.
- **HTTP/3 protocol hardening**: hardened the HTTP/3 implementation against potential QPACK decoding vulnerabilities (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-0414)).
#### Observability
- **`tracestate` is now exported on spans**: the OTLP exporter wrote an empty `trace_state` on every span, discarding the W3C `tracestate` that Ferron parses and propagates to upstreams. The span now reports it.
- **Span trace flags follow the sampling decision**: the OTLP exporter hardcoded the "sampled" flag on every span. The flag sent to the OTLP backend or collector now reflects the recorded decision.
- **Prometheus scrape error metric**: previously, `ferron_prometheus_scrape_errors_total` metric didn't increase at all. This has been fix to increase when an internal error with Prometheus endpoint occured.
- **Connection pool metric fix**: previously, outstanding connection count measured from exported metrics (like Prometheus or OTLP) was bogus, it didn't decrease properly due to a logic bug.
### Reverse proxying
- **Strict-DNS URL resolution fix**: previously, strict-DNS URL resolution dropped the upstream path prefix (like `/api` in `http://backend.example/api`). This has been fixed to preserve the upstream path prefix.
## Ferron 3.0.0-rc.9
**Released in October 4, 2026**
### Breaking changes
#### Static file serving
- **`ferron.static.bytes_sent` and `ferron.static.files_served` metrics**: these metrics are no longer emitted by the static file serving stage. This is to reduce noise in the metrics output. Check your observability workflows.
### Added
#### Observability
- **Response header fields in access logs**: HTTP access logs now include response header fields, allowing visibility into HTTP response headers, for example for HTTP redirects.
#### HTTP server core
- **Support for interpolated string sources in `map`**: added support for interpolated string sources (in addition to variable name sources) in `map` directive, allowing mapping multiple values at once into one value.
### Changed
#### HTTP server core
- **`http` block inheritance**: a host block now inherits each global `http` directive that it does not set itself. Configurations that relied on a host block replacing the global `http` block see the global values apply again for directives the host omits.
#### Gateway interfaces
- **`REMOTE_ADDR` and `SERVER_ADDR` environment variables**: `REMOTE_ADDR` and `SERVER_ADDR` environment variables are now set to the canonical IPv4/IPv6 representation of the client and server IP addresses, respectively, so that they can be used for access control and logging consistently.
#### Reverse proxying
- **Forwarded header client IP canonicalization**: the client IP in `X-Forwarded-For` and `Forwarded` headers is now canonicalized to the standard IPv4/IPv6 representation, so that it can be used for access control and logging consistently.
#### Automatic TLS
- **Reduced span attribute noise**: request spans related to HTTP-01 challenge no longer include attributes that are not useful for observability, reducing noise in observability pipelines.
### Fixed
#### HTTP server core
- **Early hints header fix**: previously, when `early_hints` block was configured with multiple `Link` header values, only the last one would be sent in the response. This has been fixed to send all header values.
- **Variable resolution fix**: previously, if Ferron cannot resolve a variable, it kept the placeholder as `name` (instead of documented `{{name}}`).
- **Per-host `trace` and `trace_sampling` fix**: previously, `http { trace { generate, trust_request } }` and `http { trace_sampling ... }` were silently ignored in host blocks and were only read from the global block, so per-host trace-header trust and per-host sampling did not work. Ferron now resolves both from the matched host block, falling back to the global block.
- **`protocol_proxy` fix**: previously, `http { protocol_proxy true }` in the global configuration block had no effect unless the configuration also had a host block without a hostname, and the `ferron doctor` check for it never ran. Ferron now reads the setting from the global `http` block or from the host block without a hostname, and reports the best-practice violation.
- **`protocols` validation fix**: `ferron validate` did not check `http { protocols }` values because it looked for the directive in the enclosing block instead of the `http` block. Invalid values such as `h4` are now reported as a configuration error.
- **`options_allowed_methods` fix**: the directive never reached the response.
- **`options_allowed_methods` response header fix**: previously, a resolved value that could not be sent as an `Allow` header, such as an interpolated value carrying a line break, panicked the worker task and dropped the connection for every `OPTIONS *` request. Ferron now falls back to the default method list and reports an unusable configured value with `ferron validate`.
- **`control_plane.span_links.sampled` boolean flag fix**: previously, the bare directive form read as `false` instead of `true`, and `ferron validate` rejected it. Ferron now reads the flag like every other boolean directive.
- **`basic_auth` realm response header fix**: previously, a `realm` value containing a control byte or a line break panicked the worker task on every `401` or `407` response, because the value is embedded in the authentication challenge header. Ferron now answers without a challenge header and reports an unusable `realm` with `ferron validate`.
- **Conditional response header fix**: previously, a `file_cache_control` value that could not be sent as a `Cache-Control` header panicked the worker task on the `304` and `412` precondition paths. Ferron now omits the header and reports an unusable value with `ferron validate`. It lives inside the `http` block, but Ferron read it as a top-level directive of the resolved configuration, so every `OPTIONS *` response used the built-in default `Allow` list. `ferron validate` and `ferron doctor` accepted the directive, which made the failure silent.
- **`http` block inheritance fix**: a host block that set any `http` directive replaced the whole global `http` block, so every global setting the host did not repeat was dropped. For example, a host that only set `timeout` lost the global `protocols`, `h1_enable_early_hints`, and HTTP/2 and HTTP/3 settings. Ferron now layers the two blocks per directive, matching how directives in every other block inherit.
- **HTTP/2 setting range validation**: `h2_max_frame_size` and `h2_initial_window_size` accepted values outside the ranges that HTTP/2 requires, so Ferron could advertise a `SETTINGS` frame that conforming clients reject. `ferron validate` now reports these values.
- **`h3_qpack_blocked_streams` usage fix**: the directive takes a stream count, but `ferron directives` reported it as a boolean flag.
#### Observability
- **Baggage with `trust_request false` fix**: previously, when `trust_request false` was set, the server would still propagate baggage from incoming requests to outgoing requests. Now, baggage is not propagated when `trust_request false` is set.
- **Host `observability` block fix**: previously, when specifying multiple `observability` blocks inside a host block with IP address or hostname, only the last one was effective. This has been fixed to use all `observability` blocks, just like with wildcard host blocks.
- **Prometheus scrape metrics fix**: the `ferron_prometheus_scrape_total` and `ferron_prometheus_scrape_errors_total` metrics have been renamed to `ferron_prometheus_scrape` and `ferron_prometheus_scrape_errors`, respectively (previous behavior would involve `_total_total` suffix).
- **Circuit breaker metric types fix**: `ferron.proxy.circuit.open_total` and `ferron.proxy.circuit.half_open_timeouts` were reported with a signed value on a counter metric. No observability backend supports that combination, so both metrics were silently dropped and never reached Prometheus or OTLP. `open_total` now reports as a gauge that rises when a circuit opens and falls when it closes or moves to HalfOpen, and `half_open_timeouts` reports as an unsigned counter.
- **Active health check state per resolved address fix**: previously, health check state was keyed by the configured upstream URL, so every address behind one hostname shared a single state entry. One unreachable address marked all of them unhealthy, which returned `503` for every request while healthy capacity was still available. Ferron now tracks health state per resolved address, the same way it already tracks circuit breaker state, and probes each address on its own connection.
- **`ferron.proxy.dns_status` attribute consistency fix**: previously, backend scoped proxy metrics were emitted with different attribute sets depending on the code path. `ferron.proxy.circuit.state` and `ferron.proxy.circuit.flapping` were exported as two separate Prometheus series for the same backend, one with `ferron.proxy.dns_status` and one without, so any aggregation over the metric counted a backend twice and the two series could disagree. Ferron now always reports `ferron.proxy.dns_status` on backend scoped proxy metrics and only gates `ferron.proxy.backend_resolved_ip` behind `metrics_resolved_ip`, matching what the directive documents.
- **Active health check probe metrics fix**: previously, the probe duration, success, and failure metrics were emitted through a process wide sink built from the global configuration, so they were discarded unless an observability provider was configured globally. The probe metrics now go to the sink of the host that owns the upstream, so a provider configured in a host block receives them. The process wide connection pool and DNS result cache metrics are unchanged, and the documentation now states that they need a globally configured provider.
- **Prometheus exporter histogram buckets fix**: previously, metric-specific histogram buckets were ignored, leading to default histogram buckets being used instead and possible incorrect bucket values being reported.
#### Access control
- **`bcrypt` password hash fix**: Ferron 3.0.0-rc.8 added support for `bcrypt` password hashes, however it could not be used due to configuration validation errors. The validator has been now updated too.
#### HTTP caching
- **Cache configuration fix**: previously, `cache { }` didn't enable caching (but `cache` and `cache { ... }` did), which could cause debugging friction.
- **`ferron.cache.evictions{reason="expired"}` metric fix**: previously, the `ferron.cache.evictions{reason="expired"}` metric was not emitted when cache entries expired, which could cause debugging friction.
- **Cache purge scope fix**: previously, cache purge requests with `X-LiteSpeed-Purge: *` header would purge all cache entries, even those that were not created by the current host when configured in a block with ambiguious hostname.
- **Cache hostname fix for wildcard host blocks**: previously, cache entries created by a wildcard host block (like `*.example`) would be incorrectly stored, leaking cache entries across hosts.
- **Cache purge host fix**: previously, `PURGE` requests could miss matching entries because the request host was read after the proxy stage consumed the request. The host is now taken from the request headers saved before proxying.
#### Static file serving
- **`mime_type` response header fix**: previously, a `mime_type` value that could not be sent as a `Content-Type` header, such as one containing a line break or a control byte, caused every static file response for the matching extension to panic and the connection to drop without a response. `ferron validate` accepted such a value. Ferron now reports the value as a configuration error, ignores the mapping when it is still reached at request time, and never panics while building a static file response.
- **`multipart/byteranges` part header fix**: previously, the `mime_type` value was written into the `content-type` line of each `multipart/byteranges` part as given, so a value containing a line break could inject extra lines into the response body.
- **`If-None-Match` wildcard fix**: the server now responds with a 304 response when a `If-None-Match: *` request header is sent, instead of a 200 response.
- **HTTP ranges with precompression support**: previously, when precompressed file was present and precompression was enabled, a range request would lead to a response with parts of compressed file, which could be malformed.
- **`If-Modified-Since` and `If-Unmodified-Since` malformed date fix**: the server now ignores malformed date values in `If-Modified-Since` and `If-Unmodified-Since` request headers instead of responding with a 400 (Bad Request) error.
- **HTTP range `bytes=-0` fix**: the server now correctly handles `bytes=-0` range requests, treating them as unsatisfiable (416) instead of invalid syntax (ignored).
- **ETag + Date conditional request precedence fix**: previously, `If-Modified-Since` was evaluated even if `If-None-Match` was present, which could lead to incorrect behavior. Similar happened with `If-Unmodified-Since` and `If-Match`.
- **Windows directory listing fix**: previously, the server would respond with a 403 Forbidden response when trying to access a directory from HTTP client when the server is running on Windows, even when the directory listings are enabled. ([GitHub issue](https://github.com/ferronweb/ferron/issues/962))
#### HTTP compression
- **304 Not Modified fix**: HTTP dynamic content compression is no longer applicable for 304 (Not Modified) HTTP responses.
- **`Accept-Encoding` wildcard fix**: the server now correctly handles `Accept-Encoding: *` request headers, selecting the most appropriate compression algorithm from the server's supported ones.
#### Reverse proxying
- **`Connection: upgrade` header fix**: previously, when there was a request header with `Upgrade` inside the `Connection` header value (if the header wasn't exactly `Upgrade`), it would erroneously set to `upgrade`.
- **Request body chunked encoding fix**: previously, when the request body was chunked-encoded, the server would wrongfully strip `Transfer-Encoding` header from the request before sending it to the upstream server, which could cause issues with some upstream servers that expect the header to be present.
- **Retry budget fix**: previously, cross-backend failovers could consume a retry token before checking whether another backend was available, and `max_retry_rate` was accepted but not enforced. The budget now charges only retries that can be attempted and enforces the configured retry-rate share.
- **Circuit breaker half-open timeout fix**: previously, the circuit breaker would not transition from half-open to closed when the HTTP pipeline timeout occurred, which could cause the circuit breaker to remain half-open until the process restart.
#### URL rewriting
- **Bare boolean subdirective fix**: fixed bare form of `once` (and similar) boolean flags not being effective.
#### Content replacement
- **`replace_last_modified` flag**: fixed bare form of `replace_last_modified` flag not being effective.
## Ferron 3.0.0-rc.8
**Released in September 28, 2026**
### Breaking changes
#### Configuration
- **Host-isolated handler directives**: backend/handler selectors (`proxy`, `fcgi`, `fcgi_php`, `cgi`, `scgi`, `forward_proxy`, `root`, `index`, `basic_auth`, `rate_limit_backend`, static-file tunables, `client_ip_from_header`, `trace_id_header`, `https_redirect`, `trailing_slash_redirect`, `disable_symlinks`, `directory_listing`) no longer inherit from the wildcard `*` host into named hosts, while global-scope defaults and `location`-from-host inheritance keep working. Previously, `* { proxy ... }` would unexpectedly reverse-proxy a named host that only set `root`. Note: a named host no longer inherits `root` from `*`, so give each host its own `root` (or set it globally).
### Added
#### Access control
- **Basic auth bcrypt hashes**: bcrypt hash formats `$2a$`, `$2b$`, `$2x$`, `$2y$` are now supported for Basic Auth. This allows using Ferron with control planes and services that expect bcrypt password hashes.
### Changed
#### HTTP cache
- **Zero-result cache purge metric**: purges with zero results are now visible via a `ferron.cache.zero_purge` metric, which can help debug or monitor cache purge requests that did not match any entries.
### Fixed
#### Configuration
- **Spurious configuration file hot-reload fix**: previously, the server would sometimes hot-reload the configuration even when no changes were made to the configuration file (something would just read the file). This has been fixed.
#### Admin API
- **Admin API listen fail fix**: previously, when configuration is reloaded very frequently, the admin API listener would sometimes fail to bind to the configured address due to a race condition.
#### Observability
- **Prometheus reload listen fail fix**: previously, when configuration is reloaded very frequently, the Prometheus listener would sometimes fail to bind to the configured address due to a race condition.
## Ferron 3.0.0-rc.7
**Released in September 24, 2026**
### Breaking changes
#### Reverse proxy
- **Shorter breaker and timeout defaults**: `circuit_breaker.open_duration` default is now `5s` (was `30s`) and upstream `connection_timeout` default is now `2s` (was `5s`), so brief Docker Compose recreates and Apache graceful reloads recover quickly. Increase them explicitly if you need the old behavior.
#### Forwarded authentication
- **Forwarded auth URI behavior change**: Ferron no longer appends the original URI path to the redirect URL by default, instead relying on `X-Forwarded-Uri` header. Use the `append_uri` subdirective to restore the previous behavior.
#### HTTP response control
- **`ferron.rule_id` no longer duplicates the status code**: the `ferron.response.status_rule_matched` metric now labels each match with the rule's `name` when set, or `status-<code>-rule-<n>` (1-based rule position) otherwise. Update dashboards that filter `ferron.rule_id` by bare status code to use `http.response.status_code` or the new identifiers instead.
#### URL rewriting
- **`rewrite_log` logs `DEBUG` instead of `INFO`**: the `rewrite_log` directive now logs each rewrite operation to the error log at `DEBUG` level instead of `INFO`, so that it does not pollute normal logs. Update log-level filters to include `DEBUG` for rewrite logs, or use the new structured log attributes instead.
### Added
#### Reverse proxy
- **`retry_interval` directive**: delay between same-upstream retry attempts plus up to 25% jitter (default `200ms`, `"0s"` disables waiting). Masks brief backend restarts such as Docker Compose recreates or Apache graceful reloads.
#### Forwarded authentication
- **`append_uri` subdirective**: when enabled, the original URI path is appended to the redirect URL. This can be used to restore the previous default behavior before this version.
#### Observability
- **HTTP/2 and HTTP/3 stream error logging**: stream failures (for example malformed requests, flow-control violations, or resets) are now logged to the configured observability sinks with client and server addresses, similarly to connection errors. HTTP/2 stream errors use `error.type` `tcp_stream_error` or `unix_stream_error`, and HTTP/3 stream errors use `quic_stream_error`.
- **Clearer HTTP/2 and HTTP/3 connection errors**: connection-level protocol violations are now returned as errors (instead of closing silently), so they are logged as connection errors with a descriptive message.
#### HTTP cache
- **LSCache staleness suffix support**: `~s` staleness suffix is now parsed in LSCache compatibility, allowing stale cache entries to be purged by single tag or URL.
- **`X-LiteSpeed-Vary: value=` support via request variables**: responses declaring `value=<name>` are now stored instead of bypassed, partitioned per request by the `set_var` variable `<name>` (empty when unset). Only one value dimension is supported (last wins); values are normalized and truncated past 256 characters.
#### HTTP response control
- **`name` subdirective for `status` rules**: `status` blocks accept an optional plain-string `name`, surfaced as the `ferron.rule_id` metric label so same-code rules can be told apart in dashboards.
#### URL rewriting
- **`name` option for `rewrite` rules**: `rewrite` blocks accept an optional plain-string `name`, surfaced as `ferron.rewrite.rule_name` across rewrite metrics, spans, logs, and access-log fields.
- **Per-rule rewrite attribution**: `ferron.rewrite.rewrites_applied` and `ferron.rewrite.invalid` now carry `ferron.rewrite.rule_index` (1-based rule position); spans, access logs, and `rewrite_log` output additionally report matched-rule counts and per-step from/to URLs for chained rewrites.
#### Abuse protection and rate limiting
- **`log_rejections` option**: `abuse_protection` and `rate_limit` blocks accept an optional `log_rejections` flag that re-enables per-request rejection debug logs for forensics (default off).
### Changed
#### Reverse proxy
- **`localhost` exception from strict DNS resolution**: `localhost` URLs are no longer resolved via strict DNS; they are treated as local addresses and bypassed to avoid unnecessary DNS lookups.
- **Stale pooled-connection forgiveness**: a first failure on a reused keepalive connection (`SendRequestError`) is no longer counted toward the circuit breaker when a same-upstream retry will be attempted. This prevents one dead socket closed during reloads from tripping the breaker when the retry succeeds.
#### URL rewriting
- **`ferron.rewrite.rewrites_applied` counts rule firings**: the counter now increments once per matched rule (instead of once per rewritten request), so chained rewrites contribute one increment per step. Single-rule rewrites are unaffected.
#### Abuse protection and rate limiting
- **Transition-only rejection logging**: ban rejections and rate-limit rejections no longer emit a debug log per request, which could exhaust the observability pipeline under abuse waves. Bans now log `Ban triggered` / `Ban expired` transitions plus a `ferron.abuseban.active_bans` gauge; rate limiting logs the first rejection per key (then at most once per minute) without the raw key value. Set the new `log_rejections` option to restore per-request debug logs for forensics.
### Fixed
#### Abuse protection and rate limiting
- **Ban eviction race fix**: observing an expired ban and evicting it is now atomic, so concurrent requests can no longer delete a freshly re-triggered ban, and each ban expiry is reported exactly once.
#### HTTP cache
- **LSCache `stale` purge now serves stale**: purge operations carrying the `stale` marker expire matching entries in place instead of deleting them, so they keep serving stale while revalidating (or on error) when a stale window applies, and miss otherwise.
#### HTTP server core
- **Jammed unquoted IP address fix**: previously, unquoted IP addresses in `ferron.conf` files could be jammed together leading to unexpected behavior.
- **HTTP/2 flow control fixes**: fixed a flow-control bug that caused stream resets when downloading large response bodies with `hyper` (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-048)).
- **HTTP/2 and HTTP/3 hardening**: improved robustness of HTTP/2 and HTTP/3 server implementation (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-0410)).
- **Active request metric decrement fix**: previously, canceled HTTP/2 connections caused active request metric to not be decremented properly, leading to incorrect active request counts.
#### Admin API
- **Admin API listen fail fix**: previously, when configuration is reloaded, the admin API listener would sometimes fail to bind to the configured address due to a race condition.
#### Observability
- **Prometheus reload listen fail fix**: previously, when configuration is reloaded, the Prometheus listener would sometimes fail to bind to the configured address due to a race condition.
- **Global span links fix**: the server now fallback back into global span links (control plane metadata) properly.
#### Reverse proxying
- **Reverse proxy stall fix**: fixed stalls on some connections caused by aborted connections when the reverse proxy was waiting for a response from an HTTP/1.x upstream server.
- **Circuit breaker metric fix**: fixed missing circuit breaker metrics when circuit breaker is enabled but not yet opened, so that the `ferron.proxy.circuit_breaker.state` metric is always emitted.
- **Reverse proxy canceled span fix**: the server now emits span attributes even when the reverse proxy request is canceled (due to timeout or explicit cancellation), so that the span is not empty and can be correlated with the request.
## Ferron 3.0.0-rc.6
**Released in September 13, 2026**
### Changed
#### HTTP server core
- **HTTP/2 and overall memory usage optimizations**: optimized both reverse proxying and HTTP/2 for lower memory usage, and fine-tuned default `jemalloc` memory allocator settings.
### Fixed
#### Observability
- **Lazy observability event channels**: the console, file, OTLP, Prometheus, and StatsD backends now allocate their event channels only when selected by the configuration, instead of pre-allocating ~4 MB each at startup. Idle servers without observability configured use ~20 MB less memory.
#### Admin API
- **Access control bugfix**: fixed access control bug that allowed unauthorized access to the admin API (`/reload` was gated by `reload_get` instead of `reload` before the bugfix).
#### HTTP server core
- **Routing bypass fix**: previously, a specially-crafted request (with URL-routing) could bypass routing and access files on unintended paths.
#### Reverse proxying
- **Connection pool memory leak fix**: fixed connection pool memory leak that occurred when connection is returned to the pool (after proxy received the initial response from an upstream).
#### Gateway interfaces
- **`PATH_INFO` fix**: a bug introduced in 3.0.0-rc.4 caused `PATH_INFO` to not be sent at all by default due to an incorrect check.
#### Forward proxy
- **`http_version` subdirective fix**: the `http_version` subdirective now correctly sets the HTTP version for forwarded requests.
- **DNS rebinding protection fix**: the forward proxy now rejects with `403` (instead of passing the request through to the normal pipeline) when a destination hostname resolves to a denied IP or DNS resolution fails, on both the `CONNECT` tunnel and absolute-URI forwarding paths. The stage also fails closed with an error status if any other proxy error leaves no response set.
- **Hop-by-hop header stripping fix**: hop-by-hop headers are now stripped from forwarded requests per RFC 7230 section 6.1, so to not forward them to the upstream server.
## Ferron 3.0.0-rc.5
**Released in September 10, 2026**
### Added
#### TLS
- **Distributed ACME via shared cache**: when `cache` points at a shared filesystem, Ferron now syncs HTTP-01 / TLS-ALPN-01 challenge data (`challenge_http_*`, `challenge_tls_*`) so any node can answer CA validation, and serializes orders with `lock_certificate_*` lockfiles (heartbeat + 5-minute stale-break, thundering-herd damping).
### Changed
#### TLS
- **Automatic STEK rotation improvements**: automatic STEK rotation feature now checks if the key file has been changed on the disk, allowing for distributed STEK rotation without race conditions.
### Fixed
#### Forwarded auth
- **Forwarded auth bugfix**: improved handling and checking the authentication status when using forwarded authentication, to improve security (thank you, [@impietic](https://github.com/impietic)).
## Ferron 3.0.0-rc.4
**Released in September 8, 2026**
### Added
#### Rate limiting
- **Distributed rate limiting with Redis/Valkey**: the `rate_limit` module now supports a `rate_limit_backend` block with `type redis` for shared token-bucket limits across Ferron instances. The block is inherited from global scope when not set per host/location.
### Fixed
#### HTTP server core
- **HTTP/1.x multiple request parsing fix**: fixed HTTP/1.x requests being rejected with 400 Bad Request after multiple requests in one connection. (see [`zincio-http` changelog](https://github.com/ferronweb/zincio-http/blob/main/CHANGELOG.md#zincio-http-046))
- **HTTPS redirect fix**: `https_redirect` directive is now effective for block levels above host block (like location or conditional blocks) if specified in the host block.
#### Gateway interfaces
- **CGI and FastCGI `PATH_INFO` fix**: previously, `PATH_INFO` was not set correctly in CGI requests (stray leading slash, if `PATH_INFO` is about to be set). Now, `PATH_INFO` is set correctly in both CGI and FastCGI (often PHP-FPM) requests.
## Ferron 3.0.0-rc.3
**Released in September 6, 2026**
### Added
#### FastCGI
- **Support for multiple FastCGI backends**: multiple FastCGI backends can now be configured using multiple `fcgi` or `fcgi_php` directives.
### Changed
#### HTTP cache
- **Automatic `_lscache_vary` vary cookies**: request cookies with names starting with `_lscache_vary` (or the `_litespeed_vary` alias) are now part of the cache key without explicit configuration, matching LiteSpeed behavior. `X-LiteSpeed-Cache-Control: no-vary` disables all cookie-based vary dimensions for the response.
#### FastCGI
- **`fcgi_php` subdirectives**: the `fcgi_php` directive now supports same subdirectives as `fcgi` (except `extension` and `pass`). This allows for more fine-grained control over PHP FastCGI requests.
### Fixed
#### HTTP cache
- **HTTP cache coalesce timeout fix**: coalesce timeout in HTTP cache no longer causes server crashes.
- **Default HTTP cache TTL fix**: default HTTP cache TTL is now 10% of the time since the resource was last modified (RFC 9111) or disabled (LSCache compatibility). This improves cache consistency.
#### HTTP compression
- **HTTP upgrade fix**: HTTP dynamic content compression is no longer applicable for 101 (Switching Protocols) responses for HTTP upgrades to work correctly.
## Ferron 3.0.0-rc.2
**Released in September 4, 2026**
### Fixed
#### HTTP server core
- **HTTP/2 empty body error fix**: HTTP/2 requests with empty bodies no longer cause server errors. This is the only significant change in the emergency Ferron 3.0.0-rc.2 release.
## Ferron 3.0.0-rc.1
**Released in September 4, 2026**
### Changed
#### Automatic TLS
- **ACME fallback improvements**: Ferron now tries each configured fallback provider when the primary provider fails when provisioning a certificate (not just on account creation).
- **ACME certificate installation**: Certificates are now reused from cache early before ACME provisioning cycles to mitigate TLS certificate errors after reloading the configuration.
#### OCSP stapling
- **OCSP status logging and metrics**: added `ferron.ocsp.cert.status` attribute to log records and metric labels, allowing the certificate's OCSP status (`good`, `revoked`, `unknown`) to be logged and monitored.
#### Observability
- **No metric exemplars linking to sampled-out traces**: metric exemplars are no longer linked to traces that are sampled out (and thus not exported) to avoid misleading observability tools and reduce debugging friction.
#### Reverse proxy
- **Connection pool racing improvements**: when the connection pool is racing and multiple connections are being established simultaneously, the connection pool now selects the connection that is ready first, avoiding unnecessary delays and improving overall performance.
### Fixed
#### HTTP server core
- **Backlog size fix**: the default backlog size is now 4096 on Linux, and -1 on Windows, *BSD, and macOS. It was -1 on all platforms before this change (which would be equivalent to 0 on Linux).
- **Response body streaming fix**: response body streaming is now fixed to avoid buffering instead of streaming, improving performance and memory usage. This also allows Ferron to be used as a reverse proxy for web applications that use SSE (Server-Sent Events). ([GitHub issue](https://github.com/ferronweb/ferron/issues/899))
#### Automatic TLS
- **Automatic renewal info fix**: fixed an issue where automatic renewal information was not applied correctly, leading to confusion about certificate expiration.
## Ferron 3.0.0-beta.11
**Released in August 31, 2026**
### Added
#### HTTP server core
- **Support for listening to multiple IP addresses**: multiple IP addresses can now be specified in the `listen` subdirective in `tcp` block for TCP and QUIC, allowing the server to listen on multiple selected network interfaces.
#### Forwarded auth
- **Forwarded auth `intercept_errors` subdirective**: added `intercept_errors` subdirective to forwarded auth stage, allowing custom handling of upstream error responses, similarly to `intercept_errors` subdirective for reverse proxying.
- **Forwarded auth `request_header` subdirective**: added `request_header` subdirective to `auth_to`, allowing headers to be added (`+Name`), removed (`-Name`), or replaced (`Name`) on the request sent to the auth backend, mirroring the reverse proxy's `request_header` subdirective.
### Changed
#### Runtime
- **`io_uring` disabled by default**: `io_uring` is now disabled by default in the runtime configuration, as small static file serving and reverse proxying performance turned out to be slower than with `epoll`.
#### Static file serving
- **Symlink check performance optimizations**: optimized symlink check performance by caching the results in file open cache along with opened files.
#### HTTP caching
- **HTTP caching performance optimizations**: performed multiple internal throughput optimizations to the in-memory HTTP cache store, especially on the cache hit path.
- **Cache key fingerprinting improvements for observability**: truncated cache key (up to 48 characters) with the query string removed, useful for diagnosing why a specific request missed. Optionally with a short non-reversible tag (`q=<16 hex chars>`), and if the base itself had to be truncated, a second tag (`h=<16 hex chars>`).
- **Cache metrics improvements**: cache decision reasons are now attached to access log entries and cache metrics.
#### Forwarded auth
- **Forwarded auth `X-Real-IP` header**: added `X-Real-IP` header to forwarded auth requests, with same value as reverse-proxied requests.
### Fixed
#### Configuration
- **Empty configuration validation**: added a check for empty configurations (no ports, no global directives/matchers) to return an error instead of silently starting.
- **Protocol + IP + port host block split fix**: previously, the protocol+IP+port combination was not properly split when used as a host block (for example, `http 127.0.0.1:18081` lead to two host blocks: `http 127` and `0.0.1:18081`).
#### HTTP server core
- **`request.uri` interpolation fix**: previously, `request.uri` interpolation included the full request URL, including the `http`/`https` scheme and hostname. This had been changed to only include the path and query string.
- **HTTP status code for overlong paths**: previously, overlong file paths led to 500 Internal Server Error status code to be returned. This has been changed to return 400 Bad Request instead.
#### Logging
- **Reduced startup logs**: removed multiple startup logs for various features to reduce noise in the logs.
#### Static file serving
- **Static file serving symlink settings fix**: previously, symlink mode (follow or not) was not properly respected when serving precompressed static files.
#### TLS
- **Certificate and private key mismatch fix**: previously, when a certificate and private key did not match for a given host, this was silently allowed. This has been changed to error out on the startup of the server.
## Ferron 3.0.0-beta.10
**Released in August 27, 2026**
### Breaking changes
#### HTTP server core
- **Linear-time regular expression engine**: The regular expression engine used by Ferron has been replaced by one using a linear-time algorithm, which means that backtracking regex patterns are no longer supported. This change was made to avoid potential ReDoS and catastrophic backtracking issues. If you are using syntax such as `(?= ...)`, `(?! ...)`, `(?<= ...)`, `(?<! ...)`, or `\1`, you need to rewrite the regexes.
- **Symlink following disable by default for webroots**: following symlinks during static file resolution from webroot is now disabled by default for symlink attack prevention. Check if web application uses symlinks (if it uses, use `disable_symlinks false` or a similar setting).
#### Automatic TLS
- **DNS provider support reduced**: The DNS provider support for automatic TLS DNS-01 challenge has been reduced to a minimal set of providers, to keep Ferron lightweight and lean. Supported providers are `rfc2136`, `route53`, `bunny`, `digitalocean`, `cloudflare`, `dnsimple`, `googlecloud`, `ovh`, `spaceship`, `porkbun`, and `desec`.
#### Configuration
- **`enabled true` no longer effective**: `tls { ocsp { enabled true } }` and `basic_auth { brute_force_protection { enabled true } }` are no longer effective and are replaced with `tls { ocsp }` and `basic_auth { brute_force_protection }` respectively, to improve consistency across configuration directives.
### Added
#### CLI utilities
- **`ferron-passwd --stdin`**: `ferron-passwd` can now read passwords from the standard input when not using TTY. Passwords should be one per line.
#### HTTP server core
- **Unix domain socket listeners (Unix only)**: added support for listening on Unix sockets for HTTP server, which can be configured using global `unix` directive. If Unix sockets are configured, Ferron disables TCP and QUIC listeners.
- **Support for h2c listeners**: added support for h2c (HTTP/2 cleartext) listeners on top of the existing HTTP/1 and HTTP/2 implementations.
#### Reverse proxy
- **Same-upstream retries**: new `max_retries_per_upstream <count>` directive controls how many times the proxy retries the same upstream on a transport or connection failure before it falls back to another backend via `retry_connection`. The retry applies only to idempotent requests with a replayable body and, when `retry_budget` is enabled, each same-upstream retry consumes a retry-budget token. Default: `max_retries_per_upstream 1`.
#### DNS / Automatic TLS
- **`command` DNS provider (external hook)**: the new `dns-command` module provides a `command` DNS provider for the ACME DNS-01 challenge. It runs an external program for each record change, passing the record details through environment variables (`FERRON_DNS_ACTION`, `FERRON_DNS_DOMAIN`, `FERRON_DNS_RECORD_TYPE`, `FERRON_DNS_RECORD_VALUE`, `FERRON_DNS_RECORD_TTL`). The program must exit `0` on success. Configure with `provider command` and the `command` directive (with optional `min_ttl`, default `60`). This delegates DNS updates to any DNS server or automation that Ferron does not support natively.
#### Observability
- **Baggage key promotion for StatsD**: the `observability-statsd` module now supports the `baggage` directive, which promotes W3C Baggage keys into DogStatsD tags (requires `datadog` mode). This matches the existing OTLP and Prometheus baggage promotion support.
### Changed
#### HTTP canary
- **Persistent affinity cookie by default**: the `canary` module now writes a persistent affinity cookie (7-day `Max-Age`, `HttpOnly`, `SameSite=Lax`) when `set_cookie` is enabled, instead of a browser-session cookie. A new `cookie` block lets you configure the cookie lifetime and attributes (`ttl`, `path`, `domain`, `secure`, `httponly`, `samesite`), matching the HTTP proxy's affinity cookie settings.
#### HTTP server core
- **HTTP/3 enabled by default**: HTTP/3 is now enabled by default in Ferron, as it's also no longer declared experimental (it was declared such, because the previous HTTP/3 implementation, `h3`, was declared experimental as well).
- **Interpolations allowed in static error page paths**: interpolations are now supported in static error page paths served by the HTTP server, allowing for using them with A/B testing.
#### Observability
- **Server and client port in error logs**: the `client.port` and `server.port` attributes are now included in error log messages, so you can easily identify which client and server ports are involved in a given error.
- **Baggage key promotion max cap more effective**: when baggage values exceed the maximum distinct value cap, they are dropped instead of being hashed (cardinality would be the same when hashed, except for hash collisions, so hashing would be ineffective).
#### Reverse proxy
- **HTTP error resilience**: when a backend fails while HTTP request is sent, if the request method is idempotent and body is not sent yet, Ferron will retry the request instead of bailing out.
### Fixed
#### HTTP cache
- **HTTP cache `Vary` correctness fix**: a bug introduced in Ferron 3.0.0-beta.9 led to some header names in `Vary` header values (`If-Match`, `If-Modified-Since`, `If-None-Match`, `If-Range`, `If-Unmodified-Since`, `Range`) being ignored by the cache, which might lead to wrong 304 responses being served. This has been fixed to properly handle these headers in `Vary` values.
#### HTTP server core
- **Configured HTTP responses after HTTP -> HTTPS redirects**: HTTP responses configured to be served after the `https_redirect` stage will now only be served after a successful HTTP-to-HTTPS redirection. This ensures that these responses are not served before the client is redirected, which could lead to unexpected behavior or security issues.
- **Webroots with interpolations in URL rewrites**: webroot paths can now contain string interpolations when using with URL rewrite rules.
- **Less QUIC packet loss and retransmissions**: previously, QUIC packet loss and retransmissions were higher due to improper routing of packets by the QUIC transport. This has been fixed to improve packet routing (via IP hashing when initializing and connection ID hashing later on) and reduce packet loss.
- **HTTP/3 stream reset errors fixed with curl**: fixed `curl: (18) HTTP/3 stream 0 reset by server (error 0x0 unknown)` on `curl` for HTTP/3 in underlying HTTP server library used by Ferron. ([GitHub issue](https://github.com/ferronweb/ferron/issues/887))
- **IPv4 host blocks never matching**: fixed IPv4 host never matching due to IP address canonicalization issues and IPv4-mapped IPv6 addresses.
- **Best practice diagnostics for `location` blocks**: the best practice diagnostics for `location` blocks with duplicate pathnames are now emitted correctly.
#### Configuration
- **Stray `}` fix**: earlier, stray `}` characters in `ferron.conf` files could cause an infinite loop. These are now properly handled by erroring out.
#### Admin API
- **Improved config redaction**: `users` subdirective used by HTTP basic auth functionality is now redacted properly in the config JSON output.
#### TLS
- **Local TLS with ALPN + OCSP**: local TLS now supports ALPN and OCSP stapling properly, for HTTP/2 to work properly.
- **OCSP stapling verification fix for some CAs**: OCSP stapling verification now works properly for some CAs that have non-standard DER encoding of `tbsResponseData` (like Actalis).
#### Observability and tracing
- **High-cardinality metric removed**: the `ferron.proxy.lb.selections` with high-cardinality `ferron.proxy.lb.score` attribute has been removed to prevent exhaustion of metric storage.
#### Forward proxy
- **Fixed `deny_ips` bypass using non-canonical IP representations**: previously, `deny_ips` could be bypassed using non-canonical IP address representations (such as `0x7f.0.0.1` or `2130706433`). This has been fixed to normalize the IP addresses first.
#### Static file serving
- **Ranges beyond the end correctness**: the server now returns `416 Range Not Satisfiable` when a range request start exceeds the available content length.
## Ferron 3.0.0-beta.9
**Released in August 16, 2026**
### Added
#### CLI
- **`ferron directives` subcommand**: new CLI subcommand that prints every registered configuration directive as structured JSON, grouped by section. Useful for tooling, editor integrations, and inspecting the directive schema of the running binary.
- **FIPS build indication**: a build compiled with the `fips` feature prints `This build is configured to use FIPS-certified cryptography.` in its version output.
#### Build & packaging
- **FIPS-certified cryptography builds**: the `fips` Cargo feature enables FIPS-certified cryptography via AWS-LC and rustls. When enabled, OCSP stapling, TLS cipher suites and key exchange groups are restricted to FIPS-approved algorithms, and HTTP basic auth password verification supports only PBKDF2 (Argon2 and scrypt hashes are rejected). You can enable the feature with `cargo build --features=fips`, or through the build tooling: `just build fips=true`, `cross-build/build.sh --fips`, the `FIPS=1` Docker build argument, and the FIPS packaging scripts for archive, Debian, RPM, and Windows installer artifacts. FIPS artifacts are named with a `+fips` suffix (for example, `ferron+fips-<version>-<target>.tar.gz`), and FIPS Docker images use the `-fips` tag suffix.
- **SBOMs for release archives**: every release now ships a software bill of materials (SBOM) archive next to the binary archives, named `ferron-<version>-<target>-sbom.tar.gz` (or `.zip` for Windows). Each archive contains CycloneDX documents in JSON and XML format that list the third-party Rust crates compiled into the binaries. FIPS releases ship matching SBOM archives with the `+fips` suffix. You can generate SBOMs locally with `just package-sbom <target> [fips=true]`.
#### Observability & tracing
- **Configuration drift hints**: new `ferron.admin.config_drift` Gauge metric and `config_drift` / `config_drift_hints_enabled` fields on the `/status` Admin API endpoint detect when configuration source files have changed on disk but have not been reloaded. Drift is detected via periodic lightweight mtime comparison (no re-parsing). Enabled by default; disable with `drift_hints false` in config adapter params (e.g., `--config-params "drift_hints=false"`). A warn-level log is emitted when drift is detected, and an info-level log when drift resolves after reload.
- **StatsD metrics export**: new `provider statsd` in `observability` blocks sends Ferron metrics to a StatsD server over UDP, with `host`, `port` (default `127.0.0.1:8125`), `prefix`, and `datadog` directives. The optional `datadog true` toggle enables DogStatsD extensions: metric tags rendered from metric attributes (and control-plane metadata) and the `h` histogram type. Without it, histograms are sent as `ms` timers (with seconds-to-milliseconds conversion). The `prefix` directive prepends a namespace to every metric name (e.g., `myapp.ferron.http.server.request_count:1|c`). Each metric event is sent as a separate UDP datagram. (`observability-statsd`)
- **OTLP metric exemplars**: the OTLP observability sink now supports exemplar attachments on metric data points, allowing the last sampled value to be attached as an exemplar. This allows for better trace correlation and debugging by attaching relevant historical context. Previously, this wasn't supported due to OpenTelemetry SDK limitations, but the limitation was removed by replacing OpenTelemetry SDK with a custom OTLP exporter implementation.
- **OTLP export batching tuning**: the `logs` and `traces` OTLP signal sub-blocks now accept `export_interval <duration>` and `export_batch_size <number>`, and the `metrics` sub-block accepts `read_interval <duration>`. This controls how often partially full batches flush and how many finished items trigger an export.
- **OTLP gzip compression**: the `logs`, `metrics`, and `traces` OTLP signal sub-blocks now accept `gzip [bool]` (default `false`). When enabled, export requests are compressed with gzip (HTTP `Content-Encoding: gzip`, gRPC gzip compression).
- **OTLP exemplar toggle**: the `metrics` sub-block now accepts `exemplars [bool]` (default `true`). Set it to `false` to stop attaching the last sampled measurement per series as an exemplar.
- **OTLP native histogram toggle**: the `metrics` sub-block now accepts `native_histograms [bool]` (default `true`). Set it to `false` to aggregate histograms with explicit bucket boundaries instead of the exponential layout.
- **Variable interpolation in log filenames**: the `access_log` and `error_log` directives now support `{{variable}}` interpolation in file paths. Access log filenames use the `accesslog.` prefix (e.g. `{{accesslog.header_host}}`) and resolve against access log event fields. Application log filenames use the `log.` prefix (e.g. `{{log.level}}`, `{{log.error.type}}`) and resolve against log event attributes. Environment variables are also available via the `env.` prefix. This enables use cases like per-host access logs (`access_log /var/log/ferron/{{accesslog.header_host}}/access.log`). ([GitHub issue](https://github.com/ferronweb/ferron/issues/442))
#### HTTP server core
- **Variable interpolation in map results**: variable interpolations (`{{name}}`) in map result values are now resolved at runtime.
- **`request.uri.query.<param>` variables**: the `request.uri.query.<param>` variables are now available for string interpolations, so to not manually create a regular expression for extracting query parameter value from a query string.
- **`request.cookie.<name>` variables**: the `request.cookie.<name>` variables are now available for string interpolations, so to not manually extract cookie values from the request.
- **CORS origin interpolations**: CORS origin values can now be interpolated using `{{name}}` variable syntax (previously they were declared invalid).
- **Request `Cache-Control` directives**: Ferron now honors the RFC 9111 §5.2.1 request directives on cache lookups. `max-age=<n>` and `min-fresh=<n>` revalidate a stored response that does not satisfy the requested freshness window, and `only-if-cached` returns `504 Gateway Timeout` on a miss instead of contacting the origin. `no-transform` is accepted and has no effect. `no-cache` (and `Pragma: no-cache`) already forced revalidation; `no-store` already bypassed the cache.
- **Canary deployments without a reverse proxy**: new `canary` directive (`http-canary`) assigns each request a weighted variant with consistent-hash sticky affinity, for canary rollouts and A/B testing of static content. Affinity is `ip` by default and can be based on a cookie, a request header, or a built-in request variable (`hash`). With the `set_cookie` sub-directive, Ferron generates a random sticky key and writes it to the affinity cookie itself when the request has none, so assignments survive client IP changes without help from the web application. Variants are exposed as `canary.variant`, `canary.weight`, and `canary.key` variables, so a document root like `root "/srv/www/{{canary.variant}}"` serves each variant its own content. Weights and variant lists can change on reload; clients near variant boundaries may move, other clients keep their variant. Observability includes the `ferron.canary.requests` counter (with `ferron.canary.name` and `ferron.canary.variant` attributes), span attributes on the `ferron.stage.canary` span, and the `ferron.canary.variant` custom access log field. (`http-canary`)
- **HTTP/3 protocol configuration directives**: added several new `h3_*` subdirectives in `http` directive for fine-tuning HTTP/3 protocol configuration.
#### HTTP caching
- **On-disk cache persistence**: the HTTP cache can now survive process restarts. Set `persist <dir>` in a cache block to write cache mutations to a journal and periodic snapshots under `<dir>/<zone>`. On startup, Ferron replays the snapshot and journal back into memory. `persist_interval <duration>` (default `30s`, minimum `1s`) controls how often Ferron flushes queued mutations. By default only public entries are persisted; `persist_private` opts into persisting private entries. Deletions are always persisted. The persistence directory is single-process only — do not share it between instances. A SIGHUP config reload does not touch the on-disk cache.
### Changed
#### HTTP server core
- **PGO for GNU/Linux and some Linux with musl**: PGO (profiled-guided optimization) is now enabled for GNU/Linux targets and some Linux targets with musl libc (64-bit x86, ARM64), for pre-built Ferron binaries. This mainly improves tail (p90/p99/max) latency for web requests.
- **`.ferron` file extension**: `.ferron` files are now supported as an alternative to `.conf` (which is a generic file extension) files for configuration ([GitHub issue](https://github.com/ferronweb/ferron/issues/838)).
- **Panic hook improvements**: panic hook has been simplified (removing backtraces that are likely unhelpful), and it now logs the Ferron version and build target.
- **HTTP/2 and HTTP/3 implementations**: HTTP/2 and HTTP/3 functionality in Ferron now depends on `vibeio-http`'s in-house protocol implementations instead of `h2` and `h3` from Hyperium. New HTTP/2 implementation features higher space savings for headers (~95%, versus ~91% with the previous implementation for a "Hello World" application), while new HTTP/3 implementation features improved protocol compliance (47/49 on `h3spec`, versus 44/49 with the previous implementation).
- **QUIC performance optimizations**: QUIC "endpoints" are now per-thread instead of single-threaded, improving throughput and tail (p90, p99) latency for HTTP/3 requests.
#### Observability & tracing
- **Non-existent directory handling for log files**: when a log file path contains a non-existent parent directory, Ferron now creates it automatically before writing to the log file.
#### Configuration validation
- **Diagnostic span location improvements**: diagnostic span location in error messages is now more accurate, showing the exact line and column where the error occurred in the configuration file.
#### Container images
- **"Slim" Debian-based Docker images**: `ferronserver/ferron:3-debian` (and other Debian-base image tags) now are based on a "slim" variant of Debian instead of the full Debian image, making the image smaller and removing unnecessary packages. The slim variant is still compatible with the full Debian image, so it should not break existing deployments.
#### Access control
- **Interpolated strings as password hashes (HTTP basic auth)**: interpolated strings can now be used as password hashes for HTTP basic authentication (for example when moving from a static password to an environment variable).
- **Password hashing backends for HTTP basic auth**: password verification no longer depends on the `password-auth` crate. PBKDF2 verification now uses aws-lc-rs, scrypt uses AWS-LC (`EVP_PBE_scrypt`), and Argon2 uses argon2-rs (the Argon2 C reference implementation) with constant-time comparison. Newly supported hash formats: `$pbkdf2-sha384$` and `$pbkdf2-sha512$`. Base64 salt and hash fields now accept both padded and unpadded encodings. PBKDF2 hashes accept a bare iteration count (`$pbkdf2-sha256$600000$...`) or the PHC parameter form (`$pbkdf2-sha256$i=600000,l=32$...`).
### Fixed
#### HTTP server core
- **Trailing slash redirects**: trailing slash redirection logic now correctly handle index files (e.g., `index.html`) and redirects to the correct URL (with trailing slash) rather than serving two URLs (both with and without trailing slash)
- **Connection accept fix for `poll`**: previously, connections at TCP listeners (including HTTP/1.x and HTTP/2) accepted only a single connection when using `poll` (not `epoll`), stalling afterwards. This has been fixed to be able to accept multiple connections (see [`vibeio` changelog](https://github.com/ferronweb/vibeio/blob/main/CHANGELOG.md#vibeio-0217)).
- **HTTP/2 host header handling correctness**: previously, if both `host` and `:authority` HTTP/2 headers were present, the web server would return a 400 error, which might not be correct according to the HTTP/2 specification (RFC 9113, section 8.3.1). This has been fixed to override `Host` header value with `:authority` header value instead of appending a new value.
- **Symlink ownership check**: previously, symlink ownership check (`disable_symlinks if_not_owner`) was effectively a stub that effectively disabled all symlinks. This has been replaced with a proper implementation.
- **Conflicting Alt-Svc headers for HTTP/3**: the server now rewrites Alt-Svc header values to remove conflicting HTTP/3 port definitions.
#### HTTP caching
- **RFC 9111 compliance**: updated freshness lifetime precedence, 304 revalidation header merging, `must-revalidate` handling, `no-cache="field-names"` support, and `206 Partial Content` cacheability to align with RFC 9111.
- **Cache key & scope**: cache keys now use resolved vhosts, normalize whitespace/sorting in `Vary` values, and exclude client IPs for private responses. Added cardinality bounds for private keys and query string stripping from fingerprints to prevent cache fragmentation and information leakage.
- **Purge & security**: scoped `PURGE` requests to requesting hosts, added constant-time `X-Purge-Secret` authentication for propagated purges, and ensured `Set-Cookie` headers are never stored verbatim.
- **SWR & performance**: improved `stale-while-revalidate` with SWR leader fresh response delivery, singleflight coalescing with timeouts, and throttled expired-entry sweeps.
- **Request handling**: `no-store` requests now serve fresh entries (but don't store), hop-by-hop headers are stripped, and fresh hits now answer client conditionals (e.g., `If-None-Match`) with local 304s.
- **Metrics & diagnostics**: corrected `Cache-Status` labels for `stale-if-error` and fixed double-counting in cache request metrics.
#### Observability
- **OTLP gRPC `no_verification` TLS handshake**: previously, when an OTLP gRPC endpoint was configured with `no_verification true` and used HTTPS, TLS handshakes failed because the custom certificate verifier rejected TLS 1.2/1.3 handshake signatures. This has been fixed to return assertion-based verification results (matching the HTTP exporter behavior).
- **`ferron.ocsp.stapling.hit_total` metric emission fix**: previously, the `ferron.ocsp.stapling.hit_total` metric emission didn't function at all. It has been fixed to emit the metric to global observability sinks correctly.
- **ACME TLS resolution errors**: previously, TLS resolution errors were always logged into the console when using automatic TLS via ACME. This has been changed to log them into configured observability sinks.
- **Spurious abrupt connection termination error logs**: earlier, some abrupt connections termination log were logged (`Reverse proxy: HTTP upgrade tunneling failed: peer closed connection without sending TLS close_notify: https://docs.rs/rustls/latest/rustls/manual/_03_howto/index.html#unexpected-eof`), even if the connection was idle. This has been fixed along with an update to the HTTP server library used by Ferron (see [`vibeio-http` changelog](https://github.com/ferronweb/vibeio-http/blob/main/CHANGELOG.md#vibeio-http-035)).
#### Configuration validation
- **JSON configuration parse error reporting**: previously, configuration parse errors were reported using human-readable error messages (even if the `--json` flag was used). This has been fixed to report parse errors in JSON when configured to do so.
#### Reverse proxy
- **Config reload cleanup**: on configuration reload, old reverse proxy health check probe tasks are now aborted and per-config caches (resolved upstreams, retry budgets, unhealthy backend counters) are invalidated. Previously these accumulated on every reload, leaking background tasks and memory.
- **Upstream resolution cache fix** - in earlier versions of Ferron 3 beta, stale upstream resolution cache (with infinite TTL) could have caused upstream connection errors, due to drift between cached state and actual DNS state. This has been fixed by removing the infinite-TTL internal upstream resolution cache.
- **`X-Forwarded-For` and `Forwarded` header value fix**: previously, `X-Forwarded-For` and `Forwarded` header values were truncated to 256 bytes maximum, which could lead to incorrect (or even malformed) client IP values in the proxy request. This has been fixed to set the header values correctly without truncation.
#### Static file serving
- **`Range` request correctness**: previously, weak ETags were included with `206 Partial Content` responses (and some other responses with `Range` request header), which is a violation of HTTP spec (RFC 7232, RFC 7233). This has been fixed to remove ETags from responses to range requests.
#### CORS
- **CORS `Vary` header correctness** - previously, if response `Vary` header was set upstream, it would be overwritten by `Vary: origin` header, which might lead to incorrect caching. This has been fixed to append `origin` to the list of header names in `Vary` header value instead.
- **CORS `Vary: Origin` header**: the `Vary: Origin` header is now always added to CORS responses (instead of selectively).
- **CORS `Origin` request header fix**: previously, if `cors` directive contained non-`*` origin, Ferron didn't add CORS headers at all. This has been fixed to add CORS headers for non-`*` origins as well.
#### CLI utilities
- **`ferron-fmt` duration string formatting fix**: previously, `ferron-fmt` formatted short duration strings (like `1s`) without quotes, which caused subsequent parse errors. This has been fixed to always quote this kind of strings.
## Ferron 3.0.0-beta.8
**Released in July 15, 2026**
### Breaking changes
#### Configuration format
- **Invalid escape sequences in quoted strings are now parse errors** — previously, unknown escape sequences (e.g., `\z`, `\$`) were silently passed through. They now produce a lexer error. Use raw string literals (`r"..."`) if you need literal backslashes in values (e.g., regex patterns).
### Added
#### Configuration format
- **Raw string literals** — new `r"..."` syntax for strings with no escape processing. Raw strings are ideal for regex patterns where backslashes should be treated literally (e.g., `r"^/api/v1(?:/|$)"` instead of `"^/api/v1(?:/|$)"`). Raw strings do not support interpolation.
- **Line continuation** — a backslash (`\`) at the end of a line joins it with the next line, allowing long directives to be split across multiple lines (e.g., `proxy http://localhost:3000 \` followed by `http://localhost:3001`).
- **Semicolons as optional delimiters** — semicolons (`;`) can now appear between statements and between host patterns as optional delimiters, providing compatibility with tools that generate semicolon-terminated output.
### Changed
#### Configuration format
- **Idiomatic configuration style** — documentation now uses the new ferronconf idiomatic style: bare strings without quotes, bare boolean flags (e.g., `io_uring` instead of `io_uring true`), raw string literals for regex patterns (`r"..."`), and consistent 4-space indentation.
- **Expanded bare string character set** — bare strings now accept `.`, `:`, `*`, `-`, `+`, `/`, `%`, `&`, `?`, `@` directly without quoting, reducing the need for quoted strings in directive values.
#### Default pages
- **Redesigned default error and directory listing pages** — the built-in error pages and static directory listings have been redesigned to match new Ferron 3 branding (see <https://ferron.sh/blog/ferron-new-look>). The installation landing page (`wwwroot`) has also been refreshed to match.
### Fixed
#### Configuration validation
- **Duplicate `observability` block warning fix** — fixed duplicate `observability` block warnings (they're now only displayed once) for `ferron doctor`.
## Ferron 3.0.0-beta.7
**Released in July 11, 2026**
### Added
#### Observability & tracing
- **Prometheus native histogram support** — the Prometheus observability backend now supports OpenMetrics native histograms alongside classic bucket histograms. Enable with `endpoint_native_histograms true` in the `observability.prometheus` block. Native histograms are only available in protobuf format; the text format continues to expose classic bucket histograms. Note that native histograms and exemplars are mutually exclusive for histograms — when native histograms are enabled, histogram exemplars are disabled.
- **Prometheus metric exemplars** — the Prometheus observability backend now emits OpenMetrics exemplars on counter and histogram metrics when request trace context is available. Each exemplar carries the `trace_id` and `span_id` of the request that triggered the observation, enabling direct correlation between metric spikes and distributed trace flame graphs in compatible observability backends (Grafana, Prometheus 2.23+). Exemplars are enabled by default for counters. For histograms, exemplars are active only when `endpoint_native_histograms` is `false` (the default), since native histograms and exemplars are mutually exclusive.
- **Configuration content hash** — new `ferron.admin.config_mtime` Gauge metric and `config_file_hash` / `config_file_mtime` fields on the `/status` Admin API endpoint expose the xxh3 content hash and last-modified time of the loaded configuration files. This enables rapid cluster-wide drift detection via a single PromQL query checking for hash mismatch across instances.
- **DNS cache TTL remaining gauge** — new `ferron.proxy.dns.cache_ttl_remaining_seconds` Gauge metric exposes aggregated remaining TTL for DNS cache entries (`min`, `max`, `avg` aggregation via `aggregation` attribute). A companion `ferron.proxy.dns.cache_entries` Gauge tracks the number of active cache entries. This allows operators to alert on impending DNS record expiration before backend connections break.
- **Pool connection limit metrics** — the `ferron.proxy.pool.local_limit` and `ferron.proxy.pool.global_limit` pool metrics are now available for monitoring connection pool usage.
- **Method and idempotency labels on retry metrics** — the `ferron.proxy.retry.count` Counter and `ferron.proxy.retry.final` Gauge now include `http.request.method` (categorized: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`, `HEAD`, `OPTIONS`, `CONNECT`, `TRACE`, `_other`) and `ferron.proxy.method_idempotent` (bool per RFC 9110 §9.2.2) labels, enabling zero-delay alerting on non-idempotent retry anomalies that indicate potential state corruption.
- **Selected backends per request metric** — the `ferron.proxy.backends.selected_per_request` Counter metric is now emitted for every proxied request, with `backend_url` and `backend_unix_path` attributes. This allows operators to see traffic amplification effects when multiple backends are selected for a single request.
#### Security
- **Rate limit throttling** — the `rate_limit` directive now supports a `throttle` subdirective that allows rate-limited requests to be delayed instead of rejected. When enabled, requests exceeding the configured rate limit are queued before being processed. This reduces the likelihood of client-side errors and improves user experience during temporary traffic spikes.
#### Reverse proxying
- **Token-bucket retry budget** — the new `retry_budget` directive implements a token-bucket-based retry budget that limits retries to a configurable fraction of steady-state traffic. When enabled alongside `retry_connection true`, retries consume tokens from a shared pool that is replenished by successful requests. If the retry budget is exhausted, further retries are refused and the request immediately returns `503 Service Unavailable`, preventing cascading retry storms from amplifying backend failures into a self-inflicted DDoS. Supports `max_retry_rate` (default: 10%), `max_tokens` (default: 10), and `refill_rate` (default: 2.0 tokens/sec) subdirectives. New metrics: `ferron.proxy.retry.budget_exhausted` (Counter) and `ferron.proxy.retry.budget_tokens_available` (Gauge).
#### HTTP server core
- **JSON error responses** — the new `json_errors` directive in the `http-jsonerror` module enables structured JSON error responses for HTTP 4xx/5xx errors. Supports both RFC 9457 `application/problem+json` (with `type`, `title`, `status`, `detail`, and optional `trace_id` fields) and simple `application/json` output formats. The `type` URI is configurable (default: `about:blank`), with support for `{status}` placeholder substitution (e.g., `https://http.dev/{status}`). Use `location /api { json_errors true }` to scope JSON errors to REST API endpoints. (`http-jsonerror`)
- **Client certificate common name variable** — the `mtls.cn` variable is now available for interpolations in the server configuration, allowing to use the client certificate's common name in routing rules and conditionals.
- **HTTP error page placeholders** — custom error page files now support optional placeholder substitution. Enable with `error_page_placeholders true`; the `{{trace.id}}` and `{{trace.spanid}}` placeholders are then replaced with the current request's trace ID and span ID before the page is served. Substitution reads the file into memory, bypassing the zerocopy/sendfile optimization for those responses.
- **Static file resolution tracing** — the file resolution process now emits a dedicated `ferron.pipeline.file_resolve` trace span with `ferron.file_resolve.request_path`, `ferron.file_resolve.root_path`, `ferron.file_resolve.outcome`, and `ferron.file_resolve.resolved_path` (success) or `ferron.file_resolve.last_candidate_path` (error) attributes. Resolution error paths (403 Forbidden, 400 Bad Request) now include `ferron.file_resolve.*` fields in access logs, replacing the previous "black box" behavior where the file resolution process produced no observability signals. Directory listings now emit `dir_path` trace span and access log attributes, and contribute to the `ferron.static.responses` counter metric with `ferron.static.outcome` values `"listing"`, `"listing_disabled"`, `"method_not_allowed"`, or `"options"`.
### Changed
#### Default pages
- **Redesigned default error and directory listing pages** — the built-in error pages and static directory listings have been redesigned with a modern, minimal, neutral style (system fonts, subtle accent gradient, responsive layout, and automatic light/dark mode). All styling remains inlined with no external assets. Default error pages now display the request's trace ID when trace context is available, improving debuggability. The installation landing page (`wwwroot`) has also been refreshed to match.
#### HTTP caching
- **Unified cache eviction log summaries** — the cache eviction log summaries are now unified to not include eviction reasons (they're already in log attributes).
#### Observability & tracing
- **Prometheus text format content-type** — the Prometheus text endpoint now serves `application/openmetrics-text; version=1.0.0` instead of `text/plain; version=0.0.4`, matching the OpenMetrics specification used by prometheus-client. Prometheus scrapers that do not support OpenMetrics content negotiation may need to be updated.
- **Native exponential histograms for OTLP latency metrics** — all OTLP histogram metrics now use Base2 Exponential Histograms instead of fixed linear buckets. This provides high-resolution percentile data across deep orders of magnitude (1ms to 100s), eliminating the need for manual bucket allocation and preserving tail-latency outliers that were previously masked by coarse bucketing. Affected metrics include `ferron.proxy.upstream.duration`, `ferron.proxy.pool.wait_time`, `ferron.proxy.tls.handshake_time`, `ferron.proxy.connect.latency`, `ferron.proxy.ttfb`, `http.server.request.duration`, and all other histogram instruments exported via OTLP.
- **QUIC client/server address logging** — QUIC connection error logs now include the client and server IP addresses, making it easier to trace connections and identify potential bottlenecks.
- **`stage.name` in traces renamed to `ferron.stage.name`** — renamed `stage.name` to `ferron.stage.name` traces to ensure consistency with other Ferron-specific attributes.
#### Reverse proxying
- **DNS thundering herd prevention** — the DNS resolver now uses a singleflight mechanism to prevent multiple concurrent DNS queries for the same hostname, reducing load on DNS servers and improving performance.
- **DNS resolution status metric attribute** — the `metrics_resolved_ip` directive now also emits a `ferron.proxy.dns_status` attribute on all proxy metrics and access logs when enabled. Values: `"resolved"` (DNS succeeded), `"nxdomain"` (domain not found), `"dns_error"` (other DNS failure), `"logical_dns"` (resolution deferred to connect time), `"static"` (DNS not applicable). This provides visibility into DNS resolution outcomes without requiring per-IP label explosion.
### Fixed
#### HTTP caching
- **Duplicate eviction metrics** — eviction metrics (and logs) were being emitted twice for cache hits, leading to incorrect metric values. This has been fixed.
#### Observability & tracing
- **Prometheus endpoint is no longer on-demand** — the Prometheus endpoint is now always available on the configured port, regardless of the number of active connections.
- **Reverse proxy metric naming consistency** — the `ferron.proxy.backend.excluded` metric name has been renamed to `ferron.proxy.backends.excluded` for consistency with other reverse proxy metrics.
## Ferron 3.0.0-beta.6
**Released in July 8, 2026**
### Added
#### Security
- **Admin API bearer token authentication** — the `admin` block now supports an `auth_token` directive for bearer token authentication on all admin API endpoints except `/health`. Clients must send `Authorization: Bearer <token>` header. The `/health` endpoint is exempt to allow load balancer and orchestrator probes.
- **Prometheus endpoint bearer token authentication** — the `observability.prometheus` block now supports an `endpoint_auth_token` directive for bearer token authentication on the `/metrics` scrape endpoint. Scrapers must send `Authorization: Bearer <token>` header.
#### Networking
- **Multipath TCP (MPTCP) support** — the `tcp` block now supports a `multipath` directive that enables Multipath TCP on the HTTP listener. MPTCP allows a single TCP connection to use multiple network interfaces simultaneously, improving throughput and connection resilience. When enabled, Ferron attempts to create an MPTCP socket; if the kernel does not support MPTCP or it is disabled, a warning is logged and the listener falls back to standard TCP. ([GitHub issue](https://github.com/ferronweb/ferron/issues/619))
#### Observability & tracing
- **Admin API request metrics** — the admin API now emits per-request metrics (`ferron.admin.request.duration`, `ferron.admin.request.count`) for all endpoints except `/health`, with `http.request.method`, `url.path`, and `http.response.status_code` attributes. A dedicated `ferron.admin.reload.count` counter tracks reload attempts.
- **Admin API structured logs** — the admin API now emits structured log events through the observability pipeline for config reloads (Info/Error) and config queries (Info). Health checks and status queries are not logged to avoid noise.
- **Prometheus scrape self-metrics** — the Prometheus endpoint now emits `ferron_prometheus_scrape_duration_seconds` (Histogram), `ferron_prometheus_scrape_total` (Counter), and `ferron_prometheus_scrape_errors_total` (Counter) for monitoring scrape performance.
- **Process identity in OTel resources** — the OTLP resource now automatically includes `process.pid` and `process.start_time` attributes, allowing observability backends to distinguish between concurrent and sequential process lifetimes. This prevents cumulative counters from adjacent process lifetimes from being visually interleaved in dashboards.
- **Status-code labels on cache store counters** — the `ferron.cache.stores` metric now includes an `http.response.status_code` attribute, allowing operators to directly query whether error responses are entering the cache layer without correlating timing across separate request and error series.
- **Cache key fingerprint in access logs** — the cache module now contributes a `ferron.cache.key_fingerprint` field to HTTP access log lines, containing a truncated representation of the cache key. This simplifies debugging why a specific request resulted in a cache miss.
- **Singleflight coalescing traceability in access logs** — the cache module now contributes `ferron.cache.coalesced` (bool) and `ferron.cache.coalesce_wait_duration_ms` (float) fields to HTTP access log lines, providing direct visibility into how long client requests are queuing inside the proxy during concurrent upstream revalidation.
- **Cache key components in OTel trace attributes** — the `ferron.stage.cache` span now includes `ferron.cache.key.uri`, `ferron.cache.key.method`, and `ferron.cache.key.evaluated_cookies` attributes, making trace flame graphs instantly diagnostic when evaluating hit-rate degradation caused by high-cardinality metadata variations.
- **Singleflight coordination metrics** — new `ferron.cache.coalesced_requests` (Counter) and `ferron.cache.singleflight_active_locks` (Gauge) metrics expose the efficiency of the request deduplication layer, providing visibility into how effectively singleflight handles concurrent thundering herds.
- **Eviction reason structured logs** — cache eviction events now emit debug-level structured log entries with `eviction.reason` (`ttl_expired` or `capacity_reached_lru`), `eviction.count`, and `ferron.cache.zone` attributes, providing precise eviction timelines alongside the existing cumulative metrics.
- **Cross-plane traceability** — the new `control_plane` directive allows embedding arbitrary key-value metadata and static OpenTelemetry span links in the server configuration at global, host, or location levels. Metadata values are automatically included as `ferron.control_plane.*` attributes on all observability signals (OTLP traces, logs, metrics, access logs) and as `[key=value]` prefixes in console and file logs. Span links establish causal relationships between control plane events and data plane traces without requiring a parent-child relationship. The most specific level wins (location > host > global), and metadata values support variable interpolation.
#### HTTP caching
- **`vary_cookies` directive** — the cache module now supports a `vary_cookies` directive that accepts an explicit list of cookie names for cache key differentiation. When set, only the listed cookies are included in the cache key, preventing high-entropy tracking or session cookies from fragmenting the cache and collapsing hit rates.
### Changed
#### Reverse proxy
- **`localhost` resolution in reverse proxy** — `localhost` is no longer exempt from STRICT_DNS-style DNS resolution; it is treated as a regular hostname and resolved via A and AAAA records.
### Fixed
#### Reverse proxy
- **Circuit breaker recording fix** — recent failures are now recorded correctly in the circuit breaker state, allowing circuit breaker logic to function as expected.
## Ferron 3.0.0-beta.5
**Released in July 5, 2026**
### Added
#### Security
- **Configurable symlink traversal protection** — the new `disable_symlinks` directive allows configuration of symlink handling during static file serving (`false`, `true`, or `if_not_owner` on Unix). When enabled, symlinks are detected without following them during path traversal, preventing symlink-based escape attacks from outside the configured webroot. Defaults to `false` for backward compatibility.
- **Error rate threshold** — the `abuse_protection` directive now supports an `error_rate_threshold` sub-directive that monitors HTTP response status codes. When a client triggers an abnormal number of configured error responses (e.g., 404, 403) within a time window, their IP is temporarily banned. This detects hostile scanning behavior such as probing for old vulnerabilities or non-existent plugin paths.
#### Observability & tracing
- **Per-host TLS handshake metrics** — new per-host metrics for TLS handshake duration (`ferron.tls.handshake.duration` histogram), handshake count (`ferron.tls.handshake.total` counter), and active connections (`ferron.tls.connections.active` up-down counter). Each metric carries `ferron.host`, `tls.protocol.version`, and `tls.cipher_suite` attributes, enabling per-host visibility into TLS performance and client compatibility.
- **TLS attributes on request traces** — the `ferron.request` root span now includes `tls.protocol.version` and `tls.cipher_suite` attributes for HTTPS connections, correlating TLS parameters with individual requests without requiring a separate TLS span.
- **Per-host OCSP metrics** — OCSP fetching metrics (`ferron.ocsp.fetches_total`, `ferron.ocsp.fetch_duration_seconds`) now include a `ferron.host` dimension, and a new `ferron.ocsp.stapling.hit_total` counter tracks per-request OCSP stapling outcomes per host.
- **Client and server IPs in error logs** — structured error logs for bad requests (400), request timeouts (408), TLS handshake failures, and TCP connection errors now include `client.address` and `server.address` attributes, enabling IP-based correlation and troubleshooting.
- **Load-balancer selection score metric** — new `ferron.proxy.lb.score` Gauge metric exposes the combined selection score used by the `two_random` and `p2c_ewma` load-balancing algorithms. For P2C-based algorithms, the two candidate scores compared during selection are captured and the winner's score is emitted per request. Lower scores indicate more preferred backends, enabling direct visibility into traffic distribution without manual TTFB correlation.
- **Upstream response truncation detection** — new `ferron.proxy.upstream.response_truncated` Counter metric and structured warning log detect when an upstream backend closes the response stream before the declared `Content-Length` bytes have been forwarded. The proxy tracks bytes received during body streaming and flags truncation when the stream ends prematurely. A structured warning log includes the backend URL, bytes received, and expected content length, enabling immediate visibility into backend issues that return partial responses with valid HTTP status codes. This does not capture response bodies; only the byte count and content length are tracked.
- **Latency-aware circuit breaker** — the `circuit_breaker` directive now supports an optional `latency_threshold` sub-directive. When configured, upstream responses exceeding this duration count as failures toward tripping the circuit, alongside transport failures and (optionally) `5xx` responses. This allows circuit breakers to eject pathologically slow backends that return successful status codes.
- **Circuit breaker flapping detection** — the `circuit_breaker` directive now supports `flapping_transitions` and `flapping_window` sub-directives for detecting rapidly oscillating upstream backends. When an upstream transitions between circuit breaker states more than `flapping_transitions` times (default: 3) within `flapping_window` (default: `10s`), a single warning log is emitted and the `ferron.proxy.circuit.flapping` Gauge metric is set to 1. Individual transition logs are suppressed while the upstream is flapping. When transitions stabilize, a recovery log is emitted and the metric resets to 0.
- **Circuit breaker slow-start** — the `circuit_breaker` directive now supports a `slow_start` duration sub-directive. When a backend transitions from half-open to closed (recovers), the load balancer applies a temporary virtual connection penalty that linearly decays over the configured duration (e.g., `slow_start 10s`), preventing thundering herd by preventing the recovered backend from being immediately overwhelmed with new connections.
- **Configurable metric cardinality control** — the `metrics_resolved_ip` directive (default: `false`) controls whether `ferron.proxy.backend_resolved_ip` is included in proxy metrics attributes. When disabled, metrics identify backends only by their configured URL, keeping cardinality low for environments with ephemeral IP addresses (e.g., Kubernetes). When enabled, each resolved IP becomes a distinct metric label value. This replaces the previous behavior of always emitting resolved IPs, which could cause cardinality explosion.
#### Reverse proxy
- **Weighted load balancing for all algorithms** — `random`, `two_random`, and `p2c_ewma` now support per-upstream `weight` directives for proportional traffic distribution, joining `round_robin` and `least_conn`. Higher weight values receive proportionally more requests across all five load balancing algorithms and session affinity.
- **Priority-based failover** — backends can now be assigned numeric priority values to implement tiered failover. Lower values indicate higher priority. When all backends in a tier are unavailable, the next tier is used as a fallback. SRV upstreams support priority via an additive offset applied to DNS SRV priorities.
- **SRV native weight support** — DNS SRV record weights are now used for weighted load balancing within priority tiers. Each backend's effective weight is `dns_weight × config_weight`. The config `weight` subdirective acts as a multiplier on top of DNS weights.
- **Strict DNS (A/AAAA) resolution** — static upstreams with hostnames now resolve A/AAAA records via Hickory DNS by default, creating a separate backend per resolved IP for per-IP load balancing, circuit breaking, and health checking. IP literals, `localhost`, Unix sockets, and `logical_dns true` upstreams are unaffected. Use `dns_servers` to specify custom resolver IPs.
- **DNS result caching** — resolved DNS results for both strict DNS (A/AAAA) and SRV upstreams are cached in memory with TTL-based expiry derived from the DNS response TTL. This avoids redundant DNS resolution and lock contention for high-traffic hostnames. Cache entries expire based on the minimum TTL from the DNS response records, with a 30-second fallback when no TTL is available.
- **HTTP upstream connection timeout** — the `connection_timeout` subdirective is now available for all upstreams, allowing configuration of the maximum time to establish a TCP connection to a backend. This prevents long hangs when backends are unreachable or slow to accept connections.
- **Health check logging** — health check initialization logs now include the HTTP method and URI in the log output, making it easier to debug and understand which health checks are being performed.
- **Upstream circuit transition logging** — circuit breaker state transitions (open/half-open/closed) now log the upstream address and duration of the open circuit, making it easier to diagnose and troubleshoot circuit breaker behavior.
#### Observability & tracing
- **Per-stage span attributes** — all HTTP pipeline stages now emit stage-specific span attributes on their `ferron.stage.<name>` spans, enabling detailed flame graph analysis without requiring metric export. Each module's per-stage span carries module-specific context such as upstream backend URLs, cache results, rate limit decisions, authentication outcomes, and response status codes. See each module's documentation for the full list of attributes and the [tracing reference](/docs/v3/configuration/observability/tracing#per-stage-spans).
- **Upstream state in client traces** — the `ferron.stage.reverse_proxy` span now includes upstream runtime state attributes (`ferron.proxy.upstream.circuit_state`, `ferron.proxy.upstream.is_flapping`, `ferron.proxy.upstream.health_status`, `ferron.proxy.upstream.consecutive_failures`, `ferron.proxy.upstream.active_connections`) correlating the selected backend's health, circuit breaker, and connection state with individual client requests for OTLP trace debugging.
- **Resolved IP address attributes** — reverse proxy metrics now include resolved IP addresses when strict DNS (A/AAAA) resolution is enabled. The `connect_to` field of `UpstreamInner` is exposed as `ferron.proxy.backend_resolved_ip` metric attribute for all backend-related metrics, allowing observability of the actual IP addresses backends are connected to. This complements existing `backend_url` and `backend_unix_path` attributes.
- **Module-injected access log fields** — all HTTP pipeline modules now emit debuggability attributes directly in access log lines, not just as trace span attributes. Proxy requests include `ferron.proxy.backend_url`, `ferron.proxy.connection_reused`, and `ferron.proxy.retry_count`. Cache results include `ferron.cache.result` and `ferron.cache.zone`. Rate limit decisions include `ferron.ratelimit.result` and `ferron.ratelimit.zone`. Auth outcomes include `ferron.basicauth.result` and `ferron.fauth.result`. Abuse protection includes `ferron.abuseban.action`. Static files include `ferron.static.file_path`. CGI/FastCGI/SCGI backends include their respective `backend_url` and `script_path` fields. See the [logging reference](/docs/v3/configuration/observability/logging#module-contributed-fields) for the full list.
- **Circuit breaker state in access logs** — access log lines for proxied requests now include `ferron.proxy.circuit_breaker_state` (`closed`, `open`, or `half_open`), and the `ferron.proxy.circuit.state` gauge is now emitted on every proxied request instead of only on state transitions. This ensures circuit breaker visibility in both log queries and metric dashboards.
### Changed
#### Static file serving
- **FD-based metadata and file handle reuse** — static file metadata is now obtained via `fstat`-like system calls instead of a separate `stat`-like calls, closing the TOCTOU window between path validation and metadata read. A per-thread file descriptor reuse pool with preemptive bulk expired removal eviction is implemented for handle reuse, improving performance. `HttpFileContext` now carries `file: ReusedFile` instead of `metadata: Metadata`, so all consumers derive metadata from the validated file handle.
- **Path resolution without canonicalization** — static file path resolution now uses component-level validation with simple PathBuf checks instead of costly filesystem canonicalization. This improves performance, eliminates path canonicalization overhead, and reduces the TOCTOU window by validating paths before symlink traversal checks. Combined with `disable_symlinks`, this implements nginx-style defense-in-depth protection.
#### Reverse proxy
- **Circuit breakers enabled by default** — the `circuit_breaker` directive is now enabled by default for all `proxy` configurations. Circuit breakers track transport failures (TCP connect errors, TLS errors) and optionally upstream `5xx` responses (when `record_5xx true`), then temporarily eject unstable backends from the load balancer. This change improves resilience by protecting against cascading failures without requiring explicit configuration. To disable circuit breakers, set `circuit_breaker false`.
- **`ferron.proxy.requests` metric with backend URL** — the `ferron.proxy.requests` counter metric now includes a backend URL (and related) attribute for all upstream requests, allowing observability of which backend URL handled each request.
#### Observability
- **Canonical IP addresses in logs and traces** - `client_ip_canonical` and `server_ip_canonical` are now available as OpenTelemetry-style log attributes and trace span attributes, replacing the legacy `client_ip`/`server_ip`.
- **Custom log fields** - custom log fields that have `.` in their names are used as-is in OTLP logs instead of being converted to `_` in the log attribute name. This allows for more flexible log field naming and avoids conflicts with existing attribute names.
### Fixed
#### Observability
- **Consistent log targets** - `tls-acme` and `tls-http` now use `ferron-tls-acme` and `ferron-tls-http` log targets respectively instead of `ferron_tls_acme` and `ferron_tls_http`.
- **Circuit breaker gauge value fix** - the `ferron.proxy.circuit.state` gauge now correctly uses `1` for `open` and `2` for `half_open`, matching the constant definitions. Previously these values were swapped.
## Ferron 3.0.0-beta.4
**Released in June 28, 2026**
### Added
#### HTTP caching
- **Cache zones** — the `cache` directive now supports a `zone` subdirective at host scope, allowing multiple hostnames to share a single in-memory cache store. Named zones can be pre-configured at global scope with custom `max_entries` capacity. When a global `cache { max_entries }` block exists without explicit `zone` blocks, all hosts share a global zone by default. Hosts can opt out with an explicit `zone` directive.
- **Generation-aware cache capacity** — `max_entries` is now only evaluated on configuration reload instead of on every request, preventing premature LRU eviction when different host blocks specify different capacities for the same zone.
- **Zone attribute in cache metrics** — all cache metrics (`ferron.cache.requests`, `ferron.cache.entries`, `ferron.cache.stores`, `ferron.cache.evictions`, `ferron.cache.purges`) now include a `ferron.cache.zone` attribute identifying the zone the request belongs to.
#### Rate limiting
- **Rate limit zones** — the `rate_limit` directive now supports a `zone` subdirective at host scope, allowing multiple hostnames to share the same rate limit token bucket registries. Named zones can be defined at global scope. When a global `rate_limit` block exists without explicit `zone` blocks, all hosts share a global zone by default. Hosts can opt out with their own `rate_limit` block.
- **Key extractor in fingerprint** — the rate limit fingerprint now includes the key extractor type (`ip`, `uri`, `header`), so rules with different key types no longer share the same registry.
- **Zone attribute in rate limit metrics** — rate limit metrics (`ferron.ratelimit.rejected`, `ferron.ratelimit.allowed`) and structured log events now include a `ferron.ratelimit.zone` attribute identifying the zone the request belongs to.
#### Static file serving
- **`If-Range` support** — The `If-Range` header is now supported for conditional range requests (RFC 7233 §3.2), allowing clients to receive a partial response when the entity tag or modification date matches, or a full response when the representation has changed.
### Fixed
#### Reverse proxy
- **SRV priority/weight swap fix** — SRV upstream resolution was reading the priority and weight fields in the wrong order, causing traffic to be routed to the wrong priority group. Priority and weight are now correctly interpreted per RFC 2782.
- **Upgrade connection pool leak fix** — HTTP 101 upgrade connections (WebSocket, etc.) were not decrementing the pool outstanding counter, permanently reducing available pool capacity by one per upgrade. The pool item now drops correctly, releasing the slot.
- **Hop-by-hop header stripping on requests** — the outgoing proxy request now strips hop-by-hop headers (`Connection`, `Keep-Alive`, `Transfer-Encoding`, `TE`, `Trailer`, `Proxy-Authorization`, `Proxy-Authenticate`) per RFC 7230 §6.1, preventing clients from injecting these headers to influence backend behavior.
#### Static file serving
- **HTTP range requests fix** — HTTP range requests now correctly handle out-of-bounds ranges, returning `206 Partial Content` with the available range instead of `416 Range Not Satisfiable` (per RFC 7233 §2.1).
- **On-the-fly compression allowed while using precompressed files** — When serving precompressed files, the content is now compressed on-the-fly if precompressed files are not available, improving performance and reducing disk usage.
- **Multipart byterange fix** — Multipart range responses now include the required `bytes` prefix in `Content-Range` part headers (per RFC 7233 §4.1) and correctly serve one additional byte per range to match inclusive-to-exclusive bounds (`is_end_stream` no longer signals end-of-stream while the last range's data is still being streamed).
- **`If-None-Match` POST fix** — POST requests with a matching `If-None-Match` header now correctly return `412 Precondition Failed` instead of `200 OK` (per RFC 7232 §4.2).
- **`If-Match: *` with non-GET/HEAD fix** — `If-Match: *` now correctly passes for POST and other non-GET/HEAD requests when a representation exists, per RFC 7232 §3.1.
- **Invalid Range header handling** — Syntactically invalid `Range` headers are now treated as absent (returning `200 OK`) instead of `416 Range Not Satisfiable`, per RFC 7233 §3.1.
- **`file_cache_control` panic fix** — Malformed `file_cache_control` config values can no longer panic the request handler; a validation warning now catches invalid characters.
- **Missing `Content-Length` for compressed responses** — Non-identity (compressed and precompressed) file responses now include the correct `Content-Length` header.
- **`bytes_sent` metric fix** — The `bytes_sent` metric now reports the actual compressed file size for precompressed responses instead of the original file size.
#### HTTP caching
- **Cache revalidation fix** - when a cached response is revalidated, the HTTP status code is now preserved from the cached response instead of always using `200 OK`.
- **Abuse protection fix** - 403 Forbidden responses generated by abuse protection module are no longer cached (as they are served before the cache).
- **Cache scope fix** - in the previous version (Ferron 3.0.0-beta.3), caches were per-host instead of global, even when maximum cache items subdirective was global-only. This has been fixed to restore the behavior before 3.0.0-beta.3.
- **Stale-while-revalidate inflight request fix** - when a stale response is revalidated, the inflight request handling no longer causes possible hangs.
- **`Expires` header in past edge case fix** - a response with a past `Expires` header and no `Cache-Control` directives was incorrectly cached for 5 minutes. This has been fixed to cache the response for 0 seconds (expires immediately) instead.
- **Stale-if-error correctness fix** - a stale response is no longer served when `must-revalidate` is set, per RFC 9111 §4.3.4.
- **Cache key fix** - an attacker could craft a cookie value containing `&cookie:` to collide with another user's cache key. This severe cache collision was fixed by using `\0` as the separator instead of `&` in the cache key.
- **304 revalidation TTL fix** - when a cached response is revalidated via a `304 Not Modified` response, the stored response's TTL, `stale-while-revalidate`, `stale-if-error`, and `must-revalidate` directives are now updated from the 304 response's `Cache-Control` headers per RFC 9111 §4.3.4. Previously only `ETag` and `Last-Modified` validators were updated, causing the original TTL to persist even when the upstream sent a different `max-age`.
- **Cache variant metadata leak fix** - the `variants_by_base` map used for cache lookups previously grew without bound, retaining variant records for all URLs ever cached even after entries were purged. This map is now cleaned up when all entries for a base key are removed via purge.
## Ferron 3.0.0-beta.3
**Released in June 26, 2026**
### Breaking changes
If you are upgrading to this beta version, you must update your configuration files to accommodate the following syntax refactors:
- **Removed `force_trace` directive** - the `force_trace` directive has been removed as trace context creation is now always enabled for every request. Configurations using `force_trace true` should remove the directive; configurations using `force_trace false` should also remove it since the behavior now matches the default.
- **Default text log format changed** - the default text access log format has been changed from Combined Log Format to Enhanced Combined Log Format, which adds `Host` header and trace ID fields. If you rely on the default text log format and want to keep the previous behavior, explicitly set the pattern to the Combined Log Format using `access_pattern "%client_ip - %auth_user [%t] \"%method %path_and_query %version\" %status %content_length \"%{Referer}i\" \"%{User-Agent}i\""` in your `log` block.
- **Circuit breaker `5xx` responses no longer counted by default** - upstream `5xx` responses no longer trip the circuit breaker unless `record_5xx true` is explicitly set. This makes the circuit breaker purely transport-failure-based by default, giving operators explicit control over Layer 7 error sensitivity.
- **`passive_check` directive removed** - the `passive_check` directive has been replaced with a more generic `circuit_breaker` directive that supports passive health checking and circuit breaking for both HTTP and gRPC services.
### Added
#### Modules
- **`http-variables`** - a new module for setting interpolation variables based on request conditions and mapping variables to custom access log fields.
#### Security
- **Custom abuse event registration** - it's now possible to register custom abuse events, for example, for honeypots.
#### Observability & metrics
- **Reload metrics and logs** - the new `metrics-reload` module emits application log events and a `ferron.reloads` counter metric around configuration reloads, with a `ferron.reload.successful` attribute indicating success or failure and an `error.message` attribute on failures.
- **Abuse ban logging** - the abuse ban logger now logs at the WARN level when an IP is triggered for banning, including the banned IP address and reason.
#### HTTP server core
- **`set_var` directive** - new directive for setting interpolation variables based on regex matching against request attributes, with optional `value`, `case_insensitive`, and `negate` subdirectives. (`http-variables`)
- **`log_field` directive** - new directive for mapping any resolvable variable (including interpolated strings) to custom access log field names, evaluated after the response is generated. (`http-variables`)
- **Support for date-based caching** - the `If-Modified-Since` header is now supported for static file responses, allowing clients to cache responses and avoid unnecessary re-downloads when the file has not been modified.
- **Auth user variable support** - the `auth.user` variable is now available in variable interpolations for the HTTP server.
- **Trace ID and span ID variable support** - the `trace.id` and `trace.spanid` variables are now available in variable interpolations for the HTTP server.
#### HTTP caching
- **RFC 9111 conditional request revalidation** - when a client sends `Cache-Control: max-age=0` or `no-cache`, the HTTP cache now performs conditional revalidation using stored `ETag` and `Last-Modified` validators instead of bypassing the cache entirely. If the upstream returns `304 Not Modified`, the cached response body is served with fresh headers. If the upstream returns `200 OK` (content changed), the cached entry is replaced. (`http-cache`)
- **`stale-while-revalidate` support** - when a cached response's `max-age` has expired but a `stale-while-revalidate` directive is present, the stale response is served immediately to the client while the cache entry remains available within the stale window. This avoids latency spikes for expired cache entries. (`http-cache`)
- **`stale-if-error` support** - when an upstream revalidation request returns a `5xx` error and a `stale-if-error` directive is present on the cached response, the stale cached response is served instead of the error. This provides resilience against backend failures. (`http-cache`)
- **`must-revalidate` and `proxy-revalidate` enforcement** - entries with `must-revalidate` or `proxy-revalidate` directives (or `s-maxage`, which implies `proxy-revalidate`) are never served stale, even within `stale-while-revalidate` or `stale-if-error` windows. (`http-cache`)
- **Ignore request cache control** - when `ignore_request_cache_control` in `cache` is enabled, request-based cache control (e.g., `Cache-Control: no-cache`) is ignored in favor of the configured cache policy.
- **Cache purge propagation** - when a cache purge occurs (via `PURGE` method or `X-LiteSpeed-Purge` header), the purge can be propagated to other instances via an external control-plane service. Edge instances send webhook POST requests to a configured control-plane URL, which broadcasts `PURGE` requests to all other registered edges. Loop prevention is handled via the `X-Purge-Source: propagation` header and origin exclusion. (`http-cache`)
#### Automatic TLS
- **HTTP auth for on-demand ask endpoint** - when `on_demand_ask_auth` is configured, HTTP Basic Auth credentials are used for the on-demand ask endpoint, allowing for secure access to the endpoint.
- **Fallback ACME providers** — the `fallback` directive allows configuring multiple ACME providers with sequential failover. When the primary provider fails (e.g., CA outage), Ferron automatically tries the next configured fallback. Each provider maintains its own cached credentials. (`tls-acme`)
#### HTTP TLS provider
- **On-demand certificate fetching** — the `tls-http` module now supports on-demand (lazy) certificate retrieval. When `on_demand true` is set in a wildcard host block (e.g., `*:443`), certificates are fetched on the first TLS handshake for each SNI hostname, rather than polling a single endpoint at startup.
- **On-demand approval endpoint** — the new `on_demand_ask`, `on_demand_ask_auth`, and `on_demand_ask_no_verification` directives control an authorization endpoint that is consulted before fetching a certificate for a hostname. The hostname is sent as a `?domain=<encoded>` query parameter.
- **Per-SNI certificate refresh** — on-demand fetched certificates are individually refreshed at the configured `refresh_interval`, ensuring certificate rotation is applied per hostname without affecting other SNIs.
#### Utilities
- **Formatter utility** - a new `ferron-fmt` command-line tool is available for formatting and validating Ferron configuration files.
### Changed
#### Admin API
- **Reload failure status** - the `/reload` endpoint now returns an error message in the response body when reload fails. Previously, it only indicated that reloading was initiated without any details about why it failed.
#### Observability & logging
- **Improved error reporting in structured logs** - reverse proxy, HTTP, TCP and QUIC error logs now include an `error.message` attribute when available. Also, some of the reverse proxy error log summaries being overly long have been fixed.
- **OCSP response caching log improvements** - OCSP response caching logs are now emitted at info level by default. The `ferron.ocsp.cert.primary_san` attribute is also included in the log message when available.
- **Trace context always enabled** - the `force_trace` directive has been removed, as trace context creation is now always enabled for every request. Additionally, when no tracing export is configured, Ferron will not generate a new span ID for incoming requests, avoiding unnecessary overhead.
#### TLS & ACME
- **ACME provisioning cycle log improvements** - ACME provisioning logs are now no longer emitted at debug level when a certificate is still valid or loaded from cache, as these messages were redundant with the `ferron.acme.domains` attribute already included in the main log message.
#### Proxying
- **Forward proxying CONNECT cycle log improvements** - CONNECT connection closure logs are now no longer emitted at debug level when a tunnel is closed to reduce noise in the application logs.
- **Circuit breaker `record_5xx` toggle** - the `circuit_breaker` directive now supports a `record_5xx` subdirective (default: `false`) to control whether upstream `5xx` responses count toward tripping the circuit.
### Fixed
#### Observability & logging
- **HTTP/2 and /3 host header log visbility fix** - HTTP/2 and HTTP/3 requests now log the `host` header correctly, ensuring visibility into the host name of the server that handled the request.
#### TLS
- **OCSP stapling ECDSA issuer fix** - when using ECDSA certificates for OCSP stapling, the issuer OID was not being correctly extracted from the certificate, leading to incorrect validation. This has been fixed to ensure the issuer OID is extracted correctly and used for OCSP validation.
- **TLS certificate provider inheritance fix** - When using TLS certificates provided by a parent configuration, the certificate provider was incorrectly inherited, leading to errors when attempting to use these certificates in child configurations. This has been fixed to ensure that the certificate provider is correctly inherited and available for use in all child configurations.
#### HTTP server core
- **Error configuration routing fix** - error configuration routing were resolved with a wrong configuration, when using a wildcard hostname in the configuration, even when no error configuration is set. This has been fixed to ensure wildcard hostnames are correctly handled.
#### Server startup
- **Critical Unix daemon initialization failure fix** - when initializing as a Unix daemon, the forked process would exit due to logging re-initialization issues. This has been fixed to ensure that the parent process continues running even in such cases. ([GitHub issue](https://github.com/ferronweb/ferron/issues/774))
## Ferron 3.0.0-beta.2
**Released in June 14, 2026**
### Breaking changes
If you are upgrading to this beta version, you must update your configuration files to accommodate the following syntax refactors:
- **Rate limit windows** - syntax updated to enforce standard duration strings (e.g., `10s`, `5m`, `1h`).
- **OTLP verification** - `no_verify` has been renamed to `no_verification` and now operates strictly as a configuration flag.
- **OTLP defaults** - OTLP sink now uses `log_style modern` default, which may break existing configurations with custom OTLP log formats.
- **Proxy configuration** - syntax for passive/active health checks, load balancing algorithms, and connection retries has been unified into a cleaner, more consistent format.
- **Incoming trace context discarded by default** - Ferron no longer trusts incoming `traceparent`, `tracestate`, or `baggage` headers unless `trust_request true` is explicitly set in the `trace` block. Previously, incoming trace context was always parsed and used as the parent span. To restore the old behavior, add `trust_request true` to your `http { trace { ... } }` block.
### Added
#### Modules
- **`http-abuseban`** - a new module for lightweight, native Fail2ban-like IP banning with temporary lockouts based on rate limit breaches and brute-force failures.
- **`http-traceid`** - a new module that injects the current request's trace ID into HTTP response headers (configurable header name, optional on-demand reflection via `X-Ferron-Trace-Reflect`).
- **`tls-http`** - support for obtaining TLS certificates from a remote HTTP endpoint, featuring automatic refresh cycles and dedicated observability metrics.
- **OS metrics** - added Windows support for native process metrics in the `metrics-process` module.
#### Reverse proxy & load balancing
- **`circuit_breaker`** - new directive with rolling failure windows, temporary backend ejection, and half-open recovery states.
- **`p2c_ewma`** - a new adaptive, latency-aware load balancing algorithm combining Power of Two Choices with Exponentially Weighted Moving Average (EWMA) latency scoring.
- **Session affinity (sticky sessions)** - native support for `cookie`, `header`, `ip`, and `hash` types utilizing a Ketama-style hash ring for deterministic backend routing.
- **String interpolation** - upstream URLs and Unix socket paths now support interpolated strings.
- **SRV routing** - added active health check support for SRV upstream URLs.
- **Outbound mTLS** - reverse proxy upstreams now support presenting client certificates (`cert` and `key` subdirectives) for mutual TLS authentication with HTTPS backends, including per-upstream credential scoping and SRV upstream support.
#### DNS & ACME
- **58 new DNS providers** - native ACME DNS-01 challenge support expanded to include Alibaba Cloud DNS, Azure DNS, ClouDNS, Hetzner DNS, Oracle Cloud DNS, Vercel, Vultr, Yandex Cloud DNS, and 50+ others.
- **CLI arguments in post-obtain command** - added support for passing CLI arguments to the post-obtain command in automatic TLS configurations.
#### HTTP server core
- **`basic_auth_concurrency`** - global directive to limit concurrent, resource-heavy password verification tasks across all `basic_auth` blocks.
- **Cache purging** - native `PURGE` HTTP method support for targeted cache invalidation via the `purge_method` and `purge_allowed_ips` subdirectives.
- **`force_trace`** - global directive to force trace context creation for every request even when tracing is not explicitly enabled by a module, useful for debugging or log correlation.
- **`trust_request`** - new directive in the `trace` block to accept incoming `traceparent`, `tracestate`, and `baggage` headers as the parent trace context. Default: `false`. When disabled (default), incoming trace headers are discarded and Ferron generates a fresh trace ID for each request.
- **Optimal server-preferred compression algorithm** - now supports automatic detection of the optimal compression algorithm based on client preferences ([GitHub issue](https://github.com/ferronweb/ferron/issues/709)).
#### Authentication
- **Forwarded authentication backend chaining** - the forwarded authentication module now supports chaining multiple backends together, allowing for flexible and secure multi-backend authentication scenarios.
#### Observability & metrics
- **Edge-case visibility** - granular HTTP observability metrics for pre-handler failures, server redirects, client-IP rewrites, CORS preflights, connection lifecycle failures, forward-proxy outcomes, reverse-proxy failures, and static-file response outcomes.
- **Admin sinks** - added a dropped-events admin metric for non-blocking observability sinks.
- **Unix file descriptor metrics** - added `process.unix.file_descriptor.count` (UpDownCounter) to track the change in number of unix file descriptors since the last measurement. (Linux only)
- **Backend exclusion tracking** - new `ferron.proxy.backend.excluded` counter that records each exclusion event with `backend_url` and `reason` attributes, covering passive-check failures, open circuit breakers, retry exclusion, and overloaded backends.
- **Retry observability** - new `ferron.proxy.retry.count` counter and `ferron.proxy.retry.final` gauge to track retry attempts and whether the final attempt succeeded.
- **Connection pool metrics** - new `ferron.proxy.pool.hit` and `ferron.proxy.pool.miss` counters, plus `ferron.proxy.pool.idle` and `ferron.proxy.pool.outstanding` worker-scoped gauges for per-upstream connection pool depth monitoring.
- **Connect latency & TTFB** - new `ferron.proxy.connect.latency` and `ferron.proxy.ttfb` histograms measuring TCP connection establishment time and time to first response byte.
- **Health check instrumentation** - new `ferron.proxy.health.success`, `ferron.proxy.health.failure`, and `ferron.proxy.health.duration` metrics for active health probe outcomes.
- **Circuit breaker metrics** - new `ferron.proxy.circuit.state` gauge (0/1/2 for Closed/HalfOpen/Open) and `ferron.proxy.circuit.open_total` counter tracking circuit breaker state transitions.
- **Unified certificate expiration gauge** - a single `ferron.tls.certificate_not_after` gauge is emitted by every TLS provider (`manual`, `acme`, `http`, `local`) whenever a certificate is mounted into the in-memory rustls context. The value is the certificate `notAfter` field as Unix epoch seconds; attributes are `ferron.host` (SNI hostname or IP), `ferron.tls.provider` (provider name), and `crypto.certificate.serial_number` (lowercase hex). Replaces the previous provider-specific expiration gauges.
- **Baggage promotion** - new `baggage` sub-directive in observability backend blocks (`otlp`, `prometheus`) to promote specific W3C Baggage keys into telemetry attributes for logs, metrics, and traces. Supports per-signal filtering and a `max_distinct` cap to prevent high-cardinality metric label explosion.
- **Baggage propagation** - Baggage is now propagated from incoming requests to outgoing ones, allowing distributed tracing context to be preserved across service boundaries.
- **CGI/FastCGI/SCGI observability metrics** - added request count, failure count, upstream duration, and stderr error metrics for the `http-fcgi`, `http-cgi`, and `http-scgi` modules. FastCGI also exposes connection pool wait and upstream duration metrics; CGI exposes process duration and exit code metrics; SCGI exposes upstream duration metrics.
- **`log_style` directive** - new `log_style legacy|modern` directive in the OTLP observability backend block to opt into OTEL-style structured logs. In `modern` mode, each log record publishes a short `summary` body plus typed per-event attributes (string, bool, integer, float) instead of the human-readable message. Access logs are remapped to OTEL semantic conventions (`url.path`, `http.request.method`, `http.response.status_code`, `client.address`, `http.server.request.duration`, etc.). The default `legacy` mode preserves existing behavior.
- **Application log formatters via `error_format`** - the `error_log` directive and `provider file` now support an `error_format` subdirective to choose how application (error) log messages are formatted. The `text` formatter (default) produces human-readable lines with timestamp, level, optional trace ID, and message. The `json` formatter produces structured JSON records with `summary`, `level`, `target`, `attributes`, and `trace_context` fields. Formatter implementations are provided by the `logformat-text` and `logformat-json` modules respectively.
- **Logs for OTLP errors** - Errors with the logs, metrics and traces providers are now logged.
#### Admin API
- Added `GET /reload` and `GET /runtime` endpoints to the admin listener.
#### Configuration syntax
- **Globs in `include` directives** - `include "path.conf"` now supports glob patterns, e.g. `include "/etc/ferron/conf.d/**/*.conf"`.
#### Configuration validation
- **Best-practice enforcement** - `ferron doctor` subcommand for validating best practices across the configuration.
- **Print "all good" message** - a message is now printed when the configuration is valid with no diagnostics.
- JSON configuration validation results are now printed to stdout when `--json` is specified, improving observability for automated tools and CI/CD pipelines.
- Unused subdirectives are now reported as well as directives.
#### Docker
- **Entrypoint configuration** - Docker images now include a dedicated entrypoint config with JSON console logging, trace ID forcing, and an `include /etc/ferron/conf.d/**/*.conf` directive for user-provided split configurations.
### Changed
#### Reverse proxy
- **Weighted load balancing** - both `least_conn` and `round_robin` algorithms now support per-upstream `weight` directives for proportional traffic distribution.
- **Metric attributes** - multiple HTTP reverse proxy metrics now embed the specific backend URL or Unix socket path as a context attribute.
#### DNS & TLS
- **Early OCSP verification** - OCSP responses are now strictly verified before being cached and stapled.
- **Verbose errors** - significantly improved error reporting layouts for local automatic TLS and specific TLS handshake failures.
- **`/var/cache/ferron-acme` prioritized over user's cache dir** - for Docker images with Ferron running as `root`, the ACME cache directory is now `/var/cache/ferron-acme`, which takes precedence over user's ACME cache directory.
#### HTTP server core
- **Safe configuration reloads** - configuration failures during a live reload no longer crash or stop the server; errors are logged, and execution safely continues using the previous valid configuration.
- **Smarter brute-force locking** - protection now locks by **IP address** instead of username, preventing malicious actors from intentionally locking out legitimate users.
- **Accurate status codes** - refactored error handling to return context-aware status codes over generic `500` or `404` errors:
- File pipeline returns `408 Request Timeout` if a request takes too long (instead of `404`).
- Basic Auth returns `429 Too Many Requests` when max failed attempts are reached.
- File serving errors return `403 Forbidden` (permissions) or `400 Bad Request` (invalid filenames) instead of `500`.
- **Support for multi-range partial static file serving** - now supports serving multiple ranges from a single request, improving efficiency and reducing latency.
- **String interpolation** - forwarded authentication now supports interpolated string values for backend URLs.
- **Security tightening** - URL canonicalization now strictly rejects paths containing null bytes (`\0` or `%00`).
- **Cache cleanliness** - `X-LiteSpeed-Cache` headers are no longer emitted by default; can be re-enabled via the `emit_litespeed_headers` subdirective.
#### Observability & tracing
- **Unified request tracing** - HTTP tracing now rolls up into a single `ferron.request` root span with nested child spans for pipelines, stages, file-serving, and error pipelines.
- **Log correlation** - OTLP request and access logs now include the active request span context out of the box.
- **Backend exporting** - admin API metrics are now also pushed directly to configured observability backends, not just the local admin endpoint.
- **Cardinality control** - Prometheus label values are now sanitized to heavily reduce high-cardinality label inflation.
- **Structured log events** - every log emission site now carries a short OTEL-friendly `summary` plus typed `attributes`, enabling downstream OTLP consumers to receive structured events without changing existing console or file log output.
- **Trace ID in console and file logs** - console and file loggers now prefix log messages with `[trace=<span_id>]` when a trace context is available, enabling grep-based filtering by trace ID.
- **Trace context injection for CGI/FastCGI/PHP-FPM/SCGI** - trace context headers are now automatically propagated to the backend environment via CGI/FastCGI/PHP-FPM/SCGI.
- **Incoming trace context discarded by default** - Ferron now generates a fresh trace ID for every request by default, discarding incoming `traceparent`/`tracestate`/`baggage` headers unless `trust_request true` is configured. Existing deployments relying on incoming trace context must add `trust_request true` to the `trace` block.
- **Sanitized trace header injection** - `inject_trace_headers` now removes any existing `traceparent`, `tracestate`, and `baggage` headers before injecting new values, preventing header duplication or conflicts.
#### Core runtime
- **Unified durations** - improved configuration-wide consistency for duration formatting values.
- **Graceful shutdown** - the server process now handles standard Unix `SIGTERM` signals for seamless graceful shutdowns.
- **Frictionless local TLS** - the server now issues a clean warning if local automatic TLS is configured but the cache directory isn't writable, instead of refusing to boot.
#### Docker
- **Split configuration layout** - Docker images now use a two-file layout: `/etc/ferron/ferron.conf` as the entrypoint config and `/etc/ferron/conf.d/` for user configuration files. Docker Compose bind mounts now target the `conf.d/` directory.
#### Package configuration
- **Log rotation and structured logging** - Default package configurations now include log rotation settings and commented-out structured logging and trace ID format options.
### Fixed
#### Reverse proxy
- Fixed a bug where pool limits weren't being respected when pulling connections from the pool, leading to possible handle exhaustion.
- Fixed a bug where reverse proxy boolean subdirectives with empty values (implying `true`) were being ignored.
- Fixed an issue where the proxy failed to strip headers specified by the `Connection` header per RFC 7230.
#### DNS & ACME
- Fixed an "invalid socket address" error that broke RFC 2136 dynamic DNS updates for the ACME DNS-01 challenge.
- Resolved an edge case where OCSP stapling failed to immediately fetch the response after new certificates were registered.
#### HTTP server core
- **Auth bypass closed** - fixed a critical flaw where a misconfigured forwarded authentication block could result in bypassing authentication entirely.
- **Cache thundering herd fixed** - implemented request coalescing for cache misses to prevent thundering herd scenarios when multiple requests hit the same uncached resource simultaneously.
- **Several HTTP/1.x protocol handling bugs fixed** - fixed chunked-length encoding DoS vulnerability and other protocol-related issues (see [`vibeio-http` changelog](https://github.com/ferronweb/vibeio-http/blob/main/CHANGELOG.md#vibeio-http-032)).
- Fixed a `500 Internal Server Error` when using the `auth_to { ... }` syntax inside forwarded authentication blocks.
- Fixed a bug where case-insensitive HTTP cache control directives were not recognized correctly.
- Fixed a bug where CONNECT requests with authority-form URIs were erroneously blocked by the URL canonicalizer.
- Fixed a bug where HTTP connections couldn't be accepted on 32-bit Windows (see [`vibeio` changelog](https://github.com/ferronweb/vibeio/blob/main/CHANGELOG.md#vibeio-0213); [GitHub issue](https://github.com/ferronweb/ferron/issues/662)).
- Fixed a bug where HTTP connections were accepted by only one OS thread on Windows.
- Fixed a bug where HTTP timeout durations were not being respected correctly.
- Fixed a bug where TLS certificate resolver from domain name level higher (non-wildcard) was incorrectly used for TLS handshakes if the one from the domain name level matching the requested SNI isn't present.
- Fixed HTTP-to-HTTPS redirects to correctly target the original requested URL rather than internal rewritten URLs.
#### Observability
- **Log injection fixed** - implemented strict sanitization of log fields to prevent potential log injection attacks for plain-text logs via malicious header values or other user input.
- Fixed a data blind spot where malformed and timed-out requests rejected before normal handler completion went uncounted by the observability pipeline.
- The server now removes sensitive HTTP headers from access logs (such as `cookie`, `authorization`), to prevent sensitive data exposure in log output.
- Corrected inaccurate memory metrics that were calculating values relative to initial memory usage instead of absolute usage.
#### Security
- **DNS rebinding fixed** - closed a vulnerability where forward-proxy DNS validation could be bypassed via a race condition combined with a DNS rebinding attack.
- Fixed an issue where forward-proxy allowed ports and denied IP addresses were being treated as additive rather than strictly respecting user configuration overrides.
#### Runtime operations
- Fixed an Admin API-initiated reload loop that caused infinite configuration reload loops.
- Fixed admin API over-redacting configuration directives in `/config` endpoint responses, which could lead to confusion when verifying configuration values.
- Eliminated a rate limiting race condition when initializing a brand new key bucket, which previously allowed traffic to briefly exceed configured capacities.
- Fixed manual TLS session ticket key rotation to properly read from configured key files instead of silently falling back to in-memory generation.
- Fixed a bug on Linux where `io_uring` could not be explicitly disabled through the server configuration file.
- Fixed an edge case where cached responses replaced by non-cached default error pages could be returned stale.
## Ferron 3.0.0-beta.1
**Released in May 5, 2026**
### Added
- CLI utility for hashing passwords.
- CLI utility for pre-compressing static files.
- CLI utility for translating Ferron 2 configurations into Ferron 3 ones.
- CLI utility for zero-configuration serving.
### Changed
- Non-existent webroots now lead to 404 Not Found errors instead of 500 Internal Server Error errors.
### Fixed
- Partial hostname resolution match in HTTP server could lead to incorrect routing.
- Redirects configured with `status` directive didn't have some placeholder locations (such as `$1`) replaced when using a regex match.
- Redirects configured with `status` directive didn't lead to any destination.
- Reverse proxy was sometimes routed to wrong backend server.
- Some default cache paths were unwritable in some cases.
- Unknown directives in global blocks for `status` directive (even though they're known in host blocks) caused the web server to fail to start.
- When using OTLP, access logs were emitted with "access_log" body, not actual access logs.
## Ferron 3.0.0-alpha.3
**Released in April 23, 2026**
### Added
- Multiple DNS providers for DNS-01 ACME challenge.
- Support for CGI (Common Gateway Interface).
- Support for FastCGI (including PHP-FPM).
- Support for forwarded authentication.
- Support for SCGI (Simple Common Gateway Interface).
### Fixed
- Connections to HTTP/2 backends in reverse proxy were aborted.
- Error interception configuration blocks weren't applied properly.
- HTTP to HTTPS redirect wasn't enabled by default.
- HTTP upgrades weren't performed properly.
- HTTP-01 ACME challenge failed due to challenge not being served for implicit automatic TLS.
- On-demand automatic TLS wasn't functioning correctly.
- Some ACME events were logged only to the console, not observability backends.
- TLS certificates with local CA weren't checked if they're expired.
## Ferron 3.0.0-alpha.2
**Released in April 17, 2026**
### Added
- Active health checking in reverse proxy support.
- Automatic TLS with local certificate authority (CA).
- Experimental HTTP/3 support.
- `map` directive for mapping variables.
- Prometheus metrics export support.
- Response body string replacement support.
- Support for body interpolation in `status` directives.
- Support for interpolated strings in header values.
- W3C Trace Context (traceparent and tracestate) propagation and generation.
### Changed
- Improved the request URL normalization.
- Requests with multiple Host headers are now rejected.
### Fixed
- PROXY protocol setting, connection retry setting and error interception weren't working for reverse proxy.
- Zerocopy static file serving wasn't working properly on Linux, because it wasn't enabled.
## Ferron 3.0.0-alpha.1
**Released in April 10, 2026**
### Changed
- First alpha release of Ferron 3.