///|
/// Terminal result of RFC 9111 storage eligibility evaluation.
pub(all) enum StorageVerdict {
Storable
NotStorable
} derive(Debug, Eq)
///|
/// Storage decision plus field handling required before persistence.
pub(all) struct StorageDecision {
verdict : StorageVerdict
fields_to_remove : Array[String]
fields_requiring_validation : Array[String]
trace : Array[TraceStep]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
pub fn StorageDecision::is_storable(self : StorageDecision) -> Bool {
self.verdict == Storable
}
///|
/// Evaluate whether a response may be stored by the selected cache mode.
/// This implementation deliberately supports representation storage for GET;
/// HEAD is handled later as metadata refresh and unsafe methods are rejected.
pub fn evaluate_storage(
request : RequestMetadata,
response : ResponseMetadata,
mode : CacheMode,
now : Timestamp,
) -> StorageDecision {
let trace : Array[TraceStep] = []
let diagnostics = request.headers.diagnostics()
for item in response.headers.diagnostics() {
diagnostics.push(item)
}
if has_error(diagnostics) {
return storage_reject(
"STORAGE_INVALID_FIELDS", "message contains an unsafe or invalid HTTP field",
"RFC 9111 3", trace, diagnostics,
)
}
if request.http_method != "GET" {
return storage_reject(
"STORAGE_METHOD_UNSUPPORTED", "only complete GET responses are stored by this engine",
"RFC 9111 3", trace, diagnostics,
)
}
if response.status < 200 || response.status > 599 {
return storage_reject(
"STORAGE_STATUS_NOT_FINAL", "response status is not a final HTTP status", "RFC 9111 3",
trace, diagnostics,
)
}
if response.status == 206 || response.status == 304 {
return storage_reject(
"STORAGE_PARTIAL_OR_VALIDATION_RESPONSE", "partial and validation responses require a pre-existing stored response",
"RFC 9111 3.3, 4.3.4", trace, diagnostics,
)
}
let request_control = parse_cache_control(request.headers)
let response_control = parse_cache_control(response.headers)
append_diagnostics(diagnostics, request_control.diagnostics)
append_diagnostics(diagnostics, response_control.diagnostics)
if request_control.contains("no-store") {
return storage_reject(
"STORAGE_REQUEST_NO_STORE", "request Cache-Control contains no-store", "RFC 9111 5.2.1.5",
trace, diagnostics,
)
}
if response_control.contains("no-store") {
return storage_reject(
"STORAGE_RESPONSE_NO_STORE", "response Cache-Control contains no-store", "RFC 9111 5.2.2.5",
trace, diagnostics,
)
}
if response_control.contains("must-understand") &&
!is_understood_status(response.status) {
return storage_reject(
"STORAGE_STATUS_NOT_UNDERSTOOD", "must-understand forbids storing an unsupported status code",
"RFC 9111 5.2.2.3", trace, diagnostics,
)
}
let private_fields = response_control.field_names("private")
if mode == SharedCache &&
response_control.contains("private") &&
private_fields.length() == 0 {
return storage_reject(
"STORAGE_SHARED_PRIVATE", "an unqualified private directive forbids shared storage",
"RFC 9111 5.2.2.7", trace, diagnostics,
)
}
if mode == SharedCache &&
request.headers.contains("authorization") &&
!response_control.contains("must-revalidate") &&
!response_control.contains("public") &&
response_control.delta("s-maxage") is DeltaMissing {
return storage_reject(
"STORAGE_AUTHORIZATION_RESTRICTED", "authenticated request lacks a directive allowing shared storage",
"RFC 9111 3.5", trace, diagnostics,
)
}
let has_explicit_permission = response_control.contains("public") ||
(mode == PrivateCache && response_control.contains("private")) ||
response.headers.contains("expires") ||
is_valid_delta(response_control.delta("max-age")) ||
(mode == SharedCache && is_valid_delta(response_control.delta("s-maxage")))
if !has_explicit_permission && !is_heuristically_cacheable(response.status) {
return storage_reject(
"STORAGE_NO_EXPLICIT_OR_DEFAULT_PERMISSION", "response has neither explicit cache permission nor a heuristically cacheable status",
"RFC 9111 3", trace, diagnostics,
)
}
trace.push(
step(
"STORAGE_ALLOWED", "response satisfies RFC storage preconditions", "RFC 9111 3",
),
)
let validation_fields = response_control.field_names("no-cache")
if private_fields.length() > 0 && mode == SharedCache {
trace.push(
step(
"STORAGE_REMOVE_PRIVATE_FIELDS", "qualified private fields must be removed before shared storage",
"RFC 9111 5.2.2.7",
),
)
}
if validation_fields.length() > 0 {
trace.push(
step(
"STORAGE_FIELD_VALIDATION_REQUIRED", "qualified no-cache fields require validation before reuse",
"RFC 9111 5.2.2.4",
),
)
}
ignore(now)
{
verdict: Storable,
fields_to_remove: if mode == SharedCache {
private_fields
} else {
[]
},
fields_requiring_validation: validation_fields,
trace,
diagnostics,
}
}
///|
fn storage_reject(
code : String,
message : String,
section : String,
trace : Array[TraceStep],
diagnostics : Array[Diagnostic],
) -> StorageDecision {
trace.push(step(code, message, section))
{
verdict: NotStorable,
fields_to_remove: [],
fields_requiring_validation: [],
trace,
diagnostics,
}
}
///|
fn step(code : String, message : String, rfc_section : String) -> TraceStep {
{ code, message, rfc_section, }
}
///|
fn has_error(diagnostics : Array[Diagnostic]) -> Bool {
for item in diagnostics {
if item.level == Error {
return true
}
}
false
}
///|
fn append_diagnostics(
target : Array[Diagnostic],
source : Array[Diagnostic],
) -> Unit {
for item in source {
target.push(item)
}
}
///|
fn is_valid_delta(value : DeltaDirective) -> Bool {
value is DeltaValid(_)
}
///|
fn is_understood_status(status : Int) -> Bool {
match status {
200
| 201
| 202
| 203
| 204
| 205
| 206
| 300
| 301
| 302
| 303
| 304
| 305
| 307
| 308
| 400
| 401
| 402
| 403
| 404
| 405
| 406
| 407
| 408
| 409
| 410
| 411
| 412
| 413
| 414
| 415
| 416
| 417
| 421
| 422
| 423
| 424
| 425
| 426
| 428
| 429
| 431
| 451
| 500
| 501
| 502
| 503
| 504
| 505
| 506
| 507
| 508
| 510
| 511 => true
_ => false
}
}
///|
fn is_heuristically_cacheable(status : Int) -> Bool {
match status {
200 | 203 | 204 | 300 | 301 | 308 | 404 | 405 | 410 | 414 | 501 => true
_ => false
}
}