LibWeb: Preserve file-backed HTTP cache bodies

Carry response chunks through LibRequests and ResourceLoader as a
ResponseData wrapper. A disk cache hit can retain its mapped storage,
while ordinary streamed chunks still use borrowed bytes.

Store completed memory cache entries as Core::ImmutableBytes. This lets
mapped disk-cache responses stay file-backed while retained by the HTTP
memory cache, instead of forcing another ByteBuffer copy.
This commit is contained in:
Andreas Kling 2026-05-15 14:49:12 +02:00 committed by Andreas Kling
parent b165bdb874
commit 54d2395a03
10 changed files with 72 additions and 24 deletions

View file

@ -109,7 +109,7 @@ void MemoryCache::create_entry(URL::URL const& url, StringView method, HeaderLis
// FIXME: It would be nicer if create_entry just returned the cache and vary keys. But the call sites of create_entry and
// finalize_entry are pretty far apart, so passing that information along is rather awkward in Fetch.
void MemoryCache::finalize_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, ByteBuffer response_body)
void MemoryCache::finalize_entry(URL::URL const& url, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, Core::ImmutableBytes response_body)
{
if (!is_cacheable(method, request_headers))
return;

View file

@ -11,6 +11,7 @@
#include <AK/NonnullRefPtr.h>
#include <AK/RefCounted.h>
#include <AK/Time.h>
#include <LibCore/ImmutableBytes.h>
#include <LibHTTP/Cache/CacheMode.h>
#include <LibHTTP/Forward.h>
#include <LibURL/URL.h>
@ -26,7 +27,7 @@ public:
ByteString reason_phrase;
NonnullRefPtr<HeaderList> request_headers;
NonnullRefPtr<HeaderList> response_headers;
ByteBuffer response_body;
Core::ImmutableBytes response_body;
UnixDateTime request_time;
UnixDateTime response_time;
@ -37,7 +38,7 @@ public:
Optional<Entry const&> 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 finalize_entry(URL::URL const&, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, ByteBuffer response_body);
void finalize_entry(URL::URL const&, StringView method, HeaderList const& request_headers, u32 status_code, HeaderList const& response_headers, Core::ImmutableBytes response_body);
private:
HashMap<u64, Vector<Entry>, IdentityHashTraits<u64>> m_pending_entries;

View file

@ -10,6 +10,7 @@ namespace Requests {
class Request;
class RequestClient;
class ResponseData;
class WebSocket;
struct RequestTimingInfo;

View file

@ -91,7 +91,7 @@ void Request::set_request_body_file(Badge<Requests::RequestClient>, int fd, u64
m_internal_stream_data->file_backed_payload = payload.release_value();
if (m_internal_stream_data->on_data_available)
m_internal_stream_data->on_data_available(m_internal_stream_data->file_backed_payload->bytes());
m_internal_stream_data->on_data_available(ResponseData::from_immutable_bytes(*m_internal_stream_data->file_backed_payload));
}
void Request::set_buffered_request_finished_callback(BufferedRequestFinished on_buffered_request_finished)
@ -131,9 +131,9 @@ void Request::set_buffered_request_finished_callback(BufferedRequestFinished on_
move(payload));
};
set_up_internal_stream_data([this](auto read_bytes) {
set_up_internal_stream_data([this](auto data) {
// FIXME: What do we do if this fails?
m_internal_buffered_data->payload_stream.write_until_depleted(read_bytes).release_value_but_fixme_should_propagate_errors();
m_internal_buffered_data->payload_stream.write_until_depleted(data.bytes()).release_value_but_fixme_should_propagate_errors();
});
}
@ -223,7 +223,7 @@ void Request::set_up_internal_stream_data(DataReceived on_data_available)
if (read_bytes.is_empty())
break;
m_internal_stream_data->on_data_available(read_bytes);
m_internal_stream_data->on_data_available(ResponseData::from_bytes(read_bytes));
} while (true);
if (m_internal_stream_data->read_stream->is_eof())

View file

@ -24,6 +24,34 @@ namespace Requests {
class RequestClient;
class ResponseData {
public:
static ResponseData from_bytes(ReadonlyBytes bytes) { return ResponseData { bytes }; }
static ResponseData from_immutable_bytes(Core::ImmutableBytes bytes) { return ResponseData { move(bytes) }; }
[[nodiscard]] ReadonlyBytes bytes() const
{
if (m_immutable_bytes.has_value())
return m_immutable_bytes->bytes();
return m_bytes;
}
[[nodiscard]] Optional<Core::ImmutableBytes> const& immutable_bytes() const { return m_immutable_bytes; }
private:
explicit ResponseData(ReadonlyBytes bytes)
: m_bytes(bytes)
{
}
explicit ResponseData(Core::ImmutableBytes bytes)
: m_immutable_bytes(move(bytes))
{
}
ReadonlyBytes m_bytes;
Optional<Core::ImmutableBytes> m_immutable_bytes;
};
class ReadStream {
public:
static ErrorOr<NonnullOwnPtr<ReadStream>> create(int reader_fd);
@ -69,7 +97,7 @@ public:
void set_buffered_request_finished_callback(BufferedRequestFinished);
using HeadersReceived = Function<void(NonnullRefPtr<HTTP::HeaderList> response_headers, Optional<u32> response_code, Optional<String> const& reason_phrase, Optional<Core::AnonymousBuffer> javascript_bytecode, Optional<u64> javascript_bytecode_cache_vary_key)>;
using DataReceived = Function<void(ReadonlyBytes data)>;
using DataReceived = Function<void(ResponseData data)>;
using RequestFinished = Function<void(u64 total_size, RequestTimingInfo const& timing_info, Optional<NetworkError> network_error)>;
// Configure the request such that the response data is provided unbuffered as it is received. Using this method is

View file

@ -40,7 +40,7 @@ void FetchedDataReceiver::set_body(GC::Ref<Fetch::Infrastructure::Body> body)
m_pre_body_sniff_buffer.clear();
}
// If the stream already completed before the body was set,
// we missed the set_sniff_bytes_complete() call in handle_network_bytes.
// we missed the set_sniff_bytes_complete() call in handle_network_data.
if (m_network_complete)
m_body->set_sniff_bytes_complete();
}
@ -56,10 +56,10 @@ void FetchedDataReceiver::visit_edges(Visitor& visitor)
// This implements the parallel steps of the pullAlgorithm in HTTP-network-fetch.
// https://fetch.spec.whatwg.org/#ref-for-in-parallel⑤
void FetchedDataReceiver::handle_network_bytes(ReadonlyBytes bytes, NetworkState state)
void FetchedDataReceiver::handle_network_data(Requests::ResponseData data, NetworkState state)
{
if (state == NetworkState::Complete) {
VERIFY(bytes.is_empty());
VERIFY(data.bytes().is_empty());
m_network_complete = true;
// Mark sniff bytes as complete when the stream ends
if (m_body)
@ -77,6 +77,7 @@ void FetchedDataReceiver::handle_network_bytes(ReadonlyBytes bytes, NetworkState
return;
// 1. If one or more bytes have been transmitted from responses message body, then:
auto bytes = data.bytes();
if (bytes.is_empty())
return;
@ -96,8 +97,17 @@ void FetchedDataReceiver::handle_network_bytes(ReadonlyBytes bytes, NetworkState
m_pre_body_sniff_buffer.append(bytes.slice(0, min(bytes.size(), space_remaining)));
}
if (m_http_cache)
m_cache_buffer.append(bytes);
if (m_http_cache) {
if (auto const& immutable_bytes = data.immutable_bytes(); immutable_bytes.has_value() && immutable_bytes->is_file_backed() && m_cache_buffer.is_empty() && !m_cache_body.has_value()) {
m_cache_body = *immutable_bytes;
} else {
if (m_cache_body.has_value()) {
m_cache_buffer.append(m_cache_body->bytes());
m_cache_body.clear();
}
m_cache_buffer.append(bytes);
}
}
// 7. Append bytes to buffer.
enqueue_into_stream(bytes);
@ -138,10 +148,14 @@ void FetchedDataReceiver::close_stream()
auto request = m_fetch_params->request();
if (m_stream->is_readable() && !m_fetch_params->is_canceled()
&& m_response && request->cache_mode() != HTTP::CacheMode::NoStore) {
m_http_cache->finalize_entry(request->current_url(), request->method(), request->header_list(), m_response->status(), m_response->header_list(), move(m_cache_buffer));
auto response_body = m_cache_body.has_value()
? m_cache_body.release_value()
: Core::ImmutableBytes::adopt(move(m_cache_buffer));
m_http_cache->finalize_entry(request->current_url(), request->method(), request->header_list(), m_response->status(), m_response->header_list(), move(response_body));
}
m_http_cache.clear();
m_cache_body.clear();
}
if (!m_stream->is_readable())

View file

@ -8,9 +8,11 @@
#pragma once
#include <AK/ByteBuffer.h>
#include <LibCore/ImmutableBytes.h>
#include <LibGC/CellAllocator.h>
#include <LibHTTP/Forward.h>
#include <LibJS/Heap/Cell.h>
#include <LibRequests/Request.h>
#include <LibWeb/Forward.h>
namespace Web::Fetch::Fetching {
@ -30,7 +32,7 @@ public:
Complete,
Error,
};
void handle_network_bytes(ReadonlyBytes, NetworkState);
void handle_network_data(Requests::ResponseData, NetworkState);
private:
FetchedDataReceiver(GC::Ref<Infrastructure::FetchParams const>, GC::Ref<Streams::ReadableStream>, RefPtr<HTTP::MemoryCache>);
@ -54,6 +56,7 @@ private:
// Whole-response buffer retained only when m_http_cache is non-null, for finalize_entry().
ByteBuffer m_cache_buffer;
Optional<Core::ImmutableBytes> m_cache_body;
bool m_network_complete { false };
};

View file

@ -2229,8 +2229,8 @@ GC::Ref<PendingResponse> nonstandard_resource_loader_file_or_http_network_fetch(
// 16. Run these steps in parallel:
// FIXME: 1. Run these steps, but abort when fetchParams is canceled:
auto on_data_received = GC::create_function(vm.heap(), [fetched_data_receiver](ReadonlyBytes bytes) {
fetched_data_receiver->handle_network_bytes(bytes, FetchedDataReceiver::NetworkState::Ongoing);
auto on_data_received = GC::create_function(vm.heap(), [fetched_data_receiver](Requests::ResponseData data) {
fetched_data_receiver->handle_network_data(move(data), FetchedDataReceiver::NetworkState::Ongoing);
});
auto on_complete = GC::create_function(vm.heap(), [&vm, &realm, pending_response, stream, fetched_data_receiver](bool success, Requests::RequestTimingInfo const&, Optional<StringView> error_message) {
@ -2238,7 +2238,7 @@ GC::Ref<PendingResponse> nonstandard_resource_loader_file_or_http_network_fetch(
HTML::TemporaryExecutionContext execution_context { realm, HTML::TemporaryExecutionContext::CallbacksEnabled::Yes };
if (success) {
fetched_data_receiver->handle_network_bytes({}, FetchedDataReceiver::NetworkState::Complete);
fetched_data_receiver->handle_network_data(Requests::ResponseData::from_bytes({}), FetchedDataReceiver::NetworkState::Complete);
} else {
// 16.1.2.2. Otherwise, if stream is readable, error stream with a TypeError.
auto error = MUST(String::formatted("Load failed: {}", error_message.value_or("Unknown error"sv)));

View file

@ -369,7 +369,7 @@ RefPtr<Requests::Request> ResourceLoader::load(LoadRequest& request, GC::Root<On
[on_headers_received = move(on_headers_received), on_data_received = move(on_data_received), on_complete = move(on_complete), request](ReadonlyBytes data, Requests::RequestTimingInfo const& timing_info, HTTP::HeaderList const& response_headers) {
log_success(request);
on_headers_received->function()(response_headers, {}, {}, {}, {});
on_data_received->function()(data);
on_data_received->function()(Requests::ResponseData::from_bytes(data));
on_complete->function()(true, timing_info, {});
});
return nullptr;
@ -380,7 +380,7 @@ RefPtr<Requests::Request> ResourceLoader::load(LoadRequest& request, GC::Root<On
request,
[on_headers_received = move(on_headers_received), on_data_received = move(on_data_received), on_complete](FileLoadResult const& load_result) {
on_headers_received->function()(load_result.response_headers, {}, {}, {}, {});
on_data_received->function()(load_result.data);
on_data_received->function()(Requests::ResponseData::from_bytes(load_result.data));
on_complete->function()(true, load_result.timing_info, {});
},
[on_complete](ByteString const& message) {
@ -396,7 +396,7 @@ RefPtr<Requests::Request> ResourceLoader::load(LoadRequest& request, GC::Root<On
[request, on_headers_received = move(on_headers_received), on_data_received = move(on_data_received), on_complete](FileLoadResult const& load_result) {
log_success(request);
on_headers_received->function()(load_result.response_headers, {}, {}, {}, {});
on_data_received->function()(load_result.data);
on_data_received->function()(Requests::ResponseData::from_bytes(load_result.data));
on_complete->function()(true, load_result.timing_info, {});
},
[on_complete, request](ByteString const& message) {
@ -431,8 +431,8 @@ RefPtr<Requests::Request> ResourceLoader::load(LoadRequest& request, GC::Root<On
auto protocol_data_received = [on_data_received = move(on_data_received), request, request_id = protocol_request->id()](auto data) {
if (auto page = request.page())
page->client().page_did_receive_network_response_body(request_id, data);
on_data_received->function()(data);
page->client().page_did_receive_network_response_body(request_id, data.bytes());
on_data_received->function()(move(data));
};
auto protocol_complete = [this, on_complete = move(on_complete), request, &protocol_request = *protocol_request](u64 total_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error) {

View file

@ -15,6 +15,7 @@
#include <LibGC/Function.h>
#include <LibHTTP/HeaderList.h>
#include <LibRequests/Forward.h>
#include <LibRequests/Request.h>
#include <LibRequests/RequestTimingInfo.h>
#include <LibURL/URL.h>
#include <LibWeb/Forward.h>
@ -33,7 +34,7 @@ public:
void set_client(NonnullRefPtr<Requests::RequestClient>);
using OnHeadersReceived = GC::Function<void(HTTP::HeaderList const& response_headers, Optional<u32> status_code, Optional<String> const& reason_phrase, Optional<Core::AnonymousBuffer> javascript_bytecode, Optional<u64> javascript_bytecode_cache_vary_key)>;
using OnDataReceived = GC::Function<void(ReadonlyBytes data)>;
using OnDataReceived = GC::Function<void(Requests::ResponseData data)>;
using OnComplete = GC::Function<void(bool success, Requests::RequestTimingInfo const& timing_info, Optional<StringView> error_message)>;
RefPtr<Requests::Request> load(LoadRequest&, GC::Root<OnHeadersReceived>, GC::Root<OnDataReceived>, GC::Root<OnComplete>);