///|
/// Validator selected for a conditional origin request.
pub(all) enum ValidatorKind {
EntityTagValidator
LastModifiedValidator
NoValidator
} derive(Debug, Eq)
///|
pub(all) struct ValidationRequest {
headers : Headers
kind : ValidatorKind
trace : Array[TraceStep]
} derive(Debug, Eq)
///|
/// Result of attempting to apply a 304 response to stored metadata.
pub(all) struct ValidationMergeResult {
merged : StoredResponse?
trace : Array[TraceStep]
diagnostics : Array[Diagnostic]
} derive(Debug, Eq)
///|
/// Prepare conditional request fields from the preferred stored validator.
pub fn prepare_validation(
stored : StoredResponse,
presented : RequestMetadata,
) -> ValidationRequest {
let trace : Array[TraceStep] = []
match stored.response.headers.first("etag") {
Some(value) if value.trim().length() > 0 => {
trace.push(
step(
"VALIDATION_IF_NONE_MATCH", "selected stored ETag for If-None-Match", "RFC 9111 4.3.1",
),
)
{
headers: presented.headers.replace("if-none-match", value),
kind: EntityTagValidator,
trace,
}
}
_ =>
match stored.response.headers.first("last-modified") {
Some(value) if value.trim().length() > 0 => {
trace.push(
step(
"VALIDATION_IF_MODIFIED_SINCE", "selected Last-Modified for If-Modified-Since",
"RFC 9111 4.3.1",
),
)
{
headers: presented.headers.replace("if-modified-since", value),
kind: LastModifiedValidator,
trace,
}
}
_ => {
trace.push(
step(
"VALIDATION_NO_VALIDATOR", "stored response has no usable validator",
"RFC 9111 4.3.1",
),
)
{ headers: presented.headers, kind: NoValidator, trace, }
}
}
}
}
///|
/// Apply metadata from a valid 304 response without changing the stored status
/// or body ownership. A conflicting ETag fails closed.
pub fn merge_not_modified(
stored : StoredResponse,
not_modified : ResponseMetadata,
request_time : Timestamp,
response_time : Timestamp,
) -> ValidationMergeResult {
let trace : Array[TraceStep] = []
let diagnostics = not_modified.headers.diagnostics()
if not_modified.status != 304 {
return merge_rejected(
"VALIDATION_STATUS_NOT_304", "validation merge requires status 304", trace,
diagnostics,
)
}
if has_error(diagnostics) {
return merge_rejected(
"VALIDATION_FIELDS_INVALID", "304 response contains unsafe HTTP fields", trace,
diagnostics,
)
}
let old_etag = stored.response.headers.first("etag")
let new_etag = not_modified.headers.first("etag")
if old_etag is Some(old_value) &&
new_etag is Some(new_value) &&
old_value.trim() != new_value.trim() {
return merge_rejected(
"VALIDATION_ETAG_CONFLICT", "304 ETag does not identify the stored response",
trace, diagnostics,
)
}
let connection_fields = connection_named_fields(not_modified.headers)
let mut updated = stored.response.headers
let processed : Array[String] = []
for entry in not_modified.headers.entries {
if array_contains(processed, entry.name) {
continue
}
processed.push(entry.name)
if should_skip_validation_update(entry.name, connection_fields) {
trace.push(
step(
"VALIDATION_FIELD_SKIPPED",
"excluded field was not copied from 304: " + entry.name,
"RFC 9111 3.2, 4.3.4",
),
)
continue
}
updated = updated.without(entry.name)
for value in not_modified.headers.values(entry.name) {
updated = updated.add(entry.name, value)
}
}
trace.push(
step(
"VALIDATION_METADATA_MERGED", "stored response metadata was freshened from 304",
"RFC 9111 4.3.4",
),
)
{
merged: Some({
request: stored.request,
response: { status: stored.response.status, headers: updated, },
request_time,
response_time,
}),
trace,
diagnostics,
}
}
///|
fn merge_rejected(
code : String,
message : String,
trace : Array[TraceStep],
diagnostics : Array[Diagnostic],
) -> ValidationMergeResult {
trace.push(step(code, message, "RFC 9111 4.3.4"))
{ merged: None, trace, diagnostics, }
}
///|
fn connection_named_fields(headers : Headers) -> Array[String] {
let fields : Array[String] = []
for value in headers.values("connection") {
let split = @lex.split_quoted_list(value)
for item in split.parts {
let name = @lex.lower(@lex.trim_ows(item))
if @lex.is_token(name) && !array_contains(fields, name) {
fields.push(name)
}
}
}
fields
}
///|
fn should_skip_validation_update(
name : String,
connection_fields : Array[String],
) -> Bool {
name == "connection" ||
name == "content-length" ||
name == "content-range" ||
name == "keep-alive" ||
name == "proxy-authenticate" ||
name == "proxy-authentication-info" ||
name == "proxy-authorization" ||
name == "proxy-connection" ||
name == "te" ||
name == "trailer" ||
name == "transfer-encoding" ||
name == "upgrade" ||
array_contains(connection_fields, name)
}