This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+3 -4
View File
@@ -29,7 +29,8 @@ pub async fn apply(
if response.headers().contains_key("content-range") {
return Ok(skip(response, None, "content_range"));
}
let Some(kind) = content_type_kind(response.headers()).or_else(|| extension_kind(path, settings))
let Some(kind) =
content_type_kind(response.headers()).or_else(|| extension_kind(path, settings))
else {
return Ok(skip(response, None, "unsupported_type"));
};
@@ -79,9 +80,7 @@ pub async fn apply(
.collect()
.await
.map_err(|error| {
anyhow::anyhow!(
"collect bounded static response for transformation: {error}"
)
anyhow::anyhow!("collect bounded static response for transformation: {error}")
})?
.to_bytes();
if raw.len() > settings.max_body_bytes {
+25 -38
View File
@@ -68,7 +68,9 @@ fn content_type_kind(headers: &HeaderMap) -> Option<Kind> {
let value = headers.get("content-type")?.to_str().ok()?;
let mime = value.split(';').next()?.trim().to_ascii_lowercase();
match mime.as_str() {
"application/javascript" | "text/javascript" | "application/ecmascript"
"application/javascript"
| "text/javascript"
| "application/ecmascript"
| "text/ecmascript" => Some(Kind::Javascript),
"text/css" => Some(Kind::Css),
"text/html" | "application/xhtml+xml" => Some(Kind::Html),
@@ -166,26 +168,19 @@ fn decode<'a>(body: &'a [u8], encoding: Option<&str>, limit: usize) -> Result<Co
// Borrow them instead of creating a second full-size allocation.
Ok(Cow::Borrowed(body))
}
Some("gzip") => read_bounded(flate2::read::GzDecoder::new(body), limit, "gzip")
.map(Cow::Owned),
Some("gzip") => {
read_bounded(flate2::read::GzDecoder::new(body), limit, "gzip").map(Cow::Owned)
}
Some("deflate") => {
let zlib = flate2::read::ZlibDecoder::new(body);
match read_bounded(zlib, limit, "deflate") {
Ok(value) => Ok(Cow::Owned(value)),
Err(_) => read_bounded(
flate2::read::DeflateDecoder::new(body),
limit,
"deflate",
)
.map(Cow::Owned),
Err(_) => read_bounded(flate2::read::DeflateDecoder::new(body), limit, "deflate")
.map(Cow::Owned),
}
}
Some("br") => read_bounded(
brotli::Decompressor::new(body, 16 * 1024),
limit,
"brotli",
)
.map(Cow::Owned),
Some("br") => read_bounded(brotli::Decompressor::new(body, 16 * 1024), limit, "brotli")
.map(Cow::Owned),
Some("zstd") => {
let decoder = zstd::stream::read::Decoder::new(body).context("open zstd decoder")?;
read_bounded(decoder, limit, "zstd").map(Cow::Owned)
@@ -204,7 +199,11 @@ fn drop_vary_accept_encoding(headers: &mut HeaderMap) {
let Ok(value) = value.to_str() else {
return;
};
for token in value.split(',').map(str::trim).filter(|token| !token.is_empty()) {
for token in value
.split(',')
.map(str::trim)
.filter(|token| !token.is_empty())
{
if token == "*" {
// `Vary: *` has special semantics and is rejected by the shared
// cache. Do not rewrite it into a different policy.
@@ -213,7 +212,10 @@ fn drop_vary_accept_encoding(headers: &mut HeaderMap) {
if token.eq_ignore_ascii_case("accept-encoding") {
continue;
}
if !retained.iter().any(|existing| existing.eq_ignore_ascii_case(token)) {
if !retained
.iter()
.any(|existing| existing.eq_ignore_ascii_case(token))
{
retained.push(token.to_owned());
}
}
@@ -235,8 +237,7 @@ fn transformed_etag(body: &[u8]) -> HeaderValue {
let _ = write!(&mut value, "{byte:02x}");
}
value.push('"');
HeaderValue::from_str(&value)
.expect("sha256 validator is always a valid header")
HeaderValue::from_str(&value).expect("sha256 validator is always a valid header")
}
fn transform_memory_required_bytes(
@@ -249,8 +250,8 @@ fn transform_memory_required_bytes(
} else {
content_length.checked_add(max_body_bytes.checked_mul(2)?)?
};
let units = estimated.checked_add(TRANSFORM_MEMORY_UNIT_BYTES - 1)?
/ TRANSFORM_MEMORY_UNIT_BYTES;
let units =
estimated.checked_add(TRANSFORM_MEMORY_UNIT_BYTES - 1)? / TRANSFORM_MEMORY_UNIT_BYTES;
units.max(1).checked_mul(TRANSFORM_MEMORY_UNIT_BYTES)
}
@@ -279,12 +280,7 @@ fn try_reserve_memory_from(
if next > budget_bytes {
return None;
}
match counter.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
match counter.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => {
return Some(TransformMemoryReservation {
counter,
@@ -300,11 +296,7 @@ fn try_reserve_transform_memory(
required_bytes: usize,
budget_bytes: usize,
) -> Option<TransformMemoryReservation<'static>> {
try_reserve_memory_from(
&TRANSFORM_MEMORY_IN_USE_BYTES,
required_bytes,
budget_bytes,
)
try_reserve_memory_from(&TRANSFORM_MEMORY_IN_USE_BYTES, required_bytes, budget_bytes)
}
struct TransformInflightReservation<'a> {
@@ -327,12 +319,7 @@ fn try_reserve_slot_from(
return None;
}
let next = current.checked_add(1)?;
match counter.compare_exchange_weak(
current,
next,
Ordering::AcqRel,
Ordering::Acquire,
) {
match counter.compare_exchange_weak(current, next, Ordering::AcqRel, Ordering::Acquire) {
Ok(_) => return Some(TransformInflightReservation { counter }),
Err(actual) => current = actual,
}
+6 -2
View File
@@ -64,8 +64,12 @@ pub fn minify(body: &[u8]) -> Result<Vec<u8>> {
}
fn needs_space(previous: Option<char>, next: Option<char>) -> bool {
let Some(previous) = previous else { return false; };
let Some(next) = next else { return false; };
let Some(previous) = previous else {
return false;
};
let Some(next) = next else {
return false;
};
!matches!(previous, '{' | '}' | ':' | ';' | ',' | '>')
&& !matches!(next, '{' | '}' | ':' | ';' | ',' | '>')
}
+5 -1
View File
@@ -106,7 +106,11 @@ fn raw_tag(tag: &str) -> Option<(RawElement, bool, bool)> {
return None;
}
let closing = inner.starts_with('/');
let inner = if closing { inner[1..].trim_start() } else { inner };
let inner = if closing {
inner[1..].trim_start()
} else {
inner
};
let self_closing = inner.ends_with('/');
let name = inner
.split(|ch: char| ch.is_ascii_whitespace() || ch == '/' || ch == '>')
+12 -8
View File
@@ -111,12 +111,10 @@ impl Default for Settings {
html: false,
svg: false,
json: false,
extensions: vec![
"js", "mjs", "cjs", "css", "html", "htm", "svg", "json",
]
.into_iter()
.map(str::to_owned)
.collect(),
extensions: vec!["js", "mjs", "cjs", "css", "html", "htm", "svg", "json"]
.into_iter()
.map(str::to_owned)
.collect(),
max_body_bytes: DEFAULT_MAX_BODY_BYTES,
memory_budget_bytes: DEFAULT_MEMORY_BUDGET_BYTES,
max_inflight: DEFAULT_MAX_INFLIGHT,
@@ -137,7 +135,10 @@ impl Settings {
(1..=1024).contains(&self.max_inflight),
"transform.max_inflight must be 1..1024"
);
ensure!(self.extensions.len() <= 64, "at most 64 transform extensions");
ensure!(
self.extensions.len() <= 64,
"at most 64 transform extensions"
);
let mut seen = std::collections::HashSet::new();
for extension in &self.extensions {
ensure!(
@@ -293,7 +294,10 @@ impl Counters {
.original_bytes
.saturating_sub(applied.transformed_bytes) as u64,
);
*self.by_kind.entry(applied.kind.as_str().into()).or_default() += 1;
*self
.by_kind
.entry(applied.kind.as_str().into())
.or_default() += 1;
}
fn record_skipped(&mut self, skipped: Skipped, elapsed: Duration) {
+37 -24
View File
@@ -15,11 +15,7 @@ mod tests {
Some(4 * 1024 * 1024)
);
assert_eq!(
transform_memory_required_bytes(
2 * 1024 * 1024,
Some("gzip"),
2 * 1024 * 1024
),
transform_memory_required_bytes(2 * 1024 * 1024, Some("gzip"), 2 * 1024 * 1024),
Some(6 * 1024 * 1024)
);
assert_eq!(
@@ -113,7 +109,9 @@ mod tests {
response
.headers_mut()
.insert("content-length", HeaderValue::from_static("128"));
assert!(apply(response, "/data.json", &settings, false).await.is_err());
assert!(apply(response, "/data.json", &settings, false)
.await
.is_err());
}
#[test]
@@ -143,10 +141,7 @@ mod tests {
assert_eq!(view.total.cache_hits, 1);
assert_eq!(view.total.saved_bytes, 30);
assert_eq!(view.total.by_kind.get("css"), Some(&1));
assert_eq!(
view.total.reasons.get("response_no_transform"),
Some(&1)
);
assert_eq!(view.total.reasons.get("response_no_transform"), Some(&1));
assert_eq!(view.scopes.len(), 1);
assert_eq!(view.scopes[0].vhost, "app");
}
@@ -185,8 +180,13 @@ mod tests {
response
.headers_mut()
.insert("vary", HeaderValue::from_static("Accept-Encoding"));
let response = apply(response, "/data.json", &settings, false).await.unwrap();
assert!(response.headers()["etag"].to_str().unwrap().starts_with("\"rustproxy-"));
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert!(response.headers()["etag"]
.to_str()
.unwrap()
.starts_with("\"rustproxy-"));
assert!(!response.headers().contains_key("vary"));
assert!(response.extensions().get::<CanonicalIdentity>().is_some());
let validators = response.extensions().get::<OriginValidators>().unwrap();
@@ -213,11 +213,12 @@ mod tests {
response
.headers_mut()
.insert("etag", HeaderValue::from_static("\"origin\""));
response.headers_mut().insert(
"vary",
HeaderValue::from_static("Accept-Encoding, X-Theme"),
);
let response = apply(response, "/data.json", &settings, false).await.unwrap();
response
.headers_mut()
.insert("vary", HeaderValue::from_static("Accept-Encoding, X-Theme"));
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert_eq!(response.headers()["etag"], "\"origin\"");
assert_eq!(response.headers()["vary"], "X-Theme");
assert!(response.extensions().get::<CanonicalIdentity>().is_some());
@@ -252,13 +253,18 @@ mod tests {
.headers_mut()
.insert("etag", HeaderValue::from_static("\"origin\""));
let response = apply(response, "/data.json", &settings, false).await.unwrap();
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert!(!response.headers().contains_key("content-encoding"));
assert_eq!(
response.headers()["content-length"].to_str().unwrap(),
identity.len().to_string()
);
assert!(response.headers()["etag"].to_str().unwrap().starts_with("\"rustproxy-"));
assert!(response.headers()["etag"]
.to_str()
.unwrap()
.starts_with("\"rustproxy-"));
let validators = response.extensions().get::<OriginValidators>().unwrap();
assert_eq!(validators.etag.as_ref().unwrap(), "\"origin\"");
let applied = response.extensions().get::<Applied>().unwrap();
@@ -287,7 +293,9 @@ mod tests {
.headers_mut()
.insert("etag", HeaderValue::from_static("\"origin\""));
let response = apply(response, "/data.json", &settings, true).await.unwrap();
let response = apply(response, "/data.json", &settings, true)
.await
.unwrap();
assert_eq!(response.headers()["etag"], "\"origin\"");
assert!(response.extensions().get::<Applied>().is_none());
let received = response.into_body().collect().await.unwrap().to_bytes();
@@ -315,7 +323,9 @@ mod tests {
HeaderValue::from_static("public, max-age=60, no-transform"),
);
let response = apply(response, "/data.json", &settings, false).await.unwrap();
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert!(response.extensions().get::<Applied>().is_none());
let received = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&received[..], body);
@@ -339,7 +349,9 @@ mod tests {
.headers_mut()
.insert("etag", HeaderValue::from_static("\"origin-head\""));
let response = apply(response, "/data.json", &settings, false).await.unwrap();
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert_eq!(response.headers()["content-length"], "76");
assert_eq!(response.headers()["etag"], "\"origin-head\"");
assert!(response.extensions().get::<Applied>().is_none());
@@ -383,7 +395,9 @@ mod tests {
.headers_mut()
.insert("etag", HeaderValue::from_static("\"origin\""));
let response = apply(response, "/data.json", &settings, false).await.unwrap();
let response = apply(response, "/data.json", &settings, false)
.await
.unwrap();
assert_eq!(response.headers()["content-encoding"], "gzip");
assert_eq!(response.headers()["etag"], "\"origin\"");
assert_eq!(response.headers()["vary"], "Accept-Encoding");
@@ -391,5 +405,4 @@ mod tests {
let received = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(&received[..], encoded.as_slice());
}
}