Skip to content

Commit

Permalink
LibWeb/Fetch: Add freshness notion to HTTP::Response
Browse files Browse the repository at this point in the history
Defined in RFC9111 "HTTP Caching".

Co-Authored-By: Andrew Kaster <[email protected]>
  • Loading branch information
2 people authored and trflynn89 committed May 23, 2024
1 parent c05f296 commit fcbc612
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 0 deletions.
101 changes: 101 additions & 0 deletions Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ JS_DEFINE_ALLOCATOR(OpaqueRedirectFilteredResponse);

Response::Response(JS::NonnullGCPtr<HeaderList> header_list)
: m_header_list(header_list)
, m_response_time(UnixDateTime::now())
{
}

Expand Down Expand Up @@ -207,6 +208,106 @@ bool Response::is_cors_cross_origin() const
return type() == Type::Opaque || type() == Type::OpaqueRedirect;
}

// https://fetch.spec.whatwg.org/#concept-fresh-response
bool Response::is_fresh() const
{
// A fresh response is a response whose current age is within its freshness lifetime.
return current_age() < freshness_lifetime();
}

// https://fetch.spec.whatwg.org/#concept-stale-while-revalidate-response
bool Response::is_stale_while_revalidate() const
{
// A stale-while-revalidate response is a response that is not a fresh response and whose current age is within the stale-while-revalidate lifetime.
return !is_fresh() && current_age() < stale_while_revalidate_lifetime();
}

// https://fetch.spec.whatwg.org/#concept-stale-response
bool Response::is_stale() const
{
// A stale response is a response that is not a fresh response or a stale-while-revalidate response.
return !is_fresh() && !is_stale_while_revalidate();
}

// https://httpwg.org/specs/rfc9111.html#age.calculations
u64 Response::current_age() const
{
// The term "age_value" denotes the value of the Age header field (Section 5.1), in a form appropriate for arithmetic operation; or 0, if not available.
Optional<Duration> age;
if (auto const age_header = header_list()->get("Age"sv.bytes()); age_header.has_value()) {
if (auto converted_age = StringView { *age_header }.to_number<u64>(); converted_age.has_value())
age = Duration::from_seconds(converted_age.value());
}

auto const age_value = age.value_or(Duration::from_seconds(0));

// The term "date_value" denotes the value of the Date header field, in a form appropriate for arithmetic operations. See Section 6.6.1 of [HTTP] for the definition of the Date header field and for requirements regarding responses without it.
// FIXME: Do we have a parser for HTTP-date?
auto const date_value = UnixDateTime::now() - Duration::from_seconds(5);

// The term "now" means the current value of this implementation's clock (Section 5.6.7 of [HTTP]).
auto const now = UnixDateTime::now();

// The value of the clock at the time of the request that resulted in the stored response.
// FIXME: Let's get the correct time.
auto const request_time = UnixDateTime::now() - Duration::from_seconds(5);

// The value of the clock at the time the response was received.
auto const response_time = m_response_time;

auto const apparent_age = max(0, (response_time - date_value).to_seconds());

auto const response_delay = response_time - request_time;
auto const corrected_age_value = age_value + response_delay;

auto const corrected_initial_age = max(apparent_age, corrected_age_value.to_seconds());

auto const resident_time = (now - response_time).to_seconds();
return corrected_initial_age + resident_time;
}

// https://httpwg.org/specs/rfc9111.html#calculating.freshness.lifetime
u64 Response::freshness_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
if (!elem.has_value())
return 0;

// FIXME: If the cache is shared and the s-maxage response directive (Section 5.2.2.10) is present, use its value

// If the max-age response directive (Section 5.2.2.1) is present, use its value, or
for (auto const& directive : *elem) {
if (directive.starts_with_bytes("max-age"sv)) {
auto equal_offset = directive.find_byte_offset('=').value();
auto const value = directive.bytes_as_string_view().substring_view(equal_offset);
return value.to_number<u64>().value();
}
}

// FIXME: If the Expires response header field (Section 5.3) is present, use its value minus the value of the Date response header field (using the time the message was received if it is not present, as per Section 6.6.1 of [HTTP]), or
// FIXME: Otherwise, no explicit expiration time is present in the response. A heuristic freshness lifetime might be applicable; see Section 4.2.2.

return 0;
}

// https://httpwg.org/specs/rfc5861.html#n-the-stale-while-revalidate-cache-control-extension
u64 Response::stale_while_revalidate_lifetime() const
{
auto const elem = header_list()->get_decode_and_split("Cache-Control"sv.bytes());
if (!elem.has_value())
return 0;

for (auto const& directive : *elem) {
if (directive.starts_with_bytes("stale-while-revalidate"sv)) {
auto equal_offset = directive.find_byte_offset('=').value();
auto const value = directive.bytes_as_string_view().substring_view(equal_offset);
return value.to_number<u64>().value();
}
}

return 0;
}

// Non-standard
Optional<StringView> Response::network_error_message() const
{
Expand Down
12 changes: 12 additions & 0 deletions Userland/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
#include <AK/Error.h>
#include <AK/Forward.h>
#include <AK/Optional.h>
#include <AK/Time.h>
#include <AK/Vector.h>
#include <LibJS/Forward.h>
#include <LibJS/Heap/Cell.h>
Expand Down Expand Up @@ -118,6 +119,10 @@ class Response : public JS::Cell {

[[nodiscard]] bool is_cors_cross_origin() const;

[[nodiscard]] bool is_fresh() const;
[[nodiscard]] bool is_stale_while_revalidate() const;
[[nodiscard]] bool is_stale() const;

// Non-standard
[[nodiscard]] Optional<StringView> network_error_message() const;

Expand Down Expand Up @@ -186,7 +191,14 @@ class Response : public JS::Cell {
// A response has an associated has-cross-origin-redirects (a boolean), which is initially false.
bool m_has_cross_origin_redirects { false };

// FIXME is the type correct?
u64 current_age() const;
u64 freshness_lifetime() const;
u64 stale_while_revalidate_lifetime() const;

// Non-standard
UnixDateTime m_response_time;

Optional<Variant<String, StringView>> m_network_error_message;
};

Expand Down

0 comments on commit fcbc612

Please sign in to comment.