mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
HTTP request notification
- Add 2 internal notifications 1 - http_request_start 2 - http_request_complete - When Network.enable CDP message is received, browser context registers for these 2 events (when Network.disable is called, it unregisters) - On http_request_start, CDP will emit a Network.requestWillBeSent message. This _does not_ include all the fields, but what we have appears to be enough for puppeteer.waitForNetworkIdle. - On http_request_complete, CDP will emit a Network.responseReceived message. This _does not_ include all the fields, bu what we have appears to be enough for puppeteer.waitForNetworkIdle. We currently don't emit any other new events, including any network-specific lifecycleEvent (i.e. Chrome will emit an networkIdle and networkAlmostIdle). To support this, the following other things were done: - CDP now has a `notification_arena` which is re-used between browser contexts. Normally, CDP code runs based on a "cmd" which has its own message_arena, but these notifications happen out-of-band, so we needed a new arena which is valid for handling 1 notification. - HTTP Client is notification-aware. The SessionState no longer includes the *http.Client directly. It instead includes an http.RequestFactory which is the combination fo the client + a specific configuration (i.e. *Notification). This ensures that all requests made from that factory have the same settings. - However, despite the above, _some_ requests do not appear to emit CDP events, such as loading a <script src="X">. So the page still deals directly with the *http.Client. - Playwright and Puppeteer (but Playwright in particular) are very sensitive to event ordering. These new events have introduced additional sensitivity. The result sent to Page.navigate had to be moved to inside the navigate event handler, which meant passing some cdp-specific data (the input.id) into the NavigateOpts. This is the only way I found to keep both happy - the sequence of events is closer (but still pretty far) from what Chrome does.
This commit is contained in:
@@ -29,6 +29,7 @@ const ArenaAllocator = std.heap.ArenaAllocator;
|
||||
const tls = @import("tls");
|
||||
const IO = @import("../runtime/loop.zig").IO;
|
||||
const Loop = @import("../runtime/loop.zig").Loop;
|
||||
const Notification = @import("../notification.zig").Notification;
|
||||
|
||||
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
|
||||
// Used to create Requests.
|
||||
pub const Client = struct {
|
||||
req_id: usize,
|
||||
allocator: Allocator,
|
||||
state_pool: StatePool,
|
||||
http_proxy: ?Uri,
|
||||
@@ -68,6 +70,7 @@ pub const Client = struct {
|
||||
errdefer connection_manager.deinit();
|
||||
|
||||
return .{
|
||||
.req_id = 0,
|
||||
.root_ca = root_ca,
|
||||
.allocator = allocator,
|
||||
.state_pool = state_pool,
|
||||
@@ -96,6 +99,25 @@ pub const Client = struct {
|
||||
|
||||
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,
|
||||
@@ -146,10 +168,12 @@ const Connection = struct {
|
||||
// (but request.deinit() should still be called to discard the request
|
||||
// before the `sendAsync` is called).
|
||||
pub const Request = struct {
|
||||
id: usize,
|
||||
|
||||
// The HTTP Method to use
|
||||
method: Method,
|
||||
|
||||
// The URI we're requested
|
||||
// The URI we requested
|
||||
request_uri: *const Uri,
|
||||
|
||||
// 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
|
||||
_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 {
|
||||
GET,
|
||||
PUT,
|
||||
@@ -230,12 +264,18 @@ pub const Request = struct {
|
||||
|
||||
fn init(client: *Client, state: *State, method: Method, uri: *const Uri) !Request {
|
||||
const decomposed = try decomposeURL(client, uri);
|
||||
|
||||
const id = client.req_id + 1;
|
||||
client.req_id = id;
|
||||
|
||||
return .{
|
||||
.id = id,
|
||||
.request_uri = uri,
|
||||
.connect_uri = decomposed.connect_uri,
|
||||
.body = null,
|
||||
.headers = .{},
|
||||
.method = method,
|
||||
.notification = null,
|
||||
.arena = state.arena.allocator(),
|
||||
._secure = decomposed.secure,
|
||||
._connect_host = decomposed.connect_host,
|
||||
@@ -247,6 +287,8 @@ pub const Request = struct {
|
||||
._keepalive = false,
|
||||
._redirect_count = 0,
|
||||
._has_host_header = false,
|
||||
._notified_start = false,
|
||||
._notified_complete = false,
|
||||
._connection_from_keepalive = false,
|
||||
._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" });
|
||||
self.requestStarting();
|
||||
}
|
||||
|
||||
// Sets up the request for redirecting.
|
||||
@@ -641,6 +684,35 @@ pub const Request = struct {
|
||||
try writer.writeAll("\r\n");
|
||||
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
|
||||
@@ -832,6 +904,7 @@ fn AsyncHandler(comptime H: type, comptime L: type) type {
|
||||
.need_more => self.receive(),
|
||||
.done => {
|
||||
const redirect = self.redirect orelse {
|
||||
self.request.requestCompleted(self.reader.response);
|
||||
self.deinit();
|
||||
return;
|
||||
};
|
||||
@@ -1236,6 +1309,8 @@ const SyncHandler = struct {
|
||||
var decompressor = std.compress.gzip.decompressor(compress_reader.reader());
|
||||
try decompressor.decompress(body.writer(request.arena));
|
||||
|
||||
self.request.requestCompleted(reader.response);
|
||||
|
||||
return .{
|
||||
.header = reader.response,
|
||||
._done = true,
|
||||
@@ -1939,7 +2014,7 @@ pub const ResponseHeader = struct {
|
||||
// value in-place.
|
||||
// The value (and key) are both safe to mutate because they're cloned from
|
||||
// the byte stream by our arena.
|
||||
const Header = struct {
|
||||
pub const Header = struct {
|
||||
name: []const u8,
|
||||
value: []u8,
|
||||
};
|
||||
@@ -2024,6 +2099,7 @@ pub const Response = struct {
|
||||
return data;
|
||||
}
|
||||
if (self._done) {
|
||||
self._request.requestCompleted(self.header);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user