Merge branch 'main' into agent

This commit is contained in:
Adrià Arrufat
2026-06-03 10:09:33 +02:00
16 changed files with 384 additions and 71 deletions

View File

@@ -532,6 +532,7 @@ pub const DumpFormat = enum {
pub const WaitUntil = enum {
load,
domcontentloaded,
networkalmostidle,
networkidle,
done,
};

View File

@@ -111,10 +111,15 @@ network: *Network,
arena_pool: *ArenaPool,
// The current proxy. CDP can change it, changeProxy(null) restores
// from config.
// The current proxy. Callers can change it, changeProxy(null) restores
// from config. May point either at `http_proxy_owned` (a caller-supplied
// dupe) or at the config string (which we must not free).
http_proxy: ?[:0]const u8 = null,
// When a caller (e.g. CDP) supplies a proxy, we have to dupe it to take ownership
// which we'll be responsible for freeing.
http_proxy_owned: ?[:0]const u8 = null,
// track if the client use a proxy for connections.
// We can't use http_proxy because we want also to track proxy configured via
// CDP.
@@ -260,6 +265,9 @@ pub fn deinit(self: *Client) void {
self.handles.deinit();
self.clearUserAgentOverride();
if (self.http_proxy_owned) |owned| {
self.allocator.free(owned);
}
self.robots_layer.deinit(self.allocator);
self.deferring_layer.deinit();
@@ -337,7 +345,22 @@ pub fn setTlsVerify(self: *Client, verify: bool) !void {
// can be changed at any point in the easy's lifecycle.
pub fn changeProxy(self: *Client, proxy: ?[:0]const u8) !void {
try self.ensureNoActiveConnection();
self.http_proxy = proxy orelse self.network.config.httpProxy();
// Free any previously-duped proxy before we overwrite http_proxy.
if (self.http_proxy_owned) |owned| {
self.allocator.free(owned);
self.http_proxy_owned = null;
}
// Reset to the config default; if dupeZ below fails, http_proxy is
// left pointing at this rather than at the freed dup.
self.http_proxy = self.network.config.httpProxy();
if (proxy) |p| {
const owned = try self.allocator.dupeZ(u8, p);
self.http_proxy_owned = owned;
self.http_proxy = owned;
}
self.use_proxy = self.http_proxy != null;
}
@@ -1564,10 +1587,28 @@ pub const Transfer = struct {
// post-perform would need to be improved),
pub fn unpark(self: *Transfer) void {
lp.assert(self.state == .parked, "Transfer.unpark", .{ .state = self.state });
self.leaveIntercept();
self.state = .created;
}
// Decrement the interception counter iff this transfer is currently
// parked for CDP interception.
fn leaveIntercept(self: *Transfer) void {
if (self.state != .parked) {
return;
}
switch (self.state.parked) {
.robots => {},
.intercept_request, .intercept_auth => {
const intercept_layer = &self.client.interception_layer;
lp.assert(intercept_layer.intercepted > 0, "Transfer.leaveIntercept", .{ .value = intercept_layer.intercepted });
intercept_layer.intercepted -= 1;
},
}
}
pub fn deinit(self: *Transfer) void {
self.leaveIntercept();
if (self._conn) |c| {
self.client.removeConn(c);
self._conn = null;
@@ -1790,6 +1831,7 @@ pub const Transfer = struct {
return writer.print("{s} {s}", .{ @tagName(req.method), req.url });
}
// `url` must have transfer-arena lifetime: it's stored as-is, not duped.
pub fn updateURL(self: *Transfer, url: [:0]const u8) !void {
self.req.url = url;
}
@@ -1829,7 +1871,9 @@ pub const Transfer = struct {
}
const base_url = try conn.getEffectiveUrl();
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{});
// base_url and location.value are owned by curl. The returned value
// will be stored in transfer.req.url, hence the always_dupe.
const resolved = try URL.resolve(arena, std.mem.span(base_url), location.value, .{ .always_dupe = true });
// RFC 7231 §7.1.2: if the Location value has no fragment, the redirect
// inherits the fragment from the URI used to generate the request.
@@ -1904,9 +1948,9 @@ pub const Transfer = struct {
log.debug(.http, "abort auth transfer", .{ .intercepted = self.client.interception_layer.intercepted });
}
self.client.interception_layer.intercepted -= 1;
// The transfer is still .parked(.intercept_auth)
// abort -> deinit -> leaveIntercept decrements the counter.
self.abort(error.AbortAuthChallenge);
return;
}
// headerDoneCallback is called once the headers have been read.
@@ -2067,13 +2111,16 @@ pub const Transfer = struct {
pub fn continueTransfer(self: *Client, transfer: *Transfer) !void {
if (comptime IS_DEBUG) {
lp.assert(self.interception_layer.intercepted > 0, "HttpClient.continueTransfer", .{ .value = self.interception_layer.intercepted });
log.debug(.http, "continue transfer", .{ .intercepted = self.interception_layer.intercepted });
}
self.interception_layer.intercepted -= 1;
transfer.unpark();
return self.process(transfer);
self.process(transfer) catch |err| {
if (transfer.state == .created) {
transfer.abort(err);
}
return err;
};
}
const Noop = struct {

View File

@@ -244,7 +244,10 @@ pub fn scheduleNavigation(self: *Page, frame: *Frame) !void {
}
pub fn findFrameByFrameId(self: *Page, frame_id: u32) ?*Frame {
return findFrameBy(&self.frame, "_frame_id", frame_id);
if (findFrameBy(&self.frame, "_frame_id", frame_id)) |found| {
return found;
}
return self.findPopupBy("_frame_id", frame_id);
}
// Returns the popup Frame registered under `name`, or null.
@@ -258,11 +261,16 @@ pub fn findPopupByName(self: *Page, name: []const u8) ?*Frame {
}
pub fn findFrameByLoaderId(self: *Page, loader_id: u32) ?*Frame {
return findFrameBy(&self.frame, "_loader_id", loader_id);
if (findFrameBy(&self.frame, "_loader_id", loader_id)) |found| {
return found;
}
return self.findPopupBy("_loader_id", loader_id);
}
fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
if (@field(frame, field) == id) return frame;
if (@field(frame, field) == id) {
return frame;
}
for (frame.child_frames.items) |f| {
if (findFrameBy(f, field, id)) |found| {
return found;
@@ -270,3 +278,12 @@ fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame {
}
return null;
}
fn findPopupBy(self: *Page, comptime field: []const u8, id: u32) ?*Frame {
for (self.popups.items) |frame| {
if (findFrameBy(frame, field, id)) |found| {
return found;
}
}
return null;
}

View File

@@ -104,6 +104,7 @@ fn _wait(self: *Runner, comptime is_cdp: bool, opts: WaitOpts) !WaitResult {
const tick_result = self._tick(is_cdp, tick_opts) catch |err| {
switch (err) {
error.JsError => {}, // already logged (with hopefully more context)
error.ClientDisconnected => {}, // CDP layer already logged this
else => log.err(.browser, "session wait", .{
.err = err,
.url = self.frame.url,
@@ -215,6 +216,9 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !TickResult {
.networkidle => if (frame._notified_network_idle == .done) {
return .done;
},
.networkalmostidle => if (frame._notified_network_almost_idle == .done) {
return .done;
},
}
if (http_active == 0 and http_next_tick == 0 and http_client.ws_active == 0 and http_client.queue.first == null and http_client.ready_queue.first == null and (comptime is_cdp) == false) {

View File

@@ -631,7 +631,7 @@ test "Env: Worker context " {
const frame = try session.createPage();
defer session.removePage();
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", frame);
const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", null, frame);
var ls: js.Local.Scope = undefined;
worker._worker_scope.js.localScope(&ls);

View File

@@ -0,0 +1,56 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<embed id="em1" src="/plugin.swf" type="application/x-shockwave-flash" width="640" height="480">
<embed id="em2">
<script id="embed-from-html">
{
const em1 = document.getElementById('em1');
testing.expectEqual('HTMLEmbedElement', em1.constructor.name);
testing.expectEqual(testing.ORIGIN + '/plugin.swf', em1.src);
testing.expectEqual('application/x-shockwave-flash', em1.type);
testing.expectEqual('640', em1.width);
testing.expectEqual('480', em1.height);
const em2 = document.getElementById('em2');
testing.expectEqual('', em2.src);
testing.expectEqual('', em2.type);
testing.expectEqual('', em2.width);
testing.expectEqual('', em2.height);
}
</script>
<script id="embed-reflection">
{
const em = document.createElement('embed');
em.type = 'video/mp4';
testing.expectEqual('video/mp4', em.getAttribute('type'));
em.setAttribute('type', 'image/svg+xml');
testing.expectEqual('image/svg+xml', em.type);
em.width = '320';
testing.expectEqual('320', em.getAttribute('width'));
em.setAttribute('width', '160');
testing.expectEqual('160', em.width);
em.height = '240';
testing.expectEqual('240', em.getAttribute('height'));
em.setAttribute('height', '120');
testing.expectEqual('120', em.height);
}
</script>
<script id="embed-src-url">
{
const em = document.createElement('embed');
testing.expectEqual('', em.src);
em.src = 'https://lightpanda.io/plugin.swf';
testing.expectEqual('https://lightpanda.io/plugin.swf', em.src);
em.src = '/relative.swf';
testing.expectEqual(testing.ORIGIN + '/relative.swf', em.src);
}
</script>

View File

@@ -0,0 +1,25 @@
// A module worker (`new Worker(url, { type: "module" })`). Unlike a classic
// worker, the entry script may use top-level static `import`/`export`, and
// `importScripts()` is not supported (it throws a TypeError).
import { baseValue } from './modules/base.js';
import { importedValue, localValue } from './modules/importer.js';
export const exported = 'top-level-export-ok';
let importScriptsError = null;
try {
importScripts('./import-script1.js');
} catch (e) {
importScriptsError = e.constructor.name;
}
onmessage = function (event) {
postMessage({
echo: event.data,
baseValue: baseValue,
importedValue: importedValue,
localValue: localValue,
importScriptsError: importScriptsError,
from: 'module-worker',
});
};

View File

@@ -380,6 +380,29 @@
}
</script>
<script id="worker_module_type" type=module>
// A module-type worker: its entry script uses top-level static import (only
// valid in module workers), and importScripts() throws a TypeError.
{
const state = await testing.async();
const worker = new Worker('./module-worker.js', { type: 'module' });
worker.onmessage = function(event) {
state.resolve(event.data);
};
worker.postMessage({ greeting: 'to-module' });
await state.done((response) => {
testing.expectEqual('to-module', response.echo.greeting);
testing.expectEqual('module-worker', response.from);
testing.expectEqual('from-base', response.baseValue);
testing.expectEqual('from-base', response.importedValue);
testing.expectEqual('local', response.localValue);
testing.expectEqual('TypeError', response.importScriptsError);
});
}
</script>
<script id="worker_from_blob_url" type=module>
// A blob: worker resolves its initial script through the HTTP client's
// synthetic-scheme path against the frame's blob registry, like any URL.

View File

@@ -36,6 +36,12 @@ const IS_DEBUG = @import("builtin").mode == .Debug;
const Worker = @This();
pub const WorkerType = enum {
classic,
module,
pub const js_enum_from_string = true;
};
// used by HttpClient when generating notification
// Ultimately used by CDP to generate request/loader ids.
_frame_id: u32,
@@ -47,6 +53,7 @@ _arena: Allocator,
_worker_scope: *WorkerGlobalScope,
_url: [:0]const u8,
_type: WorkerType = .classic,
_script_loaded: bool = false,
_script_buffer: std.ArrayList(u8) = .empty,
_http_response: ?HttpClient.Response = null,
@@ -56,7 +63,11 @@ _on_error: ?js.Function.Global = null,
_on_message: ?js.Function.Global = null,
_on_messageerror: ?js.Function.Global = null,
pub fn init(url: []const u8, frame: *Frame) !*Worker {
const WorkerOptions = struct {
type: WorkerType = .classic,
};
pub fn init(url: []const u8, options: ?WorkerOptions, frame: *Frame) !*Worker {
const session = frame._session;
const arena = try session.getArena(.large, "Worker");
@@ -68,6 +79,7 @@ pub fn init(url: []const u8, frame: *Frame) !*Worker {
._proto = undefined,
._frame = frame,
._url = resolved_url,
._type = if (options) |o| o.type else .classic,
._worker_scope = undefined,
._frame_id = session.nextFrameId(),
._loader_id = session.nextLoaderId(),
@@ -195,12 +207,24 @@ fn loadInitialScript(self: *Worker, script: []const u8) !void {
try_catch.init(&ls.local);
defer try_catch.deinit();
_ = ls.local.eval(script, self._url) catch |err| {
const caught = try_catch.caughtOrError(self._arena, err);
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
return;
};
// Classic workers evaluate the entry script as a classic script; module
// workers (`new Worker(url, { type: "module" })`) instantiate it as a
// module so top-level `import`/`export` work. Static imports load
// synchronously through ScriptManagerBase (client.tick sync_wait).
switch (self._type) {
.classic => _ = ls.local.eval(script, self._url) catch |err| {
const caught = try_catch.caughtOrError(self._arena, err);
log.err(.browser, "worker script error", .{ .url = self._url, .caught = caught });
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
return;
},
.module => self._worker_scope.js.module(false, &ls.local, script, self._url, true) catch |err| {
const caught = try_catch.caughtOrError(self._arena, err);
log.err(.browser, "worker module error", .{ .url = self._url, .caught = caught });
self.fireErrorEvent(caught.exception orelse @errorName(err), null);
return;
},
}
ls.local.runMacrotasks();
}

View File

@@ -430,6 +430,12 @@ pub fn close(self: *WorkerGlobalScope) void {
}
pub fn importScripts(self: *WorkerGlobalScope, urls: []const [:0]const u8) !void {
if (self._worker._type == .module) {
// not allowed to be called when the worker type is module (scripts should
// use actual imports).
return error.TypeError;
}
const session = self._session;
const arena = try session.getArena(.large, "importScript");
defer session.releaseArena(arena);

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("../../../js/js.zig");
const Frame = @import("../../../Frame.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
@@ -27,10 +28,50 @@ _proto: *HtmlElement,
pub fn asElement(self: *Embed) *Element {
return self._proto._proto;
}
pub fn asConstElement(self: *const Embed) *const Element {
return self._proto._proto;
}
pub fn asNode(self: *Embed) *Node {
return self.asElement().asNode();
}
pub fn getSrc(self: *const Embed, frame: *Frame) ![]const u8 {
const element = self.asConstElement();
const src = element.getAttributeSafe(comptime .wrap("src")) orelse return "";
if (src.len == 0) {
return "";
}
return element.asConstNode().resolveURL(src, frame, .{});
}
pub fn setSrc(self: *Embed, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), frame);
}
pub fn getType(self: *const Embed) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("type")) orelse "";
}
pub fn setType(self: *Embed, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("type"), .wrap(value), frame);
}
pub fn getWidth(self: *const Embed) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("width")) orelse "";
}
pub fn setWidth(self: *Embed, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("width"), .wrap(value), frame);
}
pub fn getHeight(self: *const Embed) []const u8 {
return self.asConstElement().getAttributeSafe(comptime .wrap("height")) orelse "";
}
pub fn setHeight(self: *Embed, value: []const u8, frame: *Frame) !void {
try self.asElement().setAttributeSafe(comptime .wrap("height"), .wrap(value), frame);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(Embed);
@@ -39,4 +80,14 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const height = bridge.accessor(Embed.getHeight, Embed.setHeight, .{ .ce_reactions = true });
pub const src = bridge.accessor(Embed.getSrc, Embed.setSrc, .{ .ce_reactions = true });
pub const @"type" = bridge.accessor(Embed.getType, Embed.setType, .{ .ce_reactions = true });
pub const width = bridge.accessor(Embed.getWidth, Embed.setWidth, .{ .ce_reactions = true });
};
const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Embed" {
try testing.htmlRunner("element/html/embed.html", .{});
}

View File

@@ -812,11 +812,20 @@ fn parseIdentifier(self: *Parser, arena: Allocator, err: ParseError) ParseError!
}
// Slow path: has escapes or nulls
return self.consumeEscapedIdentTail(arena, i, err);
}
// Decode the tail of an <ident-token> that contains escape sequences or null.
// The caller is expecteed to have already done a "fast path" scan, and
// `prefix_len` is the part of `self.input` that needs to this "slow path" for
// decoding
fn consumeEscapedIdentTail(self: *Parser, arena: Allocator, prefix_len: usize, err: ParseError) ![]const u8 {
const input = self.input;
var result = try std.ArrayList(u8).initCapacity(arena, input.len);
result.appendSliceAssumeCapacity(input[0..prefix_len]);
try result.appendSlice(arena, input[0..i]);
var j = i;
var j = prefix_len;
while (j < input.len) {
const b = input[j];
@@ -834,14 +843,10 @@ fn parseIdentifier(self: *Parser, arena: Allocator, err: ParseError) ParseError!
continue;
}
const is_ident_char = switch (b) {
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => true,
0x80...0xFF => true,
else => false,
};
if (!is_ident_char) {
break;
switch (b) {
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
0x80...0xFF => {},
else => break,
}
try result.append(arena, b);
j += 1;
@@ -906,7 +911,7 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
self.input = self.input[1..];
_ = self.skipSpaces();
const attr_name = try self.attributeName();
const attr_name = try self.attributeName(arena);
// Normalize the name to lowercase for fast matching (consistent with Attribute.normalizeNameForLookup)
const name = try Attribute.normalizeNameForLookupAlloc(arena, .wrap(attr_name));
@@ -954,29 +959,39 @@ fn attribute(self: *Parser, arena: Allocator) !Selector.Attribute {
return .{ .name = name, .matcher = matcher, .case_insensitive = case_insensitive };
}
fn attributeName(self: *Parser) ![]const u8 {
fn attributeName(self: *Parser, arena: Allocator) ![]const u8 {
const input = self.input;
if (input.len == 0) {
return error.InvalidAttributeSelector;
}
const first = input[0];
if (!std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
if (first != '\\' and first != 0 and !std.ascii.isAlphabetic(first) and first != '_' and first < 0x80) {
return error.InvalidAttributeSelector;
}
var i: usize = 1;
for (input[1..]) |b| {
switch (b) {
// Fast scan until we hit a character that needs special handling
var i: usize = if (first == '\\' or first == 0) 0 else 1;
while (i < input.len) : (i += 1) {
switch (input[i]) {
'a'...'z', 'A'...'Z', '0'...'9', '-', '_' => {},
0x80...0xFF => {},
else => break,
}
i += 1;
}
self.input = input[i..];
return input[0..i];
if (i == input.len or (input[i] != '\\' and input[i] != 0)) {
// Fast path fully completed (no escapes/nulls in the name)
if (i == 0) {
@branchHint(.cold);
return error.InvalidAttributeSelector;
}
self.input = input[i..];
return input[0..i];
}
// Slow path: decode escape sequences (and null -> U+FFFD).
return self.consumeEscapedIdentTail(arena, i, error.InvalidAttributeSelector);
}
fn attributeMatcher(self: *Parser) !std.meta.FieldEnum(Selector.AttributeMatcher) {
@@ -1669,3 +1684,58 @@ test "Selector: Parser.attributeValue" {
try testing.expectError(error.InvalidAttributeSelector, parser.attributeValue(arena));
}
}
test "Selector: Parser.attributeName" {
defer testing.reset();
const arena = testing.arena_allocator;
// Plain name (fast path).
{
var parser = Parser{ .input = "ng-app]" };
try testing.expectEqual("ng-app", try parser.attributeName(arena));
try testing.expectEqual("]", parser.input);
}
// Escaped colon in the name: [ng\:jq] -> attribute literally named "ng:jq".
// AngularJS probes for these during bootstrap; rejecting them aborts the
// whole framework. (see workingnomads.com regression)
{
var parser = Parser{ .input = "ng\\:jq]" };
try testing.expectEqual("ng:jq", try parser.attributeName(arena));
try testing.expectEqual("]", parser.input);
}
// Escape as the very first character.
{
var parser = Parser{ .input = "\\:foo]" };
try testing.expectEqual(":foo", try parser.attributeName(arena));
try testing.expectEqual("]", parser.input);
}
// Hex escape: [\41 bc] -> "Abc" (space terminates the hex escape).
{
var parser = Parser{ .input = "\\41 bc]" };
try testing.expectEqual("Abc", try parser.attributeName(arena));
try testing.expectEqual("]", parser.input);
}
// Name followed by a matcher stops at the matcher (fast path, no escape).
{
var parser = Parser{ .input = "data-ng-csp=foo]" };
try testing.expectEqual("data-ng-csp", try parser.attributeName(arena));
try testing.expectEqual("=foo]", parser.input);
}
// Escaped name followed by a matcher.
{
var parser = Parser{ .input = "ng\\:csp~=foo]" };
try testing.expectEqual("ng:csp", try parser.attributeName(arena));
try testing.expectEqual("~=foo]", parser.input);
}
// Invalid first character.
{
var parser = Parser{ .input = "=foo]" };
try testing.expectError(error.InvalidAttributeSelector, parser.attributeName(arena));
}
}

View File

@@ -618,18 +618,9 @@ pub const BrowserContext = struct {
// abort all intercepted requests before closing the session/page
// since some of these might callback into the page/scriptmanager.
// intercept_state stores ids — look each one up; if it's already
// gone (out-of-band destroy), there's nothing to abort, but the
// intercepted counter still needs decrementing because we
// incremented it on pause.
// intercept_state stores ids.
const http_client = &browser.http_client;
for (self.intercept_state.pendingIntercepts()) |transfer_id| {
lp.assert(
http_client.interception_layer.intercepted > 0,
"BrowserContext.deinit.intercepted",
.{ .value = http_client.interception_layer.intercepted },
);
http_client.interception_layer.intercepted -= 1;
if (http_client.findTransfer(transfer_id)) |transfer| {
transfer.abort(error.ClientDisconnect);
}

View File

@@ -198,6 +198,7 @@ pub fn requestIntercept(bc: *CDP.BrowserContext, intercept: *const Notification.
const transfer = intercept.transfer;
try bc.intercept_state.put(transfer.id);
errdefer _ = bc.intercept_state.remove(transfer.id);
try bc.cdp.sendEvent("Fetch.requestPaused", .{
.requestId = &id.toInterceptId(transfer.id),
@@ -332,23 +333,21 @@ fn continueWithAuth(cmd: *CDP.Command) !void {
return cmd.sendResult(null, .{});
}
// TODO: double-decrement of interception_layer.intercepted if
// continueTransfer fails: continueTransfer decrements unconditionally,
// and the errdefer below decrements again via abortAuthChallenge.
// Worse: if continueTransfer's failure path destroys the transfer
// (start_callback fail in makeRequest), this errdefer hits a freed
// transfer. Pre-existing; needs makeRequest failure-semantics cleanup.
errdefer transfer.abortAuthChallenge();
transfer.updateCredentials(try std.fmt.allocPrintSentinel(
transfer.arena,
"{s}:{s}",
.{
params.authChallengeResponse.username,
params.authChallengeResponse.password,
},
0,
));
{
// The transfer is still parked here; if building the credentials
// fails, release it. Scoped so the errdefer does NOT cover
// continueTransfer (which owns its failures).
errdefer transfer.abortAuthChallenge();
transfer.updateCredentials(try std.fmt.allocPrintSentinel(
transfer.arena,
"{s}:{s}",
.{
params.authChallengeResponse.username,
params.authChallengeResponse.password,
},
0,
));
}
try client.continueTransfer(transfer);
return cmd.sendResult(null, .{});
@@ -440,6 +439,7 @@ pub fn requestAuthRequired(bc: *CDP.BrowserContext, intercept: *const Notificati
const transfer = intercept.transfer;
try bc.intercept_state.put(transfer.id);
errdefer _ = bc.intercept_state.remove(transfer.id);
const request = &transfer.req;
const challenge = transfer._auth_challenge orelse return error.NullAuthChallenge;

View File

@@ -78,7 +78,8 @@
\\ Wait until the specified event. Checked before other --wait-* options.
\\ Defaults to 'done'. If --wait-selector, --wait-script or
\\ --wait-script-file specified, defaults to none.
\\ Allowed values: "load", "domcontentloaded", "networkidle", "done".
\\ Allowed values: "load", "domcontentloaded", "networkalmostidle",
\\ "networkidle", "done".
\\ --wait-selector <QUERY>
\\ Wait for an element matching the CSS selector to appear. Checked after
\\ --wait-until condition is met.

View File

@@ -194,7 +194,6 @@ pub fn continueRequest(self: *InterceptionLayer, transfer: *Transfer) anyerror!v
lp.assert(self.intercepted > 0, "InterceptionLayer.continueRequest", .{ .value = self.intercepted });
log.debug(.http, "continue transfer", .{ .intercepted = self.intercepted });
}
self.intercepted -= 1;
// Resume the layer chain. Ownership is re-handed to whichever subsequent
// layer commits the transfer (queue, multi, or another park). If the
@@ -214,7 +213,6 @@ pub fn abortRequest(self: *InterceptionLayer, transfer: *Transfer) void {
lp.assert(self.intercepted > 0, "InterceptionLayer.abortRequest", .{ .value = self.intercepted });
log.debug(.http, "abort transfer", .{ .intercepted = self.intercepted });
}
self.intercepted -= 1;
transfer.abort(error.Abort);
}
@@ -229,7 +227,6 @@ pub fn fulfillRequest(
lp.assert(self.intercepted > 0, "InterceptionLayer.fulfillRequest", .{ .value = self.intercepted });
log.debug(.http, "fulfill transfer", .{ .intercepted = self.intercepted });
}
self.intercepted -= 1;
// `done` flips true once we've called the user's done_callback. If
// done_callback itself throws, the user already saw their end-of-flow