v0.25.1
This commit is contained in:
1 parent
87e571c170
commit
fa7abc9e53
308 files changed
+49746
-50237
No files matched your search
@@ -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)))
|
||||
}
|
||||
Reference in new issue
Block a user