///|
/// Cache keys that a caller should invalidate after an unsafe request.
pub(all) struct InvalidationDecision {
invalidate : Bool
target_uris : Array[String]
trace : Array[TraceStep]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
/// Evaluate RFC 9111 section 4.4 invalidation after a state-changing request.
/// The caller remains responsible for locating and deleting stored variants.
pub fn evaluate_invalidation(
request : RequestMetadata,
response : ResponseMetadata,
) -> InvalidationDecision {
let trace : Array[TraceStep] = []
let diagnostics = request.headers.diagnostics()
append_diagnostics(diagnostics, response.headers.diagnostics())
if is_safe_method(request.http_method) {
trace.push(
step(
"INVALIDATION_SAFE_METHOD", "safe request methods do not trigger cache invalidation",
"RFC 9111 4.4",
),
)
return { invalidate: false, target_uris: [], trace, diagnostics, }
}
if response.status < 200 || response.status >= 400 {
trace.push(
step(
"INVALIDATION_UNSUCCESSFUL_RESPONSE", "non-successful response does not trigger automatic invalidation",
"RFC 9111 4.4",
),
)
return { invalidate: false, target_uris: [], trace, diagnostics, }
}
let targets : Array[String] = [request.target_uri]
for field_name in ["location", "content-location"] {
for value in response.headers.values(field_name) {
match resolve_same_origin_reference(request.target_uri, value) {
Some(uri) => if !array_contains(targets, uri) { targets.push(uri) }
None =>
diagnostics.push({
level: Info,
code: "INVALIDATION_DIFFERENT_ORIGIN_SKIPPED",
message: field_name +
" was not invalidated because it is invalid or cross-origin",
field_name: Some(field_name),
})
}
}
}
trace.push(
step(
"INVALIDATION_REQUIRED", "unsafe successful request invalidates the effective request URI and same-origin targets",
"RFC 9111 4.4",
),
)
{ invalidate: true, target_uris: targets, trace, diagnostics, }
}
///|
fn is_safe_method(http_method : String) -> Bool {
match http_method.to_upper() {
"GET" | "HEAD" | "OPTIONS" | "TRACE" => true
_ => false
}
}
///|
fn resolve_same_origin_reference(base : String, reference : String) -> String? {
let target = reference.trim().to_owned()
if target.length() == 0 ||
target.contains_char('\r') ||
target.contains_char('\n') ||
target.contains_char('\u{0000}') {
return None
}
guard split_origin(base) is Some((origin, path_start)) else { return None }
if target.has_prefix("/") {
return Some(origin + target)
}
if target.has_prefix("http://") || target.has_prefix("https://") {
guard split_origin(target) is Some((target_origin, _)) else { return None }
if @lex.equal_ignore_case(origin, target_origin) {
return Some(target)
}
return None
}
let base_path = base[path_start:].to_owned()
let slash = last_slash(base_path)
let directory = if slash is Some(index) {
base_path[0:index + 1].to_owned()
} else {
"/"
}
if target.has_prefix("../") || target.has_prefix("./") {
return None
}
Some(origin + directory + target)
}
///|
fn split_origin(value : String) -> (String, Int)? {
let start = if value.has_prefix("http://") {
7
} else if value.has_prefix("https://") {
8
} else {
return None
}
for index = start; index < value.length(); index = index + 1 {
if value[index] == '/' || value[index] == '?' || value[index] == '#' {
return Some((value[0:index].to_owned(), index))
}
}
Some((value, value.length()))
}
///|
fn last_slash(value : String) -> Int? {
let mut found : Int? = None
for index = 0; index < value.length(); index = index + 1 {
if value[index] == '/' {
found = Some(index)
}
}
found
}