///|
pub fn is_safe_http_method(http_method : String) -> Bool {
let normalized = normalize_method(http_method)
normalized == "GET" ||
normalized == "HEAD" ||
normalized == "OPTIONS" ||
normalized == "TRACE"
}
///|
fn base_without_query(uri : String) -> String {
match uri.split_once("?") {
Some((base, _)) => base.to_owned()
None => uri
}
}
///|
/// Resolve the absolute and relative URI forms used by Location and
/// Content-Location for invalidation.
pub fn resolve_related_uri(base : String, reference : String) -> String? {
let trimmed = reference.trim(chars=" \t").to_owned()
guard normalize_cache_uri(base) is Some(normalized_base) else { return None }
if trimmed == "" {
return Some(normalized_base)
}
if trimmed.has_prefix("http://") || trimmed.has_prefix("https://") {
return normalize_cache_uri(trimmed)
}
guard cache_origin(normalized_base) is Some(origin) else { return None }
if trimmed.has_prefix("/") {
return normalize_cache_uri("\{origin}\{trimmed}")
}
let base_path = base_without_query(normalized_base)
if trimmed.has_prefix("?") {
return normalize_cache_uri("\{base_path}\{trimmed}")
}
let directory = match base_path.rev_split_once("/") {
Some((before, _)) => "\{before.to_owned()}/"
None => "\{origin}/"
}
normalize_cache_uri("\{directory}\{trimmed}")
}
///|
fn append_unique_uri(uris : Array[String], uri : String) -> Bool {
if uris.contains(uri) {
false
} else {
uris.push(uri)
true
}
}
///|
pub(all) struct InvalidationPlan {
uris : Array[String]
reasons : Array[CacheReason]
} derive(Eq, Debug)
///|
/// RFC 9111 invalidation plan for a non-error response to an unsafe method.
/// Related targets are included only when their resolved URI has the same
/// origin as the request target.
pub fn plan_invalidation(
request : RequestMeta,
response : ResponseMeta,
) -> InvalidationPlan {
let uris : Array[String] = []
let reasons : Array[CacheReason] = []
if is_safe_http_method(request.http_method) {
reasons.push(
CacheReason::with_rfc(InvalidateSkippedSafeMethod, "RFC9111-4.4"),
)
return InvalidationPlan::{ uris, reasons }
}
if response.status < 200 || response.status >= 400 {
reasons.push(
CacheReason::with_rfc(InvalidateSkippedErrorStatus, "RFC9111-4.4"),
)
return InvalidationPlan::{ uris, reasons }
}
match normalize_cache_uri(request.uri) {
Some(target) => {
ignore(append_unique_uri(uris, target))
reasons.push(CacheReason::with_rfc(InvalidateUnsafeMethod, "RFC9111-4.4"))
for name in ["location", "content-location"] {
match response.headers.get_first(name) {
Some(reference) =>
match resolve_related_uri(target, reference) {
Some(related) =>
if same_cache_origin(target, related) &&
append_unique_uri(uris, related) {
reasons.push(
CacheReason::with_detail(
InvalidateRelatedUri,
"\{name}: \{related}",
),
)
}
None => ()
}
None => ()
}
}
}
None => ()
}
InvalidationPlan::{ uris, reasons }
}