Breaking changes
Static file serving
ferron.static.bytes_sentandferron.static.files_servedmetrics: 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) inmapdirective, allowing mapping multiple values at once into one value.
Changed
HTTP server core
httpblock inheritance: a host block now inherits each globalhttpdirective that it does not set itself. Configurations that relied on a host block replacing the globalhttpblock see the global values apply again for directives the host omits.
Gateway interfaces
REMOTE_ADDRandSERVER_ADDRenvironment variables:REMOTE_ADDRandSERVER_ADDRenvironment 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-ForandForwardedheaders 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_hintsblock was configured with multipleLinkheader 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
traceandtrace_samplingfix: previously,http { trace { generate, trust_request } }andhttp { 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_proxyfix: 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 theferron doctorcheck for it never ran. Ferron now reads the setting from the globalhttpblock or from the host block without a hostname, and reports the best-practice violation.protocolsvalidation fix:ferron validatedid not checkhttp { protocols }values because it looked for the directive in the enclosing block instead of thehttpblock. Invalid values such ash4are now reported as a configuration error.options_allowed_methodsfix: the directive never reached the response.options_allowed_methodsresponse header fix: previously, a resolved value that could not be sent as anAllowheader, such as an interpolated value carrying a line break, panicked the worker task and dropped the connection for everyOPTIONS *request. Ferron now falls back to the default method list and reports an unusable configured value withferron validate.control_plane.span_links.sampledboolean flag fix: previously, the bare directive form read asfalseinstead oftrue, andferron validaterejected it. Ferron now reads the flag like every other boolean directive.basic_authrealm response header fix: previously, arealmvalue containing a control byte or a line break panicked the worker task on every401or407response, because the value is embedded in the authentication challenge header. Ferron now answers without a challenge header and reports an unusablerealmwithferron validate.- Conditional response header fix: previously, a
file_cache_controlvalue that could not be sent as aCache-Controlheader panicked the worker task on the304and412precondition paths. Ferron now omits the header and reports an unusable value withferron validate. It lives inside thehttpblock, but Ferron read it as a top-level directive of the resolved configuration, so everyOPTIONS *response used the built-in defaultAllowlist.ferron validateandferron doctoraccepted the directive, which made the failure silent. httpblock inheritance fix: a host block that set anyhttpdirective replaced the whole globalhttpblock, so every global setting the host did not repeat was dropped. For example, a host that only settimeoutlost the globalprotocols,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_sizeandh2_initial_window_sizeaccepted values outside the ranges that HTTP/2 requires, so Ferron could advertise aSETTINGSframe that conforming clients reject.ferron validatenow reports these values. h3_qpack_blocked_streamsusage fix: the directive takes a stream count, butferron directivesreported it as a boolean flag.
Observability
- Baggage with
trust_request falsefix: previously, whentrust_request falsewas set, the server would still propagate baggage from incoming requests to outgoing requests. Now, baggage is not propagated whentrust_request falseis set. - Host
observabilityblock fix: previously, when specifying multipleobservabilityblocks inside a host block with IP address or hostname, only the last one was effective. This has been fixed to use allobservabilityblocks, just like with wildcard host blocks. - Prometheus scrape metrics fix: the
ferron_prometheus_scrape_totalandferron_prometheus_scrape_errors_totalmetrics have been renamed toferron_prometheus_scrapeandferron_prometheus_scrape_errors, respectively (previous behavior would involve_total_totalsuffix). - Circuit breaker metric types fix:
ferron.proxy.circuit.open_totalandferron.proxy.circuit.half_open_timeoutswere 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_totalnow reports as a gauge that rises when a circuit opens and falls when it closes or moves to HalfOpen, andhalf_open_timeoutsreports 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
503for 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_statusattribute consistency fix: previously, backend scoped proxy metrics were emitted with different attribute sets depending on the code path.ferron.proxy.circuit.stateandferron.proxy.circuit.flappingwere exported as two separate Prometheus series for the same backend, one withferron.proxy.dns_statusand one without, so any aggregation over the metric counted a backend twice and the two series could disagree. Ferron now always reportsferron.proxy.dns_statuson backend scoped proxy metrics and only gatesferron.proxy.backend_resolved_ipbehindmetrics_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
bcryptpassword hash fix: Ferron 3.0.0-rc.8 added support forbcryptpassword 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 (butcacheandcache { ... }did), which could cause debugging friction. ferron.cache.evictions{reason="expired"}metric fix: previously, theferron.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,
PURGErequests 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_typeresponse header fix: previously, amime_typevalue that could not be sent as aContent-Typeheader, 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 validateaccepted 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/byterangespart header fix: previously, themime_typevalue was written into thecontent-typeline of eachmultipart/byterangespart as given, so a value containing a line break could inject extra lines into the response body.If-None-Matchwildcard fix: the server now responds with a 304 response when aIf-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-SinceandIf-Unmodified-Sincemalformed date fix: the server now ignores malformed date values inIf-Modified-SinceandIf-Unmodified-Sincerequest headers instead of responding with a 400 (Bad Request) error.- HTTP range
bytes=-0fix: the server now correctly handlesbytes=-0range requests, treating them as unsatisfiable (416) instead of invalid syntax (ignored). - ETag + Date conditional request precedence fix: previously,
If-Modified-Sincewas evaluated even ifIf-None-Matchwas present, which could lead to incorrect behavior. Similar happened withIf-Unmodified-SinceandIf-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)
HTTP compression
- 304 Not Modified fix: HTTP dynamic content compression is no longer applicable for 304 (Not Modified) HTTP responses.
Accept-Encodingwildcard fix: the server now correctly handlesAccept-Encoding: *request headers, selecting the most appropriate compression algorithm from the server's supported ones.
Reverse proxying
Connection: upgradeheader fix: previously, when there was a request header withUpgradeinside theConnectionheader value (if the header wasn't exactlyUpgrade), it would erroneously set toupgrade.- Request body chunked encoding fix: previously, when the request body was chunked-encoded, the server would wrongfully strip
Transfer-Encodingheader 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_ratewas 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_modifiedflag: fixed bare form ofreplace_last_modifiedflag not being effective.