LibDevTools: Add request and response body viewing to network panel

This adds support for viewing request payloads (POST data) and response
bodies in the Firefox DevTools network panel.

Request bodies are captured when network requests start and passed
through IPC to the NetworkEventActor, which returns them via the
getRequestPostData protocol method.

Response bodies are streamed via a new IPC message as data is received,
accumulated in NetworkEventActor (with a 10MB size limit to prevent
memory issues), and returned via getResponseContent. Text content is
returned as UTF-8, while binary content (images, etc.) is base64.
This commit is contained in:
Andreas Kling 2026-01-14 20:47:04 +01:00 committed by Andreas Kling
parent cd8778f662
commit 31ffd2e8e5
15 changed files with 116 additions and 25 deletions

View file

@ -61,6 +61,10 @@ FrameActor::FrameActor(DevToolsServer& devtools, String name, WeakPtr<TabActor>
if (auto self = weak_self.strong_ref())
self->on_network_response_headers_received(move(data));
},
[weak_self = make_weak_ptr<FrameActor>()](u64 request_id, ByteBuffer data) {
if (auto self = weak_self.strong_ref())
self->on_network_response_body_received(request_id, move(data));
},
[weak_self = make_weak_ptr<FrameActor>()](DevToolsDelegate::NetworkRequestCompleteData data) {
if (auto self = weak_self.strong_ref())
self->on_network_request_finished(move(data));
@ -339,7 +343,7 @@ void FrameActor::on_console_message(WebView::ConsoleOutput console_output)
void FrameActor::on_network_request_started(DevToolsDelegate::NetworkRequestData data)
{
auto& actor = devtools().register_actor<NetworkEventActor>(data.request_id);
actor.set_request_info(move(data.url), move(data.method), data.start_time, move(data.request_headers));
actor.set_request_info(move(data.url), move(data.method), data.start_time, move(data.request_headers), move(data.request_body));
m_network_events.set(data.request_id, actor);
JsonArray events;
@ -417,6 +421,15 @@ void FrameActor::on_network_response_headers_received(DevToolsDelegate::NetworkR
send_message(move(message));
}
void FrameActor::on_network_response_body_received(u64 request_id, ByteBuffer data)
{
auto it = m_network_events.find(request_id);
if (it == m_network_events.end())
return;
it->value->append_response_body(move(data));
}
void FrameActor::on_network_request_finished(DevToolsDelegate::NetworkRequestCompleteData data)
{
auto it = m_network_events.find(data.request_id);

View file

@ -40,6 +40,7 @@ private:
void on_network_request_started(DevToolsDelegate::NetworkRequestData);
void on_network_response_headers_received(DevToolsDelegate::NetworkResponseData);
void on_network_response_body_received(u64 request_id, ByteBuffer data);
void on_network_request_finished(DevToolsDelegate::NetworkRequestCompleteData);
void on_navigation_started(String url);

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: BSD-2-Clause
*/
#include <AK/Base64.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <LibDevTools/Actors/NetworkEventActor.h>
@ -24,12 +25,13 @@ NetworkEventActor::NetworkEventActor(DevToolsServer& devtools, String name, u64
NetworkEventActor::~NetworkEventActor() = default;
void NetworkEventActor::set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers)
void NetworkEventActor::set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers, ByteBuffer request_body)
{
m_url = move(url);
m_method = move(method);
m_start_time = start_time;
m_request_headers = move(request_headers);
m_request_body = move(request_body);
}
void NetworkEventActor::set_response_start(u32 status_code, Optional<String> reason_phrase)
@ -43,6 +45,19 @@ void NetworkEventActor::set_response_headers(Vector<HTTP::Header> response_heade
m_response_headers = move(response_headers);
}
void NetworkEventActor::append_response_body(ByteBuffer data)
{
// Limit response body size to prevent memory issues
if (m_response_body.size() >= MAX_RESPONSE_BODY_SIZE)
return;
auto remaining_capacity = MAX_RESPONSE_BODY_SIZE - m_response_body.size();
auto bytes_to_append = min(data.size(), remaining_capacity);
if (bytes_to_append > 0)
m_response_body.append(data.bytes().slice(0, bytes_to_append));
}
void NetworkEventActor::set_request_complete(u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error)
{
m_body_size = body_size;
@ -166,7 +181,7 @@ void NetworkEventActor::get_request_cookies(Message const& message)
void NetworkEventActor::get_request_post_data(Message const& message)
{
JsonObject post_data;
post_data.set("text"sv, String {});
post_data.set("text"sv, MUST(String::from_utf8(m_request_body)));
JsonObject response;
response.set("postData"sv, move(post_data));
@ -203,16 +218,46 @@ void NetworkEventActor::get_response_cookies(Message const& message)
void NetworkEventActor::get_response_content(Message const& message)
{
// FIXME: Store and return actual response body content
// Get MIME type from Content-Type header
String mime_type = "application/octet-stream"_string;
for (auto const& header : m_response_headers) {
if (header.name.equals_ignoring_ascii_case("content-type"sv)) {
auto content_type = StringView { header.value };
// Extract just the MIME type, ignoring charset etc.
if (auto semicolon = content_type.find(';'); semicolon.has_value())
content_type = content_type.substring_view(0, *semicolon);
mime_type = MUST(String::from_utf8(content_type.trim_whitespace()));
break;
}
}
bool content_discarded = m_response_body.size() >= MAX_RESPONSE_BODY_SIZE;
// Check if content is text-based (can be displayed as UTF-8)
bool is_text = mime_type.starts_with_bytes("text/"sv)
|| mime_type == "application/json"sv
|| mime_type == "application/javascript"sv
|| mime_type == "application/xml"sv
|| mime_type.ends_with_bytes("+xml"sv)
|| mime_type.ends_with_bytes("+json"sv);
JsonObject content;
content.set("text"sv, String {});
// FIXME: Get actual MIME type from response headers
content.set("mimeType"sv, "text/html"sv);
if (is_text) {
// Try to interpret as UTF-8, fall back to empty if invalid
auto text_or_error = String::from_utf8(m_response_body);
content.set("text"sv, text_or_error.is_error() ? String {} : text_or_error.release_value());
content.set("encoding"sv, JsonValue {});
} else {
// Base64 encode binary content
content.set("text"sv, MUST(encode_base64(m_response_body)));
content.set("encoding"sv, "base64"sv);
}
content.set("mimeType"sv, mime_type);
content.set("size"sv, static_cast<i64>(m_body_size));
JsonObject response;
response.set("content"sv, move(content));
response.set("contentDiscarded"sv, true);
response.set("contentDiscarded"sv, content_discarded);
send_response(message, move(response));
}

View file

@ -25,9 +25,10 @@ public:
u64 request_id() const { return m_request_id; }
void set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers);
void set_request_info(String url, String method, UnixDateTime start_time, Vector<HTTP::Header> request_headers, ByteBuffer request_body);
void set_response_start(u32 status_code, Optional<String> reason_phrase);
void set_response_headers(Vector<HTTP::Header> response_headers);
void append_response_body(ByteBuffer data);
void set_request_complete(u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error);
JsonObject serialize_initial_event() const;
@ -51,15 +52,19 @@ private:
String m_method;
UnixDateTime m_start_time;
Vector<HTTP::Header> m_request_headers;
ByteBuffer m_request_body;
Optional<u32> m_status_code;
Optional<String> m_reason_phrase;
Vector<HTTP::Header> m_response_headers;
ByteBuffer m_response_body;
u64 m_body_size { 0 };
Requests::RequestTimingInfo m_timing_info {};
Optional<Requests::NetworkError> m_network_error;
bool m_complete { false };
static constexpr size_t MAX_RESPONSE_BODY_SIZE = 10 * MiB;
};
}

View file

@ -86,6 +86,7 @@ public:
String method;
UnixDateTime start_time;
Vector<HTTP::Header> request_headers;
ByteBuffer request_body;
};
struct NetworkResponseData {
@ -104,8 +105,9 @@ public:
using OnNetworkRequestStarted = Function<void(NetworkRequestData)>;
using OnNetworkResponseHeadersReceived = Function<void(NetworkResponseData)>;
using OnNetworkResponseBodyReceived = Function<void(u64 request_id, ByteBuffer data)>;
using OnNetworkRequestFinished = Function<void(NetworkRequestCompleteData)>;
virtual void listen_for_network_events(TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkRequestFinished) const { }
virtual void listen_for_network_events(TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkResponseBodyReceived, OnNetworkRequestFinished) const { }
virtual void stop_listening_for_network_events(TabDescription const&) const { }
using OnNavigationStarted = Function<void(String url)>;

View file

@ -426,7 +426,9 @@ void ResourceLoader::load(LoadRequest& request, GC::Root<OnHeadersReceived> on_h
on_headers_received->function()(response_headers, move(status_code), reason_phrase);
};
auto protocol_data_received = [on_data_received = move(on_data_received)](auto data) {
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);
};
@ -472,7 +474,7 @@ RefPtr<Requests::Request> ResourceLoader::start_network_request(LoadRequest cons
};
if (auto page = request.page())
page->client().page_did_start_network_request(protocol_request->id(), request.url().value(), request.method(), request.headers().headers());
page->client().page_did_start_network_request(protocol_request->id(), request.url().value(), request.method(), request.headers().headers(), request.body());
++m_pending_loads;
if (on_load_counter_change)

