v0.35.1: performance, AGPL licensing and documentation overhaul

- Web Server reads static files inline; proxy middleware chain flattened
- About window with license summary and project links; AGPL-3.0-or-later
- Rewritten documentation: feature list, screenshots, performance guide
- Fix false spikes in per-Vhost request-rate charts
This commit is contained in:
gru committed 2026-11-05 18:29:56 +01:00
1 parent d3b492874b
commit eba9f89283
135 files changed
+1425 -1064

No files matched your search

+1 -1
View File
@@ -207,7 +207,7 @@ if only_cached {
"RustProxy: no reusable cached response\n",
);
cache_settings.annotate(response.headers_mut(), "MISS", 0, 0);
response.headers_mut().remove("cache-status");
response.headers_mut().remove(::http::header::CACHE_STATUS);
return response;
}
+2 -2
View File
@@ -85,11 +85,11 @@
);
response
.headers_mut()
.insert("cache-control", HeaderValue::from_static("no-store"));
.insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
if status == StatusCode::SERVICE_UNAVAILABLE {
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
}
response
}
+11 -11
View File
@@ -22,8 +22,8 @@ if let Some(spec) = &route.redirect {
};
let status = StatusCode::from_u16(spec.status).unwrap_or(StatusCode::FOUND);
let mut response = status.into_response();
response.headers_mut().insert(http::header::LOCATION, location);
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
response.headers_mut().insert(::http::header::LOCATION, location);
response.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
return response;
}
if route.unavailable {
@@ -33,7 +33,7 @@ if route.unavailable {
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -61,7 +61,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -77,7 +77,7 @@ let (backend, backend_lease, affinity_cookie) = if let Some(pool) = &route.pool
);
response
.headers_mut()
.insert("retry-after", HeaderValue::from_static("1"));
.insert(::http::header::RETRY_AFTER, HeaderValue::from_static("1"));
if let Some(fallback) = execute_error_fallback(
state.clone(), peer, local, &snapshot, &host, tls, route,
response.status(), &fallback_request, &fallback_body,
@@ -253,7 +253,7 @@ if let Some(identity) = identity {
if websocket_legacy_support {
if let Some(upstream_host) = request.headers().get(HOST).cloned() {
if let Some(origin) = legacy_websocket_origin(&backend.request_origin, &upstream_host) {
request.headers_mut().insert("origin", origin);
request.headers_mut().insert(::http::header::ORIGIN, origin);
}
}
}
@@ -263,8 +263,8 @@ let effective_policy = cache_plan::combine_policy(original_policy, || {
request.uri().query().is_some(), cache_settings,
)
});
let force_vary_cookie = force_cache && request.headers().contains_key("cookie");
let force_vary_authorization = force_cache && request.headers().contains_key("authorization");
let force_vary_cookie = force_cache && request.headers().contains_key(::http::header::COOKIE);
let force_vary_authorization = force_cache && request.headers().contains_key(::http::header::AUTHORIZATION);
let effective_reason = effective_policy.bypass_reason();
let revalidation_reason = effective_policy.revalidation_reason();
let effective_cacheable = effective_reason.is_none();
@@ -272,15 +272,15 @@ let range_selection = cache_plan::range_selection(
effective_cacheable, request.method(), request.headers(),
);
let if_range = if effective_cacheable {
request.headers().get("if-range").cloned()
request.headers().get(::http::header::IF_RANGE).cloned()
} else { None };
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
let cache_route = route.id.as_str();
let mut cache_key_headers = None;
if effective_cacheable && ignore_static_cookie && request.headers().contains_key("cookie") {
if effective_cacheable && ignore_static_cookie && request.headers().contains_key(::http::header::COOKIE) {
let mut headers = request.headers().clone();
headers.remove("cookie");
headers.remove(::http::header::COOKIE);
cache_key_headers = Some(headers);
}
let key_headers = cache_key_headers
+5
View File
@@ -135,6 +135,11 @@ if let Some(exchange) = &exchange {
.lock()
.unwrap_or_else(|e| e.into_inner()) = reason;
}
if fill_leader.is_some() && (origin_plan.is_none() || transformed_plan.is_none()) {
if let Some(ticket) = cache_ticket.as_ref() {
state.cache.remember_uncacheable(ticket);
}
}
if let (Some(ticket), Some(plan), Some(transformed)) =
(cache_ticket, origin_plan, transformed_plan)
{
+13 -14
View File
@@ -137,8 +137,8 @@ let origin_reason = (cache_ticket.is_some() && origin_plan.is_none()).then(|| {
let origin_no_transform = crate::transform::no_transform(response.headers());
let skip_compression = crate::compression::no_transform(response.headers());
let origin_validators = crate::transform::OriginValidators {
etag: response.headers().get("etag").cloned(),
last_modified: response.headers().get("last-modified").cloned(),
etag: response.headers().get(::http::header::ETAG).cloned(),
last_modified: response.headers().get(::http::header::LAST_MODIFIED).cloned(),
};
let (mut parts, body) = response.into_parts();
parts.extensions.insert(origin_validators);
@@ -147,8 +147,8 @@ if skip_compression || range_requested {
}
headers::strip_hop_by_hop(&mut parts.headers);
if websocket_legacy_support && handshake.is_some() {
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
}
apply_response_rules(
&mut parts.headers,
@@ -161,7 +161,7 @@ if let Some(pool) = &route.pool {
pool.strip_affinity_response(&mut parts.headers);
}
if let Some(cookie) = active_affinity_cookie {
parts.headers.append("set-cookie", cookie);
parts.headers.append(::http::header::SET_COOKIE, cookie);
}
let h3_tunnel = handshake.as_ref().is_some_and(|handshake| handshake.h3())
&& upgraded.is_some();
@@ -208,9 +208,9 @@ let mut response = if h3_tunnel {
};
exchange.websocket_phase("tunnel_ready");
parts.status = StatusCode::OK;
parts.headers.remove("sec-websocket-accept");
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::SEC_WEBSOCKET_ACCEPT);
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
parts.extensions.insert(crate::compression::Skip);
parts
.extensions
@@ -301,8 +301,8 @@ let mut response = if h3_tunnel {
"RustProxy: HTTP/2 WebSocket tunnel state unavailable\n",
);
};
parts.headers.remove("content-length");
parts.headers.remove("transfer-encoding");
parts.headers.remove(::http::header::CONTENT_LENGTH);
parts.headers.remove(::http::header::TRANSFER_ENCODING);
parts.extensions.insert(crate::compression::Skip);
Response::from_parts(
parts,
@@ -314,10 +314,10 @@ let mut response = if h3_tunnel {
{
parts
.headers
.insert("connection", HeaderValue::from_static("Upgrade"));
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
parts
.headers
.insert("upgrade", HeaderValue::from_static("websocket"));
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
websocket::spawn(
state.clone(),
downstream,
@@ -342,8 +342,7 @@ if intercept_origin_error {
Some(route),
admin_control_plane,
);
response.headers_mut().insert(
"cache-control",
response.headers_mut().insert(::http::header::CACHE_CONTROL,
HeaderValue::from_static("no-store"),
);
}
+17 -16
View File
@@ -4,27 +4,27 @@ let websocket_upstream_h2 = !websocket_legacy_support
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2);
if let Some(handshake) = &handshake {
request.headers_mut().remove("content-length");
request.headers_mut().remove("transfer-encoding");
request.headers_mut().remove(::http::header::CONTENT_LENGTH);
request.headers_mut().remove(::http::header::TRANSFER_ENCODING);
if websocket_upstream_h2 {
request.headers_mut().remove("connection");
request.headers_mut().remove("upgrade");
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
request.headers_mut().remove(::http::header::CONNECTION);
request.headers_mut().remove(::http::header::UPGRADE);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
} else {
request
.headers_mut()
.insert("connection", HeaderValue::from_static("Upgrade"));
.insert(::http::header::CONNECTION, HeaderValue::from_static("Upgrade"));
request
.headers_mut()
.insert("upgrade", HeaderValue::from_static("websocket"));
.insert(::http::header::UPGRADE, HeaderValue::from_static("websocket"));
if !handshake.h1() {
*request.method_mut() = Method::GET;
request.headers_mut().remove("sec-websocket-key");
request.headers_mut().remove("sec-websocket-accept");
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_KEY);
request.headers_mut().remove(::http::header::SEC_WEBSOCKET_ACCEPT);
if let Some(key) = handshake.upstream_key() {
if let Ok(key) = HeaderValue::from_str(key) {
request.headers_mut().insert("sec-websocket-key", key);
request.headers_mut().insert(::http::header::SEC_WEBSOCKET_KEY, key);
} else {
return error(
StatusCode::INTERNAL_SERVER_ERROR,
@@ -39,7 +39,7 @@ if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
auth.apply_upstream(request.headers_mut(), basic_identity);
}
if range_requested || transform_settings.is_some_and(|settings| settings.enabled) {
request.headers_mut().remove("accept-encoding");
request.headers_mut().remove(::http::header::ACCEPT_ENCODING);
}
let mut revalidation_sent = false;
if pending_revalidation {
@@ -61,10 +61,11 @@ let pool_retry_possible = route.backend_id.is_none()
let h2_replay_possible = !admin_control_plane
&& handshake.is_none()
&& retry_safe_method(&retry_method)
&& matches!(backend.protocol,
crate::config::UpstreamProtocol::Http2
| crate::config::UpstreamProtocol::H2
| crate::config::UpstreamProtocol::Auto);
&& match backend.protocol {
crate::config::UpstreamProtocol::Http2 | crate::config::UpstreamProtocol::H2 => true,
crate::config::UpstreamProtocol::Auto => !backend.origin.starts_with("http://"),
_ => false,
};
let replay_headers = ((pool_retry_possible || h2_replay_possible)
&& (request.body().size_hint().exact() == Some(0)
|| request.version() == Version::HTTP_3))
+19 -19
View File
@@ -14,9 +14,9 @@
.get::<crate::server::sendfile::SendfileSlots>().cloned();
#[cfg(target_os = "linux")]
let identity_request = request.version() == http::Version::HTTP_11
&& !request.headers().contains_key("accept-encoding")
&& !request.headers().contains_key("range")
&& !request.headers().contains_key("if-range");
&& !request.headers().contains_key(::http::header::ACCEPT_ENCODING)
&& !request.headers().contains_key(::http::header::RANGE)
&& !request.headers().contains_key(::http::header::IF_RANGE);
let cache_settings = route.frontend.as_ref()
.map(|frontend| &frontend.cache)
.unwrap_or(&snapshot.envelope.config.cache);
@@ -54,9 +54,9 @@
request_uri.query().is_some(), cache_settings,
);
let eligible = enabled && policy.bypass_reason().is_none();
let key_headers = if eligible && ignore_cookie && request_headers.contains_key("cookie") {
let key_headers = if eligible && ignore_cookie && request_headers.contains_key(::http::header::COOKIE) {
let mut headers = request_headers.clone();
headers.remove("cookie");
headers.remove(::http::header::COOKIE);
Some(headers)
} else { None };
let cache_ticket = if eligible {
@@ -67,7 +67,7 @@
.map(|ticket| ticket.with_varnish_compatible(cache_settings.varnish_compatible))
} else { None };
let range = cache_plan::range_selection(eligible, &request_method, &request_headers);
let if_range = if eligible { request_headers.get("if-range").cloned() } else { None };
let if_range = if eligible { request_headers.get(::http::header::IF_RANGE).cloned() } else { None };
let range_requested = !matches!(range, crate::cache::RangeSelection::None);
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
state.cache.revalidation_tiered(ticket).await
@@ -181,18 +181,18 @@
}
let origin_headers = response.headers();
let origin_safe = cache_ticket.is_some() && {
let explicit_origin_policy = origin_headers.contains_key("cache-control")
|| origin_headers.contains_key("expires")
|| origin_headers.contains_key("set-cookie")
|| origin_headers.contains_key("vary");
let explicit_origin_policy = origin_headers.contains_key(::http::header::CACHE_CONTROL)
|| origin_headers.contains_key(::http::header::EXPIRES)
|| origin_headers.contains_key(::http::header::SET_COOKIE)
|| origin_headers.contains_key(::http::header::VARY);
!explicit_origin_policy || crate::cache::plan(response.status(), origin_headers,
cache_settings, started.elapsed()).is_some()
};
let no_transform = crate::transform::no_transform(origin_headers);
let skip_compression = crate::compression::no_transform(origin_headers);
let origin_validators = crate::transform::OriginValidators {
etag: origin_headers.get("etag").cloned(),
last_modified: origin_headers.get("last-modified").cloned(),
etag: origin_headers.get(::http::header::ETAG).cloned(),
last_modified: origin_headers.get(::http::header::LAST_MODIFIED).cloned(),
};
if skip_compression || range_requested {
response.extensions_mut().insert(crate::compression::Skip);
@@ -201,7 +201,7 @@
apply_response_rules(response.headers_mut(), &snapshot, route.frontend.as_deref(),
Some(route), false);
if response.extensions().get::<crate::pages::PageServed>().is_some() {
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
response.headers_mut().insert(::http::header::CACHE_CONTROL, HeaderValue::from_static("no-store"));
}
response.extensions_mut().insert(ResponseRulesApplied);
response.extensions_mut().insert(StaticSourceResponse);
@@ -250,8 +250,8 @@
let mut plan = if canonical { origin.without_vary("accept-encoding") }
else { origin };
if force_cache {
if request_headers.contains_key("cookie") { plan = plan.with_vary("cookie"); }
if request_headers.contains_key("authorization") { plan = plan.with_vary("authorization"); }
if request_headers.contains_key(::http::header::COOKIE) { plan = plan.with_vary("cookie"); }
if request_headers.contains_key(::http::header::AUTHORIZATION) { plan = plan.with_vary("authorization"); }
}
response = state.cache.wrap_with_fill(response, ticket,
plan.restrict(transformed), cache_settings, None);
@@ -278,15 +278,15 @@
if !enabled && identity_request && request_method == Method::GET
&& !transform_settings.is_some_and(|settings| settings.enabled)
&& response.status() == StatusCode::OK
&& !response.headers().contains_key("content-encoding")
&& !response.headers().contains_key("transfer-encoding")
&& !response.headers().contains_key("content-range")
&& !response.headers().contains_key(::http::header::CONTENT_ENCODING)
&& !response.headers().contains_key(::http::header::TRANSFER_ENCODING)
&& !response.headers().contains_key(::http::header::CONTENT_RANGE)
{
if let (Some(slots), Some(candidate)) = (
kernel_sendfile,
response.extensions().get::<crate::server::sendfile::SendfileCandidate>().cloned(),
) {
let correct_length = response.headers().get("content-length")
let correct_length = response.headers().get(::http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok()) == Some(candidate.length);
if correct_length {