From 4f6868728def28e02b0c2d5410b672ce33a8ae57 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 18 Feb 2026 10:52:51 +0800 Subject: [PATCH] Detach attached nodes on appendBeforeSibling callback html5ever generally makes guarantees about nodes being parentless when appending, but we've already seen 1 case where appendCallback receives a connected node. We're now seeing something in appendBeforeSiblingCallback, but we have a clearer picture of how this is happening. In this case, it's via custom element upgrading and the custom element constructor has already placed the node in the document. It's worth pointing, html5ever just has an opaque reference to our node. While it guarantees that it will give us parent-less nodes, it doesn't actually know anything about our nodes, or our node._parent. The guarantee is only from its own point of view. There's nothing stopping us from giving a node a default parent as soon as html5ever asks us to create a new node, in which case, the node _will_ have a parent. --- src/browser/parser/Parser.zig | 11 ++++++- .../tests/custom_elements/registry.html | 30 +++++++++++++++++++ src/http/Client.zig | 2 +- 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/browser/parser/Parser.zig b/src/browser/parser/Parser.zig index 2cb2acaa..08adfb47 100644 --- a/src/browser/parser/Parser.zig +++ b/src/browser/parser/Parser.zig @@ -421,7 +421,16 @@ fn appendBeforeSiblingCallback(ctx: *anyopaque, sibling_ref: *anyopaque, node_or fn _appendBeforeSiblingCallback(self: *Parser, sibling: *Node, node_or_text: h5e.NodeOrText) !void { const parent = sibling.parentNode() orelse return error.NoParent; const node: *Node = switch (node_or_text.toUnion()) { - .node => |cpn| getNode(cpn), + .node => |cpn| blk: { + const child = getNode(cpn); + if (child._parent) |previous_parent| { + // A custom element constructor may have inserted the node into the + // DOM before the parser officially places it (e.g. via foster + // parenting). Detach it first so insertNodeRelative's assertion holds. + self.page.removeNode(previous_parent, child, .{ .will_be_reconnected = parent.isConnected() }); + } + break :blk child; + }, .text => |txt| try self.page.createTextNode(txt), }; try self.page.insertNodeRelative(parent, node, .{ .before = sibling }, .{}); diff --git a/src/browser/tests/custom_elements/registry.html b/src/browser/tests/custom_elements/registry.html index 064aa9f9..2d0f5dc0 100644 --- a/src/browser/tests/custom_elements/registry.html +++ b/src/browser/tests/custom_elements/registry.html @@ -119,3 +119,33 @@ } + diff --git a/src/http/Client.zig b/src/http/Client.zig index fde007c6..1a38ef9e 100644 --- a/src/http/Client.zig +++ b/src/http/Client.zig @@ -1417,7 +1417,7 @@ pub const Transfer = struct { header_len = buf_len - 1; } - const header = buffer[0 .. header_len]; + const header = buffer[0..header_len]; // We need to parse the first line headers for each request b/c curl's // CURLINFO_RESPONSE_CODE returns the status code of the final request.