From dab5ff77792af566da7c50b3535c9436f4b44270 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 23 Jul 2026 13:37:42 +0800 Subject: [PATCH 1/2] webapi: XHR uses XML parser for XML content-type responses Expand mime type to be aware of more XML types. DOMParser and XHR now use the existing Parser.parseXML (via a Frame helper), rather than the HTML parser. Both these APIs predate the addition of parseXML. --- src/browser/Frame.zig | 36 ++++++++++++ src/browser/Mime.zig | 37 +++++++++++++ src/browser/tests/net/xhr.html | 67 +++++++++++++++++++++-- src/browser/webapi/DOMParser.zig | 60 ++++++-------------- src/browser/webapi/net/XMLHttpRequest.zig | 24 ++++---- src/testing.zig | 18 ++++++ 6 files changed, 183 insertions(+), 59 deletions(-) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 7e13f3db37..764e075e0f 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -2938,6 +2938,42 @@ fn parseHtmlAsChildrenInner(self: *Frame, node: *Node, html: []const u8, opts: F } } +// Build a detached XMLDocument from `xml` (DOMParser.parseFromString and +// XMLHttpRequest.responseXML). Returns null when the input isn't well-formed +// XML. The parse borrows the fragment parse-mode so frame-side hooks triggered +// from `Build.created` / `nodeIsReady` (external stylesheet fetches, script +// execution, mutation-observer fan-out, default-script injection) treat the +// parsed nodes as detached and skip side effects on the live document. +pub fn parseXmlDocument(self: *Frame, xml: []const u8) !?*Document.XMLDocument { + const arena = try self.getArena(.medium, "Frame.parseXmlDocument"); + defer self.releaseArena(arena); + + const previous_parse_mode = self._parse_mode; + self._parse_mode = .fragment; + defer self._parse_mode = previous_parse_mode; + + const doc = try self._factory.document(Document.XMLDocument{ ._proto = undefined }); + const doc_node = doc.asNode(); + var parser = Parser.init(arena, doc_node, self, .{}); + parser.parseXML(xml); + if (parser.terminated) { + return error.ExecutionTerminated; + } + + if (parser.err != null or doc_node.firstChild() == null) { + return null; + } + + // If first node is a `ProcessingInstruction` (e.g. the + // declaration), skip it. + const first_child = doc_node.firstChild().?; + if (first_child.getNodeType() == 7) { + _ = try doc_node.removeChild(first_child, self); + } + + return doc; +} + // Runs the "ready" work for an inserted node and, when it's an element with // children, for its descendants in tree order: appending a subtree // containing scripts must execute them all, after the whole insertion. diff --git a/src/browser/Mime.zig b/src/browser/Mime.zig index 8145f49798..72268c7fb6 100644 --- a/src/browser/Mime.zig +++ b/src/browser/Mime.zig @@ -49,6 +49,7 @@ pub const ContentTypeEnum = enum { application_json, unknown, other, + other_xml, }; pub const ContentType = union(ContentTypeEnum) { @@ -68,6 +69,7 @@ pub const ContentType = union(ContentTypeEnum) { // A valid but unrecognized type/subtype. Keeping it would require some // memory management of the input. Nothing needs it right now, so why bother. other: void, + other_xml: void, // i.e. application/xml or */*+xml }; pub fn contentTypeString(mime: *const Mime) []const u8 { @@ -346,6 +348,13 @@ pub fn isHTML(self: *const Mime) bool { return self.content_type == .text_html; } +pub fn isXML(self: *const Mime) bool { + return switch (self.content_type) { + .text_xml, .other_xml => true, + else => false, + }; +} + pub fn isText(mime: *const Mime) bool { return switch (mime.content_type) { .text_xml, .text_html, .text_javascript, .text_plain, .text_css, .text_markdown => true, @@ -378,9 +387,11 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } { @"image/webp", @"application/json", + @"application/xml", }, type_name)) |known_type| { const ct: ContentType = switch (known_type) { .@"text/xml" => .{ .text_xml = {} }, + .@"application/xml" => .{ .other_xml = {} }, .@"text/html" => .{ .text_html = {} }, .@"text/javascript", .@"application/javascript", .@"application/x-javascript" => .{ .text_javascript = {} }, .@"text/plain" => .{ .text_plain = {} }, @@ -408,6 +419,10 @@ fn parseContentType(value: []const u8) !struct { ContentType, usize } { return error.Invalid; } + if (std.mem.endsWith(u8, type_name, "+xml")) { + return .{ .{ .other_xml = {} }, attribute_start }; + } + return .{ .{ .other = {} }, attribute_start }; } @@ -897,6 +912,28 @@ test "Mime: isHTML" { try assert(false, "over/9000"); } +test "Mime: isXML" { + defer testing.reset(); + + const assert = struct { + fn assert(expected: bool, input: []const u8) !void { + const mutable_input = try testing.arena_allocator.dupe(u8, input); + var mime = try Mime.parse(mutable_input); + try testing.expectEqual(expected, mime.isXML()); + } + }.assert; + try assert(true, "text/xml"); + try assert(true, "TEXT/XML; charset=utf-8"); + try assert(true, "application/xml"); + try assert(true, "image/svg+xml"); + try assert(true, "application/xhtml+xml"); + try assert(true, "application/rss+xml;charset=utf-8"); + try assert(false, "text/html"); + try assert(false, "application/json"); + try assert(false, "application/xmlfoo"); + try assert(false, "text/xm"); +} + test "Mime: sniff" { try testing.expectEqual(null, Mime.sniff("")); try testing.expectEqual(null, Mime.sniff(" { - // End-to-end: server returns text/html, but overrideMimeType("text/xml") - // makes responseXML lazily parse the body as a Document, while - // responseText is unaffected (responseType is still the default ""). + // End-to-end: server returns text/plain, but overrideMimeType("text/xml") + // makes responseXML lazily parse the body as XML, while responseText is + // unaffected (responseType is still the default ""). const state = await testing.async(); const req = new XMLHttpRequest(); req.overrideMimeType('text/xml'); - req.open('GET', 'http://127.0.0.1:9582/xhr'); + req.open('GET', 'http://127.0.0.1:9582/xhr/xml_as_text'); req.onload = () => state.resolve(); req.send(); await state.done(() => { testing.expectEqual(200, req.status); - testing.expectEqual(100, req.responseText.length); - testing.expectEqual(true, req.responseXML instanceof Document); + testing.expectEqual(true, req.responseText.startsWith(' + + + + + + -