View file

@ -419,8 +419,9 @@ public:
virtual void page_did_change_audio_play_state(HTML::AudioPlayState) { }
virtual void page_did_start_network_request([[maybe_unused]] u64 request_id, [[maybe_unused]] URL::URL const& url, [[maybe_unused]] ByteString const& method, [[maybe_unused]] Vector<HTTP::Header> const& request_headers) { }
virtual void page_did_start_network_request([[maybe_unused]] u64 request_id, [[maybe_unused]] URL::URL const& url, [[maybe_unused]] ByteString const& method, [[maybe_unused]] Vector<HTTP::Header> const& request_headers, [[maybe_unused]] ReadonlyBytes request_body) { }
virtual void page_did_receive_network_response_headers([[maybe_unused]] u64 request_id, [[maybe_unused]] u32 status_code, [[maybe_unused]] Optional<String> reason_phrase, [[maybe_unused]] Vector<HTTP::Header> const& response_headers) { }
virtual void page_did_receive_network_response_body([[maybe_unused]] u64 request_id, [[maybe_unused]] ReadonlyBytes data) { }
virtual void page_did_finish_network_request([[maybe_unused]] u64 request_id, [[maybe_unused]] u64 body_size, [[maybe_unused]] Requests::RequestTimingInfo const& timing_info, [[maybe_unused]] Optional<Requests::NetworkError> const& network_error) { }
virtual IPC::File request_worker_agent([[maybe_unused]] Web::Bindings::AgentType worker_type) { return IPC::File {}; }

