///|
/// Result of comparing the primary cache key and Vary-selected request fields.
pub(all) struct CacheKeyResult {
matches : Bool
vary_fields : Array[String]
trace : Array[TraceStep]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
/// Compare a presented request with the request that selected a stored response.
pub fn evaluate_cache_key(
stored : StoredResponse,
presented : RequestMetadata,
) -> CacheKeyResult {
let trace : Array[TraceStep] = []
let diagnostics = presented.headers.diagnostics()
if stored.request.target_uri != presented.target_uri {
trace.push(
step(
"CACHE_KEY_URI_MISMATCH", "presented target URI differs from the stored request URI",
"RFC 9111 2, 4",
),
)
return { matches: false, vary_fields: [], trace, diagnostics, }
}
if stored.request.http_method != "GET" ||
(presented.http_method != "GET" && presented.http_method != "HEAD") {
trace.push(
step(
"CACHE_KEY_METHOD_MISMATCH", "stored and presented request methods are not reusable by this engine",
"RFC 9111 4",
),
)
return { matches: false, vary_fields: [], trace, diagnostics, }
}
let parsed = parse_vary(stored.response.headers)
append_diagnostics(diagnostics, parsed.1)
let vary_fields = parsed.0
if vary_fields.length() == 1 && vary_fields[0] == "*" {
trace.push(
step(
"CACHE_KEY_VARY_STAR", "Vary star never matches without validation", "RFC 9111 4.1",
),
)
return { matches: false, vary_fields, trace, diagnostics, }
}
for field in vary_fields {
let original = normalize_field_values(stored.request.headers.values(field))
let current = normalize_field_values(presented.headers.values(field))
if original != current {
trace.push(
step(
"CACHE_KEY_VARY_MISMATCH",
"Vary-selected request field differs: " + field,
"RFC 9111 4.1",
),
)
return { matches: false, vary_fields, trace, diagnostics, }
}
}
trace.push(
step(
"CACHE_KEY_MATCH", "target URI, method, and Vary-selected fields match", "RFC 9111 4.1",
),
)
{ matches: true, vary_fields, trace, diagnostics, }
}
///|
fn parse_vary(headers : Headers) -> (Array[String], Array[Diagnostic]) {
let fields : Array[String] = []
let diagnostics : Array[Diagnostic] = []
for line in headers.values("vary") {
let split = @lex.split_quoted_list(line)
if !split.valid {
diagnostics.push({
level: Error,
code: "VARY_SYNTAX_INVALID",
message: "Vary contains invalid quoted syntax",
field_name: Some("vary"),
})
}
for item in split.parts {
let name = @lex.lower(@lex.trim_ows(item))
if name == "*" {
return (["*"], diagnostics)
}
if !@lex.is_token(name) {
diagnostics.push({
level: Error,
code: "VARY_FIELD_NAME_INVALID",
message: "Vary member is not a valid field name",
field_name: Some("vary"),
})
} else if !array_contains(fields, name) {
fields.push(name)
}
}
}
(fields, diagnostics)
}
///|
fn normalize_field_values(values : Array[String]) -> String? {
if values.length() == 0 {
return None
}
let output = StringBuilder()
for index, value in values {
if index > 0 {
output.write_char(',')
}
output.write_string(collapse_ows(value))
}
Some(output.to_string())
}
///|
fn collapse_ows(value : String) -> String {
let output = StringBuilder()
let mut pending_space = false
let mut wrote = false
let trimmed = value.trim(chars=" \t")
for char in trimmed {
if char == ' ' || char == '\t' {
pending_space = true
} else {
if pending_space && wrote {
output.write_char(' ')
}
output.write_char(char)
pending_space = false
wrote = true
}
}
output.to_string()
}
///|
fn array_contains(values : Array[String], candidate : String) -> Bool {
for value in values {
if value == candidate {
return true
}
}
false
}