diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index ef215554..eb236811 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -949,6 +949,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/net/URLSearchParams.zig"),
@import("../webapi/net/XMLHttpRequest.zig"),
@import("../webapi/net/XMLHttpRequestEventTarget.zig"),
+ @import("../webapi/net/XMLHttpRequestUpload.zig"),
@import("../webapi/net/WebSocket.zig"),
@import("../webapi/event/CloseEvent.zig"),
@import("../webapi/streams/ReadableStream.zig"),
@@ -1036,6 +1037,7 @@ pub const WorkerJsApis = flattenTypes(&.{
@import("../webapi/canvas/OffscreenCanvasRenderingContext2D.zig"),
@import("../webapi/net/XMLHttpRequest.zig"),
@import("../webapi/net/XMLHttpRequestEventTarget.zig"),
+ @import("../webapi/net/XMLHttpRequestUpload.zig"),
@import("../webapi/net/WebSocket.zig"),
@import("../webapi/FileReader.zig"),
@import("../webapi/ImageData.zig"),
diff --git a/src/browser/tests/net/xhr.html b/src/browser/tests/net/xhr.html
index b76dde85..d35c8f0b 100644
--- a/src/browser/tests/net/xhr.html
+++ b/src/browser/tests/net/xhr.html
@@ -329,3 +329,22 @@
});
}
+
+
diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig
index a80416ea..bed5970b 100644
--- a/src/browser/webapi/net/XMLHttpRequest.zig
+++ b/src/browser/webapi/net/XMLHttpRequest.zig
@@ -34,6 +34,7 @@ const EventTarget = @import("../EventTarget.zig");
const Headers = @import("Headers.zig");
const BodyInit = @import("body_init.zig").BodyInit;
const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig");
+const XMLHttpRequestUpload = @import("XMLHttpRequestUpload.zig");
const log = lp.log;
const Execution = js.Execution;
@@ -44,6 +45,7 @@ const XMLHttpRequest = @This();
_rc: lp.RC(u8) = .{},
_exec: *const Execution,
_proto: *XMLHttpRequestEventTarget,
+_upload: ?*XMLHttpRequestUpload = null,
_arena: Allocator,
_http_response: ?HttpClient.Response = null,
_active_request: bool = false,
@@ -112,29 +114,9 @@ pub fn deinit(self: *XMLHttpRequest, page: *Page) void {
func.release();
}
- {
- const proto = self._proto;
- if (proto._on_abort) |func| {
- func.release();
- }
- if (proto._on_error) |func| {
- func.release();
- }
- if (proto._on_load) |func| {
- func.release();
- }
- if (proto._on_load_end) |func| {
- func.release();
- }
- if (proto._on_load_start) |func| {
- func.release();
- }
- if (proto._on_progress) |func| {
- func.release();
- }
- if (proto._on_timeout) |func| {
- func.release();
- }
+ self._proto.releaseListeners();
+ if (self._upload) |upload| {
+ upload._proto.releaseListeners();
}
page.releaseArena(self._arena);
@@ -289,6 +271,18 @@ pub fn send(self: *XMLHttpRequest, body_: ?BodyInit, exec_: *const Execution) !v
};
}
+// https://xhr.spec.whatwg.org/#the-upload-attribute
+// The XMLHttpRequestUpload object is created lazily and cached: scripts expect
+// the same instance on every access so their event listeners stick.
+pub fn getUpload(self: *XMLHttpRequest) !*XMLHttpRequestUpload {
+ if (self._upload) |upload| {
+ return upload;
+ }
+ const upload = try self._exec._factory.xhrEventTarget(self._arena, XMLHttpRequestUpload{ ._proto = undefined });
+ self._upload = upload;
+ return upload;
+}
+
pub fn getReadyState(self: *const XMLHttpRequest) u32 {
return @intFromEnum(self._ready_state);
}
@@ -611,6 +605,7 @@ pub const JsApi = struct {
pub const DONE = bridge.property(@intFromEnum(XMLHttpRequest.ReadyState.done), .{ .template = true });
pub const onreadystatechange = bridge.accessor(XMLHttpRequest.getOnReadyStateChange, XMLHttpRequest.setOnReadyStateChange, .{});
+ pub const upload = bridge.accessor(XMLHttpRequest.getUpload, null, .{});
pub const timeout = bridge.accessor(XMLHttpRequest.getTimeout, XMLHttpRequest.setTimeout, .{});
pub const withCredentials = bridge.accessor(XMLHttpRequest.getWithCredentials, XMLHttpRequest.setWithCredentials, .{ .dom_exception = true });
pub const open = bridge.function(XMLHttpRequest.open, .{});
diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig
index af5fd38f..a5b64dc0 100644
--- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig
+++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig
@@ -37,13 +37,24 @@ _on_timeout: ?js.Function.Temp = null,
pub const Type = union(enum) {
request: *@import("XMLHttpRequest.zig"),
- // TODO: xml_http_request_upload
+ upload: *@import("XMLHttpRequestUpload.zig"),
};
pub fn asEventTarget(self: *XMLHttpRequestEventTarget) *EventTarget {
return self._proto;
}
+pub fn releaseListeners(self: *XMLHttpRequestEventTarget) void {
+ inline for (.{
+ "_on_abort", "_on_error", "_on_load", "_on_load_end",
+ "_on_load_start", "_on_progress", "_on_timeout",
+ }) |field| {
+ if (@field(self, field)) |func| {
+ func.release();
+ }
+ }
+}
+
pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, exec: *const Execution) !void {
const field, const typ = comptime blk: {
break :blk switch (event_type) {
diff --git a/src/browser/webapi/net/XMLHttpRequestUpload.zig b/src/browser/webapi/net/XMLHttpRequestUpload.zig
new file mode 100644
index 00000000..68b04597
--- /dev/null
+++ b/src/browser/webapi/net/XMLHttpRequestUpload.zig
@@ -0,0 +1,46 @@
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as
+// published by the Free Software Foundation, either version 3 of the
+// License, or (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+const js = @import("../../js/js.zig");
+
+const EventTarget = @import("../EventTarget.zig");
+const XMLHttpRequestEventTarget = @import("XMLHttpRequestEventTarget.zig");
+
+// https://xhr.spec.whatwg.org/#xmlhttprequestupload
+//
+// Returned by XMLHttpRequest.upload. It only inherits from
+// XMLHttpRequestEventTarget; it has no members of its own. We don't yet emit
+// upload progress events, but the object still needs to exist so scripts (e.g.
+// htmx) can call addEventListener on it without throwing.
+const XMLHttpRequestUpload = @This();
+
+_proto: *XMLHttpRequestEventTarget,
+
+pub fn asEventTarget(self: *XMLHttpRequestUpload) *EventTarget {
+ return self._proto.asEventTarget();
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(XMLHttpRequestUpload);
+
+ pub const Meta = struct {
+ pub const name = "XMLHttpRequestUpload";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+};