Merge pull request #675 from lightpanda-io/http_request_notifications
Some checks failed
e2e-test / zig build release (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled

HTTP request notification
This commit is contained in:
Karl Seguin
2025-05-24 10:10:16 +08:00
committed by GitHub
10 changed files with 369 additions and 42 deletions

View File

@@ -63,7 +63,7 @@ jobs:
needs: zig-build-release needs: zig-build-release
env: env:
MAX_MEMORY: 28000 MAX_MEMORY: 29000
MAX_AVG_DURATION: 24 MAX_AVG_DURATION: 24
LIGHTPANDA_DISABLE_TELEMETRY: true LIGHTPANDA_DISABLE_TELEMETRY: true

View File

@@ -7,7 +7,7 @@ const storage = @import("storage/storage.zig");
const generate = @import("../runtime/generate.zig"); const generate = @import("../runtime/generate.zig");
const Renderer = @import("renderer.zig").Renderer; const Renderer = @import("renderer.zig").Renderer;
const Loop = @import("../runtime/loop.zig").Loop; const Loop = @import("../runtime/loop.zig").Loop;
const HttpClient = @import("../http/client.zig").Client; const RequestFactory = @import("../http/client.zig").RequestFactory;
const WebApis = struct { const WebApis = struct {
// Wrapped like this for debug ergonomics. // Wrapped like this for debug ergonomics.
@@ -54,8 +54,8 @@ pub const SessionState = struct {
window: *Window, window: *Window,
renderer: *Renderer, renderer: *Renderer,
arena: std.mem.Allocator, arena: std.mem.Allocator,
http_client: *HttpClient,
cookie_jar: *storage.CookieJar, cookie_jar: *storage.CookieJar,
request_factory: RequestFactory,
// dangerous, but set by the JS framework // dangerous, but set by the JS framework
// shorter-lived than the arena above, which // shorter-lived than the arena above, which

View File

@@ -98,7 +98,7 @@ pub const Page = struct {
.renderer = &self.renderer, .renderer = &self.renderer,
.loop = browser.app.loop, .loop = browser.app.loop,
.cookie_jar = &session.cookie_jar, .cookie_jar = &session.cookie_jar,
.http_client = browser.http_client, .request_factory = browser.http_client.requestFactory(browser.notification),
}, },
.scope = try session.executor.startScope(&self.window, &self.state, self, true), .scope = try session.executor.startScope(&self.window, &self.state, self, true),
.module_map = .empty, .module_map = .empty,
@@ -174,6 +174,7 @@ pub const Page = struct {
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void { pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
const arena = self.arena; const arena = self.arena;
const session = self.session; const session = self.session;
const notification = session.browser.notification;
log.debug("starting GET {s}", .{request_url}); log.debug("starting GET {s}", .{request_url});
@@ -195,10 +196,11 @@ pub const Page = struct {
// load the data // load the data
var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true }); var request = try self.newHTTPRequest(.GET, &self.url, .{ .navigation = true });
defer request.deinit(); defer request.deinit();
request.notification = notification;
session.browser.notification.dispatch(.page_navigate, &.{ notification.dispatch(.page_navigate, &.{
.opts = opts,
.url = &self.url, .url = &self.url,
.reason = opts.reason,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
@@ -238,7 +240,7 @@ pub const Page = struct {
self.raw_data = arr.items; self.raw_data = arr.items;
} }
session.browser.notification.dispatch(.page_navigated, &.{ notification.dispatch(.page_navigated, &.{
.url = &self.url, .url = &self.url,
.timestamp = timestamp(), .timestamp = timestamp(),
}); });
@@ -464,7 +466,9 @@ pub const Page = struct {
} }
fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request { fn newHTTPRequest(self: *const Page, method: http.Request.Method, url: *const URL, opts: storage.cookie.LookupOpts) !http.Request {
var request = try self.state.http_client.request(method, &url.uri); // Don't use the state's request_factory here, since requests made by the
// page (i.e. to load <scripts>) should not generate notifications.
var request = try self.session.browser.http_client.request(method, &url.uri);
errdefer request.deinit(); errdefer request.deinit();
var arr: std.ArrayListUnmanaged(u8) = .{}; var arr: std.ArrayListUnmanaged(u8) = .{};
@@ -661,7 +665,8 @@ pub const NavigateReason = enum {
address_bar, address_bar,
}; };
const NavigateOpts = struct { pub const NavigateOpts = struct {
cdp_id: ?i64 = null,
reason: NavigateReason = .address_bar, reason: NavigateReason = .address_bar,
}; };

View File

@@ -80,7 +80,6 @@ const XMLHttpRequestBodyInit = union(enum) {
pub const XMLHttpRequest = struct { pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
arena: Allocator, arena: Allocator,
client: *http.Client,
request: ?http.Request = null, request: ?http.Request = null,
priv_state: PrivState = .new, priv_state: PrivState = .new,
@@ -252,7 +251,6 @@ pub const XMLHttpRequest = struct {
.state = .unsent, .state = .unsent,
.url = null, .url = null,
.origin_url = session_state.url, .origin_url = session_state.url,
.client = session_state.http_client,
.cookie_jar = session_state.cookie_jar, .cookie_jar = session_state.cookie_jar,
}; };
} }
@@ -420,7 +418,7 @@ pub const XMLHttpRequest = struct {
self.send_flag = true; self.send_flag = true;
self.priv_state = .open; self.priv_state = .open;
self.request = try self.client.request(self.method, &self.url.?.uri); self.request = try session_state.request_factory.create(self.method, &self.url.?.uri);
var request = &self.request.?; var request = &self.request.?;
errdefer request.deinit(); errdefer request.deinit();

View File

@@ -69,6 +69,9 @@ pub fn CDPT(comptime TypeProvider: type) type {
// 1 message at a time. // 1 message at a time.
message_arena: std.heap.ArenaAllocator, message_arena: std.heap.ArenaAllocator,
// Used for processing notifications within a browser context.
notification_arena: std.heap.ArenaAllocator,
const Self = @This(); const Self = @This();
pub fn init(app: *App, client: TypeProvider.Client) !Self { pub fn init(app: *App, client: TypeProvider.Client) !Self {
@@ -82,6 +85,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
.allocator = allocator, .allocator = allocator,
.browser_context = null, .browser_context = null,
.message_arena = std.heap.ArenaAllocator.init(allocator), .message_arena = std.heap.ArenaAllocator.init(allocator),
.notification_arena = std.heap.ArenaAllocator.init(allocator),
}; };
} }
@@ -91,6 +95,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
} }
self.browser.deinit(); self.browser.deinit();
self.message_arena.deinit(); self.message_arena.deinit();
self.notification_arena.deinit();
} }
pub fn handleMessage(self: *Self, msg: []const u8) bool { pub fn handleMessage(self: *Self, msg: []const u8) bool {
@@ -259,7 +264,7 @@ pub fn CDPT(comptime TypeProvider: type) type {
}); });
} }
fn sendJSON(self: *Self, message: anytype) !void { pub fn sendJSON(self: *Self, message: anytype) !void {
return self.client.sendJSON(message, .{ return self.client.sendJSON(message, .{
.emit_null_optional_fields = false, .emit_null_optional_fields = false,
}); });
@@ -283,6 +288,12 @@ pub fn BrowserContext(comptime CDP_T: type) type {
// Points to the session arena // Points to the session arena
arena: Allocator, arena: Allocator,
// From the parent's notification_arena.allocator(). Most of the CDP
// code paths deal with a cmd which has its own arena (from the
// message_arena). But notifications happen outside of the typical CDP
// request->response, and thus don't have a cmd and don't have an arena.
notification_arena: Allocator,
// Maps to our Page. (There are other types of targets, but we only // Maps to our Page. (There are other types of targets, but we only
// deal with "pages" for now). Since we only allow 1 open page at a // deal with "pages" for now). Since we only allow 1 open page at a
// time, we only have 1 target_id. // time, we only have 1 target_id.
@@ -336,6 +347,7 @@ pub fn BrowserContext(comptime CDP_T: type) type {
.node_search_list = undefined, .node_search_list = undefined,
.isolated_world = null, .isolated_world = null,
.inspector = inspector, .inspector = inspector,
.notification_arena = cdp.notification_arena.allocator(),
}; };
self.node_search_list = Node.Search.List.init(allocator, &self.node_registry); self.node_search_list = Node.Search.List.init(allocator, &self.node_registry);
errdefer self.deinit(); errdefer self.deinit();
@@ -397,6 +409,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return if (raw_url.len == 0) null else raw_url; return if (raw_url.len == 0) null else raw_url;
} }
pub fn networkEnable(self: *Self) !void {
try self.cdp.browser.notification.register(.http_request_start, self, onHttpRequestStart);
try self.cdp.browser.notification.register(.http_request_complete, self, onHttpRequestComplete);
}
pub fn networkDisable(self: *Self) void {
self.cdp.browser.notification.unregister(.http_request_start, self);
self.cdp.browser.notification.unregister(.http_request_complete, self);
}
pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void { pub fn onPageRemove(ctx: *anyopaque, _: Notification.PageRemove) !void {
const self: *Self = @alignCast(@ptrCast(ctx)); const self: *Self = @alignCast(@ptrCast(ctx));
return @import("domains/page.zig").pageRemove(self); return @import("domains/page.zig").pageRemove(self);
@@ -409,7 +431,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void { pub fn onPageNavigate(ctx: *anyopaque, data: *const Notification.PageNavigate) !void {
const self: *Self = @alignCast(@ptrCast(ctx)); const self: *Self = @alignCast(@ptrCast(ctx));
return @import("domains/page.zig").pageNavigate(self, data); defer self.resetNotificationArena();
return @import("domains/page.zig").pageNavigate(self.notification_arena, self, data);
} }
pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void { pub fn onPageNavigated(ctx: *anyopaque, data: *const Notification.PageNavigated) !void {
@@ -417,6 +440,22 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageNavigated(self, data); return @import("domains/page.zig").pageNavigated(self, data);
} }
pub fn onHttpRequestStart(ctx: *anyopaque, data: *const Notification.RequestStart) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestStart(self.notification_arena, self, data);
}
pub fn onHttpRequestComplete(ctx: *anyopaque, data: *const Notification.RequestComplete) !void {
const self: *Self = @alignCast(@ptrCast(ctx));
defer self.resetNotificationArena();
return @import("domains/network.zig").httpRequestComplete(self.notification_arena, self, data);
}
fn resetNotificationArena(self: *Self) void {
defer _ = self.cdp.notification_arena.reset(.{ .retain_with_limit = 1024 * 64 });
}
pub fn callInspector(self: *const Self, msg: []const u8) void { pub fn callInspector(self: *const Self, msg: []const u8) void {
self.inspector.send(msg); self.inspector.send(msg);
// force running micro tasks after send input to the inspector. // force running micro tasks after send input to the inspector.

View File

@@ -17,15 +17,130 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Notification = @import("../../notification.zig").Notification;
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
disable,
setCacheDisabled, setCacheDisabled,
}, cmd.input.action) orelse return error.UnknownMethod; }, cmd.input.action) orelse return error.UnknownMethod;
switch (action) { switch (action) {
.enable => return cmd.sendResult(null, .{}), .enable => return enable(cmd),
.disable => return disable(cmd),
.setCacheDisabled => return cmd.sendResult(null, .{}), .setCacheDisabled => return cmd.sendResult(null, .{}),
} }
} }
fn enable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
try bc.networkEnable();
return cmd.sendResult(null, .{});
}
fn disable(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
bc.networkDisable();
return cmd.sendResult(null, .{});
}
pub fn httpRequestStart(arena: Allocator, bc: anytype, request: *const Notification.RequestStart) !void {
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
std.debug.assert(bc.session.page != null);
var cdp = bc.cdp;
// all unreachable because we _have_ to have a page.
const session_id = bc.session_id orelse unreachable;
const target_id = bc.target_id orelse unreachable;
const page = bc.session.currentPage() orelse unreachable;
const document_url = try urlToString(arena, &page.url.uri, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
const request_url = try urlToString(arena, request.url, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
const request_fragment = try urlToString(arena, request.url, .{
.fragment = true,
});
var headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty;
try headers.ensureTotalCapacity(arena, request.headers.len);
for (request.headers) |header| {
headers.putAssumeCapacity(header.name, header.value);
}
// We're missing a bunch of fields, but, for now, this seems like enough
try cdp.sendEvent("Network.requestWillBeSent", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
.frameId = target_id,
.loaderId = bc.loader_id,
.documentUrl = document_url,
.request = .{
.url = request_url,
.urlFragment = request_fragment,
.method = @tagName(request.method),
.hasPostData = request.has_body,
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
},
}, .{ .session_id = session_id });
}
pub fn httpRequestComplete(arena: Allocator, bc: anytype, request: *const Notification.RequestComplete) !void {
// Isn't possible to do a network request within a Browser (which our
// notification is tied to), without a page.
std.debug.assert(bc.session.page != null);
var cdp = bc.cdp;
// all unreachable because we _have_ to have a page.
const session_id = bc.session_id orelse unreachable;
const target_id = bc.target_id orelse unreachable;
const url = try urlToString(arena, request.url, .{
.scheme = true,
.authentication = true,
.authority = true,
.path = true,
.query = true,
});
var headers: std.StringArrayHashMapUnmanaged([]const u8) = .empty;
try headers.ensureTotalCapacity(arena, request.headers.len);
for (request.headers) |header| {
headers.putAssumeCapacity(header.name, header.value);
}
// We're missing a bunch of fields, but, for now, this seems like enough
try cdp.sendEvent("Network.responseReceived", .{
.requestId = try std.fmt.allocPrint(arena, "REQ-{d}", .{request.id}),
.frameId = target_id,
.loaderId = bc.loader_id,
.response = .{
.url = url,
.status = request.status,
.headers = std.json.ArrayHashMap([]const u8){ .map = headers },
},
}, .{ .session_id = session_id });
}
fn urlToString(arena: Allocator, url: *const std.Uri, opts: std.Uri.WriteToStreamOptions) ![]const u8 {
var buf: std.ArrayListUnmanaged(u8) = .empty;
try url.writeToStream(opts, buf.writer(arena));
return buf.items;
}

View File

@@ -21,6 +21,8 @@ const URL = @import("../../url.zig").URL;
const Page = @import("../../browser/page.zig").Page; const Page = @import("../../browser/page.zig").Page;
const Notification = @import("../../notification.zig").Notification; const Notification = @import("../../notification.zig").Notification;
const Allocator = std.mem.Allocator;
pub fn processMessage(cmd: anytype) !void { pub fn processMessage(cmd: anytype) !void {
const action = std.meta.stringToEnum(enum { const action = std.meta.stringToEnum(enum {
enable, enable,
@@ -137,7 +139,7 @@ fn navigate(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
// didn't create? // didn't create?
const target_id = bc.target_id orelse return error.TargetIdNotLoaded; // const target_id = bc.target_id orelse return error.TargetIdNotLoaded;
// didn't attach? // didn't attach?
if (bc.session_id == null) { if (bc.session_id == null) {
@@ -148,17 +150,14 @@ fn navigate(cmd: anytype) !void {
var page = bc.session.currentPage() orelse return error.PageNotLoaded; var page = bc.session.currentPage() orelse return error.PageNotLoaded;
bc.loader_id = bc.cdp.loader_id_gen.next(); bc.loader_id = bc.cdp.loader_id_gen.next();
try cmd.sendResult(.{
.frameId = target_id,
.loaderId = bc.loader_id,
}, .{});
try page.navigate(url, .{ try page.navigate(url, .{
.reason = .address_bar, .reason = .address_bar,
.cdp_id = cmd.input.id,
}); });
} }
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void { pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void {
// I don't think it's possible that we get these notifications and don't // I don't think it's possible that we get these notifications and don't
// have these things setup. // have these things setup.
std.debug.assert(bc.session.page != null); std.debug.assert(bc.session.page != null);
@@ -170,7 +169,8 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
bc.reset(); bc.reset();
if (event.reason == .anchor) { const is_anchor = event.opts.reason == .anchor;
if (is_anchor) {
try cdp.sendEvent("Page.frameScheduledNavigation", .{ try cdp.sendEvent("Page.frameScheduledNavigation", .{
.frameId = target_id, .frameId = target_id,
.delay = 0, .delay = 0,
@@ -199,6 +199,22 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
.frameId = target_id, .frameId = target_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
// Drivers are sensitive to the order of events. Some more than others.
// The result for the Page.navigate seems like it _must_ come after
// the frameStartedLoading, but before any lifecycleEvent. So we
// unfortunately have to put the input_id ito the NavigateOpts which gets
// passed back into the notification.
if (event.opts.cdp_id) |input_id| {
try cdp.sendJSON(.{
.id = input_id,
.result = .{
.frameId = target_id,
.loaderId = loader_id,
},
.sessionId = session_id,
});
}
if (bc.page_life_cycle_events) { if (bc.page_life_cycle_events) {
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.name = "init", .name = "init",
@@ -208,7 +224,7 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
if (event.reason == .anchor) { if (is_anchor) {
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{ try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
.frameId = target_id, .frameId = target_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
@@ -219,21 +235,19 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void
// The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts. // The client will expect us to send new contextCreated events, such that the client has new id's for the active contexts.
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
var buffer: [512]u8 = undefined;
{ {
var fba = std.heap.FixedBufferAllocator.init(&buffer);
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
const aux_data = try std.fmt.allocPrint(fba.allocator(), "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id}); const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}", .{target_id});
bc.inspector.contextCreated( bc.inspector.contextCreated(
page.scope, page.scope,
"", "",
try page.origin(fba.allocator()), try page.origin(arena),
aux_data, aux_data,
true, true,
); );
} }
if (bc.isolated_world) |*isolated_world| { if (bc.isolated_world) |*isolated_world| {
const aux_json = try std.fmt.bufPrint(&buffer, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id}); const aux_json = try std.fmt.allocPrint(arena, "{{\"isDefault\":false,\"type\":\"isolated\",\"frameId\":\"{s}\"}}", .{target_id});
// Calling contextCreated will assign a new Id to the context and send the contextCreated event // Calling contextCreated will assign a new Id to the context and send the contextCreated event
bc.inspector.contextCreated( bc.inspector.contextCreated(
&isolated_world.executor.scope.?, &isolated_world.executor.scope.?,

View File

@@ -29,6 +29,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const tls = @import("tls"); const tls = @import("tls");
const IO = @import("../runtime/loop.zig").IO; const IO = @import("../runtime/loop.zig").IO;
const Loop = @import("../runtime/loop.zig").Loop; const Loop = @import("../runtime/loop.zig").Loop;
const Notification = @import("../notification.zig").Notification;
const log = std.log.scoped(.http_client); const log = std.log.scoped(.http_client);
@@ -44,6 +45,7 @@ const MAX_HEADER_LINE_LEN = 4096;
// Thread-safe. Holds our root certificate, connection pool and state pool // Thread-safe. Holds our root certificate, connection pool and state pool
// Used to create Requests. // Used to create Requests.
pub const Client = struct { pub const Client = struct {
req_id: usize,
allocator: Allocator, allocator: Allocator,
state_pool: StatePool, state_pool: StatePool,
http_proxy: ?Uri, http_proxy: ?Uri,
@@ -68,6 +70,7 @@ pub const Client = struct {
errdefer connection_manager.deinit(); errdefer connection_manager.deinit();
return .{ return .{
.req_id = 0,
.root_ca = root_ca, .root_ca = root_ca,
.allocator = allocator, .allocator = allocator,
.state_pool = state_pool, .state_pool = state_pool,
@@ -96,6 +99,25 @@ pub const Client = struct {
return Request.init(self, state, method, uri); return Request.init(self, state, method, uri);
} }
pub fn requestFactory(self: *Client, notification: ?*Notification) RequestFactory {
return .{
.client = self,
.notification = notification,
};
}
};
// A factory for creating requests with a given set of options.
pub const RequestFactory = struct {
client: *Client,
notification: ?*Notification,
pub fn create(self: RequestFactory, method: Request.Method, uri: *const Uri) !Request {
var req = try self.client.request(method, uri);
req.notification = self.notification;
return req;
}
}; };
// We assume most connections are going to end up in the IdleConnnection pool, // We assume most connections are going to end up in the IdleConnnection pool,
@@ -146,10 +168,12 @@ const Connection = struct {
// (but request.deinit() should still be called to discard the request // (but request.deinit() should still be called to discard the request
// before the `sendAsync` is called). // before the `sendAsync` is called).
pub const Request = struct { pub const Request = struct {
id: usize,
// The HTTP Method to use // The HTTP Method to use
method: Method, method: Method,
// The URI we're requested // The URI we requested
request_uri: *const Uri, request_uri: *const Uri,
// The URI that we're connecting to. Can be different than request_uri when // The URI that we're connecting to. Can be different than request_uri when
@@ -211,6 +235,16 @@ pub const Request = struct {
// Whether or not we should verify that the host matches the certificate CN // Whether or not we should verify that the host matches the certificate CN
_tls_verify_host: bool, _tls_verify_host: bool,
// We only want to emit a start / complete notifications once per request.
// Because of things like redirects and error handling, it is possible for
// the notification functions to be called multiple times, so we guard them
// with these booleans
_notified_start: bool,
_notified_complete: bool,
// The notifier that we emit request notifications to, if any.
notification: ?*Notification,
pub const Method = enum { pub const Method = enum {
GET, GET,
PUT, PUT,
@@ -230,12 +264,18 @@ pub const Request = struct {
fn init(client: *Client, state: *State, method: Method, uri: *const Uri) !Request { fn init(client: *Client, state: *State, method: Method, uri: *const Uri) !Request {
const decomposed = try decomposeURL(client, uri); const decomposed = try decomposeURL(client, uri);
const id = client.req_id + 1;
client.req_id = id;
return .{ return .{
.id = id,
.request_uri = uri, .request_uri = uri,
.connect_uri = decomposed.connect_uri, .connect_uri = decomposed.connect_uri,
.body = null, .body = null,
.headers = .{}, .headers = .{},
.method = method, .method = method,
.notification = null,
.arena = state.arena.allocator(), .arena = state.arena.allocator(),
._secure = decomposed.secure, ._secure = decomposed.secure,
._connect_host = decomposed.connect_host, ._connect_host = decomposed.connect_host,
@@ -247,6 +287,8 @@ pub const Request = struct {
._keepalive = false, ._keepalive = false,
._redirect_count = 0, ._redirect_count = 0,
._has_host_header = false, ._has_host_header = false,
._notified_start = false,
._notified_complete = false,
._connection_from_keepalive = false, ._connection_from_keepalive = false,
._tls_verify_host = client.tls_verify_host, ._tls_verify_host = client.tls_verify_host,
}; };
@@ -525,6 +567,7 @@ pub const Request = struct {
} }
try self.headers.append(arena, .{ .name = "User-Agent", .value = "Lightpanda/1.0" }); try self.headers.append(arena, .{ .name = "User-Agent", .value = "Lightpanda/1.0" });
self.requestStarting();
} }
// Sets up the request for redirecting. // Sets up the request for redirecting.
@@ -641,6 +684,35 @@ pub const Request = struct {
try writer.writeAll("\r\n"); try writer.writeAll("\r\n");
return buf[0..fbs.pos]; return buf[0..fbs.pos];
} }
fn requestStarting(self: *Request) void {
const notification = self.notification orelse return;
if (self._notified_start) {
return;
}
self._notified_start = true;
notification.dispatch(.http_request_start, &.{
.id = self.id,
.url = self.request_uri,
.method = self.method,
.headers = self.headers.items,
.has_body = self.body != null,
});
}
fn requestCompleted(self: *Request, response: ResponseHeader) void {
const notification = self.notification orelse return;
if (self._notified_complete) {
return;
}
self._notified_complete = true;
notification.dispatch(.http_request_complete, &.{
.id = self.id,
.url = self.request_uri,
.status = response.status,
.headers = response.headers.items,
});
}
}; };
// Handles asynchronous requests // Handles asynchronous requests
@@ -823,6 +895,10 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
} }
const status = self.conn.received(self.read_buf[0 .. self.read_pos + n]) catch |err| { const status = self.conn.received(self.read_buf[0 .. self.read_pos + n]) catch |err| {
if (err == error.TlsAlertCloseNotify and self.state == .handshake and self.maybeRetryRequest()) {
return;
}
self.handleError("data processing", err); self.handleError("data processing", err);
return; return;
}; };
@@ -832,6 +908,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
.need_more => self.receive(), .need_more => self.receive(),
.done => { .done => {
const redirect = self.redirect orelse { const redirect = self.redirect orelse {
self.request.requestCompleted(self.reader.response);
self.deinit(); self.deinit();
return; return;
}; };
@@ -1236,6 +1313,8 @@ const SyncHandler = struct {
var decompressor = std.compress.gzip.decompressor(compress_reader.reader()); var decompressor = std.compress.gzip.decompressor(compress_reader.reader());
try decompressor.decompress(body.writer(request.arena)); try decompressor.decompress(body.writer(request.arena));
self.request.requestCompleted(reader.response);
return .{ return .{
.header = reader.response, .header = reader.response,
._done = true, ._done = true,
@@ -1939,7 +2018,7 @@ pub const ResponseHeader = struct {
// value in-place. // value in-place.
// The value (and key) are both safe to mutate because they're cloned from // The value (and key) are both safe to mutate because they're cloned from
// the byte stream by our arena. // the byte stream by our arena.
const Header = struct { pub const Header = struct {
name: []const u8, name: []const u8,
value: []u8, value: []u8,
}; };
@@ -2024,6 +2103,7 @@ pub const Response = struct {
return data; return data;
} }
if (self._done) { if (self._done) {
self._request.requestCompleted(self.header);
return null; return null;
} }

View File

@@ -2,6 +2,7 @@ const std = @import("std");
const URL = @import("url.zig").URL; const URL = @import("url.zig").URL;
const page = @import("browser/page.zig"); const page = @import("browser/page.zig");
const http_client = @import("http/client.zig");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -59,6 +60,8 @@ pub const Notification = struct {
page_created: List = .{}, page_created: List = .{},
page_navigate: List = .{}, page_navigate: List = .{},
page_navigated: List = .{}, page_navigated: List = .{},
http_request_start: List = .{},
http_request_complete: List = .{},
notification_created: List = .{}, notification_created: List = .{},
}; };
@@ -67,6 +70,8 @@ pub const Notification = struct {
page_created: *page.Page, page_created: *page.Page,
page_navigate: *const PageNavigate, page_navigate: *const PageNavigate,
page_navigated: *const PageNavigated, page_navigated: *const PageNavigated,
http_request_start: *const RequestStart,
http_request_complete: *const RequestComplete,
notification_created: *Notification, notification_created: *Notification,
}; };
const EventType = std.meta.FieldEnum(Events); const EventType = std.meta.FieldEnum(Events);
@@ -76,7 +81,7 @@ pub const Notification = struct {
pub const PageNavigate = struct { pub const PageNavigate = struct {
timestamp: u32, timestamp: u32,
url: *const URL, url: *const URL,
reason: page.NavigateReason, opts: page.NavigateOpts,
}; };
pub const PageNavigated = struct { pub const PageNavigated = struct {
@@ -84,6 +89,21 @@ pub const Notification = struct {
url: *const URL, url: *const URL,
}; };
pub const RequestStart = struct {
id: usize,
url: *const std.Uri,
method: http_client.Request.Method,
headers: []std.http.Header,
has_body: bool,
};
pub const RequestComplete = struct {
id: usize,
url: *const std.Uri,
status: u16,
headers: []http_client.Header,
};
pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification { pub fn init(allocator: Allocator, parent: ?*Notification) !*Notification {
// This is put on the heap because we want to raise a .notification_created // This is put on the heap because we want to raise a .notification_created
// event, so that, something like Telemetry, can receive the // event, so that, something like Telemetry, can receive the
@@ -128,6 +148,7 @@ pub const Notification = struct {
.list = list, .list = list,
.func = @ptrCast(func), .func = @ptrCast(func),
.receiver = receiver, .receiver = receiver,
.event = event,
.struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child), .struct_name = @typeName(@typeInfo(@TypeOf(receiver)).pointer.child),
}; };
@@ -143,6 +164,30 @@ pub const Notification = struct {
list.append(node); list.append(node);
} }
pub fn unregister(self: *Notification, comptime event: EventType, receiver: anytype) void {
var nodes = self.listeners.getPtr(@intFromPtr(receiver)) orelse return;
const node_pool = &self.node_pool;
var i: usize = 0;
while (i < nodes.items.len) {
const node = nodes.items[i];
if (node.data.event != event) {
i += 1;
continue;
}
node.data.list.remove(node);
node_pool.destroy(node);
_ = nodes.swapRemove(i);
}
if (nodes.items.len == 0) {
nodes.deinit(self.allocator);
const removed = self.listeners.remove(@intFromPtr(receiver));
std.debug.assert(removed == true);
}
}
pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void { pub fn unregisterAll(self: *Notification, receiver: *anyopaque) void {
const node_pool = &self.node_pool; const node_pool = &self.node_pool;
@@ -184,7 +229,7 @@ fn EventFunc(comptime event: Notification.EventType) type {
return *const fn (*anyopaque, ArgType(event)) anyerror!void; return *const fn (*anyopaque, ArgType(event)) anyerror!void;
} }
// An listener. This is 1 receiver, with its function, and the linked list // A listener. This is 1 receiver, with its function, and the linked list
// node that goes in the appropriate EventListeners list. // node that goes in the appropriate EventListeners list.
const Listener = struct { const Listener = struct {
// the receiver of the event, i.e. the self parameter to `func` // the receiver of the event, i.e. the self parameter to `func`
@@ -196,6 +241,8 @@ const Listener = struct {
// For logging slightly better error // For logging slightly better error
struct_name: []const u8, struct_name: []const u8,
event: Notification.EventType,
// The event list this listener belongs to. // The event list this listener belongs to.
// We need this in order to be able to remove the node from the list // We need this in order to be able to remove the node from the list
list: *List, list: *List,
@@ -210,7 +257,7 @@ test "Notification" {
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.reason = undefined, .opts = .{},
}); });
var tc = TestClient{}; var tc = TestClient{};
@@ -219,7 +266,7 @@ test "Notification" {
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.timestamp = 4, .timestamp = 4,
.url = undefined, .url = undefined,
.reason = undefined, .opts = .{},
}); });
try testing.expectEqual(4, tc.page_navigate); try testing.expectEqual(4, tc.page_navigate);
@@ -227,7 +274,7 @@ test "Notification" {
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.reason = undefined, .opts = .{},
}); });
try testing.expectEqual(4, tc.page_navigate); try testing.expectEqual(4, tc.page_navigate);
@@ -236,7 +283,7 @@ test "Notification" {
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.timestamp = 10, .timestamp = 10,
.url = undefined, .url = undefined,
.reason = undefined, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .timestamp = 6, .url = undefined });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
@@ -246,11 +293,40 @@ test "Notification" {
notifier.dispatch(.page_navigate, &.{ notifier.dispatch(.page_navigate, &.{
.timestamp = 100, .timestamp = 100,
.url = undefined, .url = undefined,
.reason = undefined, .opts = .{},
}); });
notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined }); notifier.dispatch(.page_navigated, &.{ .timestamp = 100, .url = undefined });
try testing.expectEqual(14, tc.page_navigate); try testing.expectEqual(14, tc.page_navigate);
try testing.expectEqual(6, tc.page_navigated); try testing.expectEqual(6, tc.page_navigated);
{
// unregister
try notifier.register(.page_navigate, &tc, TestClient.pageNavigate);
try notifier.register(.page_navigated, &tc, TestClient.pageNavigated);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(1006, tc.page_navigated);
notifier.unregister(.page_navigate, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
// already unregistered, try anyways
notifier.unregister(.page_navigated, &tc);
notifier.dispatch(.page_navigate, &.{ .timestamp = 100, .url = undefined, .opts = .{} });
notifier.dispatch(.page_navigated, &.{ .timestamp = 1000, .url = undefined });
try testing.expectEqual(114, tc.page_navigate);
try testing.expectEqual(2006, tc.page_navigated);
}
} }
const TestClient = struct { const TestClient = struct {

View File

@@ -418,6 +418,10 @@ pub const JsRunner = struct {
.url = try self.url.toWebApi(arena), .url = try self.url.toWebApi(arena),
}); });
self.http_client = try HttpClient.init(arena, 1, .{
.tls_verify_host = false,
});
self.state = .{ self.state = .{
.arena = arena, .arena = arena,
.loop = &self.loop, .loop = &self.loop,
@@ -425,16 +429,12 @@ pub const JsRunner = struct {
.window = &self.window, .window = &self.window,
.renderer = &self.renderer, .renderer = &self.renderer,
.cookie_jar = &self.cookie_jar, .cookie_jar = &self.cookie_jar,
.http_client = &self.http_client, .request_factory = self.http_client.requestFactory(null),
}; };
self.storage_shelf = storage.Shelf.init(arena); self.storage_shelf = storage.Shelf.init(arena);
self.window.setStorageShelf(&self.storage_shelf); self.window.setStorageShelf(&self.storage_shelf);
self.http_client = try HttpClient.init(arena, 1, .{
.tls_verify_host = false,
});
self.executor = try self.env.newExecutionWorld(); self.executor = try self.env.newExecutionWorld();
errdefer self.executor.deinit(); errdefer self.executor.deinit();