From bf56adeb1615a7a846650ae8808e101e2e50132e Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 11 Jul 2026 23:09:29 +0200 Subject: [PATCH] webapi: documents report the navigation response's content type Fixes 9 failing files under /dom/nodes/Document-contentType/contentType/ (bmp, css, gif, jpg, mimeheader_01, png, txt, xml, and keeps the rest green): a document created from a non-HTML response (the synthesized image/text/raw documents) reported the text/html default from document.contentType instead of the response's MIME type. The frame records the response's MIME essence (type/subtype, lowercased, parameters stripped) when the first chunk arrives, and applies it to the new document's _content_type override once the navigation commits. text/html responses keep the default. The remaining file in the directory (contenttype_javascripturi) needs iframes to navigate to javascript: URLs and document their string result, which is a different mechanism. Coverage: /dom/nodes/Document-contentType/: 14/15 files passing (9 newly passing, 1 subtest each). Co-Authored-By: Claude Fable 5 --- src/browser/Frame.zig | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 87ab3131c..33d391c56 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -236,6 +236,10 @@ _factory: *Factory, _load_state: LoadState = .waiting, +// The navigation response's MIME essence, recorded when the first chunk +// arrives and applied to the new document (document.contentType) once the +// navigation commits. null means the text/html default. +_pending_content_type: ?[]const u8 = null, _parse_state: ParseState = .pre, /// `frameErrorCallback` swallows the failure into a placeholder page; @@ -1427,6 +1431,19 @@ fn frameDataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void { }); } + // Record the response's MIME essence so the resulting document + // reports it (document.contentType); text/html stays the default. + self._pending_content_type = null; + if (transfer.contentType()) |ct| { + const end = std.mem.indexOfScalar(u8, ct, ';') orelse ct.len; + const essence = std.mem.trim(u8, ct[0..end], " \t"); + if (essence.len > 0 and !std.ascii.eqlIgnoreCase(essence, "text/html")) { + const lower = try self.arena.dupe(u8, essence); + _ = std.ascii.lowerString(lower, essence); + self._pending_content_type = lower; + } + } + switch (mime.content_type) { .text_html => { // Normalize and store the charset using encoding_rs canonical names @@ -1501,6 +1518,11 @@ fn frameDoneCallback(ctx: *anyopaque) !void { //We need to handle different navigation types differently. try self._session.navigation.commitNavigation(self); + if (self._pending_content_type) |content_type| { + self.document._content_type = content_type; + self._pending_content_type = null; + } + defer if (comptime IS_DEBUG) { log.debug(.frame, "frame load complete", .{ .url = self.url,