0.34.6_repo_cleanup
This commit is contained in:
1 parent
ce1a90397c
commit
133d2f0f91
273 files changed
+5969
-6926
No files matched your search
@@ -0,0 +1,658 @@
|
||||
mod cache_fast_path_tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
auth::Auth,
|
||||
config::{self, Config, Update, ValidationPolicy},
|
||||
db::Store,
|
||||
};
|
||||
use std::net::SocketAddr;
|
||||
use tempfile::TempDir;
|
||||
use tower::ServiceExt;
|
||||
|
||||
const CSS: &[u8] = b"/* origin comment */\nbody { color: red; margin: 0px; }\n";
|
||||
|
||||
struct Fixture {
|
||||
app: Arc<App>,
|
||||
_directory: TempDir,
|
||||
}
|
||||
struct Received {
|
||||
status: StatusCode,
|
||||
headers: HeaderMap,
|
||||
body: Bytes,
|
||||
}
|
||||
|
||||
impl Fixture {
|
||||
async fn new(config: Config) -> Self {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let policy = ValidationPolicy {
|
||||
tls_addr: None,
|
||||
cert_dir: None,
|
||||
data_dir: None,
|
||||
proxy_addr: "127.0.0.1:8080".parse().unwrap(),
|
||||
admin_addr: "127.0.0.1:9090".parse().unwrap(),
|
||||
};
|
||||
let store = Store::open(&directory.path().join("db"), &config)
|
||||
.await
|
||||
.unwrap();
|
||||
let auth = Auth::new(store.clone(), directory.path().to_owned())
|
||||
.await
|
||||
.unwrap();
|
||||
let snapshot = config::compile(config, 0, &policy).unwrap();
|
||||
let app = App::new(
|
||||
store,
|
||||
snapshot,
|
||||
policy,
|
||||
auth,
|
||||
32,
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(5),
|
||||
Duration::from_secs(30),
|
||||
);
|
||||
Self {
|
||||
app,
|
||||
_directory: directory,
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
headers: &[(&str, &str)],
|
||||
body: Body,
|
||||
) -> Received {
|
||||
send(self.app.clone(), method, path, headers, body).await
|
||||
}
|
||||
|
||||
async fn get(&self, path: &str, headers: &[(&str, &str)]) -> Received {
|
||||
self.send(Method::GET, path, headers, Body::empty()).await
|
||||
}
|
||||
|
||||
async fn change(&self, edit: impl FnOnce(&mut Config)) {
|
||||
let current = self.app.config.load_full();
|
||||
let mut config = current.envelope.config.clone();
|
||||
edit(&mut config);
|
||||
self.app
|
||||
.update(Update {
|
||||
expected_revision: current.envelope.revision,
|
||||
config,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn stop(self) {
|
||||
self.app.shutdown.cancel();
|
||||
self.app.tunnels.close();
|
||||
self.app.tunnels.wait().await;
|
||||
self.app.store.close().await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn send(
|
||||
app: Arc<App>,
|
||||
method: Method,
|
||||
path: &str,
|
||||
headers: &[(&str, &str)],
|
||||
body: Body,
|
||||
) -> Received {
|
||||
let mut builder = Request::builder()
|
||||
.method(method)
|
||||
.uri(path)
|
||||
.header(HOST, "localhost")
|
||||
.header(USER_AGENT, "cache-plan-regression/1.0");
|
||||
for (name, value) in headers {
|
||||
builder = builder.header(*name, *value);
|
||||
}
|
||||
let mut request = builder.body(body).unwrap();
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(PeerAddr("192.0.2.50:45678".parse().unwrap()));
|
||||
request
|
||||
.extensions_mut()
|
||||
.insert(LocalAddr("127.0.0.1:8080".parse().unwrap()));
|
||||
let response = router(app).oneshot(request).await.unwrap();
|
||||
let (parts, body) = response.into_parts();
|
||||
let body = body.collect().await.unwrap().to_bytes();
|
||||
Received {
|
||||
status: parts.status,
|
||||
headers: parts.headers,
|
||||
body,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_for(origin: SocketAddr, enabled: bool) -> Config {
|
||||
let mut config = config::demo_config(origin);
|
||||
config.cache.enabled = enabled;
|
||||
config.cache.expose_status = true;
|
||||
config.cache.expose_hits = true;
|
||||
config.cache.expose_cache_status = true;
|
||||
config.cache.expose_only_hits = false;
|
||||
config.cache.static_ignore_cookies = false;
|
||||
config
|
||||
}
|
||||
|
||||
fn web_config(directory: &TempDir, enabled: bool) -> Config {
|
||||
std::fs::write(directory.path().join("style.css"), CSS).unwrap();
|
||||
let mut config = config_for("127.0.0.1:9000".parse().unwrap(), enabled);
|
||||
config.web_services.push(crate::web_server::Service {
|
||||
id: "site".into(),
|
||||
name: "Site".into(),
|
||||
document_root: directory.path().to_owned(),
|
||||
cache_control: Some("public, max-age=600".into()),
|
||||
..Default::default()
|
||||
});
|
||||
config.vhosts[0].source = crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
};
|
||||
config.vhosts[0].backends.clear();
|
||||
config.vhosts[0].routes.clear();
|
||||
config
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_type_policy_denies_before_local_web_server_and_cache_and_updates_live() {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let mut config = web_config(&directory, true);
|
||||
config.vhosts[0].content_type_policy = crate::vhost::ContentTypePolicy {
|
||||
enabled: true,
|
||||
blocked: vec!["multipart/form-data".into(), "image/*".into()],
|
||||
};
|
||||
let fixture = Fixture::new(config).await;
|
||||
assert_eq!(fixture.get("/style.css", &[]).await.status, StatusCode::OK);
|
||||
assert_eq!(fixture.get("/style.css", &[]).await.status, StatusCode::OK);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get(
|
||||
"/style.css",
|
||||
&[("content-type", "multipart/form-data; boundary=example")]
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get("/style.css", &[("content-type", "IMAGE/PNG")])
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.send(
|
||||
Method::POST,
|
||||
"/style.css",
|
||||
&[("content-type", "multipart/form-data; boundary=x")],
|
||||
Body::from("sample")
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get("/style.css", &[("content-type", "text/plain")])
|
||||
.await
|
||||
.status,
|
||||
StatusCode::OK
|
||||
);
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get(
|
||||
"/style.css",
|
||||
&[("content-type", "application/json, multipart/form-data")]
|
||||
)
|
||||
.await
|
||||
.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
fixture
|
||||
.change(|config| config.vhosts[0].content_type_policy.enabled = false)
|
||||
.await;
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get("/style.css", &[("content-type", "multipart/form-data")])
|
||||
.await
|
||||
.status,
|
||||
StatusCode::OK
|
||||
);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn content_type_policy_denies_before_reverse_proxy_upstream_request() {
|
||||
let origin = origin().await;
|
||||
let mut config = config_for(origin.addr, false);
|
||||
config.vhosts[0].content_type_policy = crate::vhost::ContentTypePolicy {
|
||||
enabled: true,
|
||||
blocked: vec!["multipart/form-data".into()],
|
||||
};
|
||||
let fixture = Fixture::new(config).await;
|
||||
let blocked = fixture
|
||||
.send(
|
||||
Method::POST,
|
||||
"/",
|
||||
&[("content-type", "Multipart/Form-Data; boundary=abc")],
|
||||
Body::from("--abc\r\n"),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(blocked.status, StatusCode::FORBIDDEN);
|
||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 0);
|
||||
let allowed = fixture
|
||||
.get("/", &[("content-type", "application/json")])
|
||||
.await;
|
||||
assert_eq!(allowed.status, StatusCode::OK);
|
||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_cache_disabled_keeps_get_head_range_conditionals_and_observability() {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let fixture = Fixture::new(web_config(&directory, false)).await;
|
||||
let first = fixture.get("/style.css", &[]).await;
|
||||
assert_eq!(first.status, StatusCode::OK);
|
||||
assert_eq!(first.body, CSS);
|
||||
assert!(!first.headers.contains_key("x-cache"));
|
||||
let etag = first.headers["etag"].to_str().unwrap();
|
||||
let head = fixture
|
||||
.send(Method::HEAD, "/style.css", &[], Body::empty())
|
||||
.await;
|
||||
assert_eq!(head.status, StatusCode::OK);
|
||||
assert_eq!(head.headers["content-length"], CSS.len().to_string());
|
||||
assert!(head.body.is_empty());
|
||||
let partial = fixture
|
||||
.get("/style.css", &[("range", "bytes=0-3"), ("if-range", etag)])
|
||||
.await;
|
||||
assert_eq!(partial.status, StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(partial.body, &CSS[..4]);
|
||||
let mismatch = fixture
|
||||
.get(
|
||||
"/style.css",
|
||||
&[("range", "bytes=0-3"), ("if-range", "\"different\"")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(mismatch.status, StatusCode::OK);
|
||||
assert_eq!(mismatch.body, CSS);
|
||||
let conditional = fixture.get("/style.css", &[("if-none-match", etag)]).await;
|
||||
assert_eq!(conditional.status, StatusCode::NOT_MODIFIED);
|
||||
assert!(conditional.body.is_empty());
|
||||
let invalid = fixture
|
||||
.get("/style.css", &[("range", "bytes=99999-")])
|
||||
.await;
|
||||
assert_eq!(invalid.status, StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
assert_eq!(
|
||||
invalid.headers["content-range"],
|
||||
format!("bytes */{}", CSS.len())
|
||||
);
|
||||
let only_cached = fixture
|
||||
.get("/style.css", &[("cache-control", "only-if-cached")])
|
||||
.await;
|
||||
assert_eq!(only_cached.body, CSS);
|
||||
let logs = fixture.app.logs.recent();
|
||||
assert_eq!(logs.len(), 7);
|
||||
assert!(logs
|
||||
.iter()
|
||||
.all(|entry| entry.cache_reason == "cache_disabled"));
|
||||
assert!(logs.iter().all(|entry| entry.request_id.len() == 64));
|
||||
assert_eq!(fixture.app.metrics.completed.load(Ordering::Relaxed), 7);
|
||||
assert_eq!(fixture.app.metrics.active.load(Ordering::Relaxed), 0);
|
||||
assert_eq!(fixture.app.logs.stats().queue_dropped, 0);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_cache_hot_toggles_keep_miss_hit_and_bypass_diagnostics() {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let fixture = Fixture::new(web_config(&directory, false)).await;
|
||||
assert!(!fixture
|
||||
.get("/style.css", &[])
|
||||
.await
|
||||
.headers
|
||||
.contains_key("x-cache"));
|
||||
fixture.change(|config| config.cache.enabled = true).await;
|
||||
assert_eq!(
|
||||
fixture.get("/style.css", &[]).await.headers["x-cache"],
|
||||
"MISS"
|
||||
);
|
||||
let hit = fixture.get("/style.css", &[]).await;
|
||||
assert_eq!(hit.headers["x-cache"], "HIT");
|
||||
assert_eq!(hit.body, CSS);
|
||||
fixture
|
||||
.change(|config| {
|
||||
config.vhosts[0].routes.push(crate::vhost::PathRoute {
|
||||
id: "uncached".into(),
|
||||
path_prefix: "/style.css".into(),
|
||||
cache: Some(false),
|
||||
source: crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
})
|
||||
.await;
|
||||
let bypass = fixture.get("/style.css", &[]).await;
|
||||
assert_eq!(bypass.headers["x-cache"], "BYPASS");
|
||||
assert_eq!(bypass.headers["x-cache-reason"], "cache_disabled");
|
||||
fixture
|
||||
.change(|config| {
|
||||
config.vhosts[0].routes.clear();
|
||||
config.vhosts[0].cache.enabled = Some(false);
|
||||
})
|
||||
.await;
|
||||
assert!(!fixture
|
||||
.get("/style.css", &[])
|
||||
.await
|
||||
.headers
|
||||
.contains_key("x-cache"));
|
||||
fixture
|
||||
.change(|config| config.vhosts[0].cache.enabled = None)
|
||||
.await;
|
||||
assert_eq!(
|
||||
fixture.get("/style.css", &[]).await.headers["x-cache"],
|
||||
"MISS"
|
||||
);
|
||||
assert_eq!(
|
||||
fixture.get("/style.css", &[]).await.headers["x-cache"],
|
||||
"HIT"
|
||||
);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn web_transform_remains_independent_of_disabled_cache_and_range() {
|
||||
let directory = TempDir::new().unwrap();
|
||||
let mut config = web_config(&directory, false);
|
||||
config.transform.enabled = true;
|
||||
config.vhosts[0].transform.enabled = true;
|
||||
let fixture = Fixture::new(config).await;
|
||||
let normal = fixture.get("/style.css", &[]).await;
|
||||
assert_eq!(normal.status, StatusCode::OK);
|
||||
assert!(
|
||||
normal.body.len() < CSS.len(),
|
||||
"transform must remain active with cache off"
|
||||
);
|
||||
let range = fixture
|
||||
.get(
|
||||
"/style.css",
|
||||
&[("range", "bytes=0-3"), ("accept-encoding", "gzip")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(range.status, StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(range.body, &CSS[..4]);
|
||||
assert!(!range.headers.contains_key("content-encoding"));
|
||||
fixture
|
||||
.change(|config| config.vhosts[0].transform.enabled = false)
|
||||
.await;
|
||||
assert_eq!(fixture.get("/style.css", &[]).await.body, CSS);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cache_fast_paths_keep_global_and_location_access_control() {
|
||||
let directory = TempDir::new().unwrap();
|
||||
for enabled in [false, true] {
|
||||
let mut config = web_config(&directory, enabled);
|
||||
let deny = crate::acl::Acl {
|
||||
default: crate::acl::Action::Deny,
|
||||
rules: vec![],
|
||||
};
|
||||
config.host_policies.push(config::HostPolicy {
|
||||
hosts: vec!["localhost".into()],
|
||||
acl: deny.clone(),
|
||||
});
|
||||
let fixture = Fixture::new(config).await;
|
||||
assert_eq!(
|
||||
fixture.get("/style.css", &[]).await.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
fixture
|
||||
.change(|config| {
|
||||
config.host_policies.clear();
|
||||
config.vhosts[0].routes.push(crate::vhost::PathRoute {
|
||||
id: "private".into(),
|
||||
path_prefix: "/style.css".into(),
|
||||
acl: Some(deny),
|
||||
source: crate::vhost::Source::WebServer {
|
||||
web_service_id: "site".into(),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
fixture.get("/style.css", &[]).await.status,
|
||||
StatusCode::FORBIDDEN
|
||||
);
|
||||
fixture.stop().await;
|
||||
}
|
||||
}
|
||||
|
||||
struct Origin {
|
||||
addr: SocketAddr,
|
||||
requests: Arc<AtomicU64>,
|
||||
task: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
impl Drop for Origin {
|
||||
fn drop(&mut self) {
|
||||
self.task.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn origin() -> Origin {
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let requests = Arc::new(AtomicU64::new(0));
|
||||
let count = requests.clone();
|
||||
let app = Router::new().fallback(move |request: Request| {
|
||||
let count = count.clone();
|
||||
async move {
|
||||
count.fetch_add(1, Ordering::Relaxed);
|
||||
let (parts, body) = request.into_parts();
|
||||
if parts.uri.path() == "/slow" {
|
||||
tokio::time::sleep(Duration::from_millis(25)).await;
|
||||
}
|
||||
let input = body.collect().await.unwrap().to_bytes();
|
||||
let variant = parts
|
||||
.headers
|
||||
.get("x-variant")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.unwrap_or("default");
|
||||
let output = if parts.method == Method::POST {
|
||||
input
|
||||
} else {
|
||||
Bytes::from(format!("origin-{variant}"))
|
||||
};
|
||||
let mut response = Response::new(Body::from(output.clone()));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("etag", HeaderValue::from_static("\"origin-v1\""));
|
||||
response.headers_mut().insert(
|
||||
"cache-control",
|
||||
HeaderValue::from_static(if parts.uri.path() == "/private" {
|
||||
"private, max-age=600"
|
||||
} else {
|
||||
"public, max-age=600"
|
||||
}),
|
||||
);
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("vary", HeaderValue::from_static("X-Variant"));
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("content-type", HeaderValue::from_static("text/plain"));
|
||||
for name in [
|
||||
"range",
|
||||
"if-range",
|
||||
"if-none-match",
|
||||
"cookie",
|
||||
"authorization",
|
||||
"x-request-id",
|
||||
] {
|
||||
if let Some(value) = parts.headers.get(name) {
|
||||
let echoed: http::HeaderName = format!("x-seen-{name}").parse().unwrap();
|
||||
response.headers_mut().insert(echoed, value.clone());
|
||||
}
|
||||
}
|
||||
if parts
|
||||
.headers
|
||||
.get("if-none-match")
|
||||
.is_some_and(|v| v == "\"origin-v1\"")
|
||||
{
|
||||
*response.status_mut() = StatusCode::NOT_MODIFIED;
|
||||
*response.body_mut() = Body::empty();
|
||||
} else if parts.headers.get("range").is_some_and(|v| v == "bytes=0-2")
|
||||
&& parts
|
||||
.headers
|
||||
.get("if-range")
|
||||
.is_none_or(|v| v == "\"origin-v1\"")
|
||||
{
|
||||
*response.status_mut() = StatusCode::PARTIAL_CONTENT;
|
||||
response.headers_mut().insert(
|
||||
"content-range",
|
||||
format!("bytes 0-2/{}", output.len()).parse().unwrap(),
|
||||
);
|
||||
*response.body_mut() = Body::from(output.slice(..3));
|
||||
}
|
||||
response
|
||||
}
|
||||
});
|
||||
let task = tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
});
|
||||
Origin {
|
||||
addr,
|
||||
requests,
|
||||
task,
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_disabled_cache_preserves_headers_post_and_conditionals() {
|
||||
let origin = origin().await;
|
||||
let fixture = Fixture::new(config_for(origin.addr, false)).await;
|
||||
let range = fixture
|
||||
.get(
|
||||
"/inspect",
|
||||
&[
|
||||
("range", "bytes=0-2"),
|
||||
("if-range", "\"origin-v1\""),
|
||||
("cookie", "session=one"),
|
||||
("authorization", "Bearer example"),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(range.status, StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(range.body, "ori");
|
||||
assert_eq!(range.headers["x-seen-range"], "bytes=0-2");
|
||||
assert_eq!(range.headers["x-seen-if-range"], "\"origin-v1\"");
|
||||
assert_eq!(range.headers["x-seen-cookie"], "session=one");
|
||||
assert_eq!(range.headers["x-seen-authorization"], "Bearer example");
|
||||
assert_eq!(range.headers["x-seen-x-request-id"].as_bytes().len(), 64);
|
||||
assert!(!range.headers.contains_key("x-cache"));
|
||||
let conditional = fixture
|
||||
.get("/inspect", &[("if-none-match", "\"origin-v1\"")])
|
||||
.await;
|
||||
assert_eq!(conditional.status, StatusCode::NOT_MODIFIED);
|
||||
assert!(conditional.body.is_empty());
|
||||
let input = futures_util::stream::iter([
|
||||
Ok::<_, std::io::Error>(Bytes::from_static(b"streamed ")),
|
||||
Ok(Bytes::from_static(b"content")),
|
||||
]);
|
||||
let post = fixture
|
||||
.send(Method::POST, "/echo", &[], Body::from_stream(input))
|
||||
.await;
|
||||
assert_eq!(post.status, StatusCode::OK);
|
||||
assert_eq!(post.body, "streamed content");
|
||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 3);
|
||||
assert!(fixture
|
||||
.app
|
||||
.logs
|
||||
.recent()
|
||||
.iter()
|
||||
.all(|entry| entry.cache_reason == "cache_disabled"));
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_cache_on_preserves_vary_private_revalidation_and_ranges() {
|
||||
let origin = origin().await;
|
||||
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
||||
for variant in ["a", "b"] {
|
||||
assert_eq!(
|
||||
fixture
|
||||
.get("/vary", &[("x-variant", variant)])
|
||||
.await
|
||||
.headers["x-cache"],
|
||||
"MISS"
|
||||
);
|
||||
}
|
||||
let hit = fixture.get("/vary", &[("x-variant", "a")]).await;
|
||||
assert_eq!(hit.headers["x-cache"], "HIT");
|
||||
assert_eq!(hit.body, "origin-a");
|
||||
let range = fixture
|
||||
.get(
|
||||
"/vary",
|
||||
&[
|
||||
("x-variant", "a"),
|
||||
("range", "bytes=0-2"),
|
||||
("if-range", "\"origin-v1\""),
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(range.status, StatusCode::PARTIAL_CONTENT);
|
||||
assert_eq!(range.headers["x-cache"], "HIT");
|
||||
assert_eq!(range.body, "ori");
|
||||
let validated = fixture
|
||||
.get(
|
||||
"/vary",
|
||||
&[("x-variant", "a"), ("cache-control", "no-cache")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(validated.status, StatusCode::OK);
|
||||
assert_eq!(validated.headers["x-cache"], "HIT");
|
||||
assert_eq!(validated.headers["x-cache-reason"], "cache_revalidated");
|
||||
assert_eq!(validated.body, "origin-a");
|
||||
let conditional = fixture
|
||||
.get(
|
||||
"/vary",
|
||||
&[("x-variant", "a"), ("if-none-match", "\"origin-v1\"")],
|
||||
)
|
||||
.await;
|
||||
assert_eq!(conditional.status, StatusCode::NOT_MODIFIED);
|
||||
let bypass = fixture
|
||||
.get("/vary", &[("authorization", "Bearer example")])
|
||||
.await;
|
||||
assert_eq!(bypass.headers["x-cache-reason"], "request_authorization");
|
||||
for _ in 0..2 {
|
||||
assert_eq!(
|
||||
fixture.get("/private", &[]).await.headers["x-cache"],
|
||||
"MISS"
|
||||
);
|
||||
}
|
||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 6);
|
||||
fixture.stop().await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn proxy_cache_enabled_concurrent_fill_stays_single_flight() {
|
||||
let origin = origin().await;
|
||||
let fixture = Fixture::new(config_for(origin.addr, true)).await;
|
||||
let mut requests = tokio::task::JoinSet::new();
|
||||
for _ in 0..12 {
|
||||
let app = fixture.app.clone();
|
||||
requests
|
||||
.spawn(async move { send(app, Method::GET, "/slow", &[], Body::empty()).await });
|
||||
}
|
||||
while let Some(result) = requests.join_next().await {
|
||||
let response = result.unwrap();
|
||||
assert_eq!(response.status, StatusCode::OK);
|
||||
assert_eq!(response.body, "origin-default");
|
||||
}
|
||||
assert_eq!(origin.requests.load(Ordering::Relaxed), 1);
|
||||
assert_eq!(fixture.app.metrics.completed.load(Ordering::Relaxed), 12);
|
||||
assert_eq!(fixture.app.metrics.active.load(Ordering::Relaxed), 0);
|
||||
fixture.stop().await;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,6 @@ if let Some(ticket) = &cache_ticket {
|
||||
.unwrap_or_else(|e| e.into_inner()) = "cache_miss";
|
||||
}
|
||||
} else if cached_revalidation.is_none() {
|
||||
// Account a true cache miss even when the client requested validation.
|
||||
let _ = state.cache.get_tiered(ticket, cache_settings).await;
|
||||
state.cache.record(scope, &host, "MISS", "cache_miss");
|
||||
miss_recorded = true;
|
||||
@@ -199,10 +198,6 @@ if only_cached {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Foreground fills are single-flight per base cache key. Range responses are
|
||||
// never fill leaders because a 206 is not stored as a separate cache object.
|
||||
// A follower can wake after a different Vary representation was published; in
|
||||
// that case it competes for the next flight instead of stampeding the origin.
|
||||
let mut fill_leader = None;
|
||||
if handshake.is_none()
|
||||
&& matches!(range_selection, crate::cache::RangeSelection::None)
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
mod h3_navigation_tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_bodyless_get_and_head_are_normalized_for_http1_navigation() {
|
||||
use bytes::Bytes;
|
||||
for method in [Method::GET, Method::HEAD] {
|
||||
let stream = futures_util::stream::empty::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder()
|
||||
.method(method.clone())
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(request.body().is_end_stream(), "{method}");
|
||||
assert_eq!(request.body().size_hint().exact(), Some(0), "{method}");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_get_with_payload_retains_payload_for_http1_upstream() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(
|
||||
b"unexpected-but-valid",
|
||||
))]);
|
||||
let mut request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11)
|
||||
.await
|
||||
.unwrap();
|
||||
let body = request.into_body().collect().await.unwrap().to_bytes();
|
||||
assert_eq!(body, Bytes::from_static(b"unexpected-but-valid"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_stalled_cache_probe_bypasses_without_blocking_navigation() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
let empty = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(750),
|
||||
empty_for_cache(&mut request),
|
||||
)
|
||||
.await
|
||||
.expect("bounded cache preflight")
|
||||
.unwrap();
|
||||
assert!(!empty, "unknown body is not cacheable");
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_cache_probe_replays_nonempty_get_without_dropping_bytes() {
|
||||
use bytes::Bytes;
|
||||
let stream =
|
||||
futures_util::stream::iter([Ok::<_, std::io::Error>(Bytes::from_static(b"intact"))]);
|
||||
let mut request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
assert!(!empty_for_cache(&mut request).await.unwrap());
|
||||
assert_eq!(
|
||||
request.into_body().collect().await.unwrap().to_bytes(),
|
||||
Bytes::from_static(b"intact")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn h3_stalled_empty_get_does_not_block_navigation() {
|
||||
use bytes::Bytes;
|
||||
let stream = futures_util::stream::pending::<Result<Bytes, std::io::Error>>();
|
||||
let mut request = Request::builder()
|
||||
.method(Method::GET)
|
||||
.version(Version::HTTP_3)
|
||||
.uri("https://rp.example.test/")
|
||||
.body(Body::from_stream(stream))
|
||||
.unwrap();
|
||||
tokio::time::timeout(
|
||||
std::time::Duration::from_millis(750),
|
||||
normalize_h3_h1_unknown_body(&mut request, Version::HTTP_11),
|
||||
)
|
||||
.await
|
||||
.expect("bounded GET probe")
|
||||
.unwrap();
|
||||
assert_eq!(request.body().size_hint().exact(), None);
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,6 @@
|
||||
).await {
|
||||
return fallback;
|
||||
}
|
||||
// Keep the upstream body as hyper::body::Incoming until the
|
||||
// HTTP/2 WebSocket and regular response paths have adapted it.
|
||||
// The opt-in application error page is applied to Response<Body>
|
||||
// in success_prepare.rs, before transformation/cache admission.
|
||||
let (origin_validation_headers, origin_plan, origin_reason, mut response) =
|
||||
include!("success_prepare.rs");
|
||||
include!("success_finish.rs")
|
||||
@@ -46,9 +42,6 @@
|
||||
"upstream connection failed"
|
||||
);
|
||||
} else if graceful_h2_close {
|
||||
// Correlated GOAWAY(NO_ERROR) is expected connection churn. Keep
|
||||
// failed requests visible as 502/access-log entries, but avoid
|
||||
// repeating their verbose diagnostic at DEBUG severity.
|
||||
tracing::trace!(
|
||||
backend = %active_backend.id,
|
||||
origin = %active_backend.origin,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
// Global host, profile, vhost and location access policies were already enforced
|
||||
// before this source/cache path. Do not re-evaluate IP ACLs in isolation here:
|
||||
// allow-only country entries and explicit IP allows form one policy scope.
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
*exchange.route.lock().unwrap_or_else(|e| e.into_inner()) = Some(route.id.clone());
|
||||
exchange.set_source(if route.response.is_some() {
|
||||
@@ -15,8 +12,6 @@ if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
}
|
||||
});
|
||||
}
|
||||
// Local location responses intentionally still pass host/profile/location ACLs
|
||||
// and the outer response-header/security pipeline, but never select/open an upstream.
|
||||
if let Some(spec) = &route.response {
|
||||
return local_location_response(spec, request.method());
|
||||
}
|
||||
@@ -125,9 +120,6 @@ let cache_settings = route
|
||||
let transform_settings = route.frontend.as_ref().map(|f| &f.transform);
|
||||
let scope = route.frontend.as_ref().map(|f| f.id.as_str()).unwrap_or("");
|
||||
if let Some(exchange) = request.extensions().get::<Arc<Exchange>>() {
|
||||
// The resolved route was already recorded before local response, LB and
|
||||
// ACL decisions. Do not acquire the route mutex and copy its ID twice on
|
||||
// every reverse-proxy request.
|
||||
exchange.set_cache_enabled(cache_settings.active_for(route.cache));
|
||||
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
|
||||
}
|
||||
@@ -169,8 +161,6 @@ let body_empty = if cache_enabled
|
||||
} else {
|
||||
cache_enabled && request.body().is_end_stream()
|
||||
};
|
||||
// Keep the original URI alive through forwarding: ordinary targets can be
|
||||
// borrowed while route rewrites still allocate and are strictly validated.
|
||||
let public_suffix: std::borrow::Cow<'_, str> = std::borrow::Cow::Borrowed(
|
||||
routing_uri.path_and_query().map(|v| v.as_str()).unwrap_or("/")
|
||||
);
|
||||
@@ -247,9 +237,6 @@ if let Some(frontend) = &route.frontend {
|
||||
crate::headers::apply(request.headers_mut(), &frontend.headers.request);
|
||||
}
|
||||
crate::headers::apply(request.headers_mut(), &route.headers.request);
|
||||
// Canonical trusted metadata cannot be overwritten by custom request rules.
|
||||
// A vhost may intentionally withhold only the visitor IP from the application;
|
||||
// ACL/rate-limit/Tinyauth decisions above still use the authenticated client address.
|
||||
context.apply_upstream(
|
||||
request.headers_mut(),
|
||||
route
|
||||
@@ -289,9 +276,6 @@ let if_range = if effective_cacheable {
|
||||
} else { None };
|
||||
let range_requested = !matches!(range_selection, crate::cache::RangeSelection::None);
|
||||
|
||||
// Match Varnish-style cache identity: a load-balanced backend member must not
|
||||
// fragment the object cache. The vhost scope + canonical host + request target
|
||||
// already identify the public resource; Vary handles representation variants.
|
||||
let cache_route = route.id.as_str();
|
||||
let mut cache_key_headers = None;
|
||||
if effective_cacheable && ignore_static_cookie && request.headers().contains_key("cookie") {
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
// A backend 304 validates the stored representation. Merge the new
|
||||
// metadata into the cached object and answer the client's original
|
||||
// condition locally, just like Varnish's conditional delivery path.
|
||||
if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
|
||||
if let (Some(ticket), Some(cached)) =
|
||||
(cache_ticket.as_ref(), cached_revalidation.as_ref())
|
||||
@@ -39,9 +36,6 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
|
||||
cache_settings.annotate_resident_age(response.headers_mut(), resident_age);
|
||||
Some(response)
|
||||
} else if state.cache.ticket_current(ticket) {
|
||||
// The validation succeeded but the refreshed representation is no
|
||||
// longer eligible for shared storage (for example new no-store).
|
||||
// Serve it once, then discard the stored copy.
|
||||
state.cache.discard(ticket);
|
||||
let mut response = if !client_not_modified {
|
||||
if let crate::cache::RangeSelection::Single(range) = range_selection {
|
||||
@@ -91,8 +85,6 @@ if response.status() == StatusCode::NOT_MODIFIED && revalidation_sent {
|
||||
}
|
||||
}
|
||||
}
|
||||
// Both origin and transformed policies must be cacheable; hiding a
|
||||
// Set-Cookie/private header never authorizes storage of private content.
|
||||
let transformed_plan =
|
||||
if origin_plan.is_none()
|
||||
|| (ignore_static_cookie && crate::cache::varies_on(response.headers(), "cookie")) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
{
|
||||
// Only a real cache revalidation needs origin headers after response rules.
|
||||
let origin_validation_headers = if revalidation_sent
|
||||
&& response.status() == StatusCode::NOT_MODIFIED
|
||||
{
|
||||
@@ -112,9 +111,6 @@ if handshake.as_ref().is_some_and(|handshake| {
|
||||
"RustProxy: HTTP/1.1 WebSocket upstream returned 2xx without switching protocols\n",
|
||||
);
|
||||
}
|
||||
// Varnish built-in behavior treats non-GET/HEAD methods as pass: the
|
||||
// response is never stored, but existing cached GET representations are
|
||||
// not purged implicitly. Explicit cache purge remains an admin action.
|
||||
let origin_plan = if cache_ticket.is_some()
|
||||
&& !(ignore_static_cookie && crate::cache::varies_on(response.headers(), "cookie"))
|
||||
{
|
||||
@@ -181,9 +177,6 @@ let mut response = if h3_tunnel {
|
||||
"RustProxy: WebSocket exchange state unavailable\n",
|
||||
);
|
||||
};
|
||||
// Hyper cannot complete the client-side upgrade while the 101 body
|
||||
// remains owned here. Drop it, then prove that OnUpgrade is ready
|
||||
// BEFORE returning a successful RFC 9220 response to the H3 layer.
|
||||
drop(body);
|
||||
let stop_token = stop
|
||||
.as_ref()
|
||||
@@ -213,8 +206,6 @@ let mut response = if h3_tunnel {
|
||||
}
|
||||
};
|
||||
exchange.websocket_phase("tunnel_ready");
|
||||
// RFC 8441/9220 uses a successful 2xx response, never 101, and
|
||||
// Sec-WebSocket-Accept is not part of the downstream handshake.
|
||||
parts.status = StatusCode::OK;
|
||||
parts.headers.remove("sec-websocket-accept");
|
||||
parts.headers.remove("content-length");
|
||||
@@ -338,21 +329,11 @@ let mut response = if h3_tunnel {
|
||||
}
|
||||
Response::from_parts(parts, Body::new(body))
|
||||
};
|
||||
// The upstream HTTP client returns Response<hyper::body::Incoming>. Adapt it
|
||||
// before rendering a RustProxy error page, so HTTP/2 WebSocket tunnels can
|
||||
// continue using the original Incoming body without type erasure.
|
||||
// An internal named-location fallback has already won earlier in response.rs.
|
||||
let intercept_origin_error = !route.internal && route.frontend.as_ref().is_some_and(|frontend| {
|
||||
frontend.pages.intercept_statuses.contains(&response.status().as_u16())
|
||||
});
|
||||
// Mark every upstream response as application-supplied, even if this vhost
|
||||
// does not intercept its status. The observer must not overwrite it merely
|
||||
// because a global 404/5xx error page exists.
|
||||
response = intercept_route_origin_error(response, route, &snapshot);
|
||||
if intercept_origin_error {
|
||||
// The earlier response rules modified the original origin headers;
|
||||
// reapply them to the newly rendered page so the vhost's header policy
|
||||
// remains effective. Do not allow a custom header rule to cache errors.
|
||||
apply_response_rules(
|
||||
response.headers_mut(),
|
||||
&snapshot,
|
||||
|
||||
@@ -7,8 +7,6 @@ if let Some(handshake) = &handshake {
|
||||
request.headers_mut().remove("content-length");
|
||||
request.headers_mut().remove("transfer-encoding");
|
||||
if websocket_upstream_h2 {
|
||||
// Preserve CONNECT + :protocol=websocket for an explicit HTTP/2
|
||||
// upstream. HTTP/1.1-only fields are forbidden on this hop.
|
||||
request.headers_mut().remove("connection");
|
||||
request.headers_mut().remove("upgrade");
|
||||
request.headers_mut().remove("sec-websocket-key");
|
||||
@@ -21,8 +19,6 @@ if let Some(handshake) = &handshake {
|
||||
.headers_mut()
|
||||
.insert("upgrade", HeaderValue::from_static("websocket"));
|
||||
if !handshake.h1() {
|
||||
// Extended CONNECT replaces the RFC 6455 proof headers on the
|
||||
// downstream hop. Synthesize them only for an HTTP/1.1 backend.
|
||||
*request.method_mut() = Method::GET;
|
||||
request.headers_mut().remove("sec-websocket-key");
|
||||
request.headers_mut().remove("sec-websocket-accept");
|
||||
@@ -40,11 +36,8 @@ if let Some(handshake) = &handshake {
|
||||
}
|
||||
}
|
||||
if let Some(auth) = frontend.and_then(|f| f.basic_auth.as_ref()) {
|
||||
// Enforce after all custom request-header rules; they cannot restore credentials.
|
||||
auth.apply_upstream(request.headers_mut(), basic_identity);
|
||||
}
|
||||
// Transformation operates on the identity representation. Prefer identity from
|
||||
// the origin, while still decoding a supported encoding if the origin ignores it.
|
||||
if range_requested || transform_settings.is_some_and(|settings| settings.enabled) {
|
||||
request.headers_mut().remove("accept-encoding");
|
||||
}
|
||||
@@ -54,16 +47,9 @@ if pending_revalidation {
|
||||
revalidation_sent = cached.apply_validators(request.headers_mut());
|
||||
}
|
||||
}
|
||||
// Varnish hashes HEAD with GET and turns a regular cacheable backend fetch
|
||||
// into GET. The outer response middleware still strips the body for the
|
||||
// original HEAD client, while a MISS can populate the shared GET object.
|
||||
if request.method() == Method::HEAD && cache_ticket.is_some() {
|
||||
*request.method_mut() = Method::GET;
|
||||
}
|
||||
// Save the pre-normalization headers only for replayable requests that may
|
||||
// actually be retried. The common one-backend HTTP/1 route has no retry peer
|
||||
// or H2 GOAWAY handling, so copying HeaderMap for every request was wasted.
|
||||
// Never replay writes, streaming uploads, auth or WebSocket requests.
|
||||
let retry_method = request.method().clone();
|
||||
let retry_policy = route.pool.as_ref().map(|pool| &pool.settings.retry);
|
||||
let pool_retry_possible = route.backend_id.is_none()
|
||||
@@ -137,8 +123,6 @@ if websocket_upstream_h2 {
|
||||
*request.body_mut() = websocket::wrap_h2_request_body(body, session.clone(), exchange);
|
||||
h2_direct_session = Some(session);
|
||||
}
|
||||
// Do not classify a slow client upload as a slow origin response. Probes
|
||||
// independently measure origin liveness/latency with body-free requests.
|
||||
let request_body_empty = request.body().size_hint().exact() == Some(0);
|
||||
let proactive_base_headers = if cache_ticket.is_some() && cache_settings.proactive_refresh_enabled {
|
||||
request.headers().clone()
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
{
|
||||
// Web Server is an origin just like a reverse-proxy backend. It must pass
|
||||
// through the same vhost cache policy, transform stage and response rules.
|
||||
if handshake.is_some() {
|
||||
return error(StatusCode::BAD_REQUEST,
|
||||
"RustProxy: Web Server source does not support WebSocket upgrades\n");
|
||||
@@ -30,20 +28,13 @@
|
||||
exchange.set_cache_enabled(enabled);
|
||||
exchange.set_transform_enabled(transform_settings.is_some_and(|settings| settings.enabled));
|
||||
}
|
||||
// The authority is already validated by effective_host. HTTP/2 and HTTP/3
|
||||
// requests commonly have :authority but no Host field, which otherwise
|
||||
// makes PHP-FPM omit HTTP_HOST, breaking WordPress.
|
||||
request.headers_mut().insert(HOST, original_host.clone());
|
||||
let request_method = request.method().clone();
|
||||
// This branch owns local-origin delivery. Move its headers instead of
|
||||
// cloning them; the request remains only for exchange/body extensions.
|
||||
let mut request_headers = std::mem::take(request.headers_mut());
|
||||
let request_uri = request.uri().clone();
|
||||
let request_version = request.version();
|
||||
let public_suffix = request_uri.path_and_query()
|
||||
.map(|item| item.as_str()).unwrap_or("/").to_owned();
|
||||
// H2/H3 may expose an unknown-length empty stream before its final frame.
|
||||
// Reuse the bounded probe that preserves any nonempty streaming body.
|
||||
let empty_body = if enabled && (request_method == Method::GET || request_method == Method::HEAD) {
|
||||
match empty_for_cache(&mut request).await {
|
||||
Ok(empty) => empty,
|
||||
@@ -62,8 +53,6 @@
|
||||
&request_method, &request_headers, empty_body, &path,
|
||||
request_uri.query().is_some(), cache_settings,
|
||||
);
|
||||
// HEAD can use an existing GET representation but may not fill a cache
|
||||
// entry with its empty downstream body.
|
||||
let eligible = enabled && policy.bypass_reason().is_none();
|
||||
let key_headers = if eligible && ignore_cookie && request_headers.contains_key("cookie") {
|
||||
let mut headers = request_headers.clone();
|
||||
@@ -79,9 +68,6 @@
|
||||
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 range_requested = !matches!(range, crate::cache::RangeSelection::None);
|
||||
// Unlike the original Web Server path, retain stale and client-forced
|
||||
// revalidation candidates. A local 304 can refresh them without replacing
|
||||
// the cached body or resetting X-Cache-Hits (same as Reverse Proxy).
|
||||
let cached_revalidation = if let Some(ticket) = cache_ticket.as_ref() {
|
||||
state.cache.revalidation_tiered(ticket).await
|
||||
} else { None };
|
||||
@@ -91,10 +77,7 @@
|
||||
if let Some(ticket) = &cache_ticket {
|
||||
if !needs_validation {
|
||||
if let Some(mut hit) = state.cache.get_tiered(ticket, cache_settings).await {
|
||||
// Filesystem/PHP does not have the reverse-proxy background
|
||||
// revalidator. Refresh stale representations synchronously.
|
||||
if hit.stale {
|
||||
// Fall through and fetch from the local origin.
|
||||
} else {
|
||||
let conditional = crate::cache::conditional_not_modified(
|
||||
&request_headers, hit.response.headers());
|
||||
@@ -130,15 +113,11 @@
|
||||
return error(StatusCode::GATEWAY_TIMEOUT,
|
||||
"RustProxy: only-if-cached request has no usable Web Server representation\n");
|
||||
}
|
||||
// Neither the Request nor the streaming Body is held across a borrowed
|
||||
// await: the filesystem/PHP source owns the body for its full lifetime.
|
||||
let static_body = std::mem::replace(request.body_mut(), Body::empty());
|
||||
let (origin_request_headers, revalidation_sent) = if cache_ticket.is_some() {
|
||||
web_cache_revalidation::web_origin_request_headers(
|
||||
&request_headers, cached_revalidation.as_ref())
|
||||
} else {
|
||||
// No cache result can need the original conditionals later. Preserve
|
||||
// all origin HTTP semantics by handing over the untouched header map.
|
||||
(std::mem::take(&mut request_headers), false)
|
||||
};
|
||||
let started = Instant::now();
|
||||
@@ -180,8 +159,6 @@
|
||||
hit.response.extensions_mut().insert(StaticSourceResponse);
|
||||
return intercept_route_origin_error(hit.response, route, &snapshot);
|
||||
}
|
||||
// A simultaneous PURGE/config change invalidated the ticket. Do
|
||||
// not accidentally return an origin 304 without a reusable body.
|
||||
return error(StatusCode::SERVICE_UNAVAILABLE,
|
||||
"RustProxy: Web Server cache entry invalidated during revalidation\n");
|
||||
}
|
||||
@@ -189,16 +166,10 @@
|
||||
if cached_revalidation.is_some() && response.status() != StatusCode::NOT_MODIFIED
|
||||
&& response.status().as_u16() < 500
|
||||
{
|
||||
// A changed response (including a 206, 404 or an uncacheable 200)
|
||||
// supersedes the old object. Also discard when the old entry has no
|
||||
// validators and therefore must be fetched unconditionally. A complete
|
||||
// cacheable GET below can refill the entry.
|
||||
if let Some(ticket) = cache_ticket.as_ref() {
|
||||
state.cache.discard(ticket);
|
||||
}
|
||||
}
|
||||
// Do not allow a later response-header rewrite to override a private,
|
||||
// explicitly uncacheable PHP/CGI origin response.
|
||||
let origin_headers = response.headers();
|
||||
let origin_safe = cache_ticket.is_some() && {
|
||||
let explicit_origin_policy = origin_headers.contains_key("cache-control")
|
||||
@@ -221,14 +192,11 @@
|
||||
apply_response_rules(response.headers_mut(), &snapshot, route.frontend.as_deref(),
|
||||
Some(route), false);
|
||||
if response.extensions().get::<crate::pages::PageServed>().is_some() {
|
||||
// A vhost header rule must not make a replaced error cacheable.
|
||||
response.headers_mut().insert("cache-control", HeaderValue::from_static("no-store"));
|
||||
}
|
||||
response.extensions_mut().insert(ResponseRulesApplied);
|
||||
response.extensions_mut().insert(StaticSourceResponse);
|
||||
|
||||
// Cache representation must reflect vhost response rules and transforms,
|
||||
// not solely the Web Service's Cache-Control header.
|
||||
let origin_eligible_plan = if origin_safe
|
||||
&& !(ignore_cookie && crate::cache::varies_on(response.headers(), "cookie"))
|
||||
{
|
||||
@@ -288,11 +256,6 @@
|
||||
cache_settings.annotate_reason(response.headers_mut(), outcome, 0, 0, reason);
|
||||
let transformed = response.extensions().get::<crate::transform::Applied>().is_some();
|
||||
cache_settings.annotate_transformed(response.headers_mut(), transformed);
|
||||
// Register only AFTER the vhost response policy, cache and transformation
|
||||
// decisions are final. Never feed synthetic transport placeholders into
|
||||
// cache, compression or the transformation engine. The ordinary body
|
||||
// remains the fallback on every other protocol/response and when the
|
||||
// per-connection bridge has no capacity.
|
||||
#[cfg(target_os = "linux")]
|
||||
if !enabled && identity_request && request_method == Method::GET
|
||||
&& !transform_settings.is_some_and(|settings| settings.enabled)
|
||||
|
||||
Reference in new issue
Block a user