View file

@ -1295,20 +1295,24 @@ void Application::stop_listening_for_console_messages(DevTools::TabDescription c
view->on_console_message = nullptr;
}
void Application::listen_for_network_events(DevTools::TabDescription const& description, OnNetworkRequestStarted on_request_started, OnNetworkResponseHeadersReceived on_response_headers, OnNetworkRequestFinished on_request_finished) const
void Application::listen_for_network_events(DevTools::TabDescription const& description, OnNetworkRequestStarted on_request_started, OnNetworkResponseHeadersReceived on_response_headers, OnNetworkResponseBodyReceived on_response_body, OnNetworkRequestFinished on_request_finished) const
{
auto view = ViewImplementation::find_view_by_id(description.id);
if (!view.has_value())
return;
view->on_network_request_started = [on_request_started = move(on_request_started)](u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& headers) {
on_request_started({ request_id, url.to_string(), MUST(String::from_byte_string(method)), UnixDateTime::now(), headers });
view->on_network_request_started = [on_request_started = move(on_request_started)](u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& headers, ByteBuffer request_body) {
on_request_started({ request_id, url.to_string(), MUST(String::from_byte_string(method)), UnixDateTime::now(), headers, move(request_body) });
};
view->on_network_response_headers_received = [on_response_headers = move(on_response_headers)](u64 request_id, u32 status_code, Optional<String> const& reason_phrase, Vector<HTTP::Header> const& headers) {
on_response_headers({ request_id, status_code, reason_phrase, headers });
};
view->on_network_response_body_received = [on_response_body = move(on_response_body)](u64 request_id, ByteBuffer data) {
on_response_body(request_id, move(data));
};
view->on_network_request_finished = [on_request_finished = move(on_request_finished)](u64 request_id, u64 body_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error) {
on_request_finished({ request_id, body_size, timing_info, network_error });
};
@ -1322,6 +1326,7 @@ void Application::stop_listening_for_network_events(DevTools::TabDescription con
view->on_network_request_started = nullptr;
view->on_network_response_headers_received = nullptr;
view->on_network_response_body_received = nullptr;
view->on_network_request_finished = nullptr;
}

View file

@ -190,7 +190,7 @@ private:
virtual void evaluate_javascript(DevTools::TabDescription const&, String const&, OnScriptEvaluationComplete) const override;
virtual void listen_for_console_messages(DevTools::TabDescription const&, OnConsoleMessage) const override;
virtual void stop_listening_for_console_messages(DevTools::TabDescription const&) const override;
virtual void listen_for_network_events(DevTools::TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkRequestFinished) const override;
virtual void listen_for_network_events(DevTools::TabDescription const&, OnNetworkRequestStarted, OnNetworkResponseHeadersReceived, OnNetworkResponseBodyReceived, OnNetworkRequestFinished) const override;
virtual void stop_listening_for_network_events(DevTools::TabDescription const&) const override;
virtual void listen_for_navigation_events(DevTools::TabDescription const&, OnNavigationStarted, OnNavigationFinished) const override;
virtual void stop_listening_for_navigation_events(DevTools::TabDescription const&) const override;

View file

@ -210,8 +210,9 @@ public:
Function<void(Web::CSS::StyleSheetIdentifier const&, URL::URL const&, String const&)> on_received_style_sheet_source;
Function<void(JsonValue)> on_received_js_console_result;
Function<void(ConsoleOutput)> on_console_message;
Function<void(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&)> on_network_request_started;
Function<void(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&, ByteBuffer)> on_network_request_started;
Function<void(u64 request_id, u32 status_code, Optional<String> const&, Vector<HTTP::Header> const&)> on_network_response_headers_received;
Function<void(u64 request_id, ByteBuffer)> on_network_response_body_received;
Function<void(u64 request_id, u64 body_size, Requests::RequestTimingInfo const&, Optional<Requests::NetworkError> const&)> on_network_request_finished;
Function<void(i32 count_waiting)> on_resource_status_change;
Function<void()> on_restore_window;

View file

@ -436,11 +436,11 @@ void WebContentClient::did_output_js_console_message(u64 page_id, ConsoleOutput
}
}
void WebContentClient::did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers)
void WebContentClient::did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers, ByteBuffer request_body)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_network_request_started)
view->on_network_request_started(request_id, url, method, request_headers);
view->on_network_request_started(request_id, url, method, request_headers, move(request_body));
}
}
@ -452,6 +452,14 @@ void WebContentClient::did_receive_network_response_headers(u64 page_id, u64 req
}
}
void WebContentClient::did_receive_network_response_body(u64 page_id, u64 request_id, ByteBuffer data)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {
if (view->on_network_response_body_received)
view->on_network_response_body_received(request_id, move(data));
}
}
void WebContentClient::did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error)
{
if (auto view = view_for_page_id(page_id); view.has_value()) {

View file

@ -96,8 +96,9 @@ private:
virtual void did_get_internal_page_info(u64 page_id, PageInfoType, String) override;
virtual void did_execute_js_console_input(u64 page_id, JsonValue) override;
virtual void did_output_js_console_message(u64 page_id, ConsoleOutput) override;
virtual void did_start_network_request(u64 page_id, u64 request_id, URL::URL, ByteString method, Vector<HTTP::Header>) override;
virtual void did_start_network_request(u64 page_id, u64 request_id, URL::URL, ByteString method, Vector<HTTP::Header>, ByteBuffer request_body) override;
virtual void did_receive_network_response_headers(u64 page_id, u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header>) override;
virtual void did_receive_network_response_body(u64 page_id, u64 request_id, ByteBuffer data) override;
virtual void did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo, Optional<Requests::NetworkError>) override;
virtual void did_change_favicon(u64 page_id, Gfx::ShareableBitmap) override;
virtual void did_request_alert(u64 page_id, String) override;

View file

@ -751,9 +751,9 @@ void PageClient::received_message_from_web_ui(String const& name, JS::Value data
m_web_ui->received_message_from_web_ui(name, data);
}
void PageClient::page_did_start_network_request(u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& request_headers)
void PageClient::page_did_start_network_request(u64 request_id, URL::URL const& url, ByteString const& method, Vector<HTTP::Header> const& request_headers, ReadonlyBytes request_body)
{
client().async_did_start_network_request(m_id, request_id, url, method, request_headers);
client().async_did_start_network_request(m_id, request_id, url, method, request_headers, request_body);
}
void PageClient::page_did_receive_network_response_headers(u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header> const& response_headers)
@ -761,6 +761,11 @@ void PageClient::page_did_receive_network_response_headers(u64 request_id, u32 s
client().async_did_receive_network_response_headers(m_id, request_id, status_code, move(reason_phrase), response_headers);
}
void PageClient::page_did_receive_network_response_body(u64 request_id, ReadonlyBytes data)
{
client().async_did_receive_network_response_body(m_id, request_id, data);
}
void PageClient::page_did_finish_network_request(u64 request_id, u64 body_size, Requests::RequestTimingInfo const& timing_info, Optional<Requests::NetworkError> const& network_error)
{
client().async_did_finish_network_request(m_id, request_id, body_size, timing_info, network_error);

View file

@ -179,8 +179,9 @@ private:
virtual void page_did_paint(Gfx::IntRect const& content_rect, i32 bitmap_id) override;
virtual void page_did_take_screenshot(Gfx::ShareableBitmap const& screenshot) override;
virtual void received_message_from_web_ui(String const& name, JS::Value data) override;
virtual void page_did_start_network_request(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&) override;
virtual void page_did_start_network_request(u64 request_id, URL::URL const&, ByteString const&, Vector<HTTP::Header> const&, ReadonlyBytes) override;
virtual void page_did_receive_network_response_headers(u64 request_id, u32 status_code, Optional<String>, Vector<HTTP::Header> const&) override;
virtual void page_did_receive_network_response_body(u64 request_id, ReadonlyBytes) override;
virtual void page_did_finish_network_request(u64 request_id, u64 body_size, Requests::RequestTimingInfo const&, Optional<Requests::NetworkError> const&) override;
void setup_palette();

View file

@ -115,8 +115,9 @@ endpoint WebContentClient
did_execute_js_console_input(u64 page_id, JsonValue result) =|
did_output_js_console_message(u64 page_id, WebView::ConsoleOutput console_output) =|
did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers) =|
did_start_network_request(u64 page_id, u64 request_id, URL::URL url, ByteString method, Vector<HTTP::Header> request_headers, ByteBuffer request_body) =|
did_receive_network_response_headers(u64 page_id, u64 request_id, u32 status_code, Optional<String> reason_phrase, Vector<HTTP::Header> response_headers) =|
did_receive_network_response_body(u64 page_id, u64 request_id, ByteBuffer data) =|
did_finish_network_request(u64 page_id, u64 request_id, u64 body_size, Requests::RequestTimingInfo timing_info, Optional<Requests::NetworkError> network_error) =|
did_finish_test(u64 page_id, String text) =|