diff --git a/Libraries/LibHTTP/Cache/MemoryCache.cpp b/Libraries/LibHTTP/Cache/MemoryCache.cpp index eb165a076a..fcaa076a0f 100644 --- a/Libraries/LibHTTP/Cache/MemoryCache.cpp +++ b/Libraries/LibHTTP/Cache/MemoryCache.cpp @@ -15,6 +15,14 @@ NonnullRefPtr MemoryCache::create() return adopt_ref(*new MemoryCache()); } +// A stored response satisfies a request only if the request header fields nominated by the response's Vary header +// match those of the request that produced the entry, per RFC 9111 4.1. open_entry() and update_javascript_bytecode_cache() +// must select entries identically, so share the predicate. +static bool entry_matches_request(HeaderList const& request_headers, MemoryCache::Entry const& entry) +{ + return create_vary_key(request_headers, entry.response_headers) == entry.vary_key; +} + // https://httpwg.org/specs/rfc9111.html#constructing.responses.from.caches Optional MemoryCache::open_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, CacheMode cache_mode) { @@ -38,7 +46,7 @@ Optional MemoryCache::open_entry(URL::URL const& url, // - request header fields nominated by the stored response (if any) match those presented (see Section 4.1), and auto cache_entry = find_value(*cache_entries, [&](auto const& entry) { - return create_vary_key(request_headers, entry.response_headers) == entry.vary_key; + return entry_matches_request(request_headers, entry); }); if (!cache_entry.has_value()) { dbgln_if(HTTP_MEMORY_CACHE_DEBUG, "\033[37m[memory]\033[0m \033[35;1mVary mismatch for\033[0m {}", url); @@ -75,7 +83,7 @@ Optional MemoryCache::open_entry(URL::URL const& url, VERIFY_NOT_REACHED(); } -void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers) +void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers, Optional javascript_bytecode_cache, Optional javascript_bytecode_cache_vary_key) { if (!is_cacheable(method, request_headers)) return; @@ -99,6 +107,8 @@ void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderLis .request_headers = move(request_headers_copy), .response_headers = move(response_headers_copy), .response_body = {}, + .javascript_bytecode_cache = move(javascript_bytecode_cache), + .javascript_bytecode_cache_vary_key = javascript_bytecode_cache_vary_key, .request_time = request_time, .response_time = UnixDateTime::now(), }; @@ -139,4 +149,33 @@ void MemoryCache::finalize_entry(URL::URL const& url, StringView method, HeaderL } } +void MemoryCache::update_javascript_bytecode_cache(URL::URL const& url, StringView method, HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache) +{ + if (!is_cacheable(method, request_headers)) + return; + + auto serialized_url = serialize_url_for_cache_storage(url); + auto cache_key = create_cache_key(serialized_url, method); + + // Attach the generated bytecode to every cache entry this request would select. Match entries the same way + // open_entry() does, by re-deriving the vary key from the request and the entry's stored response headers, rather + // than trusting the supplied vary key (which was computed in another process and can diverge for Vary responses). + // The supplied vary key is still kept as javascript_bytecode_cache_vary_key so it can be replayed onto a served + // response. + auto update_entries = [&](Vector& entries) { + for (auto& entry : entries) { + if (!entry_matches_request(request_headers, entry)) + continue; + entry.javascript_bytecode_cache = javascript_bytecode_cache; + entry.javascript_bytecode_cache_vary_key = vary_key; + } + }; + + if (auto cache_entries = m_complete_entries.get(cache_key); cache_entries.has_value()) + update_entries(*cache_entries); + + if (auto cache_entries = m_pending_entries.get(cache_key); cache_entries.has_value()) + update_entries(*cache_entries); +} + } diff --git a/Libraries/LibHTTP/Cache/MemoryCache.h b/Libraries/LibHTTP/Cache/MemoryCache.h index 10f3446406..1bc5eaaa47 100644 --- a/Libraries/LibHTTP/Cache/MemoryCache.h +++ b/Libraries/LibHTTP/Cache/MemoryCache.h @@ -28,6 +28,8 @@ public: NonnullRefPtr request_headers; NonnullRefPtr response_headers; Core::ImmutableBytes response_body; + Optional javascript_bytecode_cache; + Optional javascript_bytecode_cache_vary_key; UnixDateTime request_time; UnixDateTime response_time; @@ -37,8 +39,9 @@ public: Optional open_entry(URL::URL const&, StringView method, HeaderList const& request_headers, CacheMode); - void create_entry(URL::URL const&, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers); + void create_entry(URL::URL const&, StringView method, HeaderList const& request_headers, UnixDateTime request_time, u32 status_code, ByteString reason_phrase, HeaderList const& response_headers, Optional javascript_bytecode_cache = {}, Optional javascript_bytecode_cache_vary_key = {}); void finalize_entry(URL::URL const&, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, Core::ImmutableBytes response_body); + void update_javascript_bytecode_cache(URL::URL const&, StringView method, HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache); private: HashMap, IdentityHashTraits> m_pending_entries; diff --git a/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp b/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp index 1c2014632f..18be074fbe 100644 --- a/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp +++ b/Libraries/LibWeb/Fetch/Fetching/Fetching.cpp @@ -93,6 +93,14 @@ public: }); } + HTTP::MemoryCache* get_if_exists(Infrastructure::NetworkPartitionKey const& key) + { + auto it = m_cache.find(key); + if (it == m_cache.end()) + return nullptr; + return it->value.ptr(); + } + static HTTPCache& the() { static HTTPCache& cache = *new HTTPCache; @@ -138,6 +146,9 @@ static GC::Ptr select_response_from_cache(JS::Realm& r response->set_status(cache_entry->status_code); response->set_status_message(cache_entry->reason_phrase); response->set_header_list(cache_entry->response_headers); + response->set_javascript_bytecode_cache(cache_entry->javascript_bytecode_cache); + response->set_javascript_bytecode_cache_vary_key(cache_entry->javascript_bytecode_cache_vary_key); + response->set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create(cache_entry->request_headers->headers())); auto [response_body, _] = safely_extract_body(realm, cache_entry->response_body.bytes()); response->set_body(response_body); @@ -145,14 +156,15 @@ static GC::Ptr select_response_from_cache(JS::Realm& r return response; } -static void store_response_in_cache(HTTP::MemoryCache& http_cache, Infrastructure::Request const& request, Infrastructure::Response const& response) +static void store_response_in_cache(HTTP::MemoryCache& http_cache, Infrastructure::Request const& request, Infrastructure::Response& response) { if (!g_http_memory_cache_enabled) return; if (request.cache_mode() == HTTP::CacheMode::NoStore) return; - http_cache.create_entry(request.current_url(), request.method(), request.header_list(), request.request_time(), response.status(), response.status_message(), response.header_list()); + response.set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create(request.header_list()->headers())); + http_cache.create_entry(request.current_url(), request.method(), request.header_list(), request.request_time(), response.status(), response.status_message(), response.header_list(), response.javascript_bytecode_cache(), response.javascript_bytecode_cache_vary_key()); } // https://fetch.spec.whatwg.org/#concept-fetch @@ -2610,4 +2622,14 @@ void clear_http_memory_cache() HTTPCache::the().clear_cache(); } +void update_javascript_bytecode_cache_in_http_memory_cache(Infrastructure::NetworkPartitionKey const& partition_key, URL::URL const& url, ByteString const& method, HTTP::HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes javascript_bytecode_cache) +{ + if (!g_http_memory_cache_enabled) + return; + + // Only back-fill into a partition that already has a cache; do not create an empty one just to drop bytecode into. + if (auto* http_cache = HTTPCache::the().get_if_exists(partition_key)) + http_cache->update_javascript_bytecode_cache(url, method, request_headers, vary_key, move(javascript_bytecode_cache)); +} + } diff --git a/Libraries/LibWeb/Fetch/Fetching/Fetching.h b/Libraries/LibWeb/Fetch/Fetching/Fetching.h index a401bb5a7e..8c89a79882 100644 --- a/Libraries/LibWeb/Fetch/Fetching/Fetching.h +++ b/Libraries/LibWeb/Fetch/Fetching/Fetching.h @@ -9,11 +9,14 @@ #include #include +#include #include #include #include #include +#include #include +#include #include namespace Web::Fetch::Fetching { @@ -60,5 +63,6 @@ void append_fetch_metadata_headers_for_request(Infrastructure::Request&); WEB_API void set_http_memory_cache_enabled(bool enabled); WEB_API bool http_memory_cache_enabled(); WEB_API void clear_http_memory_cache(); +void update_javascript_bytecode_cache_in_http_memory_cache(Infrastructure::NetworkPartitionKey const&, URL::URL const&, ByteString const& method, HTTP::HeaderList const& request_headers, u64 vary_key, Core::ImmutableBytes); } diff --git a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp index ab5d1678d5..8609a6f362 100644 --- a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp +++ b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.cpp @@ -182,6 +182,8 @@ GC::Ref Response::clone(JS::Realm& realm) const new_response->set_body_info(m_body_info); new_response->set_javascript_bytecode_cache(m_javascript_bytecode_cache); new_response->set_javascript_bytecode_cache_vary_key(m_javascript_bytecode_cache_vary_key); + if (m_javascript_bytecode_cache_memory_cache_request_headers.has_value()) + new_response->set_javascript_bytecode_cache_memory_cache_request_headers(HTTP::HeaderList::create((*m_javascript_bytecode_cache_memory_cache_request_headers)->headers())); // FIXME: service worker timing info // 3. If response’s body is non-null, then set newResponse’s body to the result of cloning response’s body. diff --git a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h index a2c66ce0ef..6d377301c5 100644 --- a/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h +++ b/Libraries/LibWeb/Fetch/Infrastructure/HTTP/Responses.h @@ -110,6 +110,8 @@ public: void set_javascript_bytecode_cache(Optional javascript_bytecode_cache) { m_javascript_bytecode_cache = move(javascript_bytecode_cache); } [[nodiscard]] Optional javascript_bytecode_cache_vary_key() const { return m_javascript_bytecode_cache_vary_key; } void set_javascript_bytecode_cache_vary_key(Optional javascript_bytecode_cache_vary_key) { m_javascript_bytecode_cache_vary_key = javascript_bytecode_cache_vary_key; } + [[nodiscard]] Optional> const& javascript_bytecode_cache_memory_cache_request_headers() const { return m_javascript_bytecode_cache_memory_cache_request_headers; } + void set_javascript_bytecode_cache_memory_cache_request_headers(Optional> request_headers) { m_javascript_bytecode_cache_memory_cache_request_headers = move(request_headers); } [[nodiscard]] RedirectTaint redirect_taint() const { return m_redirect_taint; } void set_redirect_taint(RedirectTaint redirect_taint) { m_redirect_taint = redirect_taint; } @@ -203,6 +205,7 @@ private: Optional m_network_error_message; Optional m_javascript_bytecode_cache; Optional m_javascript_bytecode_cache_vary_key; + Optional> m_javascript_bytecode_cache_memory_cache_request_headers; public: [[nodiscard]] ByteString const& method() const { return m_method; } diff --git a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp index ece950f8fa..9c39a6a1ff 100644 --- a/Libraries/LibWeb/HTML/Scripting/Fetching.cpp +++ b/Libraries/LibWeb/HTML/Scripting/Fetching.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include #include #include @@ -59,7 +60,9 @@ struct BytecodeCacheContext { URL::URL url; ByteString method; NonnullRefPtr request_headers; + RefPtr memory_cache_request_headers; u64 vary_key { 0 }; + Optional memory_cache_partition_key; }; using BytecodeCacheSourceHash = ::Crypto::Hash::Digest<::Crypto::Hash::SHA256::DigestSize * 8>; @@ -156,11 +159,17 @@ static Optional bytecode_cache_context_for_request(Fetch:: if (!vary_key.has_value()) return {}; + RefPtr memory_cache_request_headers; + if (auto const& response_request_headers = response.javascript_bytecode_cache_memory_cache_request_headers(); response_request_headers.has_value()) + memory_cache_request_headers = HTTP::HeaderList::create((*response_request_headers)->headers()); + return BytecodeCacheContext { .url = response_url, .method = request.method(), .request_headers = HTTP::HeaderList::create(request.header_list()->headers()), + .memory_cache_request_headers = move(memory_cache_request_headers), .vary_key = *vary_key, + .memory_cache_partition_key = Fetch::Infrastructure::determine_the_network_partition_key(request), }; } @@ -188,6 +197,8 @@ static void schedule_bytecode_cache_generation(NonnullRefPtrstore_cache_associated_data(cache_context.url, cache_context.method, *cache_context.request_headers, cache_context.vary_key, HTTP::CacheEntryAssociatedData::JavaScriptBytecode, immutable_blob.bytes()); + if (cache_context.memory_cache_partition_key.has_value() && cache_context.memory_cache_request_headers) + Fetch::Fetching::update_javascript_bytecode_cache_in_http_memory_cache(*cache_context.memory_cache_partition_key, cache_context.url, cache_context.method, *cache_context.memory_cache_request_headers, cache_context.vary_key, immutable_blob); }); Threading::ThreadPool::the().submit([filename = move(filename), source_code = move(source_code), type, line_number_offset, callback, &main_thread_event_loop, source_hash]() mutable { diff --git a/Tests/LibHTTP/CMakeLists.txt b/Tests/LibHTTP/CMakeLists.txt index f1d956e714..cc635aa2b9 100644 --- a/Tests/LibHTTP/CMakeLists.txt +++ b/Tests/LibHTTP/CMakeLists.txt @@ -11,3 +11,4 @@ endforeach() ladybird_test("TestCacheIndex.cpp" LibWeb LIBS LibHTTP LibDatabase) ladybird_test("TestDiskCache.cpp" LibWeb LIBS LibHTTP LibURL) +ladybird_test("TestMemoryCache.cpp" LibWeb LIBS LibHTTP LibURL) diff --git a/Tests/LibHTTP/TestMemoryCache.cpp b/Tests/LibHTTP/TestMemoryCache.cpp new file mode 100644 index 0000000000..45b517aa65 --- /dev/null +++ b/Tests/LibHTTP/TestMemoryCache.cpp @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include + +static URL::URL parse_url(StringView url) +{ + return URL::Parser::basic_parse(url).release_value(); +} + +static NonnullRefPtr create_cacheable_request_headers() +{ + return HTTP::HeaderList::create({ + { "Accept"sv, "*/*"sv }, + }); +} + +static NonnullRefPtr create_cacheable_response_headers() +{ + return HTTP::HeaderList::create({ + { "Cache-Control"sv, "max-age=60"sv }, + }); +} + +static NonnullRefPtr create_vary_user_agent_response_headers() +{ + return HTTP::HeaderList::create({ + { "Cache-Control"sv, "max-age=60"sv }, + { "Vary"sv, "User-Agent"sv }, + }); +} + +static Core::ImmutableBytes immutable_bytes(StringView bytes) +{ + return MUST(Core::ImmutableBytes::copy(bytes.bytes())); +} + +TEST_CASE(javascript_bytecode_cache_round_trips_with_memory_cache_entry) +{ + auto cache = HTTP::MemoryCache::create(); + auto url = parse_url("https://example.com/script.js"sv); + auto request_headers = create_cacheable_request_headers(); + auto response_headers = create_cacheable_response_headers(); + auto bytecode = immutable_bytes("cached bytecode"sv); + + cache->create_entry(url, "GET"sv, *request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers, bytecode, 0); + cache->finalize_entry(url, "GET"sv, *request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv)); + + auto entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default); + VERIFY(entry.has_value()); + VERIFY(entry->javascript_bytecode_cache.has_value()); + EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes()); + EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional { 0 }); +} + +TEST_CASE(javascript_bytecode_cache_update_matches_memory_cache_vary_headers) +{ + auto cache = HTTP::MemoryCache::create(); + auto url = parse_url("https://example.com/script.js"sv); + auto script_request_headers = create_cacheable_request_headers(); + auto cache_request_headers = HTTP::HeaderList::create({ + { "Accept"sv, "*/*"sv }, + { "User-Agent"sv, "Ladybird"sv }, + }); + auto response_headers = create_vary_user_agent_response_headers(); + auto bytecode = immutable_bytes("generated bytecode"sv); + auto vary_key = HTTP::create_vary_key(*cache_request_headers, *response_headers); + + cache->create_entry(url, "GET"sv, *cache_request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers); + cache->finalize_entry(url, "GET"sv, *cache_request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv)); + + cache->update_javascript_bytecode_cache(url, "GET"sv, *script_request_headers, vary_key, bytecode); + + auto entry = cache->open_entry(url, "GET"sv, *cache_request_headers, HTTP::CacheMode::Default); + VERIFY(entry.has_value()); + EXPECT(!entry->javascript_bytecode_cache.has_value()); + EXPECT(!entry->javascript_bytecode_cache_vary_key.has_value()); + + cache->update_javascript_bytecode_cache(url, "GET"sv, *cache_request_headers, vary_key, bytecode); + + entry = cache->open_entry(url, "GET"sv, *cache_request_headers, HTTP::CacheMode::Default); + VERIFY(entry.has_value()); + VERIFY(entry->javascript_bytecode_cache.has_value()); + EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes()); + EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional { vary_key }); +} + +TEST_CASE(javascript_bytecode_cache_can_be_added_after_memory_cache_entry_is_complete) +{ + auto cache = HTTP::MemoryCache::create(); + auto url = parse_url("https://example.com/script.js"sv); + auto request_headers = create_cacheable_request_headers(); + auto response_headers = create_cacheable_response_headers(); + auto bytecode = immutable_bytes("generated bytecode"sv); + + cache->create_entry(url, "GET"sv, *request_headers, UnixDateTime::now(), 200, "OK"sv, *response_headers); + cache->finalize_entry(url, "GET"sv, *request_headers, 200, *response_headers, immutable_bytes("console.log('hello');"sv)); + + auto entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default); + VERIFY(entry.has_value()); + EXPECT(!entry->javascript_bytecode_cache.has_value()); + EXPECT(!entry->javascript_bytecode_cache_vary_key.has_value()); + + cache->update_javascript_bytecode_cache(url, "GET"sv, *request_headers, 0, bytecode); + + entry = cache->open_entry(url, "GET"sv, *request_headers, HTTP::CacheMode::Default); + VERIFY(entry.has_value()); + VERIFY(entry->javascript_bytecode_cache.has_value()); + EXPECT_EQ(entry->javascript_bytecode_cache->bytes(), bytecode.bytes()); + EXPECT_EQ(entry->javascript_bytecode_cache_vary_key, Optional { 0 }); +} diff --git a/Tests/LibWeb/CMakeLists.txt b/Tests/LibWeb/CMakeLists.txt index 91f14ed9c9..a2097b3005 100644 --- a/Tests/LibWeb/CMakeLists.txt +++ b/Tests/LibWeb/CMakeLists.txt @@ -8,6 +8,7 @@ set(TEST_SOURCES TestCSSSyntaxParser.cpp TestCSSTokenizer.cpp TestCSSTokenStream.cpp + TestFetchResponse.cpp TestFetchURL.cpp TestHTMLTokenizer.cpp TestMicrosyntax.cpp @@ -27,6 +28,7 @@ ladybird_utility(css-tokenizer SOURCES css-tokenizer.cpp LIBS LibFileSystem LibM target_link_libraries(TestContentBlocker PRIVATE LibURL) target_link_libraries(TestControlMessageQueue PRIVATE LibSync) +target_link_libraries(TestFetchResponse PRIVATE LibGC LibHTTP LibJS) target_link_libraries(TestFetchURL PRIVATE LibURL) target_link_libraries(TestSecureContexts PRIVATE LibURL) target_link_libraries(TestSourceHighlighter PRIVATE LibURL LibWebView) diff --git a/Tests/LibWeb/TestFetchResponse.cpp b/Tests/LibWeb/TestFetchResponse.cpp new file mode 100644 index 0000000000..5445058098 --- /dev/null +++ b/Tests/LibWeb/TestFetchResponse.cpp @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2026-present, the Ladybird developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include + +TEST_CASE(javascript_bytecode_cache_memory_cache_request_headers_are_cloned) +{ + auto vm = JS::VM::create(); + auto root_execution_context = JS::create_simple_execution_context(*vm); + auto& realm = *root_execution_context->realm; + + auto request_headers = HTTP::HeaderList::create({ + { "User-Agent"sv, "Ladybird"sv }, + }); + auto response = Web::Fetch::Infrastructure::Response::create(*vm); + response->set_javascript_bytecode_cache_memory_cache_request_headers(request_headers); + + auto cloned_response = response->clone(realm); + auto const& cloned_request_headers = cloned_response->javascript_bytecode_cache_memory_cache_request_headers(); + request_headers->set({ "User-Agent"sv, "Changed"sv }); + + VERIFY(cloned_request_headers.has_value()); + EXPECT_EQ((*cloned_request_headers)->get("User-Agent"sv), Optional { "Ladybird" }); +}