v0.34.7
This commit is contained in:
1 parent
1cd6b7ac99
commit
58c7d63194
5 files changed
+280
-70
No files matched your search
@@ -12,9 +12,6 @@ Release history focused on **features, settings, default behavior, compatibility
|
|||||||
|
|
||||||
- **Settings → Cache → Varnish-compatible cache policy (`varnish_compatible`)** stays **ON by default**, including upgraded configurations without the field. An adjacent **i** popover (PL/EN) explains its effects, limitations and safety controls. OFF retains strict client hard-reload behavior. The live toggle invalidates existing entries across HOT/COLD.
|
- **Settings → Cache → Varnish-compatible cache policy (`varnish_compatible`)** stays **ON by default**, including upgraded configurations without the field. An adjacent **i** popover (PL/EN) explains its effects, limitations and safety controls. OFF retains strict client hard-reload behavior. The live toggle invalidates existing entries across HOT/COLD.
|
||||||
- Only client hard-reload `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` are ignored when considering a **fresh HIT**, retaining `Age` and hit counters. Kept the original `Cache-Control: no-store` bypass, `only-if-cached` 504 on cache MISS and no background origin refresh on HIT, and `min-fresh`/`max-stale`/positive `max-age` freshness constraints (including explicit `max-stale=0` during grace). Original request headers, Vary, access/privacy rules and Web Server handling remain intact.
|
- Only client hard-reload `Cache-Control: no-cache`, `max-age=0` and `Pragma: no-cache` are ignored when considering a **fresh HIT**, retaining `Age` and hit counters. Kept the original `Cache-Control: no-store` bypass, `only-if-cached` 504 on cache MISS and no background origin refresh on HIT, and `min-fresh`/`max-stale`/positive `max-age` freshness constraints (including explicit `max-stale=0` during grace). Original request headers, Vary, access/privacy rules and Web Server handling remain intact.
|
||||||
- **Origin `no-cache` regression fix:** ETag/Last-Modified entries remain cacheable for conditional revalidation when fallback TTL is disabled. With **explicit positive fallback TTL**, only origin responses marked **`public`** may be reused for that TTL (capped by max TTL), even without origin validators. This is an explicit administrative override of origin `no-cache`, not the default. `private`, `no-store`, `Set-Cookie` and compatibility-mode `Surrogate-Control: no-store` remain vetoes.
|
|
||||||
- **Zero TTL regression fix:** keep expired representations for revalidation, client `max-stale` and configured `stale-if-error`; an expired object does not become a fresh HIT. Implicit grace defaults to **60 seconds**, can be set in Settings → Cache (`varnish_grace_seconds`, 0 disables it), and applies only to eligible objects stored fresh with no `stale-while-revalidate` directive; explicit `stale-while-revalidate=0` is never overwritten.
|
|
||||||
- **UI layout:** nested Settings tab bars no longer stick over Cache checkboxes while scrolling; the top-level Settings navigation remains sticky.
|
|
||||||
- Source-level regression cases cover hard reload, fallback TTL, validator revalidation, no-store/private/Surrogate protections, zero-TTL stale-if-error, min-fresh/max-stale, and `only-if-cached` for proxy and Web Server. Native Rust compilation and runtime acceptance are required before a production release claim. This remains a Varnish-inspired policy subset, not a VCL clone.
|
- Source-level regression cases cover hard reload, fallback TTL, validator revalidation, no-store/private/Surrogate protections, zero-TTL stale-if-error, min-fresh/max-stale, and `only-if-cached` for proxy and Web Server. Native Rust compilation and runtime acceptance are required before a production release claim. This remains a Varnish-inspired policy subset, not a VCL clone.
|
||||||
|
|
||||||
## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules (source candidate)
|
## 0.34.6 — Docker live logs, neutral Publish switch and Vhost Content-Type rules (source candidate)
|
||||||
|
|||||||
@@ -614,7 +614,10 @@ mod cache_fast_path_tests {
|
|||||||
.await;
|
.await;
|
||||||
assert_eq!(validated.status, StatusCode::OK);
|
assert_eq!(validated.status, StatusCode::OK);
|
||||||
assert_eq!(validated.headers["x-cache"], "HIT");
|
assert_eq!(validated.headers["x-cache"], "HIT");
|
||||||
assert_eq!(validated.headers["x-cache-reason"], "cache_hit", "default compatibility mode must not revalidate a fresh object");
|
assert_eq!(
|
||||||
|
validated.headers["x-cache-reason"], "cache_hit",
|
||||||
|
"default compatibility mode must not revalidate a fresh object"
|
||||||
|
);
|
||||||
assert_eq!(validated.body, "origin-a");
|
assert_eq!(validated.body, "origin-a");
|
||||||
let conditional = fixture
|
let conditional = fixture
|
||||||
.get(
|
.get(
|
||||||
@@ -641,17 +644,29 @@ mod cache_fast_path_tests {
|
|||||||
async fn compatible_only_if_cached_on_miss_never_contacts_origin() {
|
async fn compatible_only_if_cached_on_miss_never_contacts_origin() {
|
||||||
let origin = origin().await;
|
let origin = origin().await;
|
||||||
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
||||||
let miss = fixture.get("/missing", &[("cache-control", "no-cache, only-if-cached")]).await;
|
let miss = fixture
|
||||||
|
.get("/missing", &[("cache-control", "no-cache, only-if-cached")])
|
||||||
|
.await;
|
||||||
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
|
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
|
||||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 0, "only-if-cached MISS must not contact backend");
|
assert_eq!(
|
||||||
|
origin.requests.load(Ordering::Relaxed),
|
||||||
|
0,
|
||||||
|
"only-if-cached MISS must not contact backend"
|
||||||
|
);
|
||||||
let primed = fixture.get("/vary", &[]).await;
|
let primed = fixture.get("/vary", &[]).await;
|
||||||
assert_eq!(primed.headers["x-cache"], "MISS");
|
assert_eq!(primed.headers["x-cache"], "MISS");
|
||||||
let hit = fixture.get("/vary", &[("cache-control", "only-if-cached")]).await;
|
let hit = fixture
|
||||||
|
.get("/vary", &[("cache-control", "only-if-cached")])
|
||||||
|
.await;
|
||||||
assert_eq!(hit.headers["x-cache"], "HIT");
|
assert_eq!(hit.headers["x-cache"], "HIT");
|
||||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
|
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
|
||||||
let blocked = fixture.get("/vary", &[("cache-control", "no-store")]).await;
|
let blocked = fixture.get("/vary", &[("cache-control", "no-store")]).await;
|
||||||
assert_eq!(blocked.headers["x-cache-reason"], "request_no_store");
|
assert_eq!(blocked.headers["x-cache-reason"], "request_no_store");
|
||||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 2, "no-store must bypass even a fresh HIT");
|
assert_eq!(
|
||||||
|
origin.requests.load(Ordering::Relaxed),
|
||||||
|
2,
|
||||||
|
"no-store must bypass even a fresh HIT"
|
||||||
|
);
|
||||||
fixture.stop().await;
|
fixture.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -661,7 +676,10 @@ mod cache_fast_path_tests {
|
|||||||
let mut config = config_for(origin.addr, true);
|
let mut config = config_for(origin.addr, true);
|
||||||
config.cache.default_ttl_seconds = 0;
|
config.cache.default_ttl_seconds = 0;
|
||||||
let fixture = Fixture::new(config).await;
|
let fixture = Fixture::new(config).await;
|
||||||
assert_eq!(fixture.get("/backend-no-cache", &[]).await.headers["x-cache"], "MISS");
|
assert_eq!(
|
||||||
|
fixture.get("/backend-no-cache", &[]).await.headers["x-cache"],
|
||||||
|
"MISS"
|
||||||
|
);
|
||||||
let cached_only = fixture
|
let cached_only = fixture
|
||||||
.get("/backend-no-cache", &[("cache-control", "only-if-cached")])
|
.get("/backend-no-cache", &[("cache-control", "only-if-cached")])
|
||||||
.await;
|
.await;
|
||||||
@@ -678,11 +696,18 @@ mod cache_fast_path_tests {
|
|||||||
let mut config = config_for(origin.addr, true);
|
let mut config = config_for(origin.addr, true);
|
||||||
config.cache.default_ttl_seconds = 45;
|
config.cache.default_ttl_seconds = 45;
|
||||||
let fixture = Fixture::new(config).await;
|
let fixture = Fixture::new(config).await;
|
||||||
assert_eq!(fixture.get("/backend-no-cache", &[]).await.headers["x-cache"], "MISS");
|
assert_eq!(
|
||||||
|
fixture.get("/backend-no-cache", &[]).await.headers["x-cache"],
|
||||||
|
"MISS"
|
||||||
|
);
|
||||||
let reused = fixture.get("/backend-no-cache", &[]).await;
|
let reused = fixture.get("/backend-no-cache", &[]).await;
|
||||||
assert_eq!(reused.headers["x-cache"], "HIT");
|
assert_eq!(reused.headers["x-cache"], "HIT");
|
||||||
assert_eq!(reused.headers["x-cache-reason"], "cache_hit");
|
assert_eq!(reused.headers["x-cache-reason"], "cache_hit");
|
||||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 1, "configured fallback TTL avoids origin revalidation");
|
assert_eq!(
|
||||||
|
origin.requests.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"configured fallback TTL avoids origin revalidation"
|
||||||
|
);
|
||||||
fixture.stop().await;
|
fixture.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,12 +717,21 @@ mod cache_fast_path_tests {
|
|||||||
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
||||||
assert_eq!(fixture.get("/grace", &[]).await.headers["x-cache"], "MISS");
|
assert_eq!(fixture.get("/grace", &[]).await.headers["x-cache"], "MISS");
|
||||||
tokio::time::sleep(Duration::from_millis(1200)).await;
|
tokio::time::sleep(Duration::from_millis(1200)).await;
|
||||||
let stale = fixture.get("/grace", &[("cache-control", "only-if-cached")]).await;
|
let stale = fixture
|
||||||
|
.get("/grace", &[("cache-control", "only-if-cached")])
|
||||||
|
.await;
|
||||||
assert_eq!(stale.status, StatusCode::OK);
|
assert_eq!(stale.status, StatusCode::OK);
|
||||||
assert_eq!(stale.headers["x-cache"], "HIT");
|
assert_eq!(stale.headers["x-cache"], "HIT");
|
||||||
assert_eq!(stale.headers["x-cache-reason"], "cache_stale_while_revalidate");
|
assert_eq!(
|
||||||
|
stale.headers["x-cache-reason"],
|
||||||
|
"cache_stale_while_revalidate"
|
||||||
|
);
|
||||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 1, "only-if-cached must not start async validation");
|
assert_eq!(
|
||||||
|
origin.requests.load(Ordering::Relaxed),
|
||||||
|
1,
|
||||||
|
"only-if-cached must not start async validation"
|
||||||
|
);
|
||||||
fixture.stop().await;
|
fixture.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -705,14 +739,32 @@ mod cache_fast_path_tests {
|
|||||||
async fn web_server_only_if_cached_on_miss_never_reads_origin() {
|
async fn web_server_only_if_cached_on_miss_never_reads_origin() {
|
||||||
let directory = TempDir::new().unwrap();
|
let directory = TempDir::new().unwrap();
|
||||||
let fixture = Fixture::new(web_config(&directory, true)).await;
|
let fixture = Fixture::new(web_config(&directory, true)).await;
|
||||||
let miss = fixture.get("/style.css", &[("cache-control", "only-if-cached")]).await;
|
let miss = fixture
|
||||||
|
.get("/style.css", &[("cache-control", "only-if-cached")])
|
||||||
|
.await;
|
||||||
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
|
assert_eq!(miss.status, StatusCode::GATEWAY_TIMEOUT);
|
||||||
assert_eq!(fixture.get("/style.css", &[]).await.headers["x-cache"], "MISS");
|
assert_eq!(
|
||||||
let hit = fixture.get("/style.css", &[("cache-control", "only-if-cached")]).await;
|
fixture.get("/style.css", &[]).await.headers["x-cache"],
|
||||||
|
"MISS"
|
||||||
|
);
|
||||||
|
let hit = fixture
|
||||||
|
.get("/style.css", &[("cache-control", "only-if-cached")])
|
||||||
|
.await;
|
||||||
assert_eq!(hit.headers["x-cache"], "HIT");
|
assert_eq!(hit.headers["x-cache"], "HIT");
|
||||||
let bypass = fixture.get("/style.css", &[("cache-control", "no-store, only-if-cached")]).await;
|
let bypass = fixture
|
||||||
assert_eq!(bypass.status, StatusCode::GATEWAY_TIMEOUT, "uncacheable only-if-cached must not read the Web Server file");
|
.get(
|
||||||
let reload = fixture.get("/style.css", &[("cache-control", "no-cache")]).await;
|
"/style.css",
|
||||||
|
&[("cache-control", "no-store, only-if-cached")],
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!(
|
||||||
|
bypass.status,
|
||||||
|
StatusCode::GATEWAY_TIMEOUT,
|
||||||
|
"uncacheable only-if-cached must not read the Web Server file"
|
||||||
|
);
|
||||||
|
let reload = fixture
|
||||||
|
.get("/style.css", &[("cache-control", "no-cache")])
|
||||||
|
.await;
|
||||||
assert_eq!(reload.headers["x-cache-reason"], "cache_hit");
|
assert_eq!(reload.headers["x-cache-reason"], "cache_hit");
|
||||||
fixture.stop().await;
|
fixture.stop().await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ pub(super) fn web_origin_request_headers(
|
|||||||
cached: Option<&Revalidation>,
|
cached: Option<&Revalidation>,
|
||||||
) -> (HeaderMap, bool) {
|
) -> (HeaderMap, bool) {
|
||||||
let mut headers = client.clone();
|
let mut headers = client.clone();
|
||||||
let Some(cached) = cached else { return (headers, false) };
|
let Some(cached) = cached else {
|
||||||
|
return (headers, false);
|
||||||
|
};
|
||||||
headers.remove("if-none-match");
|
headers.remove("if-none-match");
|
||||||
headers.remove("if-modified-since");
|
headers.remove("if-modified-since");
|
||||||
let sent = cached.apply_validators(&mut headers);
|
let sent = cached.apply_validators(&mut headers);
|
||||||
@@ -25,7 +27,12 @@ fn web_merged_validation_headers(cached: &Revalidation, validation: &HeaderMap)
|
|||||||
let mut merged = cached.merge_validation_headers(validation);
|
let mut merged = cached.merge_validation_headers(validation);
|
||||||
if cached.transformed() {
|
if cached.transformed() {
|
||||||
for name in [
|
for name in [
|
||||||
"content-encoding", "vary", "accept-ranges", "content-md5", "digest", "content-digest",
|
"content-encoding",
|
||||||
|
"vary",
|
||||||
|
"accept-ranges",
|
||||||
|
"content-md5",
|
||||||
|
"digest",
|
||||||
|
"content-digest",
|
||||||
] {
|
] {
|
||||||
merged.remove(name);
|
merged.remove(name);
|
||||||
for value in cached.headers().get_all(name) {
|
for value in cached.headers().get_all(name) {
|
||||||
@@ -50,7 +57,8 @@ pub(super) fn finish_web_revalidation(
|
|||||||
apply_rules: impl FnOnce(&mut HeaderMap),
|
apply_rules: impl FnOnce(&mut HeaderMap),
|
||||||
) -> Option<WebRevalidated> {
|
) -> Option<WebRevalidated> {
|
||||||
let origin_merged = web_merged_validation_headers(cached, validation);
|
let origin_merged = web_merged_validation_headers(cached, validation);
|
||||||
let origin_safe = crate::cache::plan(StatusCode::OK, &origin_merged, settings, elapsed).is_some();
|
let origin_safe =
|
||||||
|
crate::cache::plan(StatusCode::OK, &origin_merged, settings, elapsed).is_some();
|
||||||
let mut merged = origin_merged;
|
let mut merged = origin_merged;
|
||||||
apply_rules(&mut merged);
|
apply_rules(&mut merged);
|
||||||
if force_cache {
|
if force_cache {
|
||||||
@@ -66,8 +74,17 @@ pub(super) fn finish_web_revalidation(
|
|||||||
let range = crate::cache::request_range(method, client);
|
let range = crate::cache::request_range(method, client);
|
||||||
let if_range = client.get("if-range");
|
let if_range = client.get("if-range");
|
||||||
let published = if can_store {
|
let published = if can_store {
|
||||||
cache.publish_revalidated(ticket.clone(), cached, merged.clone(), validation, settings, elapsed)
|
cache.publish_revalidated(
|
||||||
} else { None };
|
ticket.clone(),
|
||||||
|
cached,
|
||||||
|
merged.clone(),
|
||||||
|
validation,
|
||||||
|
settings,
|
||||||
|
elapsed,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let (mut response, hits, ttl, resident_age, transformed, reason) =
|
let (mut response, hits, ttl, resident_age, transformed, reason) =
|
||||||
if let Some(mut hit) = published {
|
if let Some(mut hit) = published {
|
||||||
if !client_not_modified {
|
if !client_not_modified {
|
||||||
@@ -76,8 +93,12 @@ pub(super) fn finish_web_revalidation(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
hit.response, hit.hits, hit.remaining_ttl, hit.resident_age_seconds,
|
hit.response,
|
||||||
hit.transformed, "cache_revalidated",
|
hit.hits,
|
||||||
|
hit.remaining_ttl,
|
||||||
|
hit.resident_age_seconds,
|
||||||
|
hit.transformed,
|
||||||
|
"cache_revalidated",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if !cache.ticket_current(ticket) {
|
if !cache.ticket_current(ticket) {
|
||||||
@@ -93,7 +114,14 @@ pub(super) fn finish_web_revalidation(
|
|||||||
} else {
|
} else {
|
||||||
cached.response(merged)
|
cached.response(merged)
|
||||||
};
|
};
|
||||||
(response, 0, 0, 0, cached.transformed(), "cache_revalidated_uncached")
|
(
|
||||||
|
response,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
cached.transformed(),
|
||||||
|
"cache_revalidated_uncached",
|
||||||
|
)
|
||||||
};
|
};
|
||||||
if client_not_modified {
|
if client_not_modified {
|
||||||
response = crate::cache::make_not_modified(response);
|
response = crate::cache::make_not_modified(response);
|
||||||
@@ -104,7 +132,11 @@ pub(super) fn finish_web_revalidation(
|
|||||||
settings.annotate_reason(response.headers_mut(), "HIT", hits, ttl, reason);
|
settings.annotate_reason(response.headers_mut(), "HIT", hits, ttl, reason);
|
||||||
settings.annotate_transformed(response.headers_mut(), transformed);
|
settings.annotate_transformed(response.headers_mut(), transformed);
|
||||||
settings.annotate_resident_age(response.headers_mut(), resident_age);
|
settings.annotate_resident_age(response.headers_mut(), resident_age);
|
||||||
Some(WebRevalidated { response, transformed, reason })
|
Some(WebRevalidated {
|
||||||
|
response,
|
||||||
|
transformed,
|
||||||
|
reason,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use super::*;
|
use super::*;
|
||||||
use crate::{cache, web_server};
|
use crate::{cache, web_server};
|
||||||
use http_body_util::BodyExt;
|
use http_body_util::BodyExt;
|
||||||
use tempfile::TempDir;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
fn cache_settings() -> Settings {
|
fn cache_settings() -> Settings {
|
||||||
Settings {
|
Settings {
|
||||||
@@ -14,7 +14,13 @@ fn cache_settings() -> Settings {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn fixture() -> (TempDir, web_server::CompiledService, Arc<Cache>, Ticket, Settings) {
|
async fn fixture() -> (
|
||||||
|
TempDir,
|
||||||
|
web_server::CompiledService,
|
||||||
|
Arc<Cache>,
|
||||||
|
Ticket,
|
||||||
|
Settings,
|
||||||
|
) {
|
||||||
let dir = TempDir::new().unwrap();
|
let dir = TempDir::new().unwrap();
|
||||||
std::fs::write(dir.path().join("shCore.js"), b"before").unwrap();
|
std::fs::write(dir.path().join("shCore.js"), b"before").unwrap();
|
||||||
let service = web_server::Service {
|
let service = web_server::Service {
|
||||||
@@ -23,26 +29,51 @@ async fn fixture() -> (TempDir, web_server::CompiledService, Arc<Cache>, Ticket,
|
|||||||
document_root: dir.path().to_path_buf(),
|
document_root: dir.path().to_path_buf(),
|
||||||
cache_control: Some("public, max-age=604800".into()),
|
cache_control: Some("public, max-age=604800".into()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}.compile().unwrap();
|
}
|
||||||
|
.compile()
|
||||||
|
.unwrap();
|
||||||
let cache = Cache::new(1);
|
let cache = Cache::new(1);
|
||||||
let settings = cache_settings();
|
let settings = cache_settings();
|
||||||
let ticket = cache.ticket(
|
let ticket = cache
|
||||||
1, "https", "blog.example.test", "wordpress",
|
.ticket(
|
||||||
"/shCore.js?ver=3.0.9b", &HeaderMap::new(),
|
1,
|
||||||
).unwrap();
|
"https",
|
||||||
|
"blog.example.test",
|
||||||
|
"wordpress",
|
||||||
|
"/shCore.js?ver=3.0.9b",
|
||||||
|
&HeaderMap::new(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let initial = serve_file(&service, Method::GET, HeaderMap::new()).await;
|
let initial = serve_file(&service, Method::GET, HeaderMap::new()).await;
|
||||||
assert_eq!(initial.status(), StatusCode::OK);
|
assert_eq!(initial.status(), StatusCode::OK);
|
||||||
let plan = cache::plan(initial.status(), initial.headers(), &settings, Duration::ZERO)
|
let plan = cache::plan(
|
||||||
|
initial.status(),
|
||||||
|
initial.headers(),
|
||||||
|
&settings,
|
||||||
|
Duration::ZERO,
|
||||||
|
)
|
||||||
.expect("static origin response is cacheable");
|
.expect("static origin response is cacheable");
|
||||||
cache.wrap(initial, ticket.clone(), plan, &settings)
|
cache
|
||||||
.into_body().collect().await.unwrap();
|
.wrap(initial, ticket.clone(), plan, &settings)
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
(dir, service, cache, ticket, settings)
|
(dir, service, cache, ticket, settings)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_file(service: &web_server::CompiledService, method: Method, headers: HeaderMap) -> Response {
|
async fn serve_file(
|
||||||
|
service: &web_server::CompiledService,
|
||||||
|
method: Method,
|
||||||
|
headers: HeaderMap,
|
||||||
|
) -> Response {
|
||||||
web_server::serve(
|
web_server::serve(
|
||||||
service, method, headers,
|
service,
|
||||||
"/shCore.js?ver=3.0.9b".parse().unwrap(), "/shCore.js", Body::empty(),
|
method,
|
||||||
|
headers,
|
||||||
|
"/shCore.js?ver=3.0.9b".parse().unwrap(),
|
||||||
|
"/shCore.js",
|
||||||
|
Body::empty(),
|
||||||
web_server::RequestContext {
|
web_server::RequestContext {
|
||||||
client_ip: "127.0.0.1".parse().unwrap(),
|
client_ip: "127.0.0.1".parse().unwrap(),
|
||||||
client_port: Some(12345),
|
client_port: Some(12345),
|
||||||
@@ -51,22 +82,41 @@ async fn serve_file(service: &web_server::CompiledService, method: Method, heade
|
|||||||
tls: true,
|
tls: true,
|
||||||
version: http::Version::HTTP_2,
|
version: http::Version::HTTP_2,
|
||||||
},
|
},
|
||||||
).await
|
)
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn validate(
|
async fn validate(
|
||||||
service: &web_server::CompiledService, cache: &Cache, ticket: &Ticket,
|
service: &web_server::CompiledService,
|
||||||
settings: &Settings, method: Method, client: HeaderMap,
|
cache: &Cache,
|
||||||
|
ticket: &Ticket,
|
||||||
|
settings: &Settings,
|
||||||
|
method: Method,
|
||||||
|
client: HeaderMap,
|
||||||
) -> Option<WebRevalidated> {
|
) -> Option<WebRevalidated> {
|
||||||
let cached = cache.revalidation_tiered(ticket).await.unwrap();
|
let cached = cache.revalidation_tiered(ticket).await.unwrap();
|
||||||
let (headers, sent) = web_origin_request_headers(&client, Some(&cached));
|
let (headers, sent) = web_origin_request_headers(&client, Some(&cached));
|
||||||
assert!(sent, "stored validator must be used for local source revalidation");
|
assert!(
|
||||||
|
sent,
|
||||||
|
"stored validator must be used for local source revalidation"
|
||||||
|
);
|
||||||
let origin = serve_file(service, method.clone(), headers).await;
|
let origin = serve_file(service, method.clone(), headers).await;
|
||||||
if origin.status() != StatusCode::NOT_MODIFIED {
|
if origin.status() != StatusCode::NOT_MODIFIED {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
finish_web_revalidation(cache, ticket, &cached, origin.headers(), settings,
|
finish_web_revalidation(
|
||||||
Duration::ZERO, &client, &method, false, false, |_| {})
|
cache,
|
||||||
|
ticket,
|
||||||
|
&cached,
|
||||||
|
origin.headers(),
|
||||||
|
settings,
|
||||||
|
Duration::ZERO,
|
||||||
|
&client,
|
||||||
|
&method,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -77,13 +127,22 @@ async fn browser_refresh_304_preserves_web_server_hit_counter() {
|
|||||||
let mut client = HeaderMap::new();
|
let mut client = HeaderMap::new();
|
||||||
client.insert("cache-control", directive.parse().unwrap());
|
client.insert("cache-control", directive.parse().unwrap());
|
||||||
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
||||||
.await.unwrap().response;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
assert_eq!(response.headers()["x-cache"], "HIT");
|
assert_eq!(response.headers()["x-cache"], "HIT");
|
||||||
let hits = response.headers()["x-cache-hits"].to_str().unwrap().parse::<u64>().unwrap();
|
let hits = response.headers()["x-cache-hits"]
|
||||||
|
.to_str()
|
||||||
|
.unwrap()
|
||||||
|
.parse::<u64>()
|
||||||
|
.unwrap();
|
||||||
assert!(hits >= 2);
|
assert!(hits >= 2);
|
||||||
assert_eq!(response.headers()["x-cache-reason"], "cache_revalidated");
|
assert_eq!(response.headers()["x-cache-reason"], "cache_revalidated");
|
||||||
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
|
assert_eq!(
|
||||||
|
response.into_body().collect().await.unwrap().to_bytes(),
|
||||||
|
"before"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 4);
|
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 4);
|
||||||
}
|
}
|
||||||
@@ -96,11 +155,19 @@ async fn browser_conditional_refresh_returns_304_without_resetting_hits() {
|
|||||||
client.insert("cache-control", HeaderValue::from_static("no-cache"));
|
client.insert("cache-control", HeaderValue::from_static("no-cache"));
|
||||||
client.insert("if-none-match", etag);
|
client.insert("if-none-match", etag);
|
||||||
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
||||||
.await.unwrap().response;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response;
|
||||||
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
assert_eq!(response.status(), StatusCode::NOT_MODIFIED);
|
||||||
assert!(!response.headers().contains_key("content-length"));
|
assert!(!response.headers().contains_key("content-length"));
|
||||||
assert_eq!(response.headers()["x-cache-hits"], "1");
|
assert_eq!(response.headers()["x-cache-hits"], "1");
|
||||||
assert!(response.into_body().collect().await.unwrap().to_bytes().is_empty());
|
assert!(response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.is_empty());
|
||||||
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 2);
|
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +178,14 @@ async fn mismatched_browser_validator_cannot_validate_different_cached_object()
|
|||||||
client.insert("cache-control", HeaderValue::from_static("no-cache"));
|
client.insert("cache-control", HeaderValue::from_static("no-cache"));
|
||||||
client.insert("if-none-match", HeaderValue::from_static("\"unrelated\""));
|
client.insert("if-none-match", HeaderValue::from_static("\"unrelated\""));
|
||||||
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
let response = validate(&service, &cache, &ticket, &settings, Method::GET, client)
|
||||||
.await.unwrap().response;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response;
|
||||||
assert_eq!(response.status(), StatusCode::OK);
|
assert_eq!(response.status(), StatusCode::OK);
|
||||||
assert_eq!(response.into_body().collect().await.unwrap().to_bytes(), "before");
|
assert_eq!(
|
||||||
|
response.into_body().collect().await.unwrap().to_bytes(),
|
||||||
|
"before"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -123,13 +195,33 @@ async fn range_and_head_requests_use_validated_cached_body() {
|
|||||||
range.insert("cache-control", HeaderValue::from_static("max-age=0"));
|
range.insert("cache-control", HeaderValue::from_static("max-age=0"));
|
||||||
range.insert("range", HeaderValue::from_static("bytes=0-2"));
|
range.insert("range", HeaderValue::from_static("bytes=0-2"));
|
||||||
let partial = validate(&service, &cache, &ticket, &settings, Method::GET, range)
|
let partial = validate(&service, &cache, &ticket, &settings, Method::GET, range)
|
||||||
.await.unwrap().response;
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response;
|
||||||
assert_eq!(partial.status(), StatusCode::PARTIAL_CONTENT);
|
assert_eq!(partial.status(), StatusCode::PARTIAL_CONTENT);
|
||||||
assert_eq!(partial.into_body().collect().await.unwrap().to_bytes(), "bef");
|
assert_eq!(
|
||||||
let head = validate(&service, &cache, &ticket, &settings, Method::HEAD, HeaderMap::new())
|
partial.into_body().collect().await.unwrap().to_bytes(),
|
||||||
.await.unwrap().response;
|
"bef"
|
||||||
|
);
|
||||||
|
let head = validate(
|
||||||
|
&service,
|
||||||
|
&cache,
|
||||||
|
&ticket,
|
||||||
|
&settings,
|
||||||
|
Method::HEAD,
|
||||||
|
HeaderMap::new(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.response;
|
||||||
assert_eq!(head.status(), StatusCode::OK);
|
assert_eq!(head.status(), StatusCode::OK);
|
||||||
assert!(head.into_body().collect().await.unwrap().to_bytes().is_empty());
|
assert!(head
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes()
|
||||||
|
.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
@@ -143,8 +235,12 @@ async fn changed_local_file_is_not_falsely_revalidated() {
|
|||||||
assert_eq!(origin.status(), StatusCode::OK);
|
assert_eq!(origin.status(), StatusCode::OK);
|
||||||
let plan = cache::plan(origin.status(), origin.headers(), &settings, Duration::ZERO).unwrap();
|
let plan = cache::plan(origin.status(), origin.headers(), &settings, Duration::ZERO).unwrap();
|
||||||
cache.discard(&ticket);
|
cache.discard(&ticket);
|
||||||
cache.wrap(origin, ticket.clone(), plan, &settings)
|
cache
|
||||||
.into_body().collect().await.unwrap();
|
.wrap(origin, ticket.clone(), plan, &settings)
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
|
assert_eq!(cache.get_tiered(&ticket, &settings).await.unwrap().hits, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,12 +249,35 @@ async fn response_304_with_private_policy_discards_cached_representation() {
|
|||||||
let (_dir, _service, cache, ticket, settings) = fixture().await;
|
let (_dir, _service, cache, ticket, settings) = fixture().await;
|
||||||
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
|
let cached = cache.revalidation_tiered(&ticket).await.unwrap();
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert("cache-control", HeaderValue::from_static("private, max-age=300"));
|
headers.insert(
|
||||||
|
"cache-control",
|
||||||
|
HeaderValue::from_static("private, max-age=300"),
|
||||||
|
);
|
||||||
headers.insert("etag", cached.headers()["etag"].clone());
|
headers.insert("etag", cached.headers()["etag"].clone());
|
||||||
let outcome = finish_web_revalidation(&cache, &ticket, &cached, &headers,
|
let outcome = finish_web_revalidation(
|
||||||
&settings, Duration::ZERO, &HeaderMap::new(), &Method::GET,
|
&cache,
|
||||||
false, false, |_| {}).unwrap();
|
&ticket,
|
||||||
|
&cached,
|
||||||
|
&headers,
|
||||||
|
&settings,
|
||||||
|
Duration::ZERO,
|
||||||
|
&HeaderMap::new(),
|
||||||
|
&Method::GET,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
|_| {},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
assert_eq!(outcome.reason, "cache_revalidated_uncached");
|
assert_eq!(outcome.reason, "cache_revalidated_uncached");
|
||||||
assert_eq!(outcome.response.into_body().collect().await.unwrap().to_bytes(), "before");
|
assert_eq!(
|
||||||
|
outcome
|
||||||
|
.response
|
||||||
|
.into_body()
|
||||||
|
.collect()
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.to_bytes(),
|
||||||
|
"before"
|
||||||
|
);
|
||||||
assert!(cache.revalidation_tiered(&ticket).await.is_none());
|
assert!(cache.revalidation_tiered(&ticket).await.is_none());
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,10 @@ pub(super) fn fastcgi_params(
|
|||||||
if context.tls { "on" } else { "off" }.into(),
|
if context.tls { "on" } else { "off" }.into(),
|
||||||
);
|
);
|
||||||
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
params.insert("GATEWAY_INTERFACE".into(), "CGI/1.1".into());
|
||||||
params.insert("SERVER_SOFTWARE".into(), concat!("RustProxy/", env!("CARGO_PKG_VERSION")).into());
|
params.insert(
|
||||||
|
"SERVER_SOFTWARE".into(),
|
||||||
|
concat!("RustProxy/", env!("CARGO_PKG_VERSION")).into(),
|
||||||
|
);
|
||||||
params.insert("REDIRECT_STATUS".into(), "200".into());
|
params.insert("REDIRECT_STATUS".into(), "200".into());
|
||||||
if let Some(value) = headers
|
if let Some(value) = headers
|
||||||
.get(CONTENT_TYPE)
|
.get(CONTENT_TYPE)
|
||||||
@@ -199,7 +202,9 @@ pub(super) fn fastcgi_params(
|
|||||||
})
|
})
|
||||||
.or_insert_with(|| value.to_owned());
|
.or_insert_with(|| value.to_owned());
|
||||||
}
|
}
|
||||||
params.entry("HTTP_HOST".into()).or_insert_with(|| context.server_name.clone());
|
params
|
||||||
|
.entry("HTTP_HOST".into())
|
||||||
|
.or_insert_with(|| context.server_name.clone());
|
||||||
for (name, value) in &php.custom_params {
|
for (name, value) in &php.custom_params {
|
||||||
params.insert(name.clone(), value.clone());
|
params.insert(name.clone(), value.clone());
|
||||||
}
|
}
|
||||||
@@ -216,7 +221,12 @@ pub(super) fn fastcgi_params(
|
|||||||
params.insert("PHP_VALUE".into(), value);
|
params.insert("PHP_VALUE".into(), value);
|
||||||
}
|
}
|
||||||
if !php.php_admin_values.is_empty() {
|
if !php.php_admin_values.is_empty() {
|
||||||
let value = php.php_admin_values.iter().map(|(name, item)| format!("{name}={item}")).collect::<Vec<_>>().join("\n");
|
let value = php
|
||||||
|
.php_admin_values
|
||||||
|
.iter()
|
||||||
|
.map(|(name, item)| format!("{name}={item}"))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
params.insert("PHP_ADMIN_VALUE".into(), value);
|
params.insert("PHP_ADMIN_VALUE".into(), value);
|
||||||
}
|
}
|
||||||
let mut encoded = Vec::with_capacity(4096);
|
let mut encoded = Vec::with_capacity(4096);
|
||||||
|
|||||||
Reference in new issue
Block a user