replace zig-async-io and std.http.Client with a custom HTTP client

This commit is contained in:
Karl Seguin
2025-03-14 19:40:56 +08:00
parent fd35724aa8
commit 2017d4785b
17 changed files with 1566 additions and 2145 deletions

View File

@@ -31,7 +31,7 @@ const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEven
const Mime = @import("../browser/mime.zig").Mime;
const Loop = jsruntime.Loop;
const Client = @import("asyncio").Client;
const http = @import("../http/client.zig");
const parser = @import("netsurf");
@@ -95,14 +95,12 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) {
pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
alloc: std.mem.Allocator,
cli: *Client,
io: Client.IO,
client: *http.Client,
request: ?http.Request = null,
priv_state: PrivState = .new,
req: ?Client.Request = null,
ctx: ?Client.Ctx = null,
method: std.http.Method,
method: http.Request.Method,
state: State,
url: ?[]const u8,
uri: std.Uri,
@@ -125,7 +123,7 @@ pub const XMLHttpRequest = struct {
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: ?[]const u8 = null,
response_bytes: std.ArrayListUnmanaged(u8) = .{},
response_type: ResponseType = .Empty,
response_headers: Headers,
@@ -133,7 +131,7 @@ pub const XMLHttpRequest = struct {
// use 16KB for headers buffer size.
response_header_buffer: [1024 * 16]u8 = undefined,
response_status: u10 = 0,
response_status: u16 = 0,
// TODO uncomment this field causes casting issue with
// XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but
@@ -246,13 +244,6 @@ pub const XMLHttpRequest = struct {
fn all(self: Headers) []std.http.Header {
return self.list.items;
}
fn load(self: *Headers, it: *std.http.HeaderIterator) !void {
while (true) {
const h = it.next() orelse break;
_ = try self.append(h.name, h.value);
}
}
};
const Response = union(ResponseType) {
@@ -290,17 +281,16 @@ pub const XMLHttpRequest = struct {
const min_delay: u64 = 50000000; // 50ms
pub fn constructor(alloc: std.mem.Allocator, loop: *Loop, userctx: UserContext) !XMLHttpRequest {
pub fn constructor(alloc: std.mem.Allocator, userctx: UserContext) !XMLHttpRequest {
return .{
.alloc = alloc,
.headers = Headers.init(alloc),
.response_headers = Headers.init(alloc),
.io = Client.IO.init(loop),
.method = undefined,
.url = null,
.uri = undefined,
.state = .unsent,
.cli = userctx.httpClient,
.client = userctx.http_client,
};
}
@@ -311,7 +301,6 @@ pub const XMLHttpRequest = struct {
if (self.payload) |v| alloc.free(v);
self.payload = null;
if (self.response_bytes) |v| alloc.free(v);
if (self.response_obj) |v| v.deinit();
self.response_obj = null;
@@ -329,12 +318,6 @@ pub const XMLHttpRequest = struct {
self.send_flag = false;
self.priv_state = .new;
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void {
@@ -449,7 +432,7 @@ pub const XMLHttpRequest = struct {
}
const methods = [_]struct {
tag: std.http.Method,
tag: http.Request.Method,
name: []const u8,
}{
.{ .tag = .DELETE, .name = "DELETE" },
@@ -461,7 +444,7 @@ pub const XMLHttpRequest = struct {
};
const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" };
pub fn validMethod(m: []const u8) DOMError!std.http.Method {
pub fn validMethod(m: []const u8) DOMError!http.Request.Method {
for (methods) |method| {
if (std.ascii.eqlIgnoreCase(method.name, m)) {
return method.tag;
@@ -485,7 +468,7 @@ pub const XMLHttpRequest = struct {
}
// TODO body can be either a XMLHttpRequestBodyInit or a document
pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void {
pub fn _send(self: *XMLHttpRequest, loop: *Loop, alloc: std.mem.Allocator, body: ?[]const u8) !void {
if (self.state != .opened) return DOMError.InvalidState;
if (self.send_flag) return DOMError.InvalidState;
@@ -515,153 +498,77 @@ pub const XMLHttpRequest = struct {
self.priv_state = .open;
self.req = try self.cli.create(self.method, self.uri, .{
.server_header_buffer = &self.response_header_buffer,
.extra_headers = self.headers.all(),
});
errdefer {
self.req.?.deinit();
self.req = null;
}
self.request = try self.client.request(self.method, self.uri);
self.ctx = try Client.Ctx.init(&self.io, &self.req.?);
errdefer {
self.ctx.?.deinit();
self.ctx = null;
}
self.ctx.?.userData = self;
var request = &self.request.?;
errdefer request.deinit();
try self.cli.async_open(
self.method,
self.uri,
.{ .server_header_buffer = &self.response_header_buffer },
&self.ctx.?,
onRequestConnect,
);
for (self.headers.list.items) |hdr| {
try request.addHeader(hdr.name, hdr.value, .{});
}
request.body = self.payload;
try request.sendAsync(loop, self, .{});
}
fn onRequestWait(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
pub fn onHttpResponse(self: *XMLHttpRequest, progress_: http.Error!http.Progress) !void {
const progress = progress_ catch |err| {
self.onErr(err);
return err;
};
log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status });
if (progress.first) {
const header = progress.header;
log.info("{any} {any} {d}", .{ self.method, self.uri, header.status });
self.priv_state = .done;
var it = self.req.?.response.iterateHeaders();
self.response_headers.load(&it) catch |e| return self.onErr(e);
self.priv_state = .done;
// extract a mime type from headers.
const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
for (header.headers.items) |hdr| {
try self.response_headers.append(hdr.name, hdr.value);
}
// TODO handle override mime type
// extract a mime type from headers.
const ct = header.get("Content-Type") orelse "text/xml";
self.response_mime = Mime.parse(self.alloc, ct) catch |e| return self.onErr(e);
self.state = .headers_received;
self.dispatchEvt("readystatechange");
self.response_status = @intFromEnum(self.req.?.response.status);
var buf: std.ArrayListUnmanaged(u8) = .{};
// TODO set correct length
const total = 0;
var loaded: u64 = 0;
// dispatch a progress event loadstart.
self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total });
// TODO read async
const reader = self.req.?.reader();
var buffer: [1024]u8 = undefined;
var ln = buffer.len;
var prev_dispatch: ?std.time.Instant = null;
while (ln > 0) {
ln = reader.read(&buffer) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
loaded = loaded + ln;
// Dispatch only if 50ms have passed.
const now = std.time.Instant.now() catch |e| {
buf.deinit(self.alloc);
return self.onErr(e);
};
if (prev_dispatch != null and now.since(prev_dispatch.?) < min_delay) continue;
defer prev_dispatch = now;
self.state = .loading;
// TODO handle override mime type
self.state = .headers_received;
self.dispatchEvt("readystatechange");
// dispatch a progress event progress.
self.dispatchProgressEvent("progress", .{
.loaded = loaded,
.total = total,
});
}
self.response_bytes = buf.items;
self.send_flag = false;
self.response_status = header.status;
// TODO correct total
self.dispatchProgressEvent("loadstart", .{ .loaded = 0, .total = 0 });
self.state = .loading;
}
const data = progress.data orelse return;
const buf = &self.response_bytes;
try buf.appendSlice(self.alloc, data);
const total_len = buf.items.len;
// TODO: don't dispatch this more than once every 50ms
// dispatch a progress event progress.
self.dispatchEvt("readystatechange");
self.dispatchProgressEvent("progress", .{
.total = buf.items.len,
.loaded = buf.items.len,
});
if (progress.done == false) {
return;
}
self.send_flag = false;
self.state = .done;
self.dispatchEvt("readystatechange");
// dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total });
self.dispatchProgressEvent("load", .{ .loaded = total_len, .total = total_len });
// dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
fn onRequestFinish(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .wait;
return ctx.req.async_wait(ctx, onRequestWait) catch |e| return self.onErr(e);
}
fn onRequestSend(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
if (self.payload) |payload| {
self.priv_state = .write;
return ctx.req.async_writeAll(payload, ctx, onRequestWrite) catch |e| return self.onErr(e);
}
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestWrite(ctx: *Client.Ctx, res: anyerror!void) !void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
self.priv_state = .finish;
return ctx.req.async_finish(ctx, onRequestFinish) catch |e| return self.onErr(e);
}
fn onRequestConnect(ctx: *Client.Ctx, res: anyerror!void) anyerror!void {
var self = selfCtx(ctx);
res catch |err| return self.onErr(err);
// prepare payload transfert.
if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len };
self.priv_state = .send;
return ctx.req.async_send(ctx, onRequestSend) catch |err| return self.onErr(err);
}
fn selfCtx(ctx: *Client.Ctx) *XMLHttpRequest {
return @ptrCast(@alignCast(ctx.userData));
self.dispatchProgressEvent("loadend", .{ .loaded = total_len, .total = total_len });
}
fn onErr(self: *XMLHttpRequest, err: anyerror) void {
@@ -675,12 +582,6 @@ pub const XMLHttpRequest = struct {
self.dispatchProgressEvent("loadend", .{});
log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err });
if (self.ctx) |*c| c.deinit();
self.ctx = null;
if (self.req) |*r| r.deinit();
self.req = null;
}
pub fn _abort(self: *XMLHttpRequest) void {
@@ -803,7 +704,7 @@ pub const XMLHttpRequest = struct {
}
if (self.response_type == .JSON) {
if (self.response_bytes == null) return null;
if (self.response_bytes.items.len == 0) return null;
// TODO Let jsonObject be the result of running parse JSON from bytes
// on thiss received bytes. If that threw an exception, then return
@@ -841,7 +742,7 @@ pub const XMLHttpRequest = struct {
};
defer alloc.free(ccharset);
var fbs = std.io.fixedBufferStream(self.response_bytes.?);
var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch {
self.response_obj = .{ .Failure = true };
return;
@@ -862,7 +763,7 @@ pub const XMLHttpRequest = struct {
const p = std.json.parseFromSlice(
JSONValue,
alloc,
self.response_bytes.?,
self.response_bytes.items,
.{},
) catch |e| {
log.err("parse JSON: {}", .{e});
@@ -875,8 +776,7 @@ pub const XMLHttpRequest = struct {
pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 {
if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState;
return if (self.response_bytes) |v| v else "";
return self.response_bytes.items;
}
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {