mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-06-11 01:25:53 -04:00
Implement XMLHttpRequest.overrideMimeType()
Adds the overrideMimeType(mime) method.
Wires the final MIME type (override ?? response) into responseXML for
the default response type (""): when the final MIME is text/xml, the
body is lazily parsed into a Document and cached in a new
_response_xml field. Other XML MIME types (application/xml, image/svg+xml)
land in Mime.ContentType.other whose backing slices are unsafe to read
after Mime.parse returns; supporting them is left for a follow-up.
The override does not affect response / responseText today —
responseText charset decoding and the document responseType's
HTML-vs-XML parser choice are noted as follow-ups in the code.
This commit is contained in:
@@ -306,6 +306,98 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=xhr_override_mime type=module>
|
||||
{
|
||||
// overrideMimeType is callable in state UNSENT (no open() yet)
|
||||
const req = new XMLHttpRequest();
|
||||
testing.expectEqual(0, req.readyState);
|
||||
req.overrideMimeType('text/xml');
|
||||
}
|
||||
|
||||
{
|
||||
// overrideMimeType is callable in state OPENED
|
||||
const req = new XMLHttpRequest();
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
testing.expectEqual(1, req.readyState);
|
||||
req.overrideMimeType('text/xml; charset=utf-8');
|
||||
}
|
||||
|
||||
{
|
||||
// Invalid mime values must NOT throw; they fall back to
|
||||
// application/octet-stream per spec.
|
||||
const req = new XMLHttpRequest();
|
||||
req.overrideMimeType('!!!');
|
||||
req.overrideMimeType('');
|
||||
req.overrideMimeType('not a mime');
|
||||
}
|
||||
|
||||
{
|
||||
// After send() reaches DONE, overrideMimeType throws InvalidStateError.
|
||||
const state = await testing.async();
|
||||
const req = new XMLHttpRequest();
|
||||
req.onload = () => state.resolve();
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
req.send();
|
||||
await state.done(() => {
|
||||
testing.expectEqual(4, req.readyState);
|
||||
testing.withError((err) => {
|
||||
testing.expectEqual('InvalidStateError', err.name);
|
||||
}, () => {
|
||||
req.overrideMimeType('text/xml');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// Override survives open() (spec: open() resets state but keeps the
|
||||
// override MIME type). Re-opening then sending must not throw on the
|
||||
// prior override.
|
||||
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');
|
||||
req.onload = () => state.resolve();
|
||||
req.send();
|
||||
await state.done(() => {
|
||||
testing.expectEqual(200, req.status);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// 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 "").
|
||||
const state = await testing.async();
|
||||
const req = new XMLHttpRequest();
|
||||
req.overrideMimeType('text/xml');
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
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);
|
||||
// Cached across calls.
|
||||
testing.expectEqual(req.responseXML, req.responseXML);
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// Without overrideMimeType, a text/html response still yields
|
||||
// responseXML === null when responseType is the default.
|
||||
const state = await testing.async();
|
||||
const req = new XMLHttpRequest();
|
||||
req.open('GET', 'http://127.0.0.1:9582/xhr');
|
||||
req.onload = () => state.resolve();
|
||||
req.send();
|
||||
await state.done(() => {
|
||||
testing.expectEqual(200, req.status);
|
||||
testing.expectEqual(null, req.responseXML);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id=xhr_timeout type=module>
|
||||
{
|
||||
// timeout property: default is 0
|
||||
|
||||
@@ -59,6 +59,7 @@ _response_status: u16 = 0,
|
||||
_response_len: ?usize = 0,
|
||||
_response_url: [:0]const u8 = "",
|
||||
_response_mime: ?Mime = null,
|
||||
_override_mime: ?Mime = null,
|
||||
_response_headers: std.ArrayList([]const u8) = .empty,
|
||||
_response_type: ResponseType = .text,
|
||||
|
||||
@@ -200,7 +201,8 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
|
||||
self._http_response = null;
|
||||
}
|
||||
|
||||
// Reset internal state
|
||||
// Reset internal state. _override_mime intentionally survives open()
|
||||
// per https://xhr.spec.whatwg.org/#the-overridemimetype()-method.
|
||||
self._response = null;
|
||||
self._response_data.clearRetainingCapacity();
|
||||
self._response_status = 0;
|
||||
@@ -223,6 +225,15 @@ pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const
|
||||
return self._request_headers.append(name, value, exec);
|
||||
}
|
||||
|
||||
// https://xhr.spec.whatwg.org/#the-overridemimetype()-method
|
||||
pub fn overrideMimeType(self: *XMLHttpRequest, mime: []const u8) !void {
|
||||
if (self._ready_state == .loading or self._ready_state == .done) {
|
||||
return error.InvalidStateError;
|
||||
}
|
||||
self._override_mime = Mime.parse(mime) catch
|
||||
Mime.parse("application/octet-stream") catch unreachable;
|
||||
}
|
||||
|
||||
pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.http, "XMLHttpRequest.send", .{ .url = self._url });
|
||||
@@ -338,6 +349,10 @@ pub fn setResponseType(self: *XMLHttpRequest, value: []const u8) void {
|
||||
}
|
||||
|
||||
pub fn getResponseText(self: *const XMLHttpRequest) []const u8 {
|
||||
// TODO: per WHATWG XHR "get a text response", the bytes must be decoded
|
||||
// using the final encoding derived from the final MIME type
|
||||
// (_override_mime ?? _response_mime). Currently the raw bytes are
|
||||
// returned and V8 treats them as UTF-8.
|
||||
return self._response_data.items;
|
||||
}
|
||||
|
||||
@@ -373,6 +388,12 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response {
|
||||
.document => blk: {
|
||||
// responseType=document is only meaningful in a Frame; workers
|
||||
// have no DOM. Drastically different impls -> switch on global.
|
||||
//
|
||||
// TODO: per WHATWG XHR "set a document response", the final MIME
|
||||
// type (_override_mime ?? _response_mime) should select an XML
|
||||
// parser when it is an XML MIME type, and the HTML parser
|
||||
// otherwise. We only have an HTML parser today, so the body is
|
||||
// always parsed as HTML regardless of the override.
|
||||
switch (exec.js.global) {
|
||||
.frame => |frame| {
|
||||
const document = try exec._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });
|
||||
@@ -390,11 +411,36 @@ pub fn getResponse(self: *XMLHttpRequest, exec: *const Execution) !?Response {
|
||||
}
|
||||
|
||||
pub fn getResponseXML(self: *XMLHttpRequest, exec: *const Execution) !?*Node.Document {
|
||||
const res = (try self.getResponse(exec)) orelse return null;
|
||||
return switch (res) {
|
||||
.document => |doc| doc,
|
||||
else => null,
|
||||
};
|
||||
if (self._ready_state != .done) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// responseType="document": getResponse already parses + caches it.
|
||||
if (self._response_type == .document) {
|
||||
const res = (try self.getResponse(exec)) orelse return null;
|
||||
return switch (res) {
|
||||
.document => |doc| doc,
|
||||
else => null,
|
||||
};
|
||||
}
|
||||
|
||||
// responseType="" (we map "" to .text — see setResponseType): lazily
|
||||
// produce a Document when the final MIME type is XML, per WHATWG XHR
|
||||
// "set a document response". For an HTML final MIME the spec returns
|
||||
// null in this branch, so we only act on text/xml.
|
||||
if (self._response_type != .text) return null;
|
||||
|
||||
const final = self._override_mime orelse self._response_mime orelse return null;
|
||||
if (final.content_type != .text_xml) return null;
|
||||
|
||||
switch (exec.js.global) {
|
||||
.frame => |frame| {
|
||||
const document = try exec._factory.node(Node.Document{ ._proto = undefined, ._type = .generic });
|
||||
try frame.parseHtmlAsChildren(document.asNode(), self._response_data.items);
|
||||
return document;
|
||||
},
|
||||
.worker => return error.NotSupportedInWorker,
|
||||
}
|
||||
}
|
||||
|
||||
fn httpStartCallback(response: HttpClient.Response) !void {
|
||||
@@ -624,6 +670,7 @@ pub const JsApi = struct {
|
||||
pub const responseXML = bridge.accessor(XMLHttpRequest.getResponseXML, null, .{});
|
||||
pub const responseURL = bridge.accessor(XMLHttpRequest.getResponseURL, null, .{});
|
||||
pub const setRequestHeader = bridge.function(XMLHttpRequest.setRequestHeader, .{ .dom_exception = true });
|
||||
pub const overrideMimeType = bridge.function(XMLHttpRequest.overrideMimeType, .{ .dom_exception = true });
|
||||
pub const getResponseHeader = bridge.function(XMLHttpRequest.getResponseHeader, .{});
|
||||
pub const getAllResponseHeaders = bridge.function(XMLHttpRequest.getAllResponseHeaders, .{});
|
||||
pub const abort = bridge.function(XMLHttpRequest.abort, .{});
|
||||
|
||||
Reference in New Issue
Block a user