This commit is contained in:
Mateusz Gruszczyński committed 2026-10-04 11:15:58 +02:00
1 parent 87e571c170
commit fa7abc9e53
308 files changed
+49746 -50237

No files matched your search

+167
View File
@@ -0,0 +1,167 @@
/// Transform an eligible response. The call is fail-open for unsupported media,
/// encodings and malformed minifiable content; transport/body collection errors
/// still propagate because the original stream can no longer be replayed safely.
pub async fn apply(
response: Response,
path: &str,
settings: &Settings,
origin_no_transform: bool,
) -> Result<Response> {
let skip = |mut response: Response, kind: Option<Kind>, reason: &'static str| {
response.extensions_mut().insert(Skipped { kind, reason });
response
};
if !settings.enabled {
return Ok(skip(response, None, "disabled"));
}
if origin_no_transform {
return Ok(skip(response, None, "origin_no_transform"));
}
if no_transform(response.headers()) {
return Ok(skip(response, None, "response_no_transform"));
}
if response.status().is_informational()
|| response.status().as_u16() == 204
|| response.status().as_u16() == 304
{
return Ok(skip(response, None, "status_ineligible"));
}
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))
else {
return Ok(skip(response, None, "unsupported_type"));
};
if !kind_enabled(kind, settings) {
return Ok(skip(response, Some(kind), "type_disabled"));
}
let Some(encoding) = supported_encoding(response.headers()) else {
return Ok(skip(response, Some(kind), "unsupported_encoding"));
};
let Some(content_length) = response
.headers()
.get("content-length")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.parse::<usize>().ok())
else {
// Keep streaming semantics for unknown-size bodies instead of introducing
// unbounded buffering into the dataplane.
return Ok(skip(response, Some(kind), "unknown_length"));
};
if content_length > settings.max_body_bytes {
return Ok(skip(response, Some(kind), "too_large"));
}
let Some(required_memory) =
transform_memory_required_bytes(content_length, encoding, settings.max_body_bytes)
else {
return Ok(skip(response, Some(kind), "memory_budget"));
};
let Some(_inflight_slot) = try_reserve_transform_slot(settings.max_inflight) else {
return Ok(skip(response, Some(kind), "concurrency_pressure"));
};
let Some(_memory_reservation) =
try_reserve_transform_memory(required_memory, settings.memory_budget_bytes)
else {
// Transformations are an optimization. Under concurrent allocation
// pressure, preserve streaming/origin bytes instead of queueing more
// full-response buffers behind the memory budget.
return Ok(skip(response, Some(kind), "memory_pressure"));
};
let (mut parts, body) = response.into_parts();
// Do not trust Content-Length as a memory bound. A broken or malicious
// upstream can send more bytes than declared; Limited stops aggregation at
// the configured ceiling instead of letting BodyExt::collect grow without
// bound. Once aggregation starts an over-limit stream cannot be replayed
// safely, so it is treated like the other body/transport collection errors.
let raw = Limited::new(body, settings.max_body_bytes)
.collect()
.await
.map_err(|error| {
anyhow::anyhow!(
"collect bounded static response for transformation: {error}"
)
})?
.to_bytes();
if raw.len() > settings.max_body_bytes {
let mut response = Response::from_parts(parts, Body::from(raw));
response.extensions_mut().insert(Skipped {
kind: Some(kind),
reason: "too_large",
});
return Ok(response);
}
let decoded = match decode(&raw, encoding, settings.max_body_bytes) {
Ok(value) => value,
Err(_) => {
let mut response = Response::from_parts(parts, Body::from(raw));
response.extensions_mut().insert(Skipped {
kind: Some(kind),
reason: "decode_fail_open",
});
return Ok(response);
}
};
let transformed = match transformer(kind).transform(decoded.as_ref()) {
Ok(value) if !value.is_empty() => value,
_ => {
let mut response = Response::from_parts(parts, Body::from(raw));
response.extensions_mut().insert(Skipped {
kind: Some(kind),
reason: "transform_fail_open",
});
return Ok(response);
}
};
if transformed.as_slice() == decoded.as_ref() && encoding.is_none() {
// The origin was explicitly requested without Accept-Encoding and the
// eligible response is already identity. The bytes/validator stay intact,
// but the cache no longer needs an Accept-Encoding variant dimension.
drop_vary_accept_encoding(&mut parts.headers);
parts.extensions.insert(CanonicalIdentity);
parts.extensions.insert(Skipped {
kind: Some(kind),
reason: "unchanged",
});
return Ok(Response::from_parts(parts, Body::from(raw)));
}
let original_bytes = decoded.len();
let released_bytes = raw.len().saturating_add(decoded.len());
// The successful transformed response no longer needs the collected origin
// body or decoded temporary buffer. Drop both before building the response so
// large short-lived allocations do not overlap longer than necessary.
drop(decoded);
drop(raw);
trim_allocator_after_large_transform(released_bytes);
let validators = parts
.extensions
.get::<OriginValidators>()
.cloned()
.unwrap_or_else(|| OriginValidators {
etag: parts.headers.get("etag").cloned(),
last_modified: parts.headers.get("last-modified").cloned(),
});
parts.headers.remove("content-encoding");
parts.headers.remove("content-length");
parts.headers.remove("content-md5");
parts.headers.remove("digest");
parts.headers.remove("content-digest");
parts.headers.remove("accept-ranges");
parts.headers.insert("etag", transformed_etag(&transformed));
parts.headers.insert(
"content-length",
HeaderValue::from_str(&transformed.len().to_string()).expect("integer header"),
);
drop_vary_accept_encoding(&mut parts.headers);
parts.extensions.insert(validators);
parts.extensions.insert(CanonicalIdentity);
parts.extensions.insert(Applied {
kind,
original_bytes,
transformed_bytes: transformed.len(),
});
Ok(Response::from_parts(parts, Body::from(transformed)))
}