From 0ac605ab6dec2400a4e62a3ff9145d28bcbff75f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 15 Feb 2024 16:31:00 +0100 Subject: [PATCH 01/46] upgrade jsruntime --- vendor/jsruntime-lib | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vendor/jsruntime-lib b/vendor/jsruntime-lib index 8fe165bc..2d7b816f 160000 --- a/vendor/jsruntime-lib +++ b/vendor/jsruntime-lib @@ -1 +1 @@ -Subproject commit 8fe165bc49ddd235701ecb1903f2cff3928e2535 +Subproject commit 2d7b816f48da724036e41a956c996e775f9b226a From df7d17cd303c1ae9dff80e3802f7d3bde9fc7f3c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 16 Jan 2024 17:22:38 +0100 Subject: [PATCH 02/46] xhr: start implementation --- src/xhr/xhr.zig | 58 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/xhr/xhr.zig diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig new file mode 100644 index 00000000..e2cb0430 --- /dev/null +++ b/src/xhr/xhr.zig @@ -0,0 +1,58 @@ +const std = @import("std"); + +const generate = @import("../generate.zig"); +const EventTarget = @import("../dom/event_target.zig").EventTarget; + +// XHR interfaces +// https://xhr.spec.whatwg.org/#interface-xmlhttprequest +pub const Interfaces = generate.Tuple(.{ + XMLHttpRequestEventTarget, + XMLHttpRequestUpload, +}); + +pub const XMLHttpRequestEventTarget = struct { + pub const prototype = *EventTarget; + pub const mem_guarantied = true; +}; + +pub const XMLHttpRequestUpload = struct { + pub const prototype = *XMLHttpRequestEventTarget; + pub const mem_guarantied = true; +}; + +pub const XMLHttpRequest = struct { + pub const prototype = *XMLHttpRequestEventTarget; + pub const mem_guarantied = true; + + pub fn constructor() XMLHttpRequest { + return XMLHttpRequest{}; + } + + pub const UNSENT: u16 = 0; + pub const OPENED: u16 = 1; + pub const HEADERS_RECEIVED: u16 = 2; + pub const LOADING: u16 = 3; + pub const DONE: u16 = 4; + + readyState: u16 = UNSENT, + + pub fn get_readyState(self: *XMLHttpRequest) u16 { + return self.readyState; + } + + pub fn _open( + self: *XMLHttpRequest, + method: []const u8, + url: []const u8, + asyn: ?bool, + username: ?[]const u8, + password: ?[]const u8, + ) !void { + _ = self; + _ = method; + _ = url; + _ = asyn; + _ = username; + _ = password; + } +}; From f714d86bb8a218aed9d0295240160178a2552763 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 17 Jan 2024 09:37:44 +0100 Subject: [PATCH 03/46] xhr: validate method --- src/run_tests.zig | 18 ++++++++++++++++++ src/xhr/xhr.zig | 29 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/run_tests.zig b/src/run_tests.zig index ae5a247e..dea89d6f 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -23,6 +23,7 @@ const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn; +const xhr = @import("xhr/xhr.zig"); pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -146,3 +147,20 @@ test "Window is a libdom event target" { const et = @as(*parser.EventTarget, @ptrCast(&window)); _ = try parser.eventTargetDispatchEvent(et, event); } + +test "XMLHttpRequest.validMethod" { + // valid methods + for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| { + try xhr.XMLHttpRequest.validMethod(tc); + } + + // forbidden + for ([_][]const u8{ "connect", "CONNECT" }) |tc| { + try std.testing.expectError(parser.DOMError.Security, xhr.XMLHttpRequest.validMethod(tc)); + } + + // syntax + for ([_][]const u8{ "foo", "BAR" }) |tc| { + try std.testing.expectError(parser.DOMError.Syntax, xhr.XMLHttpRequest.validMethod(tc)); + } +} diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index e2cb0430..5abcec12 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -3,6 +3,8 @@ const std = @import("std"); const generate = @import("../generate.zig"); const EventTarget = @import("../dom/event_target.zig").EventTarget; +const DOMError = @import("../netsurf.zig").DOMError; + // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest pub const Interfaces = generate.Tuple(.{ @@ -49,10 +51,35 @@ pub const XMLHttpRequest = struct { password: ?[]const u8, ) !void { _ = self; - _ = method; _ = url; _ = asyn; _ = username; _ = password; + + // TODO If this’s relevant global object is a Window object and its + // associated Document is not fully active, then throw an + // "InvalidStateError" DOMException. + + try validMethod(method); + } + + const methods = [_][]const u8{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; + const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" }; + + pub fn validMethod(m: []const u8) DOMError!void { + for (methods) |method| { + if (std.ascii.eqlIgnoreCase(method, m)) { + return; + } + } + // If method is a forbidden method, then throw a "SecurityError" DOMException. + for (methods_forbidden) |method| { + if (std.ascii.eqlIgnoreCase(method, m)) { + return DOMError.Security; + } + } + + // If method is not a method, then throw a "SyntaxError" DOMException. + return DOMError.Syntax; } }; From 9d26a43aa8f3071e4e40b138c4160d12708d7e45 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 29 Jan 2024 11:19:24 +0100 Subject: [PATCH 04/46] async: copy stdlib http client --- src/async/Client.zig | 1654 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1654 insertions(+) create mode 100644 src/async/Client.zig diff --git a/src/async/Client.zig b/src/async/Client.zig new file mode 100644 index 00000000..8438a7bb --- /dev/null +++ b/src/async/Client.zig @@ -0,0 +1,1654 @@ +//! HTTP(S) Client implementation. +//! +//! Connections are opened in a thread-safe manner, but individual Requests are not. +//! +//! TLS support may be disabled via `std.options.http_disable_tls`. + +const std = @import("../std.zig"); +const builtin = @import("builtin"); +const testing = std.testing; +const http = std.http; +const mem = std.mem; +const net = std.net; +const Uri = std.Uri; +const Allocator = mem.Allocator; +const assert = std.debug.assert; +const use_vectors = builtin.zig_backend != .stage2_x86_64; + +const Client = @This(); +const proto = @import("protocol.zig"); + +pub const disable_tls = std.options.http_disable_tls; + +/// Allocator used for all allocations made by the client. +/// +/// This allocator must be thread-safe. +allocator: Allocator, + +ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, +ca_bundle_mutex: std.Thread.Mutex = .{}, + +/// When this is `true`, the next time this client performs an HTTPS request, +/// it will first rescan the system for root certificates. +next_https_rescan_certs: bool = true, + +/// The pool of connections that can be reused (and currently in use). +connection_pool: ConnectionPool = .{}, + +/// This is the proxy that will handle http:// connections. It *must not* be modified when the client has any active connections. +http_proxy: ?Proxy = null, + +/// This is the proxy that will handle https:// connections. It *must not* be modified when the client has any active connections. +https_proxy: ?Proxy = null, + +/// A set of linked lists of connections that can be reused. +pub const ConnectionPool = struct { + /// The criteria for a connection to be considered a match. + pub const Criteria = struct { + host: []const u8, + port: u16, + protocol: Connection.Protocol, + }; + + const Queue = std.DoublyLinkedList(Connection); + pub const Node = Queue.Node; + + mutex: std.Thread.Mutex = .{}, + /// Open connections that are currently in use. + used: Queue = .{}, + /// Open connections that are not currently in use. + free: Queue = .{}, + free_len: usize = 0, + free_size: usize = 32, + + /// Finds and acquires a connection from the connection pool matching the criteria. This function is threadsafe. + /// If no connection is found, null is returned. + pub fn findConnection(pool: *ConnectionPool, criteria: Criteria) ?*Connection { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + var next = pool.free.last; + while (next) |node| : (next = node.prev) { + if (node.data.protocol != criteria.protocol) continue; + if (node.data.port != criteria.port) continue; + + // Domain names are case-insensitive (RFC 5890, Section 2.3.2.4) + if (!std.ascii.eqlIgnoreCase(node.data.host, criteria.host)) continue; + + pool.acquireUnsafe(node); + return &node.data; + } + + return null; + } + + /// Acquires an existing connection from the connection pool. This function is not threadsafe. + pub fn acquireUnsafe(pool: *ConnectionPool, node: *Node) void { + pool.free.remove(node); + pool.free_len -= 1; + + pool.used.append(node); + } + + /// Acquires an existing connection from the connection pool. This function is threadsafe. + pub fn acquire(pool: *ConnectionPool, node: *Node) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + return pool.acquireUnsafe(node); + } + + /// Tries to release a connection back to the connection pool. This function is threadsafe. + /// If the connection is marked as closing, it will be closed instead. + /// + /// The allocator must be the owner of all nodes in this pool. + /// The allocator must be the owner of all resources associated with the connection. + pub fn release(pool: *ConnectionPool, allocator: Allocator, connection: *Connection) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + const node = @fieldParentPtr(Node, "data", connection); + + pool.used.remove(node); + + if (node.data.closing or pool.free_size == 0) { + node.data.close(allocator); + return allocator.destroy(node); + } + + if (pool.free_len >= pool.free_size) { + const popped = pool.free.popFirst() orelse unreachable; + pool.free_len -= 1; + + popped.data.close(allocator); + allocator.destroy(popped); + } + + if (node.data.proxied) { + pool.free.prepend(node); // proxied connections go to the end of the queue, always try direct connections first + } else { + pool.free.append(node); + } + + pool.free_len += 1; + } + + /// Adds a newly created node to the pool of used connections. This function is threadsafe. + pub fn addUsed(pool: *ConnectionPool, node: *Node) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + pool.used.append(node); + } + + /// Resizes the connection pool. This function is threadsafe. + /// + /// If the new size is smaller than the current size, then idle connections will be closed until the pool is the new size. + pub fn resize(pool: *ConnectionPool, allocator: Allocator, new_size: usize) void { + pool.mutex.lock(); + defer pool.mutex.unlock(); + + const next = pool.free.first; + _ = next; + while (pool.free_len > new_size) { + const popped = pool.free.popFirst() orelse unreachable; + pool.free_len -= 1; + + popped.data.close(allocator); + allocator.destroy(popped); + } + + pool.free_size = new_size; + } + + /// Frees the connection pool and closes all connections within. This function is threadsafe. + /// + /// All future operations on the connection pool will deadlock. + pub fn deinit(pool: *ConnectionPool, allocator: Allocator) void { + pool.mutex.lock(); + + var next = pool.free.first; + while (next) |node| { + defer allocator.destroy(node); + next = node.next; + + node.data.close(allocator); + } + + next = pool.used.first; + while (next) |node| { + defer allocator.destroy(node); + next = node.next; + + node.data.close(allocator); + } + + pool.* = undefined; + } +}; + +/// An interface to either a plain or TLS connection. +pub const Connection = struct { + pub const buffer_size = std.crypto.tls.max_ciphertext_record_len; + const BufferSize = std.math.IntFittingRange(0, buffer_size); + + pub const Protocol = enum { plain, tls }; + + stream: net.Stream, + /// undefined unless protocol is tls. + tls_client: if (!disable_tls) *std.crypto.tls.Client else void, + + /// The protocol that this connection is using. + protocol: Protocol, + + /// The host that this connection is connected to. + host: []u8, + + /// The port that this connection is connected to. + port: u16, + + /// Whether this connection is proxied and is not directly connected. + proxied: bool = false, + + /// Whether this connection is closing when we're done with it. + closing: bool = false, + + read_start: BufferSize = 0, + read_end: BufferSize = 0, + write_end: BufferSize = 0, + read_buf: [buffer_size]u8 = undefined, + write_buf: [buffer_size]u8 = undefined, + + pub fn readvDirectTls(conn: *Connection, buffers: []std.os.iovec) ReadError!usize { + return conn.tls_client.readv(conn.stream, buffers) catch |err| { + // https://github.com/ziglang/zig/issues/2473 + if (mem.startsWith(u8, @errorName(err), "TlsAlert")) return error.TlsAlert; + + switch (err) { + error.TlsConnectionTruncated, error.TlsRecordOverflow, error.TlsDecodeError, error.TlsBadRecordMac, error.TlsBadLength, error.TlsIllegalParameter, error.TlsUnexpectedMessage => return error.TlsFailure, + error.ConnectionTimedOut => return error.ConnectionTimedOut, + error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, + else => return error.UnexpectedReadFailure, + } + }; + } + + pub fn readvDirect(conn: *Connection, buffers: []std.os.iovec) ReadError!usize { + if (conn.protocol == .tls) { + if (disable_tls) unreachable; + + return conn.readvDirectTls(buffers); + } + + return conn.stream.readv(buffers) catch |err| switch (err) { + error.ConnectionTimedOut => return error.ConnectionTimedOut, + error.ConnectionResetByPeer, error.BrokenPipe => return error.ConnectionResetByPeer, + else => return error.UnexpectedReadFailure, + }; + } + + /// Refills the read buffer with data from the connection. + pub fn fill(conn: *Connection) ReadError!void { + if (conn.read_end != conn.read_start) return; + + var iovecs = [1]std.os.iovec{ + .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len }, + }; + const nread = try conn.readvDirect(&iovecs); + if (nread == 0) return error.EndOfStream; + conn.read_start = 0; + conn.read_end = @intCast(nread); + } + + /// Returns the current slice of buffered data. + pub fn peek(conn: *Connection) []const u8 { + return conn.read_buf[conn.read_start..conn.read_end]; + } + + /// Discards the given number of bytes from the read buffer. + pub fn drop(conn: *Connection, num: BufferSize) void { + conn.read_start += num; + } + + /// Reads data from the connection into the given buffer. + pub fn read(conn: *Connection, buffer: []u8) ReadError!usize { + const available_read = conn.read_end - conn.read_start; + const available_buffer = buffer.len; + + if (available_read > available_buffer) { // partially read buffered data + @memcpy(buffer[0..available_buffer], conn.read_buf[conn.read_start..conn.read_end][0..available_buffer]); + conn.read_start += @intCast(available_buffer); + + return available_buffer; + } else if (available_read > 0) { // fully read buffered data + @memcpy(buffer[0..available_read], conn.read_buf[conn.read_start..conn.read_end]); + conn.read_start += available_read; + + return available_read; + } + + var iovecs = [2]std.os.iovec{ + .{ .iov_base = buffer.ptr, .iov_len = buffer.len }, + .{ .iov_base = &conn.read_buf, .iov_len = conn.read_buf.len }, + }; + const nread = try conn.readvDirect(&iovecs); + + if (nread > buffer.len) { + conn.read_start = 0; + conn.read_end = @intCast(nread - buffer.len); + return buffer.len; + } + + return nread; + } + + pub const ReadError = error{ + TlsFailure, + TlsAlert, + ConnectionTimedOut, + ConnectionResetByPeer, + UnexpectedReadFailure, + EndOfStream, + }; + + pub const Reader = std.io.Reader(*Connection, ReadError, read); + + pub fn reader(conn: *Connection) Reader { + return Reader{ .context = conn }; + } + + pub fn writeAllDirectTls(conn: *Connection, buffer: []const u8) WriteError!void { + return conn.tls_client.writeAll(conn.stream, buffer) catch |err| switch (err) { + error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, + else => return error.UnexpectedWriteFailure, + }; + } + + pub fn writeAllDirect(conn: *Connection, buffer: []const u8) WriteError!void { + if (conn.protocol == .tls) { + if (disable_tls) unreachable; + + return conn.writeAllDirectTls(buffer); + } + + return conn.stream.writeAll(buffer) catch |err| switch (err) { + error.BrokenPipe, error.ConnectionResetByPeer => return error.ConnectionResetByPeer, + else => return error.UnexpectedWriteFailure, + }; + } + + /// Writes the given buffer to the connection. + pub fn write(conn: *Connection, buffer: []const u8) WriteError!usize { + if (conn.write_end + buffer.len > conn.write_buf.len) { + try conn.flush(); + + if (buffer.len > conn.write_buf.len) { + try conn.writeAllDirect(buffer); + return buffer.len; + } + } + + @memcpy(conn.write_buf[conn.write_end..][0..buffer.len], buffer); + conn.write_end += @intCast(buffer.len); + + return buffer.len; + } + + /// Flushes the write buffer to the connection. + pub fn flush(conn: *Connection) WriteError!void { + if (conn.write_end == 0) return; + + try conn.writeAllDirect(conn.write_buf[0..conn.write_end]); + conn.write_end = 0; + } + + pub const WriteError = error{ + ConnectionResetByPeer, + UnexpectedWriteFailure, + }; + + pub const Writer = std.io.Writer(*Connection, WriteError, write); + + pub fn writer(conn: *Connection) Writer { + return Writer{ .context = conn }; + } + + /// Closes the connection. + pub fn close(conn: *Connection, allocator: Allocator) void { + if (conn.protocol == .tls) { + if (disable_tls) unreachable; + + // try to cleanly close the TLS connection, for any server that cares. + _ = conn.tls_client.writeEnd(conn.stream, "", true) catch {}; + allocator.destroy(conn.tls_client); + } + + conn.stream.close(); + allocator.free(conn.host); + } +}; + +/// The mode of transport for requests. +pub const RequestTransfer = union(enum) { + content_length: u64, + chunked: void, + none: void, +}; + +/// The decompressor for response messages. +pub const Compression = union(enum) { + pub const DeflateDecompressor = std.compress.zlib.DecompressStream(Request.TransferReader); + pub const GzipDecompressor = std.compress.gzip.Decompress(Request.TransferReader); + pub const ZstdDecompressor = std.compress.zstd.DecompressStream(Request.TransferReader, .{}); + + deflate: DeflateDecompressor, + gzip: GzipDecompressor, + zstd: ZstdDecompressor, + none: void, +}; + +/// A HTTP response originating from a server. +pub const Response = struct { + pub const ParseError = Allocator.Error || error{ + HttpHeadersInvalid, + HttpHeaderContinuationsUnsupported, + HttpTransferEncodingUnsupported, + HttpConnectionHeaderUnsupported, + InvalidContentLength, + CompressionNotSupported, + }; + + pub fn parse(res: *Response, bytes: []const u8, trailing: bool) ParseError!void { + var it = mem.tokenizeAny(u8, bytes, "\r\n"); + + const first_line = it.next() orelse return error.HttpHeadersInvalid; + if (first_line.len < 12) + return error.HttpHeadersInvalid; + + const version: http.Version = switch (int64(first_line[0..8])) { + int64("HTTP/1.0") => .@"HTTP/1.0", + int64("HTTP/1.1") => .@"HTTP/1.1", + else => return error.HttpHeadersInvalid, + }; + if (first_line[8] != ' ') return error.HttpHeadersInvalid; + const status: http.Status = @enumFromInt(parseInt3(first_line[9..12])); + const reason = mem.trimLeft(u8, first_line[12..], " "); + + res.version = version; + res.status = status; + res.reason = reason; + + res.headers.clearRetainingCapacity(); + + while (it.next()) |line| { + if (line.len == 0) return error.HttpHeadersInvalid; + switch (line[0]) { + ' ', '\t' => return error.HttpHeaderContinuationsUnsupported, + else => {}, + } + + var line_it = mem.tokenizeAny(u8, line, ": "); + const header_name = line_it.next() orelse return error.HttpHeadersInvalid; + const header_value = line_it.rest(); + + try res.headers.append(header_name, header_value); + + if (trailing) continue; + + if (std.ascii.eqlIgnoreCase(header_name, "transfer-encoding")) { + // Transfer-Encoding: second, first + // Transfer-Encoding: deflate, chunked + var iter = mem.splitBackwardsScalar(u8, header_value, ','); + + const first = iter.first(); + const trimmed_first = mem.trim(u8, first, " "); + + var next: ?[]const u8 = first; + if (std.meta.stringToEnum(http.TransferEncoding, trimmed_first)) |transfer| { + if (res.transfer_encoding != .none) return error.HttpHeadersInvalid; // we already have a transfer encoding + res.transfer_encoding = transfer; + + next = iter.next(); + } + + if (next) |second| { + const trimmed_second = mem.trim(u8, second, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed_second)) |transfer| { + if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; // double compression is not supported + res.transfer_compression = transfer; + } else { + return error.HttpTransferEncodingUnsupported; + } + } + + if (iter.next()) |_| return error.HttpTransferEncodingUnsupported; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-length")) { + const content_length = std.fmt.parseInt(u64, header_value, 10) catch return error.InvalidContentLength; + + if (res.content_length != null and res.content_length != content_length) return error.HttpHeadersInvalid; + + res.content_length = content_length; + } else if (std.ascii.eqlIgnoreCase(header_name, "content-encoding")) { + if (res.transfer_compression != .identity) return error.HttpHeadersInvalid; + + const trimmed = mem.trim(u8, header_value, " "); + + if (std.meta.stringToEnum(http.ContentEncoding, trimmed)) |ce| { + res.transfer_compression = ce; + } else { + return error.HttpTransferEncodingUnsupported; + } + } + } + } + + inline fn int64(array: *const [8]u8) u64 { + return @bitCast(array.*); + } + + fn parseInt3(text: *const [3]u8) u10 { + if (use_vectors) { + const nnn: @Vector(3, u8) = text.*; + const zero: @Vector(3, u8) = .{ '0', '0', '0' }; + const mmm: @Vector(3, u10) = .{ 100, 10, 1 }; + return @reduce(.Add, @as(@Vector(3, u10), nnn -% zero) *% mmm); + } + return std.fmt.parseInt(u10, text, 10) catch unreachable; + } + + test parseInt3 { + const expectEqual = testing.expectEqual; + try expectEqual(@as(u10, 0), parseInt3("000")); + try expectEqual(@as(u10, 418), parseInt3("418")); + try expectEqual(@as(u10, 999), parseInt3("999")); + } + + /// The HTTP version this response is using. + version: http.Version, + + /// The status code of the response. + status: http.Status, + + /// The reason phrase of the response. + reason: []const u8, + + /// If present, the number of bytes in the response body. + content_length: ?u64 = null, + + /// If present, the transfer encoding of the response body, otherwise none. + transfer_encoding: http.TransferEncoding = .none, + + /// If present, the compression of the response body, otherwise identity (no compression). + transfer_compression: http.ContentEncoding = .identity, + + /// The headers received from the server. + headers: http.Headers, + parser: proto.HeadersParser, + compression: Compression = .none, + + /// Whether the response body should be skipped. Any data read from the response body will be discarded. + skip: bool = false, +}; + +/// A HTTP request that has been sent. +/// +/// Order of operations: open -> send[ -> write -> finish] -> wait -> read +pub const Request = struct { + /// The uri that this request is being sent to. + uri: Uri, + + /// The client that this request was created from. + client: *Client, + + /// Underlying connection to the server. This is null when the connection is released. + connection: ?*Connection, + + method: http.Method, + version: http.Version = .@"HTTP/1.1", + + /// The list of HTTP request headers. + headers: http.Headers, + + /// The transfer encoding of the request body. + transfer_encoding: RequestTransfer = .none, + + /// The redirect quota left for this request. + redirects_left: u32, + + /// Whether the request should follow redirects. + handle_redirects: bool, + + /// Whether the request should handle a 100-continue response before sending the request body. + handle_continue: bool, + + /// The response associated with this request. + /// + /// This field is undefined until `wait` is called. + response: Response, + + /// Used as a allocator for resolving redirects locations. + arena: std.heap.ArenaAllocator, + + /// Frees all resources associated with the request. + pub fn deinit(req: *Request) void { + switch (req.response.compression) { + .none => {}, + .deflate => |*deflate| deflate.deinit(), + .gzip => |*gzip| gzip.deinit(), + .zstd => |*zstd| zstd.deinit(), + } + + req.headers.deinit(); + req.response.headers.deinit(); + + if (req.response.parser.header_bytes_owned) { + req.response.parser.header_bytes.deinit(req.client.allocator); + } + + if (req.connection) |connection| { + if (!req.response.parser.done) { + // If the response wasn't fully read, then we need to close the connection. + connection.closing = true; + } + req.client.connection_pool.release(req.client.allocator, connection); + } + + req.arena.deinit(); + req.* = undefined; + } + + // This function must deallocate all resources associated with the request, or keep those which will be used + // This needs to be kept in sync with deinit and request + fn redirect(req: *Request, uri: Uri) !void { + assert(req.response.parser.done); + + switch (req.response.compression) { + .none => {}, + .deflate => |*deflate| deflate.deinit(), + .gzip => |*gzip| gzip.deinit(), + .zstd => |*zstd| zstd.deinit(), + } + + req.client.connection_pool.release(req.client.allocator, req.connection.?); + req.connection = null; + + const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; + + const port: u16 = uri.port orelse switch (protocol) { + .plain => 80, + .tls => 443, + }; + + const host = uri.host orelse return error.UriMissingHost; + + req.uri = uri; + req.connection = try req.client.connect(host, port, protocol); + req.redirects_left -= 1; + req.response.headers.clearRetainingCapacity(); + req.response.parser.reset(); + + req.response = .{ + .status = undefined, + .reason = undefined, + .version = undefined, + .headers = req.response.headers, + .parser = req.response.parser, + }; + } + + pub const SendError = Connection.WriteError || error{ InvalidContentLength, UnsupportedTransferEncoding }; + + pub const SendOptions = struct { + /// Specifies that the uri should be used as is. You guarantee that the uri is already escaped. + raw_uri: bool = false, + }; + + /// Send the HTTP request headers to the server. + pub fn send(req: *Request, options: SendOptions) SendError!void { + if (!req.method.requestHasBody() and req.transfer_encoding != .none) return error.UnsupportedTransferEncoding; + + const w = req.connection.?.writer(); + + try req.method.write(w); + try w.writeByte(' '); + + if (req.method == .CONNECT) { + try req.uri.writeToStream(.{ .authority = true }, w); + } else { + try req.uri.writeToStream(.{ + .scheme = req.connection.?.proxied, + .authentication = req.connection.?.proxied, + .authority = req.connection.?.proxied, + .path = true, + .query = true, + .raw = options.raw_uri, + }, w); + } + try w.writeByte(' '); + try w.writeAll(@tagName(req.version)); + try w.writeAll("\r\n"); + + if (!req.headers.contains("host")) { + try w.writeAll("Host: "); + try req.uri.writeToStream(.{ .authority = true }, w); + try w.writeAll("\r\n"); + } + + if (!req.headers.contains("user-agent")) { + try w.writeAll("User-Agent: zig/"); + try w.writeAll(builtin.zig_version_string); + try w.writeAll(" (std.http)\r\n"); + } + + if (!req.headers.contains("connection")) { + try w.writeAll("Connection: keep-alive\r\n"); + } + + if (!req.headers.contains("accept-encoding")) { + try w.writeAll("Accept-Encoding: gzip, deflate, zstd\r\n"); + } + + if (!req.headers.contains("te")) { + try w.writeAll("TE: gzip, deflate, trailers\r\n"); + } + + const has_transfer_encoding = req.headers.contains("transfer-encoding"); + const has_content_length = req.headers.contains("content-length"); + + if (!has_transfer_encoding and !has_content_length) { + switch (req.transfer_encoding) { + .chunked => try w.writeAll("Transfer-Encoding: chunked\r\n"), + .content_length => |content_length| try w.print("Content-Length: {d}\r\n", .{content_length}), + .none => {}, + } + } else { + if (has_transfer_encoding) { + const transfer_encoding = req.headers.getFirstValue("transfer-encoding").?; + if (std.mem.eql(u8, transfer_encoding, "chunked")) { + req.transfer_encoding = .chunked; + } else { + return error.UnsupportedTransferEncoding; + } + } else if (has_content_length) { + const content_length = std.fmt.parseInt(u64, req.headers.getFirstValue("content-length").?, 10) catch return error.InvalidContentLength; + + req.transfer_encoding = .{ .content_length = content_length }; + } else { + req.transfer_encoding = .none; + } + } + + for (req.headers.list.items) |entry| { + if (entry.value.len == 0) continue; + + try w.writeAll(entry.name); + try w.writeAll(": "); + try w.writeAll(entry.value); + try w.writeAll("\r\n"); + } + + if (req.connection.?.proxied) { + const proxy_headers: ?http.Headers = switch (req.connection.?.protocol) { + .plain => if (req.client.http_proxy) |proxy| proxy.headers else null, + .tls => if (req.client.https_proxy) |proxy| proxy.headers else null, + }; + + if (proxy_headers) |headers| { + for (headers.list.items) |entry| { + if (entry.value.len == 0) continue; + + try w.writeAll(entry.name); + try w.writeAll(": "); + try w.writeAll(entry.value); + try w.writeAll("\r\n"); + } + } + } + + try w.writeAll("\r\n"); + + try req.connection.?.flush(); + } + + const TransferReadError = Connection.ReadError || proto.HeadersParser.ReadError; + + const TransferReader = std.io.Reader(*Request, TransferReadError, transferRead); + + fn transferReader(req: *Request) TransferReader { + return .{ .context = req }; + } + + fn transferRead(req: *Request, buf: []u8) TransferReadError!usize { + if (req.response.parser.done) return 0; + + var index: usize = 0; + while (index == 0) { + const amt = try req.response.parser.read(req.connection.?, buf[index..], req.response.skip); + if (amt == 0 and req.response.parser.done) break; + index += amt; + } + + return index; + } + + pub const WaitError = RequestError || SendError || TransferReadError || proto.HeadersParser.CheckCompleteHeadError || Response.ParseError || Uri.ParseError || error{ TooManyHttpRedirects, RedirectRequiresResend, HttpRedirectMissingLocation, CompressionInitializationFailed, CompressionNotSupported }; + + /// Waits for a response from the server and parses any headers that are sent. + /// This function will block until the final response is received. + /// + /// If `handle_redirects` is true and the request has no payload, then this function will automatically follow + /// redirects. If a request payload is present, then this function will error with error.RedirectRequiresResend. + /// + /// Must be called after `send` and, if any data was written to the request body, then also after `finish`. + pub fn wait(req: *Request) WaitError!void { + while (true) { // handle redirects + while (true) { // read headers + try req.connection.?.fill(); + + const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); + req.connection.?.drop(@intCast(nchecked)); + + if (req.response.parser.state.isContent()) break; + } + + try req.response.parse(req.response.parser.header_bytes.items, false); + + if (req.response.status == .@"continue") { + req.response.parser.done = true; // we're done parsing the continue response, reset to prepare for the real response + req.response.parser.reset(); + + if (req.handle_continue) + continue; + + return; // we're not handling the 100-continue, return to the caller + } + + // we're switching protocols, so this connection is no longer doing http + if (req.method == .CONNECT and req.response.status.class() == .success) { + req.connection.?.closing = false; + req.response.parser.done = true; + + return; // the connection is not HTTP past this point, return to the caller + } + + // we default to using keep-alive if not provided in the client if the server asks for it + const req_connection = req.headers.getFirstValue("connection"); + const req_keepalive = req_connection != null and !std.ascii.eqlIgnoreCase("close", req_connection.?); + + const res_connection = req.response.headers.getFirstValue("connection"); + const res_keepalive = res_connection != null and !std.ascii.eqlIgnoreCase("close", res_connection.?); + if (res_keepalive and (req_keepalive or req_connection == null)) { + req.connection.?.closing = false; + } else { + req.connection.?.closing = true; + } + + // Any response to a HEAD request and any response with a 1xx (Informational), 204 (No Content), or 304 (Not Modified) + // status code is always terminated by the first empty line after the header fields, regardless of the header fields + // present in the message + if (req.method == .HEAD or req.response.status.class() == .informational or req.response.status == .no_content or req.response.status == .not_modified) { + req.response.parser.done = true; + + return; // the response is empty, no further setup or redirection is necessary + } + + if (req.response.transfer_encoding != .none) { + switch (req.response.transfer_encoding) { + .none => unreachable, + .chunked => { + req.response.parser.next_chunk_length = 0; + req.response.parser.state = .chunk_head_size; + }, + } + } else if (req.response.content_length) |cl| { + req.response.parser.next_chunk_length = cl; + + if (cl == 0) req.response.parser.done = true; + } else { + // read until the connection is closed + req.response.parser.next_chunk_length = std.math.maxInt(u64); + } + + if (req.response.status.class() == .redirect and req.handle_redirects) { + req.response.skip = true; + + // skip the body of the redirect response, this will at least leave the connection in a known good state. + const empty = @as([*]u8, undefined)[0..0]; + assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary + + if (req.redirects_left == 0) return error.TooManyHttpRedirects; + + const location = req.response.headers.getFirstValue("location") orelse + return error.HttpRedirectMissingLocation; + + const arena = req.arena.allocator(); + + const location_duped = try arena.dupe(u8, location); + + const new_url = Uri.parse(location_duped) catch try Uri.parseWithoutScheme(location_duped); + const resolved_url = try req.uri.resolve(new_url, false, arena); + + // is the redirect location on the same domain, or a subdomain of the original request? + const is_same_domain_or_subdomain = std.ascii.endsWithIgnoreCase(resolved_url.host.?, req.uri.host.?) and (resolved_url.host.?.len == req.uri.host.?.len or resolved_url.host.?[resolved_url.host.?.len - req.uri.host.?.len - 1] == '.'); + + if (resolved_url.host == null or !is_same_domain_or_subdomain or !std.ascii.eqlIgnoreCase(resolved_url.scheme, req.uri.scheme)) { + // we're redirecting to a different domain, strip privileged headers like cookies + _ = req.headers.delete("authorization"); + _ = req.headers.delete("www-authenticate"); + _ = req.headers.delete("cookie"); + _ = req.headers.delete("cookie2"); + } + + if (req.response.status == .see_other or ((req.response.status == .moved_permanently or req.response.status == .found) and req.method == .POST)) { + // we're redirecting to a GET, so we need to change the method and remove the body + req.method = .GET; + req.transfer_encoding = .none; + _ = req.headers.delete("transfer-encoding"); + _ = req.headers.delete("content-length"); + _ = req.headers.delete("content-type"); + } + + if (req.transfer_encoding != .none) { + return error.RedirectRequiresResend; // The request body has already been sent. The request is still in a valid state, but the redirect must be handled manually. + } + + try req.redirect(resolved_url); + + try req.send(.{}); + } else { + req.response.skip = false; + if (!req.response.parser.done) { + switch (req.response.transfer_compression) { + .identity => req.response.compression = .none, + .compress, .@"x-compress" => return error.CompressionNotSupported, + .deflate => req.response.compression = .{ + .deflate = std.compress.zlib.decompressStream(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, + }, + .gzip, .@"x-gzip" => req.response.compression = .{ + .gzip = std.compress.gzip.decompress(req.client.allocator, req.transferReader()) catch return error.CompressionInitializationFailed, + }, + .zstd => req.response.compression = .{ + .zstd = std.compress.zstd.decompressStream(req.client.allocator, req.transferReader()), + }, + } + } + + break; + } + } + } + + pub const ReadError = TransferReadError || proto.HeadersParser.CheckCompleteHeadError || error{ DecompressionFailure, InvalidTrailers }; + + pub const Reader = std.io.Reader(*Request, ReadError, read); + + pub fn reader(req: *Request) Reader { + return .{ .context = req }; + } + + /// Reads data from the response body. Must be called after `wait`. + pub fn read(req: *Request, buffer: []u8) ReadError!usize { + const out_index = switch (req.response.compression) { + .deflate => |*deflate| deflate.read(buffer) catch return error.DecompressionFailure, + .gzip => |*gzip| gzip.read(buffer) catch return error.DecompressionFailure, + .zstd => |*zstd| zstd.read(buffer) catch return error.DecompressionFailure, + else => try req.transferRead(buffer), + }; + + if (out_index == 0) { + const has_trail = !req.response.parser.state.isContent(); + + while (!req.response.parser.state.isContent()) { // read trailing headers + try req.connection.?.fill(); + + const nchecked = try req.response.parser.checkCompleteHead(req.client.allocator, req.connection.?.peek()); + req.connection.?.drop(@intCast(nchecked)); + } + + if (has_trail) { + // The response headers before the trailers are already guaranteed to be valid, so they will always be parsed again and cannot return an error. + // This will *only* fail for a malformed trailer. + req.response.parse(req.response.parser.header_bytes.items, true) catch return error.InvalidTrailers; + } + } + + return out_index; + } + + /// Reads data from the response body. Must be called after `wait`. + pub fn readAll(req: *Request, buffer: []u8) !usize { + var index: usize = 0; + while (index < buffer.len) { + const amt = try read(req, buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; + } + + pub const WriteError = Connection.WriteError || error{ NotWriteable, MessageTooLong }; + + pub const Writer = std.io.Writer(*Request, WriteError, write); + + pub fn writer(req: *Request) Writer { + return .{ .context = req }; + } + + /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. + /// Must be called after `send` and before `finish`. + pub fn write(req: *Request, bytes: []const u8) WriteError!usize { + switch (req.transfer_encoding) { + .chunked => { + try req.connection.?.writer().print("{x}\r\n", .{bytes.len}); + try req.connection.?.writer().writeAll(bytes); + try req.connection.?.writer().writeAll("\r\n"); + + return bytes.len; + }, + .content_length => |*len| { + if (len.* < bytes.len) return error.MessageTooLong; + + const amt = try req.connection.?.write(bytes); + len.* -= amt; + return amt; + }, + .none => return error.NotWriteable, + } + } + + /// Write `bytes` to the server. The `transfer_encoding` field determines how data will be sent. + /// Must be called after `send` and before `finish`. + pub fn writeAll(req: *Request, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try write(req, bytes[index..]); + } + } + + pub const FinishError = WriteError || error{MessageNotCompleted}; + + /// Finish the body of a request. This notifies the server that you have no more data to send. + /// Must be called after `send`. + pub fn finish(req: *Request) FinishError!void { + switch (req.transfer_encoding) { + .chunked => try req.connection.?.writer().writeAll("0\r\n\r\n"), + .content_length => |len| if (len != 0) return error.MessageNotCompleted, + .none => {}, + } + + try req.connection.?.flush(); + } +}; + +/// A HTTP proxy server. +pub const Proxy = struct { + allocator: Allocator, + headers: http.Headers, + + protocol: Connection.Protocol, + host: []const u8, + port: u16, + + supports_connect: bool = true, +}; + +/// Release all associated resources with the client. +/// +/// All pending requests must be de-initialized and all active connections released +/// before calling this function. +pub fn deinit(client: *Client) void { + assert(client.connection_pool.used.first == null); // There are still active requests. + + client.connection_pool.deinit(client.allocator); + + if (client.http_proxy) |*proxy| { + proxy.allocator.free(proxy.host); + proxy.headers.deinit(); + } + + if (client.https_proxy) |*proxy| { + proxy.allocator.free(proxy.host); + proxy.headers.deinit(); + } + + if (!disable_tls) + client.ca_bundle.deinit(client.allocator); + + client.* = undefined; +} + +/// Uses the *_proxy environment variable to set any unset proxies for the client. +/// This function *must not* be called when the client has any active connections. +pub fn loadDefaultProxies(client: *Client) !void { + // Prevent any new connections from being created. + client.connection_pool.mutex.lock(); + defer client.connection_pool.mutex.unlock(); + + assert(client.connection_pool.used.first == null); // There are still active requests. + + if (client.http_proxy == null) http: { + const content: []const u8 = if (std.process.hasEnvVarConstant("http_proxy")) + try std.process.getEnvVarOwned(client.allocator, "http_proxy") + else if (std.process.hasEnvVarConstant("HTTP_PROXY")) + try std.process.getEnvVarOwned(client.allocator, "HTTP_PROXY") + else if (std.process.hasEnvVarConstant("all_proxy")) + try std.process.getEnvVarOwned(client.allocator, "all_proxy") + else if (std.process.hasEnvVarConstant("ALL_PROXY")) + try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY") + else + break :http; + defer client.allocator.free(content); + + const uri = Uri.parse(content) catch + Uri.parseWithoutScheme(content) catch + break :http; + + const protocol = if (uri.scheme.len == 0) + .plain // No scheme, assume http:// + else + protocol_map.get(uri.scheme) orelse break :http; // Unknown scheme, ignore + + const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :http; // Missing host, ignore + client.http_proxy = .{ + .allocator = client.allocator, + .headers = .{ .allocator = client.allocator }, + + .protocol = protocol, + .host = host, + .port = uri.port orelse switch (protocol) { + .plain => 80, + .tls => 443, + }, + }; + + if (uri.user != null and uri.password != null) { + const prefix = "Basic "; + + const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? }); + defer client.allocator.free(unencoded); + + const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix.len); + defer client.allocator.free(buffer); + + const result = std.base64.standard.Encoder.encode(buffer[prefix.len..], unencoded); + @memcpy(buffer[0..prefix.len], prefix); + + try client.http_proxy.?.headers.append("proxy-authorization", result); + } + } + + if (client.https_proxy == null) https: { + const content: []const u8 = if (std.process.hasEnvVarConstant("https_proxy")) + try std.process.getEnvVarOwned(client.allocator, "https_proxy") + else if (std.process.hasEnvVarConstant("HTTPS_PROXY")) + try std.process.getEnvVarOwned(client.allocator, "HTTPS_PROXY") + else if (std.process.hasEnvVarConstant("all_proxy")) + try std.process.getEnvVarOwned(client.allocator, "all_proxy") + else if (std.process.hasEnvVarConstant("ALL_PROXY")) + try std.process.getEnvVarOwned(client.allocator, "ALL_PROXY") + else + break :https; + defer client.allocator.free(content); + + const uri = Uri.parse(content) catch + Uri.parseWithoutScheme(content) catch + break :https; + + const protocol = if (uri.scheme.len == 0) + .plain // No scheme, assume http:// + else + protocol_map.get(uri.scheme) orelse break :https; // Unknown scheme, ignore + + const host = if (uri.host) |host| try client.allocator.dupe(u8, host) else break :https; // Missing host, ignore + client.https_proxy = .{ + .allocator = client.allocator, + .headers = .{ .allocator = client.allocator }, + + .protocol = protocol, + .host = host, + .port = uri.port orelse switch (protocol) { + .plain => 80, + .tls => 443, + }, + }; + + if (uri.user != null and uri.password != null) { + const prefix = "Basic "; + + const unencoded = try std.fmt.allocPrint(client.allocator, "{s}:{s}", .{ uri.user.?, uri.password.? }); + defer client.allocator.free(unencoded); + + const buffer = try client.allocator.alloc(u8, std.base64.standard.Encoder.calcSize(unencoded.len) + prefix.len); + defer client.allocator.free(buffer); + + const result = std.base64.standard.Encoder.encode(buffer[prefix.len..], unencoded); + @memcpy(buffer[0..prefix.len], prefix); + + try client.https_proxy.?.headers.append("proxy-authorization", result); + } + } +} + +pub const ConnectTcpError = Allocator.Error || error{ ConnectionRefused, NetworkUnreachable, ConnectionTimedOut, ConnectionResetByPeer, TemporaryNameServerFailure, NameServerFailure, UnknownHostName, HostLacksNetworkAddresses, UnexpectedConnectFailure, TlsInitializationFailed }; + +/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. +/// +/// This function is threadsafe. +pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectTcpError!*Connection { + if (client.connection_pool.findConnection(.{ + .host = host, + .port = port, + .protocol = protocol, + })) |node| + return node; + + if (disable_tls and protocol == .tls) + return error.TlsInitializationFailed; + + const conn = try client.allocator.create(ConnectionPool.Node); + errdefer client.allocator.destroy(conn); + conn.* = .{ .data = undefined }; + + const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { + error.ConnectionRefused => return error.ConnectionRefused, + error.NetworkUnreachable => return error.NetworkUnreachable, + error.ConnectionTimedOut => return error.ConnectionTimedOut, + error.ConnectionResetByPeer => return error.ConnectionResetByPeer, + error.TemporaryNameServerFailure => return error.TemporaryNameServerFailure, + error.NameServerFailure => return error.NameServerFailure, + error.UnknownHostName => return error.UnknownHostName, + error.HostLacksNetworkAddresses => return error.HostLacksNetworkAddresses, + else => return error.UnexpectedConnectFailure, + }; + errdefer stream.close(); + + conn.data = .{ + .stream = stream, + .tls_client = undefined, + + .protocol = protocol, + .host = try client.allocator.dupe(u8, host), + .port = port, + }; + errdefer client.allocator.free(conn.data.host); + + if (protocol == .tls) { + if (disable_tls) unreachable; + + conn.data.tls_client = try client.allocator.create(std.crypto.tls.Client); + errdefer client.allocator.destroy(conn.data.tls_client); + + conn.data.tls_client.* = std.crypto.tls.Client.init(stream, client.ca_bundle, host) catch return error.TlsInitializationFailed; + // This is appropriate for HTTPS because the HTTP headers contain + // the content length which is used to detect truncation attacks. + conn.data.tls_client.allow_truncation_attacks = true; + } + + client.connection_pool.addUsed(conn); + + return &conn.data; +} + +pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError; + +/// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. +/// +/// This function is threadsafe. +pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { + if (!net.has_unix_sockets) return error.Unsupported; + + if (client.connection_pool.findConnection(.{ + .host = path, + .port = 0, + .protocol = .plain, + })) |node| + return node; + + const conn = try client.allocator.create(ConnectionPool.Node); + errdefer client.allocator.destroy(conn); + conn.* = .{ .data = undefined }; + + const stream = try std.net.connectUnixSocket(path); + errdefer stream.close(); + + conn.data = .{ + .stream = stream, + .tls_client = undefined, + .protocol = .plain, + + .host = try client.allocator.dupe(u8, path), + .port = 0, + }; + errdefer client.allocator.free(conn.data.host); + + client.connection_pool.addUsed(conn); + + return &conn.data; +} + +/// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open. +/// +/// This function is threadsafe. +pub fn connectTunnel( + client: *Client, + proxy: *Proxy, + tunnel_host: []const u8, + tunnel_port: u16, +) !*Connection { + if (!proxy.supports_connect) return error.TunnelNotSupported; + + if (client.connection_pool.findConnection(.{ + .host = tunnel_host, + .port = tunnel_port, + .protocol = proxy.protocol, + })) |node| + return node; + + var maybe_valid = false; + (tunnel: { + const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); + errdefer { + conn.closing = true; + client.connection_pool.release(client.allocator, conn); + } + + const uri = Uri{ + .scheme = "http", + .user = null, + .password = null, + .host = tunnel_host, + .port = tunnel_port, + .path = "", + .query = null, + .fragment = null, + }; + + // we can use a small buffer here because a CONNECT response should be very small + var buffer: [8096]u8 = undefined; + + var req = client.open(.CONNECT, uri, proxy.headers, .{ + .handle_redirects = false, + .connection = conn, + .header_strategy = .{ .static = &buffer }, + }) catch |err| { + std.log.debug("err {}", .{err}); + break :tunnel err; + }; + defer req.deinit(); + + req.send(.{ .raw_uri = true }) catch |err| break :tunnel err; + req.wait() catch |err| break :tunnel err; + + if (req.response.status.class() == .server_error) { + maybe_valid = true; + break :tunnel error.ServerError; + } + + if (req.response.status != .ok) break :tunnel error.ConnectionRefused; + + // this connection is now a tunnel, so we can't use it for anything else, it will only be released when the client is de-initialized. + req.connection = null; + + client.allocator.free(conn.host); + conn.host = try client.allocator.dupe(u8, tunnel_host); + errdefer client.allocator.free(conn.host); + + conn.port = tunnel_port; + conn.closing = false; + + return conn; + }) catch { + // something went wrong with the tunnel + proxy.supports_connect = maybe_valid; + return error.TunnelNotSupported; + }; +} + +// Prevents a dependency loop in open() +const ConnectErrorPartial = ConnectTcpError || error{ UnsupportedUrlScheme, ConnectionRefused }; +pub const ConnectError = ConnectErrorPartial || RequestError; + +/// Connect to `host:port` using the specified protocol. This will reuse a connection if one is already open. +/// If a proxy is configured for the client, then the proxy will be used to connect to the host. +/// +/// This function is threadsafe. +pub fn connect(client: *Client, host: []const u8, port: u16, protocol: Connection.Protocol) ConnectError!*Connection { + // pointer required so that `supports_connect` can be updated if a CONNECT fails + const potential_proxy: ?*Proxy = switch (protocol) { + .plain => if (client.http_proxy) |*proxy_info| proxy_info else null, + .tls => if (client.https_proxy) |*proxy_info| proxy_info else null, + }; + + if (potential_proxy) |proxy| { + // don't attempt to proxy the proxy thru itself. + if (std.mem.eql(u8, proxy.host, host) and proxy.port == port and proxy.protocol == protocol) { + return client.connectTcp(host, port, protocol); + } + + if (proxy.supports_connect) tunnel: { + return connectTunnel(client, proxy, host, port) catch |err| switch (err) { + error.TunnelNotSupported => break :tunnel, + else => |e| return e, + }; + } + + // fall back to using the proxy as a normal http proxy + const conn = try client.connectTcp(proxy.host, proxy.port, proxy.protocol); + errdefer { + conn.closing = true; + client.connection_pool.release(conn); + } + + conn.proxied = true; + return conn; + } + + return client.connectTcp(host, port, protocol); +} + +pub const RequestError = ConnectTcpError || ConnectErrorPartial || Request.SendError || std.fmt.ParseIntError || Connection.WriteError || error{ + UnsupportedUrlScheme, + UriMissingHost, + + CertificateBundleLoadFailure, + UnsupportedTransferEncoding, +}; + +pub const RequestOptions = struct { + version: http.Version = .@"HTTP/1.1", + + /// Automatically ignore 100 Continue responses. This assumes you don't care, and will have sent the body before you + /// wait for the response. + /// + /// If this is not the case AND you know the server will send a 100 Continue, set this to false and wait for a + /// response before sending the body. If you wait AND the server does not send a 100 Continue before you finish the + /// request, then the request *will* deadlock. + handle_continue: bool = true, + + /// Automatically follow redirects. This will only follow redirects for repeatable requests (ie. with no payload or the server has acknowledged the payload) + handle_redirects: bool = true, + + /// How many redirects to follow before returning an error. + max_redirects: u32 = 3, + header_strategy: StorageStrategy = .{ .dynamic = 16 * 1024 }, + + /// Must be an already acquired connection. + connection: ?*Connection = null, + + pub const StorageStrategy = union(enum) { + /// In this case, the client's Allocator will be used to store the + /// entire HTTP header. This value is the maximum total size of + /// HTTP headers allowed, otherwise + /// error.HttpHeadersExceededSizeLimit is returned from read(). + dynamic: usize, + /// This is used to store the entire HTTP header. If the HTTP + /// header is too big to fit, `error.HttpHeadersExceededSizeLimit` + /// is returned from read(). When this is used, `error.OutOfMemory` + /// cannot be returned from `read()`. + static: []u8, + }; +}; + +pub const protocol_map = std.ComptimeStringMap(Connection.Protocol, .{ + .{ "http", .plain }, + .{ "ws", .plain }, + .{ "https", .tls }, + .{ "wss", .tls }, +}); + +/// Open a connection to the host specified by `uri` and prepare to send a HTTP request. +/// +/// `uri` must remain alive during the entire request. +/// `headers` is cloned and may be freed after this function returns. +/// +/// The caller is responsible for calling `deinit()` on the `Request`. +/// This function is threadsafe. +pub fn open(client: *Client, method: http.Method, uri: Uri, headers: http.Headers, options: RequestOptions) RequestError!Request { + const protocol = protocol_map.get(uri.scheme) orelse return error.UnsupportedUrlScheme; + + const port: u16 = uri.port orelse switch (protocol) { + .plain => 80, + .tls => 443, + }; + + const host = uri.host orelse return error.UriMissingHost; + + if (protocol == .tls and @atomicLoad(bool, &client.next_https_rescan_certs, .Acquire)) { + if (disable_tls) unreachable; + + client.ca_bundle_mutex.lock(); + defer client.ca_bundle_mutex.unlock(); + + if (client.next_https_rescan_certs) { + client.ca_bundle.rescan(client.allocator) catch return error.CertificateBundleLoadFailure; + @atomicStore(bool, &client.next_https_rescan_certs, false, .Release); + } + } + + const conn = options.connection orelse try client.connect(host, port, protocol); + + var req: Request = .{ + .uri = uri, + .client = client, + .connection = conn, + .headers = try headers.clone(client.allocator), // Headers must be cloned to properly handle header transformations in redirects. + .method = method, + .version = options.version, + .redirects_left = options.max_redirects, + .handle_redirects = options.handle_redirects, + .handle_continue = options.handle_continue, + .response = .{ + .status = undefined, + .reason = undefined, + .version = undefined, + .headers = http.Headers{ .allocator = client.allocator, .owned = false }, + .parser = switch (options.header_strategy) { + .dynamic => |max| proto.HeadersParser.initDynamic(max), + .static => |buf| proto.HeadersParser.initStatic(buf), + }, + }, + .arena = undefined, + }; + errdefer req.deinit(); + + req.arena = std.heap.ArenaAllocator.init(client.allocator); + + return req; +} + +pub const FetchOptions = struct { + pub const Location = union(enum) { + url: []const u8, + uri: Uri, + }; + + pub const Payload = union(enum) { + string: []const u8, + file: std.fs.File, + none, + }; + + pub const ResponseStrategy = union(enum) { + storage: RequestOptions.StorageStrategy, + file: std.fs.File, + none, + }; + + header_strategy: RequestOptions.StorageStrategy = .{ .dynamic = 16 * 1024 }, + response_strategy: ResponseStrategy = .{ .storage = .{ .dynamic = 16 * 1024 * 1024 } }, + + location: Location, + method: http.Method = .GET, + headers: http.Headers = http.Headers{ .allocator = std.heap.page_allocator, .owned = false }, + payload: Payload = .none, + raw_uri: bool = false, +}; + +pub const FetchResult = struct { + status: http.Status, + body: ?[]const u8 = null, + headers: http.Headers, + + allocator: Allocator, + options: FetchOptions, + + pub fn deinit(res: *FetchResult) void { + if (res.options.response_strategy == .storage and res.options.response_strategy.storage == .dynamic) { + if (res.body) |body| res.allocator.free(body); + } + + res.headers.deinit(); + } +}; + +/// Perform a one-shot HTTP request with the provided options. +/// +/// This function is threadsafe. +pub fn fetch(client: *Client, allocator: Allocator, options: FetchOptions) !FetchResult { + const has_transfer_encoding = options.headers.contains("transfer-encoding"); + const has_content_length = options.headers.contains("content-length"); + + if (has_content_length or has_transfer_encoding) return error.UnsupportedHeader; + + const uri = switch (options.location) { + .url => |u| try Uri.parse(u), + .uri => |u| u, + }; + + var req = try open(client, options.method, uri, options.headers, .{ + .header_strategy = options.header_strategy, + .handle_redirects = options.payload == .none, + }); + defer req.deinit(); + + { // Block to maintain lock of file to attempt to prevent a race condition where another process modifies the file while we are reading it. + // This relies on other processes actually obeying the advisory lock, which is not guaranteed. + if (options.payload == .file) try options.payload.file.lock(.shared); + defer if (options.payload == .file) options.payload.file.unlock(); + + switch (options.payload) { + .string => |str| req.transfer_encoding = .{ .content_length = str.len }, + .file => |file| req.transfer_encoding = .{ .content_length = (try file.stat()).size }, + .none => {}, + } + + try req.send(.{ .raw_uri = options.raw_uri }); + + switch (options.payload) { + .string => |str| try req.writeAll(str), + .file => |file| { + try file.seekTo(0); + var fifo = std.fifo.LinearFifo(u8, .{ .Static = 8192 }).init(); + try fifo.pump(file.reader(), req.writer()); + }, + .none => {}, + } + + try req.finish(); + } + + try req.wait(); + + var res = FetchResult{ + .status = req.response.status, + .headers = try req.response.headers.clone(allocator), + + .allocator = allocator, + .options = options, + }; + + switch (options.response_strategy) { + .storage => |storage| switch (storage) { + .dynamic => |max| res.body = try req.reader().readAllAlloc(allocator, max), + .static => |buf| res.body = buf[0..try req.reader().readAll(buf)], + }, + .file => |file| { + var fifo = std.fifo.LinearFifo(u8, .{ .Static = 8192 }).init(); + try fifo.pump(req.reader(), file.writer()); + }, + .none => { // Take advantage of request internals to discard the response body and make the connection available for another request. + req.response.skip = true; + + const empty = @as([*]u8, undefined)[0..0]; + assert(try req.transferRead(empty) == 0); // we're skipping, no buffer is necessary + }, + } + + return res; +} + +test { + const native_endian = comptime builtin.cpu.arch.endian(); + if (builtin.zig_backend == .stage2_llvm and native_endian == .big) { + // https://github.com/ziglang/zig/issues/13782 + return error.SkipZigTest; + } + + if (builtin.os.tag == .wasi) return error.SkipZigTest; + + if (builtin.zig_backend == .stage2_x86_64 and + !comptime std.Target.x86.featureSetHas(builtin.cpu.features, .avx)) return error.SkipZigTest; + + std.testing.refAllDecls(@This()); +} From 511e9b969a254073a4197363cb2139926e59132d Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 30 Jan 2024 08:57:42 +0100 Subject: [PATCH 05/46] async: use std http client with loop --- src/async/Client.zig | 55 ++++---------- src/async/stream.zig | 168 +++++++++++++++++++++++++++++++++++++++++++ src/async/tcp.zig | 62 ++++++++++++++++ src/async/test.zig | 60 ++++++++++++++++ src/run_tests.zig | 5 ++ 5 files changed, 309 insertions(+), 41 deletions(-) create mode 100644 src/async/stream.zig create mode 100644 src/async/tcp.zig create mode 100644 src/async/test.zig diff --git a/src/async/Client.zig b/src/async/Client.zig index 8438a7bb..3af59134 100644 --- a/src/async/Client.zig +++ b/src/async/Client.zig @@ -3,9 +3,13 @@ //! Connections are opened in a thread-safe manner, but individual Requests are not. //! //! TLS support may be disabled via `std.options.http_disable_tls`. +//! +//! This file is a copy of the original std.http.Client with little changes to +//! handle non-blocking I/O with the jsruntime.Loop. -const std = @import("../std.zig"); +const std = @import("std"); const builtin = @import("builtin"); +const Stream = @import("stream.zig").Stream; const testing = std.testing; const http = std.http; const mem = std.mem; @@ -16,7 +20,10 @@ const assert = std.debug.assert; const use_vectors = builtin.zig_backend != .stage2_x86_64; const Client = @This(); -const proto = @import("protocol.zig"); +const proto = http.protocol; + +const Loop = @import("jsruntime").Loop; +const tcp = @import("tcp.zig"); pub const disable_tls = std.options.http_disable_tls; @@ -25,6 +32,9 @@ pub const disable_tls = std.options.http_disable_tls; /// This allocator must be thread-safe. allocator: Allocator, +// std.net.Stream implementation using jsruntime Loop +loop: *Loop, + ca_bundle: if (disable_tls) void else std.crypto.Certificate.Bundle = if (disable_tls) {} else .{}, ca_bundle_mutex: std.Thread.Mutex = .{}, @@ -194,7 +204,7 @@ pub const Connection = struct { pub const Protocol = enum { plain, tls }; - stream: net.Stream, + stream: Stream, /// undefined unless protocol is tls. tls_client: if (!disable_tls) *std.crypto.tls.Client else void, @@ -1210,7 +1220,7 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec errdefer client.allocator.destroy(conn); conn.* = .{ .data = undefined }; - const stream = net.tcpConnectToHost(client.allocator, host, port) catch |err| switch (err) { + const stream = tcp.tcpConnectToHost(client.allocator, client.loop, host, port) catch |err| switch (err) { error.ConnectionRefused => return error.ConnectionRefused, error.NetworkUnreachable => return error.NetworkUnreachable, error.ConnectionTimedOut => return error.ConnectionTimedOut, @@ -1250,43 +1260,6 @@ pub fn connectTcp(client: *Client, host: []const u8, port: u16, protocol: Connec return &conn.data; } -pub const ConnectUnixError = Allocator.Error || std.os.SocketError || error{ NameTooLong, Unsupported } || std.os.ConnectError; - -/// Connect to `path` as a unix domain socket. This will reuse a connection if one is already open. -/// -/// This function is threadsafe. -pub fn connectUnix(client: *Client, path: []const u8) ConnectUnixError!*Connection { - if (!net.has_unix_sockets) return error.Unsupported; - - if (client.connection_pool.findConnection(.{ - .host = path, - .port = 0, - .protocol = .plain, - })) |node| - return node; - - const conn = try client.allocator.create(ConnectionPool.Node); - errdefer client.allocator.destroy(conn); - conn.* = .{ .data = undefined }; - - const stream = try std.net.connectUnixSocket(path); - errdefer stream.close(); - - conn.data = .{ - .stream = stream, - .tls_client = undefined, - .protocol = .plain, - - .host = try client.allocator.dupe(u8, path), - .port = 0, - }; - errdefer client.allocator.free(conn.data.host); - - client.connection_pool.addUsed(conn); - - return &conn.data; -} - /// Connect to `tunnel_host:tunnel_port` using the specified proxy with HTTP CONNECT. This will reuse a connection if one is already open. /// /// This function is threadsafe. diff --git a/src/async/stream.zig b/src/async/stream.zig new file mode 100644 index 00000000..e1f2537f --- /dev/null +++ b/src/async/stream.zig @@ -0,0 +1,168 @@ +const std = @import("std"); +const builtin = @import("builtin"); +const os = std.os; +const io = std.io; +const assert = std.debug.assert; + +const Loop = @import("jsruntime").Loop; + +const WriteCmd = struct { + const Self = @This(); + + stream: Stream, + done: bool = false, + res: usize = undefined, + err: ?anyerror = null, + + fn run(self: *Self, buffer: []const u8) void { + self.stream.loop.send(*Self, self, callback, self.stream.handle, buffer); + } + + fn callback(self: *Self, err: ?anyerror, res: usize) void { + self.res = res; + self.err = err; + self.done = true; + } + + fn wait(self: *Self) !usize { + while (!self.done) try self.stream.loop.tick(); + if (self.err) |err| return err; + return self.res; + } +}; + +const ReadCmd = struct { + const Self = @This(); + + stream: Stream, + done: bool = false, + res: usize = undefined, + err: ?anyerror = null, + + fn run(self: *Self, buffer: []u8) void { + self.stream.loop.receive(*Self, self, callback, self.stream.handle, buffer); + } + + fn callback(self: *Self, _: []const u8, err: ?anyerror, res: usize) void { + self.res = res; + self.err = err; + self.done = true; + } + + fn wait(self: *Self) !usize { + while (!self.done) try self.stream.loop.tick(); + if (self.err) |err| return err; + return self.res; + } +}; + +pub const Stream = struct { + loop: *Loop, + + handle: std.os.socket_t, + + pub fn close(self: Stream) void { + os.closeSocket(self.handle); + } + + pub const ReadError = os.ReadError; + pub const WriteError = os.WriteError; + + pub const Reader = io.Reader(Stream, ReadError, read); + pub const Writer = io.Writer(Stream, WriteError, write); + + pub fn reader(self: Stream) Reader { + return .{ .context = self }; + } + + pub fn writer(self: Stream) Writer { + return .{ .context = self }; + } + + pub fn read(self: Stream, buffer: []u8) ReadError!usize { + var cmd = ReadCmd{ .stream = self }; + cmd.run(buffer); + return cmd.wait() catch |err| switch (err) { + else => return error.Unexpected, + }; + } + + pub fn readv(s: Stream, iovecs: []const os.iovec) ReadError!usize { + return os.readv(s.handle, iovecs); + } + + /// Returns the number of bytes read. If the number read is smaller than + /// `buffer.len`, it means the stream reached the end. Reaching the end of + /// a stream is not an error condition. + pub fn readAll(s: Stream, buffer: []u8) ReadError!usize { + return readAtLeast(s, buffer, buffer.len); + } + + /// Returns the number of bytes read, calling the underlying read function + /// the minimal number of times until the buffer has at least `len` bytes + /// filled. If the number read is less than `len` it means the stream + /// reached the end. Reaching the end of the stream is not an error + /// condition. + pub fn readAtLeast(s: Stream, buffer: []u8, len: usize) ReadError!usize { + assert(len <= buffer.len); + var index: usize = 0; + while (index < len) { + const amt = try s.read(buffer[index..]); + if (amt == 0) break; + index += amt; + } + return index; + } + + /// TODO in evented I/O mode, this implementation incorrectly uses the event loop's + /// file system thread instead of non-blocking. It needs to be reworked to properly + /// use non-blocking I/O. + pub fn write(self: Stream, buffer: []const u8) WriteError!usize { + var cmd = WriteCmd{ .stream = self }; + cmd.run(buffer); + + return cmd.wait() catch |err| switch (err) { + error.AccessDenied => error.AccessDenied, + error.WouldBlock => error.WouldBlock, + error.ConnectionResetByPeer => error.ConnectionResetByPeer, + error.MessageTooBig => error.FileTooBig, + error.BrokenPipe => error.BrokenPipe, + else => return error.Unexpected, + }; + } + + pub fn writeAll(self: Stream, bytes: []const u8) WriteError!void { + var index: usize = 0; + while (index < bytes.len) { + index += try self.write(bytes[index..]); + } + } + + /// See https://github.com/ziglang/zig/issues/7699 + /// See equivalent function: `std.fs.File.writev`. + pub fn writev(self: Stream, iovecs: []const os.iovec_const) WriteError!usize { + if (iovecs.len == 0) return 0; + const first_buffer = iovecs[0].iov_base[0..iovecs[0].iov_len]; + return try self.write(first_buffer); + } + + /// The `iovecs` parameter is mutable because this function needs to mutate the fields in + /// order to handle partial writes from the underlying OS layer. + /// See https://github.com/ziglang/zig/issues/7699 + /// See equivalent function: `std.fs.File.writevAll`. + pub fn writevAll(self: Stream, iovecs: []os.iovec_const) WriteError!void { + if (iovecs.len == 0) return; + + var i: usize = 0; + while (true) { + var amt = try self.writev(iovecs[i..]); + while (amt >= iovecs[i].iov_len) { + amt -= iovecs[i].iov_len; + i += 1; + if (i >= iovecs.len) return; + } + iovecs[i].iov_base += amt; + iovecs[i].iov_len -= amt; + } + } +}; diff --git a/src/async/tcp.zig b/src/async/tcp.zig new file mode 100644 index 00000000..a1a7f5b6 --- /dev/null +++ b/src/async/tcp.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const net = std.net; +const Stream = @import("stream.zig").Stream; +const Loop = @import("jsruntime").Loop; + +const ConnectCmd = struct { + const Self = @This(); + + loop: *Loop, + socket: std.os.socket_t, + err: ?anyerror = null, + done: bool = false, + + fn run(self: *Self, addr: std.net.Address) !void { + self.loop.connect(*Self, self, callback, self.socket, addr); + } + + fn callback(self: *Self, _: std.os.socket_t, err: ?anyerror) void { + self.err = err; + self.done = true; + } + + fn wait(self: *Self) !void { + while (!self.done) try self.loop.tick(); + if (self.err) |err| return err; + } +}; + +pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, port: u16) !Stream { + // TODO async resolve + const list = try net.getAddressList(alloc, name, port); + defer list.deinit(); + + if (list.addrs.len == 0) return error.UnknownHostName; + + for (list.addrs) |addr| { + return tcpConnectToAddress(loop, addr) catch |err| switch (err) { + error.ConnectionRefused => { + continue; + }, + else => return err, + }; + } + return std.os.ConnectError.ConnectionRefused; +} + +pub fn tcpConnectToAddress(loop: *Loop, addr: net.Address) !Stream { + const sockfd = try loop.open(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP); + errdefer std.os.closeSocket(sockfd); + + var cmd = ConnectCmd{ + .loop = loop, + .socket = sockfd, + }; + try cmd.run(addr); + try cmd.wait(); + + return Stream{ + .loop = loop, + .handle = sockfd, + }; +} diff --git a/src/async/test.zig b/src/async/test.zig new file mode 100644 index 00000000..10081e9e --- /dev/null +++ b/src/async/test.zig @@ -0,0 +1,60 @@ +const std = @import("std"); +const http = std.http; +const StdClient = @import("Client.zig"); +// const hasync = @import("http.zig"); + +pub const Loop = @import("jsruntime").Loop; + +const url = "https://www.w3.org/"; + +test "blocking mode fetch API" { + const alloc = std.testing.allocator; + + var loop = try Loop.init(alloc); + defer loop.deinit(); + + var client: StdClient = .{ + .allocator = alloc, + .loop = &loop, + }; + defer client.deinit(); + + // force client's CA cert scan from system. + try client.ca_bundle.rescan(client.allocator); + + var res = try client.fetch(alloc, .{ + .location = .{ .uri = try std.Uri.parse(url) }, + .payload = .none, + }); + defer res.deinit(); + + try std.testing.expect(res.status == .ok); +} + +test "blocking mode open/send/wait API" { + const alloc = std.testing.allocator; + + var loop = try Loop.init(alloc); + defer loop.deinit(); + + var client: StdClient = .{ + .allocator = alloc, + .loop = &loop, + }; + defer client.deinit(); + + // force client's CA cert scan from system. + try client.ca_bundle.rescan(client.allocator); + + var headers = try std.http.Headers.initList(alloc, &[_]std.http.Field{}); + defer headers.deinit(); + + var req = try client.open(.GET, try std.Uri.parse(url), headers, .{}); + defer req.deinit(); + + try req.send(.{}); + try req.finish(); + try req.wait(); + + try std.testing.expect(req.response.status == .ok); +} diff --git a/src/run_tests.zig b/src/run_tests.zig index dea89d6f..592bc08e 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -94,6 +94,11 @@ pub fn main() !void { } } +test { + const TestAsync = @import("async/test.zig"); + std.testing.refAllDecls(TestAsync); +} + test "jsruntime" { // generate tests try generate.tests(); From c200f60d7d3bd70193d9d3778b6dfcd0373ed75f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 30 Jan 2024 09:32:20 +0100 Subject: [PATCH 06/46] async: add pure async http client --- src/async/http.zig | 65 ++++++++++++++++++++++++++++++++++++++++++++++ src/async/test.zig | 24 ++++++++++++++++- 2 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 src/async/http.zig diff --git a/src/async/http.zig b/src/async/http.zig new file mode 100644 index 00000000..1a631959 --- /dev/null +++ b/src/async/http.zig @@ -0,0 +1,65 @@ +const std = @import("std"); +const http = std.http; +const stdcli = @import("Client.zig"); + +pub const Loop = @import("jsruntime").Loop; + +pub const Client = struct { + cli: stdcli, + + pub fn init(alloc: std.mem.Allocator, loop: *Loop) Client { + return .{ .cli = .{ + .allocator = alloc, + .loop = loop, + } }; + } + + pub fn deinit(self: *Client) void { + self.cli.deinit(); + } + + pub fn create(self: *Client, uri: std.Uri) Request { + return .{ + .cli = &self.cli, + .uri = uri, + .headers = .{ .allocator = self.cli.allocator, .owned = false }, + }; + } +}; + +pub const Request = struct { + cli: *stdcli, + uri: std.Uri, + headers: std.http.Headers, + + done: bool = false, + err: ?anyerror = null, + + pub fn deinit(self: *Request) void { + self.headers.deinit(); + } + + pub fn fetch(self: *Request) !void { + self.cli.loop.yield(*Request, self, callback); + } + + fn onerr(self: *Request, err: anyerror) void { + self.err = err; + } + + fn callback(self: *Request, err: ?anyerror) void { + if (err) |e| return self.onerr(e); + defer self.done = true; + var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + defer req.deinit(); + + req.send(.{}) catch |e| return self.onerr(e); + req.finish() catch |e| return self.onerr(e); + req.wait() catch |e| return self.onerr(e); + } + + pub fn wait(self: *Request) !void { + while (!self.done) try self.cli.loop.tick(); + if (self.err) |err| return err; + } +}; diff --git a/src/async/test.zig b/src/async/test.zig index 10081e9e..d5262d06 100644 --- a/src/async/test.zig +++ b/src/async/test.zig @@ -1,7 +1,8 @@ const std = @import("std"); const http = std.http; const StdClient = @import("Client.zig"); -// const hasync = @import("http.zig"); +const AsyncClient = @import("http.zig").Client; +const AsyncRequest = @import("http.zig").Request; pub const Loop = @import("jsruntime").Loop; @@ -58,3 +59,24 @@ test "blocking mode open/send/wait API" { try std.testing.expect(req.response.status == .ok); } + +test "non blocking mode API" { + const alloc = std.testing.allocator; + + var loop = try Loop.init(alloc); + defer loop.deinit(); + + var client = AsyncClient.init(alloc, &loop); + defer client.deinit(); + + var reqs: [10]AsyncRequest = undefined; + for (0..reqs.len) |i| { + reqs[i] = client.create(try std.Uri.parse(url)); + try reqs[i].fetch(); + } + + for (0..reqs.len) |i| { + try reqs[i].wait(); + reqs[i].deinit(); + } +} From 2fa66f93fd5de8d57010064d84568be3a8c1cca6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 30 Jan 2024 16:59:00 +0100 Subject: [PATCH 07/46] async: refacto with comptime generation --- src/async/http.zig | 31 +++++++++++------ src/async/stream.zig | 65 ++++------------------------------- src/async/tcp.zig | 81 +++++++++++++++++++++++++++++++------------- src/async/test.zig | 10 +++--- 4 files changed, 90 insertions(+), 97 deletions(-) diff --git a/src/async/http.zig b/src/async/http.zig index 1a631959..3cd095a7 100644 --- a/src/async/http.zig +++ b/src/async/http.zig @@ -3,27 +3,34 @@ const http = std.http; const stdcli = @import("Client.zig"); pub const Loop = @import("jsruntime").Loop; +const YieldImpl = Loop.Yield(Request); pub const Client = struct { cli: stdcli, pub fn init(alloc: std.mem.Allocator, loop: *Loop) Client { - return .{ .cli = .{ - .allocator = alloc, - .loop = loop, - } }; + return .{ + .cli = .{ + .allocator = alloc, + .loop = loop, + }, + }; } pub fn deinit(self: *Client) void { self.cli.deinit(); } - pub fn create(self: *Client, uri: std.Uri) Request { - return .{ + pub fn create(self: *Client, uri: std.Uri) !*Request { + var req = try self.cli.allocator.create(Request); + req.* = Request{ + .impl = undefined, .cli = &self.cli, .uri = uri, .headers = .{ .allocator = self.cli.allocator, .owned = false }, }; + req.impl = YieldImpl.init(self.cli.loop, req); + return req; } }; @@ -32,24 +39,26 @@ pub const Request = struct { uri: std.Uri, headers: std.http.Headers, + impl: YieldImpl, done: bool = false, err: ?anyerror = null, pub fn deinit(self: *Request) void { self.headers.deinit(); + self.cli.allocator.destroy(self); } - pub fn fetch(self: *Request) !void { - self.cli.loop.yield(*Request, self, callback); + pub fn fetch(self: *Request) void { + return self.impl.yield(); } fn onerr(self: *Request, err: anyerror) void { self.err = err; } - fn callback(self: *Request, err: ?anyerror) void { - if (err) |e| return self.onerr(e); + pub fn onYield(self: *Request, err: ?anyerror) void { defer self.done = true; + if (err) |e| return self.onerr(e); var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); defer req.deinit(); @@ -59,7 +68,7 @@ pub const Request = struct { } pub fn wait(self: *Request) !void { - while (!self.done) try self.cli.loop.tick(); + while (!self.done) try self.impl.tick(); if (self.err) |err| return err; } }; diff --git a/src/async/stream.zig b/src/async/stream.zig index e1f2537f..4aa5c04f 100644 --- a/src/async/stream.zig +++ b/src/async/stream.zig @@ -4,65 +4,17 @@ const os = std.os; const io = std.io; const assert = std.debug.assert; -const Loop = @import("jsruntime").Loop; - -const WriteCmd = struct { - const Self = @This(); - - stream: Stream, - done: bool = false, - res: usize = undefined, - err: ?anyerror = null, - - fn run(self: *Self, buffer: []const u8) void { - self.stream.loop.send(*Self, self, callback, self.stream.handle, buffer); - } - - fn callback(self: *Self, err: ?anyerror, res: usize) void { - self.res = res; - self.err = err; - self.done = true; - } - - fn wait(self: *Self) !usize { - while (!self.done) try self.stream.loop.tick(); - if (self.err) |err| return err; - return self.res; - } -}; - -const ReadCmd = struct { - const Self = @This(); - - stream: Stream, - done: bool = false, - res: usize = undefined, - err: ?anyerror = null, - - fn run(self: *Self, buffer: []u8) void { - self.stream.loop.receive(*Self, self, callback, self.stream.handle, buffer); - } - - fn callback(self: *Self, _: []const u8, err: ?anyerror, res: usize) void { - self.res = res; - self.err = err; - self.done = true; - } - - fn wait(self: *Self) !usize { - while (!self.done) try self.stream.loop.tick(); - if (self.err) |err| return err; - return self.res; - } -}; +const tcp = @import("tcp.zig"); pub const Stream = struct { - loop: *Loop, + alloc: std.mem.Allocator, + conn: *tcp.Conn, handle: std.os.socket_t, pub fn close(self: Stream) void { os.closeSocket(self.handle); + self.alloc.destroy(self.conn); } pub const ReadError = os.ReadError; @@ -80,9 +32,7 @@ pub const Stream = struct { } pub fn read(self: Stream, buffer: []u8) ReadError!usize { - var cmd = ReadCmd{ .stream = self }; - cmd.run(buffer); - return cmd.wait() catch |err| switch (err) { + return self.conn.receive(self.handle, buffer) catch |err| switch (err) { else => return error.Unexpected, }; } @@ -118,10 +68,7 @@ pub const Stream = struct { /// file system thread instead of non-blocking. It needs to be reworked to properly /// use non-blocking I/O. pub fn write(self: Stream, buffer: []const u8) WriteError!usize { - var cmd = WriteCmd{ .stream = self }; - cmd.run(buffer); - - return cmd.wait() catch |err| switch (err) { + return self.conn.send(self.handle, buffer) catch |err| switch (err) { error.AccessDenied => error.AccessDenied, error.WouldBlock => error.WouldBlock, error.ConnectionResetByPeer => error.ConnectionResetByPeer, diff --git a/src/async/tcp.zig b/src/async/tcp.zig index a1a7f5b6..b490b78f 100644 --- a/src/async/tcp.zig +++ b/src/async/tcp.zig @@ -2,27 +2,64 @@ const std = @import("std"); const net = std.net; const Stream = @import("stream.zig").Stream; const Loop = @import("jsruntime").Loop; +const NetworkImpl = Loop.Network(Conn.Command); -const ConnectCmd = struct { - const Self = @This(); +// Conn is a TCP connection using jsruntime Loop async I/O. +// connect, send and receive are blocking, but use async I/O in the background. +// Client doesn't own the socket used for the connection, the caller is +// responsible for closing it. +pub const Conn = struct { + const Command = struct { + impl: NetworkImpl, + + done: bool = false, + err: ?anyerror = null, + ln: usize = 0, + + fn ok(self: *Command, err: ?anyerror, ln: usize) void { + self.err = err; + self.ln = ln; + self.done = true; + } + + fn wait(self: *Command) !usize { + while (!self.done) try self.impl.tick(); + + if (self.err) |err| return err; + return self.ln; + } + pub fn onConnect(self: *Command, err: ?anyerror) void { + self.ok(err, 0); + } + pub fn onSend(self: *Command, ln: usize, err: ?anyerror) void { + self.ok(err, ln); + } + pub fn onReceive(self: *Command, ln: usize, err: ?anyerror) void { + self.ok(err, ln); + } + }; loop: *Loop, - socket: std.os.socket_t, - err: ?anyerror = null, - done: bool = false, - fn run(self: *Self, addr: std.net.Address) !void { - self.loop.connect(*Self, self, callback, self.socket, addr); + pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void { + var cmd = Command{ .impl = undefined }; + cmd.impl = NetworkImpl.init(self.loop, &cmd); + cmd.impl.connect(socket, address); + _ = try cmd.wait(); } - fn callback(self: *Self, _: std.os.socket_t, err: ?anyerror) void { - self.err = err; - self.done = true; + pub fn send(self: *Conn, socket: std.os.socket_t, buffer: []const u8) !usize { + var cmd = Command{ .impl = undefined }; + cmd.impl = NetworkImpl.init(self.loop, &cmd); + cmd.impl.send(socket, buffer); + return try cmd.wait(); } - fn wait(self: *Self) !void { - while (!self.done) try self.loop.tick(); - if (self.err) |err| return err; + pub fn receive(self: *Conn, socket: std.os.socket_t, buffer: []u8) !usize { + var cmd = Command{ .impl = undefined }; + cmd.impl = NetworkImpl.init(self.loop, &cmd); + cmd.impl.receive(socket, buffer); + return try cmd.wait(); } }; @@ -34,7 +71,7 @@ pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, if (list.addrs.len == 0) return error.UnknownHostName; for (list.addrs) |addr| { - return tcpConnectToAddress(loop, addr) catch |err| switch (err) { + return tcpConnectToAddress(alloc, loop, addr) catch |err| switch (err) { error.ConnectionRefused => { continue; }, @@ -44,19 +81,17 @@ pub fn tcpConnectToHost(alloc: std.mem.Allocator, loop: *Loop, name: []const u8, return std.os.ConnectError.ConnectionRefused; } -pub fn tcpConnectToAddress(loop: *Loop, addr: net.Address) !Stream { - const sockfd = try loop.open(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP); +pub fn tcpConnectToAddress(alloc: std.mem.Allocator, loop: *Loop, addr: net.Address) !Stream { + const sockfd = try std.os.socket(addr.any.family, std.os.SOCK.STREAM, std.os.IPPROTO.TCP); errdefer std.os.closeSocket(sockfd); - var cmd = ConnectCmd{ - .loop = loop, - .socket = sockfd, - }; - try cmd.run(addr); - try cmd.wait(); + var conn = try alloc.create(Conn); + conn.* = Conn{ .loop = loop }; + try conn.connect(sockfd, addr); return Stream{ - .loop = loop, + .alloc = alloc, + .conn = conn, .handle = sockfd, }; } diff --git a/src/async/test.zig b/src/async/test.zig index d5262d06..de3446a2 100644 --- a/src/async/test.zig +++ b/src/async/test.zig @@ -6,7 +6,9 @@ const AsyncRequest = @import("http.zig").Request; pub const Loop = @import("jsruntime").Loop; -const url = "https://www.w3.org/"; +const TCPClient = @import("tcp.zig").Client; + +const url = "https://w3.org"; test "blocking mode fetch API" { const alloc = std.testing.allocator; @@ -69,10 +71,10 @@ test "non blocking mode API" { var client = AsyncClient.init(alloc, &loop); defer client.deinit(); - var reqs: [10]AsyncRequest = undefined; + var reqs: [10]*AsyncRequest = undefined; for (0..reqs.len) |i| { - reqs[i] = client.create(try std.Uri.parse(url)); - try reqs[i].fetch(); + reqs[i] = try client.create(try std.Uri.parse(url)); + reqs[i].fetch(); } for (0..reqs.len) |i| { From 2b79a65d6de9979d78deba591807b36dd552f0b7 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 09:46:12 +0100 Subject: [PATCH 08/46] xhr: implement async http client --- src/apiweb.zig | 2 + src/run_tests.zig | 2 + src/xhr/xhr.zig | 121 +++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 117 insertions(+), 8 deletions(-) diff --git a/src/apiweb.zig b/src/apiweb.zig index 40cc8779..a08f9f41 100644 --- a/src/apiweb.zig +++ b/src/apiweb.zig @@ -5,6 +5,7 @@ const Console = @import("jsruntime").Console; const DOM = @import("dom/dom.zig"); const HTML = @import("html/html.zig"); const Events = @import("events/event.zig"); +const XHR = @import("xhr/xhr.zig"); pub const HTMLDocument = @import("html/document.zig").HTMLDocument; @@ -14,4 +15,5 @@ pub const Interfaces = generate.Tuple(.{ DOM.Interfaces, Events.Interfaces, HTML.Interfaces, + XHR.Interfaces, }); diff --git a/src/run_tests.zig b/src/run_tests.zig index 592bc08e..86e09a8f 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -24,6 +24,7 @@ const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn; const xhr = @import("xhr/xhr.zig"); +const XHRTestExecFn = xhr.testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -79,6 +80,7 @@ fn testsAllExecFn( AttrTestExecFn, EventTargetTestExecFn, EventTestExecFn, + XHRTestExecFn, }; inline for (testFns) |testFn| { diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 5abcec12..71db0823 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -1,20 +1,55 @@ const std = @import("std"); +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); -const EventTarget = @import("../dom/event_target.zig").EventTarget; +const EventTarget = @import("../dom/event_target.zig").EventTarget; +const Callback = jsruntime.Callback; const DOMError = @import("../netsurf.zig").DOMError; +const Loop = jsruntime.Loop; +const YieldImpl = Loop.Yield(XMLHttpRequest); +const Client = @import("../async/Client.zig"); + // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest pub const Interfaces = generate.Tuple(.{ XMLHttpRequestEventTarget, XMLHttpRequestUpload, + XMLHttpRequest, }); pub const XMLHttpRequestEventTarget = struct { pub const prototype = *EventTarget; pub const mem_guarantied = true; + + onloadstart_cbk: ?Callback = null, + onprogress_cbk: ?Callback = null, + onabort_cbk: ?Callback = null, + onload_cbk: ?Callback = null, + ontimeout_cbk: ?Callback = null, + onloadend_cbk: ?Callback = null, + + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onloadstart_cbk = handler; + } + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onprogress_cbk = handler; + } + pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onabort = handler; + } + pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onload = handler; + } + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.ontimeout = handler; + } + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.onloadend = handler; + } }; pub const XMLHttpRequestUpload = struct { @@ -26,17 +61,43 @@ pub const XMLHttpRequest = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; - pub fn constructor() XMLHttpRequest { - return XMLHttpRequest{}; - } - pub const UNSENT: u16 = 0; pub const OPENED: u16 = 1; pub const HEADERS_RECEIVED: u16 = 2; pub const LOADING: u16 = 3; pub const DONE: u16 = 4; + cli: Client, + impl: YieldImpl, + readyState: u16 = UNSENT, + uri: std.Uri, + headers: std.http.Headers, + asyn: bool = true, + err: ?anyerror = null, + + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !*XMLHttpRequest { + var req = try alloc.create(XMLHttpRequest); + req.* = XMLHttpRequest{ + .headers = .{ .allocator = alloc, .owned = false }, + .impl = undefined, + .uri = undefined, + // TODO retrieve the HTTP client globally to reuse existing connections. + .cli = .{ + .allocator = alloc, + .loop = loop, + }, + }; + req.impl = YieldImpl.init(loop, req); + return req; + } + + pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + self.headers.deinit(); + // TODO the client must be shared between requests. + self.cli.deinit(); + alloc.destroy(self); + } pub fn get_readyState(self: *XMLHttpRequest) u16 { return self.readyState; @@ -50,9 +111,6 @@ pub const XMLHttpRequest = struct { username: ?[]const u8, password: ?[]const u8, ) !void { - _ = self; - _ = url; - _ = asyn; _ = username; _ = password; @@ -61,6 +119,9 @@ pub const XMLHttpRequest = struct { // "InvalidStateError" DOMException. try validMethod(method); + + self.uri = try std.Uri.parse(url); + self.asyn = if (asyn) |b| b else true; } const methods = [_][]const u8{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; @@ -82,4 +143,48 @@ pub const XMLHttpRequest = struct { // If method is not a method, then throw a "SyntaxError" DOMException. return DOMError.Syntax; } + + pub fn _send(self: *XMLHttpRequest) void { + self.impl.yield(); + } + + fn onerr(self: *XMLHttpRequest, err: anyerror) void { + self.err = err; + self.readyState = DONE; + } + + pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { + if (err) |e| return self.onerr(e); + var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + defer req.deinit(); + + self.readyState = OPENED; + + req.send(.{}) catch |e| return self.onerr(e); + req.finish() catch |e| return self.onerr(e); + req.wait() catch |e| return self.onerr(e); + self.readyState = HEADERS_RECEIVED; + self.readyState = LOADING; + self.readyState = DONE; + } }; + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var send = [_]Case{ + .{ .src = + \\var nb = 0; var evt; + \\function cbk(event) { + \\ evt = event; + \\ nb ++; + \\} + , .ex = "undefined" }, + .{ .src = "const req = new XMLHttpRequest();", .ex = "undefined" }, + .{ .src = "req.onload = cbk; true;", .ex = "true" }, + .{ .src = "req.open('GET', 'https://w3.org');", .ex = "undefined" }, + .{ .src = "req.send();", .ex = "undefined" }, + }; + try checkCases(js_env, &send); +} From af20584ff23e48d8632c24f56682ea1d9e144573 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 09:58:16 +0100 Subject: [PATCH 09/46] async: remove useless http.zig Replace it with a tested sample implementation --- src/async/http.zig | 74 ----------------------------------------- src/async/test.zig | 83 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 76 insertions(+), 81 deletions(-) delete mode 100644 src/async/http.zig diff --git a/src/async/http.zig b/src/async/http.zig deleted file mode 100644 index 3cd095a7..00000000 --- a/src/async/http.zig +++ /dev/null @@ -1,74 +0,0 @@ -const std = @import("std"); -const http = std.http; -const stdcli = @import("Client.zig"); - -pub const Loop = @import("jsruntime").Loop; -const YieldImpl = Loop.Yield(Request); - -pub const Client = struct { - cli: stdcli, - - pub fn init(alloc: std.mem.Allocator, loop: *Loop) Client { - return .{ - .cli = .{ - .allocator = alloc, - .loop = loop, - }, - }; - } - - pub fn deinit(self: *Client) void { - self.cli.deinit(); - } - - pub fn create(self: *Client, uri: std.Uri) !*Request { - var req = try self.cli.allocator.create(Request); - req.* = Request{ - .impl = undefined, - .cli = &self.cli, - .uri = uri, - .headers = .{ .allocator = self.cli.allocator, .owned = false }, - }; - req.impl = YieldImpl.init(self.cli.loop, req); - return req; - } -}; - -pub const Request = struct { - cli: *stdcli, - uri: std.Uri, - headers: std.http.Headers, - - impl: YieldImpl, - done: bool = false, - err: ?anyerror = null, - - pub fn deinit(self: *Request) void { - self.headers.deinit(); - self.cli.allocator.destroy(self); - } - - pub fn fetch(self: *Request) void { - return self.impl.yield(); - } - - fn onerr(self: *Request, err: anyerror) void { - self.err = err; - } - - pub fn onYield(self: *Request, err: ?anyerror) void { - defer self.done = true; - if (err) |e| return self.onerr(e); - var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); - defer req.deinit(); - - req.send(.{}) catch |e| return self.onerr(e); - req.finish() catch |e| return self.onerr(e); - req.wait() catch |e| return self.onerr(e); - } - - pub fn wait(self: *Request) !void { - while (!self.done) try self.impl.tick(); - if (self.err) |err| return err; - } -}; diff --git a/src/async/test.zig b/src/async/test.zig index de3446a2..6038e908 100644 --- a/src/async/test.zig +++ b/src/async/test.zig @@ -1,8 +1,7 @@ const std = @import("std"); const http = std.http; -const StdClient = @import("Client.zig"); -const AsyncClient = @import("http.zig").Client; -const AsyncRequest = @import("http.zig").Request; +const Client = @import("Client.zig"); +const Request = @import("Client.zig").Request; pub const Loop = @import("jsruntime").Loop; @@ -16,7 +15,7 @@ test "blocking mode fetch API" { var loop = try Loop.init(alloc); defer loop.deinit(); - var client: StdClient = .{ + var client: Client = .{ .allocator = alloc, .loop = &loop, }; @@ -40,7 +39,7 @@ test "blocking mode open/send/wait API" { var loop = try Loop.init(alloc); defer loop.deinit(); - var client: StdClient = .{ + var client: Client = .{ .allocator = alloc, .loop = &loop, }; @@ -62,7 +61,77 @@ test "blocking mode open/send/wait API" { try std.testing.expect(req.response.status == .ok); } -test "non blocking mode API" { +// Example how to write an async http client using the modified standard client. +const AsyncClient = struct { + cli: Client, + + const YieldImpl = Loop.Yield(AsyncRequest); + const AsyncRequest = struct { + cli: *Client, + uri: std.Uri, + headers: std.http.Headers, + + impl: YieldImpl, + done: bool = false, + err: ?anyerror = null, + + pub fn deinit(self: *AsyncRequest) void { + self.headers.deinit(); + self.cli.allocator.destroy(self); + } + + pub fn fetch(self: *AsyncRequest) void { + return self.impl.yield(); + } + + fn onerr(self: *AsyncRequest, err: anyerror) void { + self.err = err; + } + + pub fn onYield(self: *AsyncRequest, err: ?anyerror) void { + defer self.done = true; + if (err) |e| return self.onerr(e); + var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + defer req.deinit(); + + req.send(.{}) catch |e| return self.onerr(e); + req.finish() catch |e| return self.onerr(e); + req.wait() catch |e| return self.onerr(e); + } + + pub fn wait(self: *AsyncRequest) !void { + while (!self.done) try self.impl.tick(); + if (self.err) |err| return err; + } + }; + + pub fn init(alloc: std.mem.Allocator, loop: *Loop) AsyncClient { + return .{ + .cli = .{ + .allocator = alloc, + .loop = loop, + }, + }; + } + + pub fn deinit(self: *AsyncClient) void { + self.cli.deinit(); + } + + pub fn create(self: *AsyncClient, uri: std.Uri) !*AsyncRequest { + var req = try self.cli.allocator.create(AsyncRequest); + req.* = AsyncRequest{ + .impl = undefined, + .cli = &self.cli, + .uri = uri, + .headers = .{ .allocator = self.cli.allocator, .owned = false }, + }; + req.impl = YieldImpl.init(self.cli.loop, req); + return req; + } +}; + +test "non blocking client" { const alloc = std.testing.allocator; var loop = try Loop.init(alloc); @@ -71,7 +140,7 @@ test "non blocking mode API" { var client = AsyncClient.init(alloc, &loop); defer client.deinit(); - var reqs: [10]*AsyncRequest = undefined; + var reqs: [10]*AsyncClient.AsyncRequest = undefined; for (0..reqs.len) |i| { reqs[i] = try client.create(try std.Uri.parse(url)); reqs[i].fetch(); From 8b6d7d0db0b632be786a64d51cb4dbd84a114011 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 11:28:52 +0100 Subject: [PATCH 10/46] xhr: implement prototype chain correctly --- src/xhr/xhr.zig | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 71db0823..c07aeb8c 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -32,6 +32,10 @@ pub const XMLHttpRequestEventTarget = struct { ontimeout_cbk: ?Callback = null, onloadend_cbk: ?Callback = null, + pub fn constructor() !XMLHttpRequestEventTarget { + return .{}; + } + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback) void { self.onloadstart_cbk = handler; } @@ -55,6 +59,8 @@ pub const XMLHttpRequestEventTarget = struct { pub const XMLHttpRequestUpload = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; + + proto: XMLHttpRequestEventTarget, }; pub const XMLHttpRequest = struct { @@ -67,6 +73,7 @@ pub const XMLHttpRequest = struct { pub const LOADING: u16 = 3; pub const DONE: u16 = 4; + proto: XMLHttpRequestEventTarget, cli: Client, impl: YieldImpl, @@ -79,6 +86,7 @@ pub const XMLHttpRequest = struct { pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !*XMLHttpRequest { var req = try alloc.create(XMLHttpRequest); req.* = XMLHttpRequest{ + .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = false }, .impl = undefined, .uri = undefined, From c2bc48ba0f212c6ac25effd63514f2409297df1f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 14:40:55 +0100 Subject: [PATCH 11/46] xhr: fix implementation errors --- src/xhr/xhr.zig | 63 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index c07aeb8c..cf41e938 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -43,16 +43,25 @@ pub const XMLHttpRequestEventTarget = struct { self.onprogress_cbk = handler; } pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onabort = handler; + self.onabort_cbk = handler; } pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onload = handler; + self.onload_cbk = handler; } pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.ontimeout = handler; + self.ontimeout_cbk = handler; } pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onloadend = handler; + self.onloadend_cbk = handler; + } + + pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { + if (self.onloadstart_cbk) |cbk| cbk.deinit(alloc); + if (self.onprogress_cbk) |cbk| cbk.deinit(alloc); + if (self.onabort_cbk) |cbk| cbk.deinit(alloc); + if (self.onload_cbk) |cbk| cbk.deinit(alloc); + if (self.ontimeout_cbk) |cbk| cbk.deinit(alloc); + if (self.onloadend_cbk) |cbk| cbk.deinit(alloc); } }; @@ -77,7 +86,8 @@ pub const XMLHttpRequest = struct { cli: Client, impl: YieldImpl, - readyState: u16 = UNSENT, + readyState: u16, + url: ?[]const u8, uri: std.Uri, headers: std.http.Headers, asyn: bool = true, @@ -89,7 +99,9 @@ pub const XMLHttpRequest = struct { .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = false }, .impl = undefined, + .url = null, .uri = undefined, + .readyState = UNSENT, // TODO retrieve the HTTP client globally to reuse existing connections. .cli = .{ .allocator = alloc, @@ -101,7 +113,9 @@ pub const XMLHttpRequest = struct { } pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + self.proto.deinit(alloc); self.headers.deinit(); + if (self.url) |url| alloc.free(url); // TODO the client must be shared between requests. self.cli.deinit(); alloc.destroy(self); @@ -113,6 +127,7 @@ pub const XMLHttpRequest = struct { pub fn _open( self: *XMLHttpRequest, + alloc: std.mem.Allocator, method: []const u8, url: []const u8, asyn: ?bool, @@ -128,8 +143,11 @@ pub const XMLHttpRequest = struct { try validMethod(method); - self.uri = try std.Uri.parse(url); + self.url = try alloc.dupe(u8, url); + self.uri = try std.Uri.parse(self.url.?); self.asyn = if (asyn) |b| b else true; + + self.readyState = OPENED; } const methods = [_][]const u8{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; @@ -166,14 +184,24 @@ pub const XMLHttpRequest = struct { var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); defer req.deinit(); - self.readyState = OPENED; - req.send(.{}) catch |e| return self.onerr(e); req.finish() catch |e| return self.onerr(e); req.wait() catch |e| return self.onerr(e); + self.readyState = HEADERS_RECEIVED; + + // TODO read response body + self.readyState = LOADING; self.readyState = DONE; + + // TODO use events instead + if (self.proto.onload_cbk) |cbk| { + // TODO pass an EventProgress + cbk.call(null) catch |e| { + std.debug.print("--- CALLBACK ERROR: {any}\n", .{e}); + }; // TODO handle error + } } }; @@ -182,17 +210,14 @@ pub fn testExecFn( js_env: *jsruntime.Env, ) anyerror!void { var send = [_]Case{ - .{ .src = - \\var nb = 0; var evt; - \\function cbk(event) { - \\ evt = event; - \\ nb ++; - \\} - , .ex = "undefined" }, - .{ .src = "const req = new XMLHttpRequest();", .ex = "undefined" }, - .{ .src = "req.onload = cbk; true;", .ex = "true" }, - .{ .src = "req.open('GET', 'https://w3.org');", .ex = "undefined" }, - .{ .src = "req.send();", .ex = "undefined" }, + .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, + .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, + .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, + .{ .src = "req.send(); nb", .ex = "0" }, + // Each case executed waits for all loop callaback calls. + // So the url has been retrieved. + .{ .src = "nb", .ex = "1" }, }; try checkCases(js_env, &send); } From 89409a48478eb733a3957177c11eb79c4c1a1005 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 14:41:23 +0100 Subject: [PATCH 12/46] xhr: add a setOnload due to a setter issue see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 --- src/xhr/xhr.zig | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index cf41e938..a1f05404 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -45,6 +45,11 @@ pub const XMLHttpRequestEventTarget = struct { pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void { self.onabort_cbk = handler; } + // TODO remove-me, test func du to an issue w/ the setter. + // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 + pub fn _setOnload(self: *XMLHttpRequestEventTarget, handler: Callback) void { + self.set_onload(handler); + } pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void { self.onload_cbk = handler; } @@ -212,7 +217,12 @@ pub fn testExecFn( var send = [_]Case{ .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + + // TODO remove-me, test func du to an issue w/ the setter. + // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 + .{ .src = "req.setOnload(cbk)", .ex = "undefined" }, + // .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, .{ .src = "req.send(); nb", .ex = "0" }, // Each case executed waits for all loop callaback calls. From 0693011ad3a04c1919b42879d3fdb3a5fbcc69c2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 15:14:55 +0100 Subject: [PATCH 13/46] async: remove context from loop impl init --- src/async/tcp.zig | 15 ++++++--------- src/async/test.zig | 14 +++++--------- src/xhr/xhr.zig | 17 +++++------------ 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/src/async/tcp.zig b/src/async/tcp.zig index b490b78f..c4284bfd 100644 --- a/src/async/tcp.zig +++ b/src/async/tcp.zig @@ -42,23 +42,20 @@ pub const Conn = struct { loop: *Loop, pub fn connect(self: *Conn, socket: std.os.socket_t, address: std.net.Address) !void { - var cmd = Command{ .impl = undefined }; - cmd.impl = NetworkImpl.init(self.loop, &cmd); - cmd.impl.connect(socket, address); + var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; + cmd.impl.connect(&cmd, socket, address); _ = try cmd.wait(); } pub fn send(self: *Conn, socket: std.os.socket_t, buffer: []const u8) !usize { - var cmd = Command{ .impl = undefined }; - cmd.impl = NetworkImpl.init(self.loop, &cmd); - cmd.impl.send(socket, buffer); + var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; + cmd.impl.send(&cmd, socket, buffer); return try cmd.wait(); } pub fn receive(self: *Conn, socket: std.os.socket_t, buffer: []u8) !usize { - var cmd = Command{ .impl = undefined }; - cmd.impl = NetworkImpl.init(self.loop, &cmd); - cmd.impl.receive(socket, buffer); + var cmd = Command{ .impl = NetworkImpl.init(self.loop) }; + cmd.impl.receive(&cmd, socket, buffer); return try cmd.wait(); } }; diff --git a/src/async/test.zig b/src/async/test.zig index 6038e908..763e43d8 100644 --- a/src/async/test.zig +++ b/src/async/test.zig @@ -77,11 +77,10 @@ const AsyncClient = struct { pub fn deinit(self: *AsyncRequest) void { self.headers.deinit(); - self.cli.allocator.destroy(self); } pub fn fetch(self: *AsyncRequest) void { - return self.impl.yield(); + return self.impl.yield(self); } fn onerr(self: *AsyncRequest, err: anyerror) void { @@ -118,16 +117,13 @@ const AsyncClient = struct { self.cli.deinit(); } - pub fn create(self: *AsyncClient, uri: std.Uri) !*AsyncRequest { - var req = try self.cli.allocator.create(AsyncRequest); - req.* = AsyncRequest{ - .impl = undefined, + pub fn create(self: *AsyncClient, uri: std.Uri) !AsyncRequest { + return .{ + .impl = YieldImpl.init(self.cli.loop), .cli = &self.cli, .uri = uri, .headers = .{ .allocator = self.cli.allocator, .owned = false }, }; - req.impl = YieldImpl.init(self.cli.loop, req); - return req; } }; @@ -140,7 +136,7 @@ test "non blocking client" { var client = AsyncClient.init(alloc, &loop); defer client.deinit(); - var reqs: [10]*AsyncClient.AsyncRequest = undefined; + var reqs: [10]AsyncClient.AsyncRequest = undefined; for (0..reqs.len) |i| { reqs[i] = try client.create(try std.Uri.parse(url)); reqs[i].fetch(); diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index a1f05404..dadc947e 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -98,23 +98,17 @@ pub const XMLHttpRequest = struct { asyn: bool = true, err: ?anyerror = null, - pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !*XMLHttpRequest { - var req = try alloc.create(XMLHttpRequest); - req.* = XMLHttpRequest{ + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { + return .{ .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = false }, - .impl = undefined, + .impl = YieldImpl.init(loop), .url = null, .uri = undefined, .readyState = UNSENT, // TODO retrieve the HTTP client globally to reuse existing connections. - .cli = .{ - .allocator = alloc, - .loop = loop, - }, + .cli = .{ .allocator = alloc, .loop = loop }, }; - req.impl = YieldImpl.init(loop, req); - return req; } pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { @@ -123,7 +117,6 @@ pub const XMLHttpRequest = struct { if (self.url) |url| alloc.free(url); // TODO the client must be shared between requests. self.cli.deinit(); - alloc.destroy(self); } pub fn get_readyState(self: *XMLHttpRequest) u16 { @@ -176,7 +169,7 @@ pub const XMLHttpRequest = struct { } pub fn _send(self: *XMLHttpRequest) void { - self.impl.yield(); + self.impl.yield(self); } fn onerr(self: *XMLHttpRequest, err: anyerror) void { From 6f46d76c93239e188a73b57ad1d6dc016d463ff6 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 16:49:46 +0100 Subject: [PATCH 14/46] xhr: implementation follow up --- src/run_tests.zig | 2 +- src/xhr/xhr.zig | 102 +++++++++++++++++++++++++++++++++++++--------- 2 files changed, 83 insertions(+), 21 deletions(-) diff --git a/src/run_tests.zig b/src/run_tests.zig index 86e09a8f..4e22c04f 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -158,7 +158,7 @@ test "Window is a libdom event target" { test "XMLHttpRequest.validMethod" { // valid methods for ([_][]const u8{ "get", "GET", "head", "HEAD" }) |tc| { - try xhr.XMLHttpRequest.validMethod(tc); + _ = try xhr.XMLHttpRequest.validMethod(tc); } // forbidden diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index dadc947e..ad8a11c4 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -91,21 +91,30 @@ pub const XMLHttpRequest = struct { cli: Client, impl: YieldImpl, - readyState: u16, + method: std.http.Method, + state: u16, url: ?[]const u8, uri: std.Uri, headers: std.http.Headers, - asyn: bool = true, + sync: bool = true, err: ?anyerror = null, + upload: ?XMLHttpRequestUpload = null, + timeout: u32 = 0, + withCredentials: bool = false, + // TODO: response readonly attribute any response; + response_bytes: ?[]const u8 = null, + send_flag: bool = false, + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { return .{ .proto = try XMLHttpRequestEventTarget.constructor(), - .headers = .{ .allocator = alloc, .owned = false }, + .headers = .{ .allocator = alloc, .owned = true }, .impl = YieldImpl.init(loop), + .method = undefined, .url = null, .uri = undefined, - .readyState = UNSENT, + .state = UNSENT, // TODO retrieve the HTTP client globally to reuse existing connections. .cli = .{ .allocator = alloc, .loop = loop }, }; @@ -114,13 +123,37 @@ pub const XMLHttpRequest = struct { pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { self.proto.deinit(alloc); self.headers.deinit(); - if (self.url) |url| alloc.free(url); + if (self.url) |v| alloc.free(v); + if (self.response_bytes) |v| alloc.free(v); // TODO the client must be shared between requests. self.cli.deinit(); } pub fn get_readyState(self: *XMLHttpRequest) u16 { - return self.readyState; + return self.state; + } + + pub fn get_timeout(self: *XMLHttpRequest) u32 { + return self.timeout; + } + + pub fn set_timeout(self: *XMLHttpRequest, timeout: u32) !void { + // TODO If the current global object is a Window object and this’s + // synchronous flag is set, then throw an "InvalidAccessError" + // DOMException. + // https://xhr.spec.whatwg.org/#dom-xmlhttprequest-timeout + self.timeout = timeout; + } + + pub fn get_withCredentials(self: *XMLHttpRequest) bool { + return self.withCredentials; + } + + pub fn set_withCredentials(self: *XMLHttpRequest, withCredentials: bool) !void { + if (self.state != OPENED and self.state != UNSENT) return DOMError.InvalidState; + if (self.send_flag) return DOMError.InvalidState; + + self.withCredentials = withCredentials; } pub fn _open( @@ -139,22 +172,35 @@ pub const XMLHttpRequest = struct { // associated Document is not fully active, then throw an // "InvalidStateError" DOMException. - try validMethod(method); + self.method = try validMethod(method); self.url = try alloc.dupe(u8, url); - self.uri = try std.Uri.parse(self.url.?); - self.asyn = if (asyn) |b| b else true; + self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax; + self.sync = if (asyn) |b| !b else false; + self.send_flag = false; - self.readyState = OPENED; + if (self.response_bytes) |v| alloc.free(v); + + self.state = OPENED; } - const methods = [_][]const u8{ "DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT" }; + const methods = [_]struct { + tag: std.http.Method, + name: []const u8, + }{ + .{ .tag = .DELETE, .name = "DELETE" }, + .{ .tag = .GET, .name = "GET" }, + .{ .tag = .HEAD, .name = "HEAD" }, + .{ .tag = .OPTIONS, .name = "OPTIONS" }, + .{ .tag = .POST, .name = "POST" }, + .{ .tag = .PUT, .name = "PUT" }, + }; const methods_forbidden = [_][]const u8{ "CONNECT", "TRACE", "TRACK" }; - pub fn validMethod(m: []const u8) DOMError!void { + pub fn validMethod(m: []const u8) DOMError!std.http.Method { for (methods) |method| { - if (std.ascii.eqlIgnoreCase(method, m)) { - return; + if (std.ascii.eqlIgnoreCase(method.name, m)) { + return method.tag; } } // If method is a forbidden method, then throw a "SecurityError" DOMException. @@ -168,30 +214,45 @@ pub const XMLHttpRequest = struct { return DOMError.Syntax; } - pub fn _send(self: *XMLHttpRequest) void { + pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void { + return try self.headers.append(name, value); + } + + // TODO body can be either a string or a document + pub fn _send(self: *XMLHttpRequest, body: ?[]const u8) !void { + if (self.state != OPENED) return DOMError.InvalidState; + if (self.send_flag) return DOMError.InvalidState; + + // The body argument provides the request body, if any, and is ignored + // if the request method is GET or HEAD. + // https://xhr.spec.whatwg.org/#the-send()-method + _ = body; + // TODO set Content-Type header according to the given body. + + self.send_flag = true; self.impl.yield(self); } fn onerr(self: *XMLHttpRequest, err: anyerror) void { self.err = err; - self.readyState = DONE; + self.state = DONE; } pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { if (err) |e| return self.onerr(e); - var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + var req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e); defer req.deinit(); req.send(.{}) catch |e| return self.onerr(e); req.finish() catch |e| return self.onerr(e); req.wait() catch |e| return self.onerr(e); - self.readyState = HEADERS_RECEIVED; + self.state = HEADERS_RECEIVED; // TODO read response body - self.readyState = LOADING; - self.readyState = DONE; + self.state = LOADING; + self.state = DONE; // TODO use events instead if (self.proto.onload_cbk) |cbk| { @@ -210,6 +271,7 @@ pub fn testExecFn( var send = [_]Case{ .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, + .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, // TODO remove-me, test func du to an issue w/ the setter. // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 From 3915c609134ec078fef353eab39242a467f42a0a Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 18:07:22 +0100 Subject: [PATCH 15/46] xhr: handle response headers --- src/xhr/xhr.zig | 65 +++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index ad8a11c4..066b93ab 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -87,6 +87,16 @@ pub const XMLHttpRequest = struct { pub const LOADING: u16 = 3; pub const DONE: u16 = 4; + // https://xhr.spec.whatwg.org/#response-type + const ResponseType = enum { + Empty, + Text, + ArrayBuffer, + Blob, + Document, + JSON, + }; + proto: XMLHttpRequestEventTarget, cli: Client, impl: YieldImpl, @@ -104,12 +114,15 @@ pub const XMLHttpRequest = struct { withCredentials: bool = false, // TODO: response readonly attribute any response; response_bytes: ?[]const u8 = null, + response_type: ResponseType = .Empty, + response_headers: std.http.Headers, send_flag: bool = false, pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { return .{ .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = true }, + .response_headers = .{ .allocator = alloc, .owned = true }, .impl = YieldImpl.init(loop), .method = undefined, .url = null, @@ -123,8 +136,10 @@ pub const XMLHttpRequest = struct { pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { self.proto.deinit(alloc); self.headers.deinit(); + self.response_headers.deinit(); if (self.url) |v| alloc.free(v); if (self.response_bytes) |v| alloc.free(v); + if (self.response_headers) |v| alloc.free(v); // TODO the client must be shared between requests. self.cli.deinit(); } @@ -179,6 +194,11 @@ pub const XMLHttpRequest = struct { self.sync = if (asyn) |b| !b else false; self.send_flag = false; + // TODO should we clearRetainingCapacity instead? + self.headers.clearAndFree(); + self.response_headers.clearAndFree(); + + self.response_type = .Empty; if (self.response_bytes) |v| alloc.free(v); self.state = OPENED; @@ -215,6 +235,8 @@ pub const XMLHttpRequest = struct { } pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void { + if (self.state != OPENED) return DOMError.InvalidState; + if (self.send_flag) return DOMError.InvalidState; return try self.headers.append(name, value); } @@ -233,11 +255,6 @@ pub const XMLHttpRequest = struct { self.impl.yield(self); } - fn onerr(self: *XMLHttpRequest, err: anyerror) void { - self.err = err; - self.state = DONE; - } - pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { if (err) |e| return self.onerr(e); var req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e); @@ -247,11 +264,12 @@ pub const XMLHttpRequest = struct { req.finish() catch |e| return self.onerr(e); req.wait() catch |e| return self.onerr(e); + self.response_headers = req.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e); + self.state = HEADERS_RECEIVED; - // TODO read response body - self.state = LOADING; + self.state = DONE; // TODO use events instead @@ -262,6 +280,36 @@ pub const XMLHttpRequest = struct { }; // TODO handle error } } + + fn onerr(self: *XMLHttpRequest, err: anyerror) void { + self.err = err; + self.state = DONE; + } + + pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { + if (self.state != LOADING and self.state != DONE) return DOMError.InvalidState; + if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; + return if (self.response_bytes) |v| v else ""; + } + + // the caller owns the string. + pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 { + self.response_headers.sort(); + + var buf: std.ArrayListUnmanaged(u8) = .{}; + const w = buf.writer(alloc); + + for (self.response_headers.list.items) |entry| { + if (entry.value.len == 0) continue; + + try w.writeAll(entry.name); + try w.writeAll(": "); + try w.writeAll(entry.value); + try w.writeAll("\r\n"); + } + + return buf.items; + } }; pub fn testExecFn( @@ -271,7 +319,6 @@ pub fn testExecFn( var send = [_]Case{ .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, // TODO remove-me, test func du to an issue w/ the setter. // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 @@ -279,10 +326,12 @@ pub fn testExecFn( // .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, + .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, .{ .src = "req.send(); nb", .ex = "0" }, // Each case executed waits for all loop callaback calls. // So the url has been retrieved. .{ .src = "nb", .ex = "1" }, + .{ .src = "req.getAllResponseHeaders()", .ex = "undefined" }, }; try checkCases(js_env, &send); } From 5bd5905da7fe9679fbd460eda46cbcf055f99c57 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 31 Jan 2024 18:35:54 +0100 Subject: [PATCH 16/46] xhr: implement responseText --- src/xhr/xhr.zig | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 066b93ab..f80bf42c 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -98,6 +98,7 @@ pub const XMLHttpRequest = struct { }; proto: XMLHttpRequestEventTarget, + alloc: std.mem.Allocator, cli: Client, impl: YieldImpl, @@ -120,6 +121,7 @@ pub const XMLHttpRequest = struct { pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { return .{ + .alloc = alloc, .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = true }, .response_headers = .{ .allocator = alloc, .owned = true }, @@ -270,6 +272,23 @@ pub const XMLHttpRequest = struct { self.state = LOADING; + var buf: std.ArrayListUnmanaged(u8) = .{}; + + const reader = req.reader(); + var buffer: [1024]u8 = undefined; + var ln = buffer.len; + 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); + }; + } + self.response_bytes = buf.items; + self.state = DONE; // TODO use events instead @@ -289,14 +308,20 @@ pub const XMLHttpRequest = struct { pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { if (self.state != LOADING and self.state != DONE) return DOMError.InvalidState; if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; + return if (self.response_bytes) |v| v else ""; } - // the caller owns the string. + // The caller owns the string returned. + // TODO change the return type to express the string ownership and let + // jsruntime free the string once copied to v8. + // see https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 { self.response_headers.sort(); var buf: std.ArrayListUnmanaged(u8) = .{}; + errdefer buf.deinit(alloc); + const w = buf.writer(alloc); for (self.response_headers.list.items) |entry| { @@ -331,7 +356,8 @@ pub fn testExecFn( // Each case executed waits for all loop callaback calls. // So the url has been retrieved. .{ .src = "nb", .ex = "1" }, - .{ .src = "req.getAllResponseHeaders()", .ex = "undefined" }, + .{ .src = "req.getAllResponseHeaders().length > 1024", .ex = "true" }, + .{ .src = "req.responseText.length > 1024", .ex = "true" }, }; try checkCases(js_env, &send); } From 19b459b4dbd483d89471ae0c16569442ae40f140 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 2 Feb 2024 15:11:56 +0100 Subject: [PATCH 17/46] xhr: add status and statusText --- src/xhr/xhr.zig | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index f80bf42c..ce623437 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -117,6 +117,7 @@ pub const XMLHttpRequest = struct { response_bytes: ?[]const u8 = null, response_type: ResponseType = .Empty, response_headers: std.http.Headers, + response_status: u10 = 0, send_flag: bool = false, pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { @@ -199,6 +200,7 @@ pub const XMLHttpRequest = struct { // TODO should we clearRetainingCapacity instead? self.headers.clearAndFree(); self.response_headers.clearAndFree(); + self.response_status = 0; self.response_type = .Empty; if (self.response_bytes) |v| alloc.free(v); @@ -270,6 +272,8 @@ pub const XMLHttpRequest = struct { self.state = HEADERS_RECEIVED; + self.response_status = @intFromEnum(req.response.status); + self.state = LOADING; var buf: std.ArrayListUnmanaged(u8) = .{}; @@ -306,7 +310,6 @@ pub const XMLHttpRequest = struct { } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { - if (self.state != LOADING and self.state != DONE) return DOMError.InvalidState; if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; return if (self.response_bytes) |v| v else ""; @@ -317,6 +320,7 @@ pub const XMLHttpRequest = struct { // jsruntime free the string once copied to v8. // see https://github.com/lightpanda-io/jsruntime-lib/issues/195 pub fn _getAllResponseHeaders(self: *XMLHttpRequest, alloc: std.mem.Allocator) ![]const u8 { + if (self.response_headers.list.items.len == 0) return ""; self.response_headers.sort(); var buf: std.ArrayListUnmanaged(u8) = .{}; @@ -335,6 +339,16 @@ pub const XMLHttpRequest = struct { return buf.items; } + + pub fn get_status(self: *XMLHttpRequest) u16 { + return self.response_status; + } + + pub fn get_statusText(self: *XMLHttpRequest) []const u8 { + if (self.response_status == 0) return ""; + + return std.http.Status.phrase(@enumFromInt(self.response_status)) orelse ""; + } }; pub fn testExecFn( @@ -352,10 +366,20 @@ pub fn testExecFn( .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, + + // ensure open resets values + .{ .src = "req.status", .ex = "0" }, + .{ .src = "req.statusText", .ex = "" }, + .{ .src = "req.getAllResponseHeaders()", .ex = "" }, + .{ .src = "req.responseText", .ex = "" }, + .{ .src = "req.send(); nb", .ex = "0" }, + // Each case executed waits for all loop callaback calls. // So the url has been retrieved. .{ .src = "nb", .ex = "1" }, + .{ .src = "req.status", .ex = "200" }, + .{ .src = "req.statusText", .ex = "OK" }, .{ .src = "req.getAllResponseHeaders().length > 1024", .ex = "true" }, .{ .src = "req.responseText.length > 1024", .ex = "true" }, }; From cac1110993b4ea1d27ddc9b9928a9627393b983a Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 2 Feb 2024 15:21:33 +0100 Subject: [PATCH 18/46] xhr: add getResponseHeader --- src/xhr/xhr.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index ce623437..761f60e0 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -315,6 +315,10 @@ pub const XMLHttpRequest = struct { return if (self.response_bytes) |v| v else ""; } + pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 { + return self.response_headers.getFirstValue(name); + } + // The caller owns the string returned. // TODO change the return type to express the string ownership and let // jsruntime free the string once copied to v8. @@ -371,6 +375,7 @@ pub fn testExecFn( .{ .src = "req.status", .ex = "0" }, .{ .src = "req.statusText", .ex = "" }, .{ .src = "req.getAllResponseHeaders()", .ex = "" }, + .{ .src = "req.getResponseHeader('Content-Type')", .ex = "null" }, .{ .src = "req.responseText", .ex = "" }, .{ .src = "req.send(); nb", .ex = "0" }, @@ -380,6 +385,7 @@ pub fn testExecFn( .{ .src = "nb", .ex = "1" }, .{ .src = "req.status", .ex = "200" }, .{ .src = "req.statusText", .ex = "OK" }, + .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=UTF-8" }, .{ .src = "req.getAllResponseHeaders().length > 1024", .ex = "true" }, .{ .src = "req.responseText.length > 1024", .ex = "true" }, }; From f3a1920d8fd2da7405e7c6fe549cf8c7ce3eb5e2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 6 Feb 2024 15:42:13 +0100 Subject: [PATCH 19/46] async: yield between fetch steps in test cli --- src/async/test.zig | 51 +++++++++++++++++++++++++++++++++------------- 1 file changed, 37 insertions(+), 14 deletions(-) diff --git a/src/async/test.zig b/src/async/test.zig index 763e43d8..56bfc09c 100644 --- a/src/async/test.zig +++ b/src/async/test.zig @@ -5,8 +5,6 @@ const Request = @import("Client.zig").Request; pub const Loop = @import("jsruntime").Loop; -const TCPClient = @import("tcp.zig").Client; - const url = "https://w3.org"; test "blocking mode fetch API" { @@ -67,39 +65,65 @@ const AsyncClient = struct { const YieldImpl = Loop.Yield(AsyncRequest); const AsyncRequest = struct { + const State = enum { new, open, send, finish, wait, done }; + cli: *Client, uri: std.Uri, headers: std.http.Headers, + req: ?Request = undefined, + state: State = .new, + impl: YieldImpl, - done: bool = false, err: ?anyerror = null, pub fn deinit(self: *AsyncRequest) void { + if (self.req) |*r| r.deinit(); self.headers.deinit(); } pub fn fetch(self: *AsyncRequest) void { + self.state = .new; return self.impl.yield(self); } fn onerr(self: *AsyncRequest, err: anyerror) void { + self.state = .done; self.err = err; } pub fn onYield(self: *AsyncRequest, err: ?anyerror) void { - defer self.done = true; if (err) |e| return self.onerr(e); - var req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); - defer req.deinit(); - req.send(.{}) catch |e| return self.onerr(e); - req.finish() catch |e| return self.onerr(e); - req.wait() catch |e| return self.onerr(e); + switch (self.state) { + .new => { + self.state = .open; + self.req = self.cli.open(.GET, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + }, + .open => { + self.state = .send; + self.req.?.send(.{}) catch |e| return self.onerr(e); + }, + .send => { + self.state = .finish; + self.req.?.finish() catch |e| return self.onerr(e); + }, + .finish => { + self.state = .wait; + self.req.?.wait() catch |e| return self.onerr(e); + }, + .wait => { + self.state = .done; + return; + }, + .done => return, + } + + return self.impl.yield(self); } pub fn wait(self: *AsyncRequest) !void { - while (!self.done) try self.impl.tick(); + while (self.state != .done) try self.impl.tick(); if (self.err) |err| return err; } }; @@ -117,7 +141,7 @@ const AsyncClient = struct { self.cli.deinit(); } - pub fn create(self: *AsyncClient, uri: std.Uri) !AsyncRequest { + pub fn createRequest(self: *AsyncClient, uri: std.Uri) !AsyncRequest { return .{ .impl = YieldImpl.init(self.cli.loop), .cli = &self.cli, @@ -136,12 +160,11 @@ test "non blocking client" { var client = AsyncClient.init(alloc, &loop); defer client.deinit(); - var reqs: [10]AsyncClient.AsyncRequest = undefined; + var reqs: [3]AsyncClient.AsyncRequest = undefined; for (0..reqs.len) |i| { - reqs[i] = try client.create(try std.Uri.parse(url)); + reqs[i] = try client.createRequest(try std.Uri.parse(url)); reqs[i].fetch(); } - for (0..reqs.len) |i| { try reqs[i].wait(); reqs[i].deinit(); From 8a61f0f4549e66484a2e9eee94bacf5b725e0fb0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 6 Feb 2024 16:01:48 +0100 Subject: [PATCH 20/46] xhr: yield each fetch steps --- src/xhr/xhr.zig | 111 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 79 insertions(+), 32 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 761f60e0..d47a5054 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -97,11 +97,16 @@ pub const XMLHttpRequest = struct { JSON, }; + const PrivState = enum { new, open, send, finish, wait, done }; + proto: XMLHttpRequestEventTarget, alloc: std.mem.Allocator, cli: Client, impl: YieldImpl, + priv_state: PrivState = .new, + req: ?Client.Request = null, + method: std.http.Method, state: u16, url: ?[]const u8, @@ -143,6 +148,8 @@ pub const XMLHttpRequest = struct { if (self.url) |v| alloc.free(v); if (self.response_bytes) |v| alloc.free(v); if (self.response_headers) |v| alloc.free(v); + + if (self.req) |*r| r.deinit(); // TODO the client must be shared between requests. self.cli.deinit(); } @@ -206,6 +213,11 @@ pub const XMLHttpRequest = struct { if (self.response_bytes) |v| alloc.free(v); self.state = OPENED; + self.priv_state = .new; + if (self.req) |*r| { + r.deinit(); + self.req = null; + } } const methods = [_]struct { @@ -259,54 +271,89 @@ pub const XMLHttpRequest = struct { self.impl.yield(self); } + // onYield is a callback called between each request's steps. + // Between each step, the code is blocking. + // Yielding allows pseudo-async and gives a chance to other async process + // to be called. pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { if (err) |e| return self.onerr(e); - var req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e); - defer req.deinit(); - req.send(.{}) catch |e| return self.onerr(e); - req.finish() catch |e| return self.onerr(e); - req.wait() catch |e| return self.onerr(e); + switch (self.priv_state) { + .new => { + self.priv_state = .open; + self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + }, + .open => { + self.priv_state = .send; + self.req.?.send(.{}) catch |e| return self.onerr(e); + }, + .send => { + self.priv_state = .finish; + self.req.?.finish() catch |e| return self.onerr(e); + }, + .finish => { + self.priv_state = .wait; + self.req.?.wait() catch |e| return self.onerr(e); + }, + .wait => { + self.priv_state = .done; + self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e); - self.response_headers = req.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e); + self.state = HEADERS_RECEIVED; - self.state = HEADERS_RECEIVED; + self.response_status = @intFromEnum(self.req.?.response.status); - self.response_status = @intFromEnum(req.response.status); + self.state = LOADING; - self.state = LOADING; + var buf: std.ArrayListUnmanaged(u8) = .{}; - var buf: std.ArrayListUnmanaged(u8) = .{}; + const reader = self.req.?.reader(); + var buffer: [1024]u8 = undefined; + var ln = buffer.len; + 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); + }; + } + self.response_bytes = buf.items; - const reader = req.reader(); - var buffer: [1024]u8 = undefined; - var ln = buffer.len; - 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); - }; + self.state = DONE; + }, + .done => { + if (self.req) |*r| { + r.deinit(); + self.req = null; + } + + // TODO use events instead + if (self.proto.onload_cbk) |cbk| { + // TODO pass an EventProgress + cbk.call(null) catch |e| { + std.debug.print("--- CALLBACK ERROR: {any}\n", .{e}); + }; // TODO handle error + } + + // finalize fetch process. + return; + }, } - self.response_bytes = buf.items; - self.state = DONE; - - // TODO use events instead - if (self.proto.onload_cbk) |cbk| { - // TODO pass an EventProgress - cbk.call(null) catch |e| { - std.debug.print("--- CALLBACK ERROR: {any}\n", .{e}); - }; // TODO handle error - } + self.impl.yield(self); } fn onerr(self: *XMLHttpRequest, err: anyerror) void { self.err = err; self.state = DONE; + self.priv_state = .done; + if (self.req) |*r| { + r.deinit(); + self.req = null; + } } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { From f79189131491ef23345bf78aa963772af5666ef4 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 6 Feb 2024 17:42:28 +0100 Subject: [PATCH 21/46] xhr: dispatch generic events --- src/xhr/xhr.zig | 114 ++++++++++++++++++++++++++++++------------------ 1 file changed, 71 insertions(+), 43 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index d47a5054..b4d7e1c1 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -13,6 +13,13 @@ const Loop = jsruntime.Loop; const YieldImpl = Loop.Yield(XMLHttpRequest); const Client = @import("../async/Client.zig"); +const parser = @import("../netsurf.zig"); +const c = @cImport({ + @cInclude("events/event_target.h"); +}); + +const log = std.log.scoped(.xhr); + // XHR interfaces // https://xhr.spec.whatwg.org/#interface-xmlhttprequest pub const Interfaces = generate.Tuple(.{ @@ -25,48 +32,42 @@ pub const XMLHttpRequestEventTarget = struct { pub const prototype = *EventTarget; pub const mem_guarantied = true; - onloadstart_cbk: ?Callback = null, - onprogress_cbk: ?Callback = null, - onabort_cbk: ?Callback = null, - onload_cbk: ?Callback = null, - ontimeout_cbk: ?Callback = null, - onloadend_cbk: ?Callback = null, + // Extend libdom event target for pure zig struct. + base: parser.EventTargetTBase = parser.EventTargetTBase{}, - pub fn constructor() !XMLHttpRequestEventTarget { - return .{}; + fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { + try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false); } - pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onloadstart_cbk = handler; + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "loadstart", handler); } - pub fn set_onprogress(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onprogress_cbk = handler; + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "progress", handler); } - pub fn set_onabort(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onabort_cbk = handler; + pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "abort", handler); } // TODO remove-me, test func du to an issue w/ the setter. // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 - pub fn _setOnload(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.set_onload(handler); + pub fn _setOnload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.set_onload(alloc, handler); } - pub fn set_onload(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onload_cbk = handler; + + pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "load", handler); } - pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.ontimeout_cbk = handler; + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "timeout", handler); } - pub fn set_onloadend(self: *XMLHttpRequestEventTarget, handler: Callback) void { - self.onloadend_cbk = handler; + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + try self.register(alloc, "loadend", handler); } pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { - if (self.onloadstart_cbk) |cbk| cbk.deinit(alloc); - if (self.onprogress_cbk) |cbk| cbk.deinit(alloc); - if (self.onabort_cbk) |cbk| cbk.deinit(alloc); - if (self.onload_cbk) |cbk| cbk.deinit(alloc); - if (self.ontimeout_cbk) |cbk| cbk.deinit(alloc); - if (self.onloadend_cbk) |cbk| cbk.deinit(alloc); + parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| { + log.err("remove all listeners: {any}", .{e}); + }; } }; @@ -74,7 +75,7 @@ pub const XMLHttpRequestUpload = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; - proto: XMLHttpRequestEventTarget, + proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; pub const XMLHttpRequest = struct { @@ -99,7 +100,7 @@ pub const XMLHttpRequest = struct { const PrivState = enum { new, open, send, finish, wait, done }; - proto: XMLHttpRequestEventTarget, + proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, alloc: std.mem.Allocator, cli: Client, impl: YieldImpl, @@ -128,7 +129,6 @@ pub const XMLHttpRequest = struct { pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { return .{ .alloc = alloc, - .proto = try XMLHttpRequestEventTarget.constructor(), .headers = .{ .allocator = alloc, .owned = true }, .response_headers = .{ .allocator = alloc, .owned = true }, .impl = YieldImpl.init(loop), @@ -218,6 +218,22 @@ pub const XMLHttpRequest = struct { r.deinit(); self.req = null; } + + self.dispatchEvt("readystatechange"); + } + + // dispatch request event. + // errors are logged only. + fn dispatchEvt(self: *XMLHttpRequest, typ: []const u8) void { + const evt = parser.eventCreate() catch |e| { + return log.err("dispatch event create: {any}", .{e}); + }; + parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true }) catch |e| { + return log.err("dispatch event init: {any}", .{e}); + }; + _ = parser.eventTargetDispatchEvent(@as(*parser.EventTarget, @ptrCast(self)), evt) catch |e| { + return log.err("dispatch event: {any}", .{e}); + }; } const methods = [_]struct { @@ -300,13 +316,14 @@ pub const XMLHttpRequest = struct { self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e); self.state = HEADERS_RECEIVED; + self.dispatchEvt("readystatechange"); self.response_status = @intFromEnum(self.req.?.response.status); - self.state = LOADING; - var buf: std.ArrayListUnmanaged(u8) = .{}; + // TODO dispatch a progress event loadstart. + const reader = self.req.?.reader(); var buffer: [1024]u8 = undefined; var ln = buffer.len; @@ -319,10 +336,25 @@ pub const XMLHttpRequest = struct { buf.deinit(self.alloc); return self.onerr(e); }; + + // TODO dispatch only if 50ms have passed. + + self.state = LOADING; + self.dispatchEvt("readystatechange"); + + // TODO dispatch a progress event progress. + self.dispatchEvt("progress"); } self.response_bytes = buf.items; + self.send_flag = false; self.state = DONE; + self.dispatchEvt("readystatechange"); + + // TODO dispatch a progress event load. + self.dispatchEvt("load"); + // TODO dispatch a progress event loadend. + self.dispatchEvt("loadend"); }, .done => { if (self.req) |*r| { @@ -330,14 +362,6 @@ pub const XMLHttpRequest = struct { self.req = null; } - // TODO use events instead - if (self.proto.onload_cbk) |cbk| { - // TODO pass an EventProgress - cbk.call(null) catch |e| { - std.debug.print("--- CALLBACK ERROR: {any}\n", .{e}); - }; // TODO handle error - } - // finalize fetch process. return; }, @@ -347,13 +371,17 @@ pub const XMLHttpRequest = struct { } fn onerr(self: *XMLHttpRequest, err: anyerror) void { - self.err = err; - self.state = DONE; self.priv_state = .done; if (self.req) |*r| { r.deinit(); self.req = null; } + + self.err = err; + self.state = DONE; + self.send_flag = false; + self.dispatchEvt("readystatechange"); + self.dispatchEvt("error"); } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { From 554a05d8dda72e039c1e1ca41c18653715b5a7ba Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 7 Feb 2024 14:03:08 +0100 Subject: [PATCH 22/46] xhr: fix getter/setter for callbacks --- src/xhr/xhr.zig | 54 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index b4d7e1c1..d908678c 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -35,32 +35,69 @@ pub const XMLHttpRequestEventTarget = struct { // Extend libdom event target for pure zig struct. base: parser.EventTargetTBase = parser.EventTargetTBase{}, + onloadstart_cbk: ?Callback = null, + onprogress_cbk: ?Callback = null, + onabort_cbk: ?Callback = null, + onload_cbk: ?Callback = null, + ontimeout_cbk: ?Callback = null, + onloadend_cbk: ?Callback = null, + fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false); } + fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { + const et = @as(*parser.EventTarget, @ptrCast(self)); + // check if event target has already this listener + const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id()); + if (lst == null) { + return; + } + + // remove listener + try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); + } + + pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onloadstart_cbk; + } + pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onprogress_cbk; + } + pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onabort_cbk; + } + pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onload_cbk; + } + pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback { + return self.ontimeout_cbk; + } + pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onloadend_cbk; + } pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); try self.register(alloc, "loadstart", handler); } pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); try self.register(alloc, "progress", handler); } pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); try self.register(alloc, "abort", handler); } - // TODO remove-me, test func du to an issue w/ the setter. - // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 - pub fn _setOnload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - try self.set_onload(alloc, handler); - } - pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); try self.register(alloc, "load", handler); } pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); try self.register(alloc, "timeout", handler); } pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); try self.register(alloc, "loadend", handler); } @@ -438,9 +475,8 @@ pub fn testExecFn( .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, - // TODO remove-me, test func du to an issue w/ the setter. - // see https://lightpanda.slack.com/archives/C05TRU6RBM1/p1706708213838989 - .{ .src = "req.setOnload(cbk)", .ex = "undefined" }, + .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; }" }, // .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, From 86a69da773e590549adcd7dc284b9fa6359fdf1c Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 7 Feb 2024 18:01:39 +0100 Subject: [PATCH 23/46] xhr: add ProgressEvent type --- src/netsurf.zig | 5 +++ src/xhr/xhr.zig | 98 +++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/netsurf.zig b/src/netsurf.zig index 035912d7..5ecbb155 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -4,6 +4,7 @@ const c = @cImport({ @cInclude("dom/dom.h"); @cInclude("dom/bindings/hubbub/parser.h"); @cInclude("events/event_target.h"); + @cInclude("events/event.h"); }); const Callback = @import("jsruntime").Callback; @@ -360,6 +361,10 @@ pub const EventInit = struct { composed: bool = false, }; +pub fn eventDestroy(evt: *Event) void { + c._dom_event_destroy(evt); +} + pub fn eventInit(evt: *Event, typ: []const u8, opts: EventInit) !void { const s = try strFromData(typ); const err = c._dom_event_init(evt, s, opts.bubbles, opts.cancelable); diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index d908678c..38099376 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -6,8 +6,10 @@ const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); const EventTarget = @import("../dom/event_target.zig").EventTarget; +const Event = @import("../events/event.zig").Event; const Callback = jsruntime.Callback; const DOMError = @import("../netsurf.zig").DOMError; +const DOMException = @import("../dom/exceptions.zig").DOMException; const Loop = jsruntime.Loop; const YieldImpl = Loop.Yield(XMLHttpRequest); @@ -26,6 +28,8 @@ pub const Interfaces = generate.Tuple(.{ XMLHttpRequestEventTarget, XMLHttpRequestUpload, XMLHttpRequest, + ProgressEvent, + ProgressEventInit, }); pub const XMLHttpRequestEventTarget = struct { @@ -115,6 +119,50 @@ pub const XMLHttpRequestUpload = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; +pub const ProgressEventInit = struct { + pub const mem_guarantied = true; + + lengthComputable: bool = false, + loaded: u64 = 0, + total: u64 = 0, +}; + +pub const ProgressEvent = struct { + pub const prototype = *Event; + pub const Exception = DOMException; + pub const mem_guarantied = true; + + proto: parser.Event, + lengthComputable: bool, + loaded: u64 = 0, + total: u64 = 0, + + pub fn constructor(eventType: []const u8, opts: ProgressEventInit) !ProgressEvent { + const event = try parser.eventCreate(); + defer parser.eventDestroy(event); + try parser.eventInit(event, eventType, .{}); + + return .{ + .proto = event.*, + .lengthComputable = opts.lengthComputable, + .loaded = opts.loaded, + .total = opts.total, + }; + } + + pub fn get_lengthComputable(self: ProgressEvent) bool { + return self.lengthComputable; + } + + pub fn get_loaded(self: ProgressEvent) u64 { + return self.loaded; + } + + pub fn get_total(self: ProgressEvent) u64 { + return self.total; + } +}; + pub const XMLHttpRequest = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; @@ -273,6 +321,31 @@ pub const XMLHttpRequest = struct { }; } + fn dispatchProgressEvent( + self: *XMLHttpRequest, + typ: []const u8, + opts: ProgressEventInit, + ) void { + // TODO destroy struct + const evt = self.alloc.create(ProgressEvent) catch |e| { + return log.err("allocate progress event: {any}", .{e}); + }; + evt.* = ProgressEvent.constructor(typ, .{ + // https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface + .lengthComputable = opts.total > 0, + .total = opts.total, + .loaded = opts.loaded, + }) catch |e| { + return log.err("construct progress event: {any}", .{e}); + }; + _ = parser.eventTargetDispatchEvent( + @as(*parser.EventTarget, @ptrCast(self)), + @as(*parser.Event, @ptrCast(evt)), + ) catch |e| { + return log.err("dispatch progress event: {any}", .{e}); + }; + } + const methods = [_]struct { tag: std.http.Method, name: []const u8, @@ -359,7 +432,12 @@ pub const XMLHttpRequest = struct { var buf: std.ArrayListUnmanaged(u8) = .{}; - // TODO dispatch a progress event loadstart. + // TODO set correct length + const total = 0; + var loaded: u64 = 0; + + // dispatch a progress event loadstart. + self.dispatchProgressEvent("loadstart", .{ .loaded = loaded, .total = total }); const reader = self.req.?.reader(); var buffer: [1024]u8 = undefined; @@ -373,14 +451,18 @@ pub const XMLHttpRequest = struct { buf.deinit(self.alloc); return self.onerr(e); }; + loaded = loaded + ln; // TODO dispatch only if 50ms have passed. self.state = LOADING; self.dispatchEvt("readystatechange"); - // TODO dispatch a progress event progress. - self.dispatchEvt("progress"); + // dispatch a progress event progress. + self.dispatchProgressEvent("progress", .{ + .loaded = loaded, + .total = total, + }); } self.response_bytes = buf.items; self.send_flag = false; @@ -388,10 +470,11 @@ pub const XMLHttpRequest = struct { self.state = DONE; self.dispatchEvt("readystatechange"); - // TODO dispatch a progress event load. + // dispatch a progress event load. self.dispatchEvt("load"); - // TODO dispatch a progress event loadend. - self.dispatchEvt("loadend"); + self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total }); + // dispatch a progress event loadend. + self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total }); }, .done => { if (self.req) |*r| { @@ -418,7 +501,8 @@ pub const XMLHttpRequest = struct { self.state = DONE; self.send_flag = false; self.dispatchEvt("readystatechange"); - self.dispatchEvt("error"); + self.dispatchProgressEvent("error", .{}); + self.dispatchProgressEvent("loadend", .{}); } pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { From 0acdadfec0e2ed802a56f62edbd39ca41600a103 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 7 Feb 2024 18:31:35 +0100 Subject: [PATCH 24/46] xhr: fix listeners setters --- src/xhr/xhr.zig | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 38099376..a9d067dd 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -83,26 +83,32 @@ pub const XMLHttpRequestEventTarget = struct { pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); try self.register(alloc, "loadstart", handler); + self.onloadstart_cbk = handler; } pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); try self.register(alloc, "progress", handler); + self.onprogress_cbk = handler; } pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); try self.register(alloc, "abort", handler); + self.onabort_cbk = handler; } pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); try self.register(alloc, "load", handler); + self.onload_cbk = handler; } pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); try self.register(alloc, "timeout", handler); + self.ontimeout_cbk = handler; } pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); try self.register(alloc, "loadend", handler); + self.onloadend_cbk = handler; } pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { @@ -561,7 +567,7 @@ pub fn testExecFn( .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; }" }, - // .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, From 4a9a0e5e3c49545816f337aa6f2e2d0895336fc8 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 8 Feb 2024 09:43:40 +0100 Subject: [PATCH 25/46] xhr: progressevent accept null progressevent init --- src/xhr/xhr.zig | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index a9d067dd..40fe7da8 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -143,16 +143,18 @@ pub const ProgressEvent = struct { loaded: u64 = 0, total: u64 = 0, - pub fn constructor(eventType: []const u8, opts: ProgressEventInit) !ProgressEvent { + pub fn constructor(eventType: []const u8, opts: ?ProgressEventInit) !ProgressEvent { const event = try parser.eventCreate(); defer parser.eventDestroy(event); try parser.eventInit(event, eventType, .{}); + const o = opts orelse ProgressEventInit{}; + return .{ .proto = event.*, - .lengthComputable = opts.lengthComputable, - .loaded = opts.loaded, - .total = opts.total, + .lengthComputable = o.lengthComputable, + .loaded = o.loaded, + .total = o.total, }; } @@ -562,12 +564,12 @@ pub fn testExecFn( js_env: *jsruntime.Env, ) anyerror!void { var send = [_]Case{ - .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, + .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt = event; }", .ex = "undefined" }, .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, - // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; }" }, - .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; }" }, + .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, + // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" }, + //.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, @@ -584,6 +586,7 @@ pub fn testExecFn( // Each case executed waits for all loop callaback calls. // So the url has been retrieved. .{ .src = "nb", .ex = "1" }, + // .{ .src = "evt.__proto__.constructor.name", .ex = "ProgressEvent" }, .{ .src = "req.status", .ex = "200" }, .{ .src = "req.statusText", .ex = "OK" }, .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=UTF-8" }, @@ -591,4 +594,11 @@ pub fn testExecFn( .{ .src = "req.responseText.length > 1024", .ex = "true" }, }; try checkCases(js_env, &send); + + var progress_event = [_]Case{ + .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, + .{ .src = "pevt.loaded", .ex = "0" }, + .{ .src = "pevt.__proto__.constructor.name", .ex = "ProgressEvent" }, + }; + try checkCases(js_env, &progress_event); } From 4b75fd103602bec6036f7cd70388e075b26f3602 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 8 Feb 2024 09:45:04 +0100 Subject: [PATCH 26/46] xhr: rename onerr into onErr --- src/xhr/xhr.zig | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 40fe7da8..0abadf99 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -410,28 +410,28 @@ pub const XMLHttpRequest = struct { // Yielding allows pseudo-async and gives a chance to other async process // to be called. pub fn onYield(self: *XMLHttpRequest, err: ?anyerror) void { - if (err) |e| return self.onerr(e); + if (err) |e| return self.onErr(e); switch (self.priv_state) { .new => { self.priv_state = .open; - self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onerr(e); + self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e); }, .open => { self.priv_state = .send; - self.req.?.send(.{}) catch |e| return self.onerr(e); + self.req.?.send(.{}) catch |e| return self.onErr(e); }, .send => { self.priv_state = .finish; - self.req.?.finish() catch |e| return self.onerr(e); + self.req.?.finish() catch |e| return self.onErr(e); }, .finish => { self.priv_state = .wait; - self.req.?.wait() catch |e| return self.onerr(e); + self.req.?.wait() catch |e| return self.onErr(e); }, .wait => { self.priv_state = .done; - self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onerr(e); + self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e); self.state = HEADERS_RECEIVED; self.dispatchEvt("readystatechange"); @@ -453,11 +453,11 @@ pub const XMLHttpRequest = struct { while (ln > 0) { ln = reader.read(&buffer) catch |e| { buf.deinit(self.alloc); - return self.onerr(e); + return self.onErr(e); }; buf.appendSlice(self.alloc, buffer[0..ln]) catch |e| { buf.deinit(self.alloc); - return self.onerr(e); + return self.onErr(e); }; loaded = loaded + ln; @@ -498,7 +498,7 @@ pub const XMLHttpRequest = struct { self.impl.yield(self); } - fn onerr(self: *XMLHttpRequest, err: anyerror) void { + fn onErr(self: *XMLHttpRequest, err: anyerror) void { self.priv_state = .done; if (self.req) |*r| { r.deinit(); From 7323f2268a040e55de821d620462f41d1a8f6b4b Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 8 Feb 2024 09:45:26 +0100 Subject: [PATCH 27/46] xhr: add blocked comment --- src/xhr/xhr.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 0abadf99..ce08dc89 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -568,6 +568,8 @@ pub fn testExecFn( .{ .src = "const req = new XMLHttpRequest()", .ex = "undefined" }, .{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, + // Getter returning a callback crashes. + // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/200 // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" }, //.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, From 5aafc93a0315be884585f796ed73b009350858e3 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 8 Feb 2024 14:25:14 +0100 Subject: [PATCH 28/46] event: add remove listener test --- src/events/event.zig | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/events/event.zig b/src/events/event.zig index fe939621..f619891d 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -198,4 +198,13 @@ pub fn testExecFn( .{ .src = "nb", .ex = "1" }, }; try checkCases(js_env, &legacy); + + var remove = [_]Case{ + .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, + .{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" }, + .{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" }, + .{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" }, + .{ .src = "nb", .ex = "0" }, + }; + try checkCases(js_env, &remove); } From d24df5725c33826a05541fc6b49270ca6a2c1312 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Thu, 8 Feb 2024 14:31:15 +0100 Subject: [PATCH 29/46] xhr: use nested object for ProgressEventInit --- src/xhr/xhr.zig | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index ce08dc89..85059887 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -29,7 +29,6 @@ pub const Interfaces = generate.Tuple(.{ XMLHttpRequestUpload, XMLHttpRequest, ProgressEvent, - ProgressEventInit, }); pub const XMLHttpRequestEventTarget = struct { @@ -125,30 +124,28 @@ pub const XMLHttpRequestUpload = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; -pub const ProgressEventInit = struct { - pub const mem_guarantied = true; - - lengthComputable: bool = false, - loaded: u64 = 0, - total: u64 = 0, -}; - pub const ProgressEvent = struct { pub const prototype = *Event; pub const Exception = DOMException; pub const mem_guarantied = true; + pub const EventInit = struct { + lengthComputable: bool = false, + loaded: u64 = 0, + total: u64 = 0, + }; + proto: parser.Event, lengthComputable: bool, loaded: u64 = 0, total: u64 = 0, - pub fn constructor(eventType: []const u8, opts: ?ProgressEventInit) !ProgressEvent { + pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent { const event = try parser.eventCreate(); defer parser.eventDestroy(event); try parser.eventInit(event, eventType, .{}); - const o = opts orelse ProgressEventInit{}; + const o = opts orelse EventInit{}; return .{ .proto = event.*, @@ -332,7 +329,7 @@ pub const XMLHttpRequest = struct { fn dispatchProgressEvent( self: *XMLHttpRequest, typ: []const u8, - opts: ProgressEventInit, + opts: ProgressEvent.EventInit, ) void { // TODO destroy struct const evt = self.alloc.create(ProgressEvent) catch |e| { From 76df0a1ff78abbe25bbdf185fe68e9b04c790a1f Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 11:35:22 +0100 Subject: [PATCH 30/46] xhr: fix ProgressEvent implementation --- src/events/event.zig | 26 ++++++++++++++++++-------- src/netsurf.zig | 34 ++++++++++++++++++++++++++++++++-- src/xhr/xhr.zig | 14 ++++++++++---- 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/src/events/event.zig b/src/events/event.zig index f619891d..f192430d 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -13,6 +13,16 @@ const DOMException = @import("../dom/exceptions.zig").DOMException; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTargetUnion = @import("../dom/event_target.zig").Union; +const xhr = @import("../xhr/xhr.zig"); + +// Event interfaces +pub const Interfaces = generate.Tuple(.{ + Event, + xhr.ProgressEvent, +}); +const Generated = generate.Union.compile(Interfaces); +pub const Union = Generated._union; + // https://dom.spec.whatwg.org/#event pub const Event = struct { pub const Self = parser.Event; @@ -28,6 +38,13 @@ pub const Event = struct { pub const _AT_TARGET = 2; pub const _BUBBLING_PHASE = 3; + pub fn toInterface(evt: *parser.Event) !Union { + return switch (try parser.eventGetInternalType(evt)) { + .event => .{ .Event = evt }, + .progress_event => .{ .ProgressEvent = @as(*xhr.ProgressEvent, @ptrCast(evt)).* }, + }; + } + pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event { const event = try parser.eventCreate(); try parser.eventInit(event, eventType, opts orelse EventInit{}); @@ -104,13 +121,6 @@ pub const Event = struct { } }; -// Event interfaces -pub const Interfaces = generate.Tuple(.{ - Event, -}); -const Generated = generate.Union.compile(Interfaces); -pub const Union = Generated._union; - pub fn testExecFn( _: std.mem.Allocator, js_env: *jsruntime.Env, @@ -200,7 +210,7 @@ pub fn testExecFn( try checkCases(js_env, &legacy); var remove = [_]Case{ - .{ .src = "var nb = 0; function cbk(event) { nb ++; }", .ex = "undefined" }, + .{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" }, .{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" }, .{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" }, .{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" }, diff --git a/src/netsurf.zig b/src/netsurf.zig index 5ecbb155..57d071e2 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -8,6 +8,7 @@ const c = @cImport({ }); const Callback = @import("jsruntime").Callback; +const EventToInterface = @import("events/event.zig").Event.toInterface; // Vtable // ------ @@ -449,6 +450,23 @@ pub fn eventPreventDefault(evt: *Event) !void { try DOMErr(err); } +pub fn eventGetInternalType(evt: *Event) !EventType { + var res: u32 = undefined; + const err = c._dom_event_get_internal_type(evt, &res); + try DOMErr(err); + return @enumFromInt(res); +} + +pub fn eventSetInternalType(evt: *Event, internal_type: EventType) !void { + const err = c._dom_event_set_internal_type(evt, @intFromEnum(internal_type)); + try DOMErr(err); +} + +pub const EventType = enum(u8) { + event = 0, + progress_event = 1, +}; + // EventHandler fn event_handler_cbk(data: *anyopaque) *Callback { const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data); @@ -459,7 +477,14 @@ const event_handler = struct { fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void { if (data) |d| { const func = event_handler_cbk(d); - func.call(.{event}) catch unreachable; + + if (event) |evt| { + func.call(.{ + EventToInterface(evt) catch unreachable, + }) catch unreachable; + } else { + func.call(.{event}) catch unreachable; + } // NOTE: we can not call func.deinit here // b/c the handler can be called several times // either on this dispatch event or in anoter one @@ -652,7 +677,12 @@ pub const EventTargetTBase = struct { pub fn dispatch_event(et: [*c]c.dom_event_target, evt: ?*c.struct_dom_event, res: [*c]bool) callconv(.C) c.dom_exception { const self = @as(*Self, @ptrCast(et)); - return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_BUBBLING_PHASE, res); + // Set the event target to the target dispatched. + const e = c._dom_event_set_target(evt, et); + if (e != c.DOM_NO_ERR) { + return e; + } + return c._dom_event_target_dispatch(et, &self.eti, evt, c.DOM_AT_TARGET, res); } pub fn remove_event_listener(et: [*c]c.dom_event_target, t: [*c]c.dom_string, l: ?*c.struct_dom_event_listener, capture: bool) callconv(.C) c.dom_exception { diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 85059887..d0dc7fb1 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -28,7 +28,6 @@ pub const Interfaces = generate.Tuple(.{ XMLHttpRequestEventTarget, XMLHttpRequestUpload, XMLHttpRequest, - ProgressEvent, }); pub const XMLHttpRequestEventTarget = struct { @@ -144,6 +143,7 @@ pub const ProgressEvent = struct { const event = try parser.eventCreate(); defer parser.eventDestroy(event); try parser.eventInit(event, eventType, .{}); + try parser.eventSetInternalType(event, .progress_event); const o = opts orelse EventInit{}; @@ -476,7 +476,6 @@ pub const XMLHttpRequest = struct { self.dispatchEvt("readystatechange"); // dispatch a progress event load. - self.dispatchEvt("load"); self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = total }); // dispatch a progress event loadend. self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = total }); @@ -585,7 +584,9 @@ pub fn testExecFn( // Each case executed waits for all loop callaback calls. // So the url has been retrieved. .{ .src = "nb", .ex = "1" }, - // .{ .src = "evt.__proto__.constructor.name", .ex = "ProgressEvent" }, + .{ .src = "evt.type", .ex = "load" }, + .{ .src = "evt.loaded > 0", .ex = "true" }, + .{ .src = "evt instanceof ProgressEvent", .ex = "true" }, .{ .src = "req.status", .ex = "200" }, .{ .src = "req.statusText", .ex = "OK" }, .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=UTF-8" }, @@ -597,7 +598,12 @@ pub fn testExecFn( var progress_event = [_]Case{ .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, .{ .src = "pevt.loaded", .ex = "0" }, - .{ .src = "pevt.__proto__.constructor.name", .ex = "ProgressEvent" }, + .{ .src = "pevt instanceof ProgressEvent", .ex = "true" }, + .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" }, + .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" }, + .{ .src = "document.dispatchEvent(pevt)", .ex = "true" }, + .{ .src = "eevt.type", .ex = "foo" }, + .{ .src = "eevt instanceof ProgressEvent", .ex = "true" }, }; try checkCases(js_env, &progress_event); } From e79933990d58354192a50a96a8934d84f4972ef0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 11:35:43 +0100 Subject: [PATCH 31/46] xhr: destroy allocated mem on error --- src/xhr/xhr.zig | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index d0dc7fb1..391660ec 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -341,12 +341,14 @@ pub const XMLHttpRequest = struct { .total = opts.total, .loaded = opts.loaded, }) catch |e| { + self.alloc.destroy(evt); return log.err("construct progress event: {any}", .{e}); }; _ = parser.eventTargetDispatchEvent( @as(*parser.EventTarget, @ptrCast(self)), @as(*parser.Event, @ptrCast(evt)), ) catch |e| { + self.alloc.destroy(evt); return log.err("dispatch progress event: {any}", .{e}); }; } From 6aa182c131948b75036fbdcecafe65e0e65549c0 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 11:43:37 +0100 Subject: [PATCH 32/46] xhr: defer event destroy --- src/xhr/xhr.zig | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 391660ec..26a3e4aa 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -318,6 +318,10 @@ pub const XMLHttpRequest = struct { const evt = parser.eventCreate() catch |e| { return log.err("dispatch event create: {any}", .{e}); }; + + // We can we defer event destroy once the event is dispatched. + defer parser.eventDestroy(evt); + parser.eventInit(evt, typ, .{ .bubbles = true, .cancelable = true }) catch |e| { return log.err("dispatch event init: {any}", .{e}); }; @@ -331,24 +335,19 @@ pub const XMLHttpRequest = struct { typ: []const u8, opts: ProgressEvent.EventInit, ) void { - // TODO destroy struct - const evt = self.alloc.create(ProgressEvent) catch |e| { - return log.err("allocate progress event: {any}", .{e}); - }; - evt.* = ProgressEvent.constructor(typ, .{ + var evt = ProgressEvent.constructor(typ, .{ // https://xhr.spec.whatwg.org/#firing-events-using-the-progressevent-interface .lengthComputable = opts.total > 0, .total = opts.total, .loaded = opts.loaded, }) catch |e| { - self.alloc.destroy(evt); return log.err("construct progress event: {any}", .{e}); }; + _ = parser.eventTargetDispatchEvent( @as(*parser.EventTarget, @ptrCast(self)), - @as(*parser.Event, @ptrCast(evt)), + @as(*parser.Event, @ptrCast(&evt)), ) catch |e| { - self.alloc.destroy(evt); return log.err("dispatch progress event: {any}", .{e}); }; } From 47520ae21dca87fa188811f2a6a665c5a30f2793 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 12:04:10 +0100 Subject: [PATCH 33/46] xhr: move progress event in its own file --- src/events/event.zig | 6 ++-- src/run_tests.zig | 4 ++- src/xhr/progress_event.zig | 72 ++++++++++++++++++++++++++++++++++++++ src/xhr/xhr.zig | 58 +----------------------------- 4 files changed, 79 insertions(+), 61 deletions(-) create mode 100644 src/xhr/progress_event.zig diff --git a/src/events/event.zig b/src/events/event.zig index f192430d..83f12824 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -13,12 +13,12 @@ const DOMException = @import("../dom/exceptions.zig").DOMException; const EventTarget = @import("../dom/event_target.zig").EventTarget; const EventTargetUnion = @import("../dom/event_target.zig").Union; -const xhr = @import("../xhr/xhr.zig"); +const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent; // Event interfaces pub const Interfaces = generate.Tuple(.{ Event, - xhr.ProgressEvent, + ProgressEvent, }); const Generated = generate.Union.compile(Interfaces); pub const Union = Generated._union; @@ -41,7 +41,7 @@ pub const Event = struct { pub fn toInterface(evt: *parser.Event) !Union { return switch (try parser.eventGetInternalType(evt)) { .event => .{ .Event = evt }, - .progress_event => .{ .ProgressEvent = @as(*xhr.ProgressEvent, @ptrCast(evt)).* }, + .progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* }, }; } diff --git a/src/run_tests.zig b/src/run_tests.zig index 4e22c04f..7bebe7b1 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -7,6 +7,7 @@ const generate = @import("generate.zig"); const parser = @import("netsurf.zig"); const apiweb = @import("apiweb.zig"); const Window = @import("html/window.zig").Window; +const xhr = @import("xhr/xhr.zig"); const documentTestExecFn = @import("dom/document.zig").testExecFn; const HTMLDocumentTestExecFn = @import("html/document.zig").testExecFn; @@ -23,8 +24,8 @@ const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; const EventTestExecFn = @import("events/event.zig").testExecFn; -const xhr = @import("xhr/xhr.zig"); const XHRTestExecFn = xhr.testExecFn; +const ProgressEventTestExecFn = @import("xhr/progress_event.zig").testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -81,6 +82,7 @@ fn testsAllExecFn( EventTargetTestExecFn, EventTestExecFn, XHRTestExecFn, + ProgressEventTestExecFn, }; inline for (testFns) |testFn| { diff --git a/src/xhr/progress_event.zig b/src/xhr/progress_event.zig new file mode 100644 index 00000000..afdf36a9 --- /dev/null +++ b/src/xhr/progress_event.zig @@ -0,0 +1,72 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + +const parser = @import("../netsurf.zig"); +const Event = @import("../events/event.zig").Event; + +const DOMException = @import("../dom/exceptions.zig").DOMException; + +pub const ProgressEvent = struct { + pub const prototype = *Event; + pub const Exception = DOMException; + pub const mem_guarantied = true; + + pub const EventInit = struct { + lengthComputable: bool = false, + loaded: u64 = 0, + total: u64 = 0, + }; + + proto: parser.Event, + lengthComputable: bool, + loaded: u64 = 0, + total: u64 = 0, + + pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent { + const event = try parser.eventCreate(); + defer parser.eventDestroy(event); + try parser.eventInit(event, eventType, .{}); + try parser.eventSetInternalType(event, .progress_event); + + const o = opts orelse EventInit{}; + + return .{ + .proto = event.*, + .lengthComputable = o.lengthComputable, + .loaded = o.loaded, + .total = o.total, + }; + } + + pub fn get_lengthComputable(self: ProgressEvent) bool { + return self.lengthComputable; + } + + pub fn get_loaded(self: ProgressEvent) u64 { + return self.loaded; + } + + pub fn get_total(self: ProgressEvent) u64 { + return self.total; + } +}; + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var progress_event = [_]Case{ + .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, + .{ .src = "pevt.loaded", .ex = "0" }, + .{ .src = "pevt instanceof ProgressEvent", .ex = "true" }, + .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" }, + .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" }, + .{ .src = "document.dispatchEvent(pevt)", .ex = "true" }, + .{ .src = "eevt.type", .ex = "foo" }, + .{ .src = "eevt instanceof ProgressEvent", .ex = "true" }, + }; + try checkCases(js_env, &progress_event); +} diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 26a3e4aa..d294c3ef 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -7,6 +7,7 @@ const generate = @import("../generate.zig"); const EventTarget = @import("../dom/event_target.zig").EventTarget; const Event = @import("../events/event.zig").Event; +const ProgressEvent = @import("progress_event.zig").ProgressEvent; const Callback = jsruntime.Callback; const DOMError = @import("../netsurf.zig").DOMError; const DOMException = @import("../dom/exceptions.zig").DOMException; @@ -123,51 +124,6 @@ pub const XMLHttpRequestUpload = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; -pub const ProgressEvent = struct { - pub const prototype = *Event; - pub const Exception = DOMException; - pub const mem_guarantied = true; - - pub const EventInit = struct { - lengthComputable: bool = false, - loaded: u64 = 0, - total: u64 = 0, - }; - - proto: parser.Event, - lengthComputable: bool, - loaded: u64 = 0, - total: u64 = 0, - - pub fn constructor(eventType: []const u8, opts: ?EventInit) !ProgressEvent { - const event = try parser.eventCreate(); - defer parser.eventDestroy(event); - try parser.eventInit(event, eventType, .{}); - try parser.eventSetInternalType(event, .progress_event); - - const o = opts orelse EventInit{}; - - return .{ - .proto = event.*, - .lengthComputable = o.lengthComputable, - .loaded = o.loaded, - .total = o.total, - }; - } - - pub fn get_lengthComputable(self: ProgressEvent) bool { - return self.lengthComputable; - } - - pub fn get_loaded(self: ProgressEvent) u64 { - return self.loaded; - } - - pub fn get_total(self: ProgressEvent) u64 { - return self.total; - } -}; - pub const XMLHttpRequest = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; @@ -595,16 +551,4 @@ pub fn testExecFn( .{ .src = "req.responseText.length > 1024", .ex = "true" }, }; try checkCases(js_env, &send); - - var progress_event = [_]Case{ - .{ .src = "let pevt = new ProgressEvent('foo');", .ex = "undefined" }, - .{ .src = "pevt.loaded", .ex = "0" }, - .{ .src = "pevt instanceof ProgressEvent", .ex = "true" }, - .{ .src = "var nnb = 0; var eevt = null; function ccbk(event) { nnb ++; eevt = event; }", .ex = "undefined" }, - .{ .src = "document.addEventListener('foo', ccbk)", .ex = "undefined" }, - .{ .src = "document.dispatchEvent(pevt)", .ex = "true" }, - .{ .src = "eevt.type", .ex = "foo" }, - .{ .src = "eevt instanceof ProgressEvent", .ex = "true" }, - }; - try checkCases(js_env, &progress_event); } From 1a448b0b32c881d72fab3a1886d1b8f43e0bcf9e Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 12:42:19 +0100 Subject: [PATCH 34/46] xhr: response_type getter/setter --- src/xhr/xhr.zig | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index d294c3ef..e7eef08c 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -466,6 +466,46 @@ pub const XMLHttpRequest = struct { self.dispatchProgressEvent("loadend", .{}); } + pub fn get_responseType(self: *XMLHttpRequest) []const u8 { + return switch (self.response_type) { + .Empty => "", + .ArrayBuffer => "arraybuffer", + .Blob => "blob", + .Document => "document", + .JSON => "json", + .Text => "text", + }; + } + + pub fn set_responseType(self: *XMLHttpRequest, rtype: []const u8) !void { + if (self.state == LOADING or self.state == DONE) return DOMError.InvalidState; + + if (std.mem.eql(u8, rtype, "")) { + self.response_type = .Empty; + return; + } + if (std.mem.eql(u8, rtype, "arraybuffer")) { + self.response_type = .ArrayBuffer; + return; + } + if (std.mem.eql(u8, rtype, "blob")) { + self.response_type = .Blob; + return; + } + if (std.mem.eql(u8, rtype, "document")) { + self.response_type = .Document; + return; + } + if (std.mem.eql(u8, rtype, "json")) { + self.response_type = .JSON; + return; + } + if (std.mem.eql(u8, rtype, "text")) { + self.response_type = .Text; + return; + } + } + pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; From f22c927067839d71d68c3cc012c0f7157314b7cc Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 15:22:32 +0100 Subject: [PATCH 35/46] xhr: move XMLHttpEventTarget in its own file --- src/xhr/event_target.zig | 96 ++++++++++++++++++++++++++++++++++++++++ src/xhr/xhr.zig | 96 ++-------------------------------------- 2 files changed, 99 insertions(+), 93 deletions(-) create mode 100644 src/xhr/event_target.zig diff --git a/src/xhr/event_target.zig b/src/xhr/event_target.zig new file mode 100644 index 00000000..5830f57a --- /dev/null +++ b/src/xhr/event_target.zig @@ -0,0 +1,96 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Callback = jsruntime.Callback; + +const EventTarget = @import("../dom/event_target.zig").EventTarget; + +const parser = @import("../netsurf.zig"); + +const log = std.log.scoped(.xhr); + +pub const XMLHttpRequestEventTarget = struct { + pub const prototype = *EventTarget; + pub const mem_guarantied = true; + + // Extend libdom event target for pure zig struct. + base: parser.EventTargetTBase = parser.EventTargetTBase{}, + + onloadstart_cbk: ?Callback = null, + onprogress_cbk: ?Callback = null, + onabort_cbk: ?Callback = null, + onload_cbk: ?Callback = null, + ontimeout_cbk: ?Callback = null, + onloadend_cbk: ?Callback = null, + + fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { + try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false); + } + fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { + const et = @as(*parser.EventTarget, @ptrCast(self)); + // check if event target has already this listener + const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id()); + if (lst == null) { + return; + } + + // remove listener + try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); + } + + pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onloadstart_cbk; + } + pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onprogress_cbk; + } + pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onabort_cbk; + } + pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onload_cbk; + } + pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback { + return self.ontimeout_cbk; + } + pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback { + return self.onloadend_cbk; + } + + pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); + try self.register(alloc, "loadstart", handler); + self.onloadstart_cbk = handler; + } + pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); + try self.register(alloc, "progress", handler); + self.onprogress_cbk = handler; + } + pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); + try self.register(alloc, "abort", handler); + self.onabort_cbk = handler; + } + pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); + try self.register(alloc, "load", handler); + self.onload_cbk = handler; + } + pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); + try self.register(alloc, "timeout", handler); + self.ontimeout_cbk = handler; + } + pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { + if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); + try self.register(alloc, "loadend", handler); + self.onloadend_cbk = handler; + } + + pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { + parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| { + log.err("remove all listeners: {any}", .{e}); + }; + } +}; diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index e7eef08c..884581c9 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -5,21 +5,17 @@ const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; const generate = @import("../generate.zig"); -const EventTarget = @import("../dom/event_target.zig").EventTarget; -const Event = @import("../events/event.zig").Event; -const ProgressEvent = @import("progress_event.zig").ProgressEvent; -const Callback = jsruntime.Callback; const DOMError = @import("../netsurf.zig").DOMError; const DOMException = @import("../dom/exceptions.zig").DOMException; +const ProgressEvent = @import("progress_event.zig").ProgressEvent; +const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget; + const Loop = jsruntime.Loop; const YieldImpl = Loop.Yield(XMLHttpRequest); const Client = @import("../async/Client.zig"); const parser = @import("../netsurf.zig"); -const c = @cImport({ - @cInclude("events/event_target.h"); -}); const log = std.log.scoped(.xhr); @@ -31,92 +27,6 @@ pub const Interfaces = generate.Tuple(.{ XMLHttpRequest, }); -pub const XMLHttpRequestEventTarget = struct { - pub const prototype = *EventTarget; - pub const mem_guarantied = true; - - // Extend libdom event target for pure zig struct. - base: parser.EventTargetTBase = parser.EventTargetTBase{}, - - onloadstart_cbk: ?Callback = null, - onprogress_cbk: ?Callback = null, - onabort_cbk: ?Callback = null, - onload_cbk: ?Callback = null, - ontimeout_cbk: ?Callback = null, - onloadend_cbk: ?Callback = null, - - fn register(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { - try parser.eventTargetAddEventListener(@as(*parser.EventTarget, @ptrCast(self)), alloc, typ, cbk, false); - } - fn unregister(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, typ: []const u8, cbk: Callback) !void { - const et = @as(*parser.EventTarget, @ptrCast(self)); - // check if event target has already this listener - const lst = try parser.eventTargetHasListener(et, typ, false, cbk.id()); - if (lst == null) { - return; - } - - // remove listener - try parser.eventTargetRemoveEventListener(et, alloc, typ, lst.?, false); - } - - pub fn get_onloadstart(self: *XMLHttpRequestEventTarget) ?Callback { - return self.onloadstart_cbk; - } - pub fn get_onprogress(self: *XMLHttpRequestEventTarget) ?Callback { - return self.onprogress_cbk; - } - pub fn get_onabort(self: *XMLHttpRequestEventTarget) ?Callback { - return self.onabort_cbk; - } - pub fn get_onload(self: *XMLHttpRequestEventTarget) ?Callback { - return self.onload_cbk; - } - pub fn get_ontimeout(self: *XMLHttpRequestEventTarget) ?Callback { - return self.ontimeout_cbk; - } - pub fn get_onloadend(self: *XMLHttpRequestEventTarget) ?Callback { - return self.onloadend_cbk; - } - - pub fn set_onloadstart(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onloadstart_cbk) |cbk| try self.unregister(alloc, "loadstart", cbk); - try self.register(alloc, "loadstart", handler); - self.onloadstart_cbk = handler; - } - pub fn set_onprogress(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onprogress_cbk) |cbk| try self.unregister(alloc, "progress", cbk); - try self.register(alloc, "progress", handler); - self.onprogress_cbk = handler; - } - pub fn set_onabort(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onabort_cbk) |cbk| try self.unregister(alloc, "abort", cbk); - try self.register(alloc, "abort", handler); - self.onabort_cbk = handler; - } - pub fn set_onload(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onload_cbk) |cbk| try self.unregister(alloc, "load", cbk); - try self.register(alloc, "load", handler); - self.onload_cbk = handler; - } - pub fn set_ontimeout(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.ontimeout_cbk) |cbk| try self.unregister(alloc, "timeout", cbk); - try self.register(alloc, "timeout", handler); - self.ontimeout_cbk = handler; - } - pub fn set_onloadend(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator, handler: Callback) !void { - if (self.onloadend_cbk) |cbk| try self.unregister(alloc, "loadend", cbk); - try self.register(alloc, "loadend", handler); - self.onloadend_cbk = handler; - } - - pub fn deinit(self: *XMLHttpRequestEventTarget, alloc: std.mem.Allocator) void { - parser.eventTargetRemoveAllEventListeners(@as(*parser.EventTarget, @ptrCast(self)), alloc) catch |e| { - log.err("remove all listeners: {any}", .{e}); - }; - } -}; - pub const XMLHttpRequestUpload = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; From 84a52332454124fe205560af5b8daeb273ddeeeb Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Fri, 9 Feb 2024 19:08:42 +0100 Subject: [PATCH 36/46] xhr: implement response --- src/browser/mime.zig | 1 + src/xhr/xhr.zig | 151 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) diff --git a/src/browser/mime.zig b/src/browser/mime.zig index a880daee..f7ec4763 100644 --- a/src/browser/mime.zig +++ b/src/browser/mime.zig @@ -17,6 +17,7 @@ params: []const u8 = "", charset: ?[]const u8 = null, boundary: ?[]const u8 = null, +pub const Empty = Self{ .mtype = "", .msubtype = "" }; pub const HTML = Self{ .mtype = "text", .msubtype = "html" }; pub const Javascript = Self{ .mtype = "application", .msubtype = "javascript" }; diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 884581c9..2e7b3b30 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -11,6 +11,8 @@ const DOMException = @import("../dom/exceptions.zig").DOMException; const ProgressEvent = @import("progress_event.zig").ProgressEvent; const XMLHttpRequestEventTarget = @import("event_target.zig").XMLHttpRequestEventTarget; +const Mime = @import("../browser/mime.zig"); + const Loop = jsruntime.Loop; const YieldImpl = Loop.Yield(XMLHttpRequest); const Client = @import("../async/Client.zig"); @@ -54,6 +56,37 @@ pub const XMLHttpRequest = struct { JSON, }; + // TODO use std.json.Value instead, but it causes comptime error. + const JSONValue = u8; + + const Response = union(ResponseType) { + Empty: void, + Text: []const u8, + ArrayBuffer: void, + Blob: void, + Document: *parser.DocumentHTML, + JSON: JSONValue, + }; + + const ResponseObjTag = enum { + Document, + Failure, + JSON, + }; + const ResponseObj = union(ResponseObjTag) { + Document: *parser.DocumentHTML, + Failure: bool, + JSON: std.json.Parsed(JSONValue), + + fn deinit(self: ResponseObj) void { + return switch (self) { + .Document => |d| parser.documentHTMLClose(d) catch {}, + .JSON => |p| p.deinit(), + .Failure => {}, + }; + } + }; + const PrivState = enum { new, open, send, finish, wait, done }; proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, @@ -80,6 +113,9 @@ pub const XMLHttpRequest = struct { response_type: ResponseType = .Empty, response_headers: std.http.Headers, response_status: u10 = 0, + response_override_mime_type: ?[]const u8 = null, + response_mime: Mime = undefined, + response_obj: ?ResponseObj = null, send_flag: bool = false, pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { @@ -105,6 +141,8 @@ pub const XMLHttpRequest = struct { if (self.response_bytes) |v| alloc.free(v); if (self.response_headers) |v| alloc.free(v); + if (self.response_obj) |v| v.deinit(); + if (self.req) |*r| r.deinit(); // TODO the client must be shared between requests. self.cli.deinit(); @@ -165,6 +203,11 @@ pub const XMLHttpRequest = struct { self.response_headers.clearAndFree(); self.response_status = 0; + if (self.response_obj) |v| v.deinit(); + self.response_obj = null; + + self.response_mime = Mime.Empty; + self.response_type = .Empty; if (self.response_bytes) |v| alloc.free(v); @@ -297,6 +340,12 @@ pub const XMLHttpRequest = struct { self.priv_state = .done; self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e); + // extract a mime type from headers. + const ct = self.response_headers.getFirstValue("Content-Type") orelse "text/xml"; + self.response_mime = Mime.parse(ct) catch |e| return self.onErr(e); + + // TODO handle override mime type + self.state = HEADERS_RECEIVED; self.dispatchEvt("readystatechange"); @@ -416,6 +465,107 @@ pub const XMLHttpRequest = struct { } } + // https://xhr.spec.whatwg.org/#the-response-attribute + pub fn get_response(self: *XMLHttpRequest) !?Response { + if (self.response_type == .Empty or self.response_type == .Text) { + if (self.state == LOADING or self.state == DONE) return .{ .Text = "" }; + return .{ .Text = try self.get_responseText() }; + } + + if (self.state != DONE) return null; + + // fastpath if response is previously parsed. + if (self.response_obj) |obj| { + return switch (obj) { + .Failure => null, + .Document => |v| .{ .Document = v }, + .JSON => |v| .{ .JSON = v.value }, + }; + } + + if (self.response_type == .ArrayBuffer) { + // TODO If this’s response type is "arraybuffer", then set this’s + // response object to a new ArrayBuffer object representing this’s + // received bytes. If this throws an exception, then set this’s + // response object to failure and return null. + return null; + } + + if (self.response_type == .Blob) { + // TODO Otherwise, if this’s response type is "blob", set this’s + // response object to a new Blob object representing this’s + // received bytes with type set to the result of get a final MIME + // type for this. + return null; + } + + // Otherwise, if this’s response type is "document", set a + // document response for this. + if (self.response_type == .Document) { + self.setResponseObjDocument(); + } + + if (self.response_type == .JSON) { + if (self.response_bytes == null) return null; + + // TODO Let jsonObject be the result of running parse JSON from bytes + // on this’s received bytes. If that threw an exception, then return + // null. + } + + if (self.response_obj) |obj| { + return switch (obj) { + .Failure => null, + .Document => |v| .{ .Document = v }, + .JSON => |v| .{ .JSON = v.value }, + }; + } + + return null; + } + + // setResponseObjDocument parses the received bytes as HTML document and + // stores the result into response_obj. + // If the par sing fails, a Failure is stored in response_obj. + // TODO parse XML. + // https://xhr.spec.whatwg.org/#response-object + fn setResponseObjDocument(self: *XMLHttpRequest) void { + const isHTML = self.response_mime.eql(Mime.HTML); + + // TODO If finalMIME is not an HTML MIME type or an XML MIME type, then + // return. + if (!isHTML) return; + + if (self.response_type == .Empty) return; + + const ccharset = self.alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch { + self.response_obj = .{ .Failure = true }; + return; + }; + defer self.alloc.free(ccharset); + + var fbs = std.io.fixedBufferStream(self.response_bytes.?); + const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { + self.response_obj = .{ .Failure = true }; + return; + }; + + // TODO Set document’s URL to xhr’s response’s URL. + // TODO Set document’s origin to xhr’s relevant settings object’s origin. + + self.response_obj = .{ .Document = doc }; + } + + // setResponseObjJSON parses the received bytes as a std.json.Value. + fn setResponseObjJSON(self: *XMLHttpRequest) void { + const p = std.json.parseFromSlice(JSONValue, self.alloc, self.response_bytes, .{}) catch { + self.response_obj = .{ .Failure = true }; + return; + }; + + self.response_obj = .{ .JSON = p }; + } + pub fn get_responseText(self: *XMLHttpRequest) ![]const u8 { if (self.response_type != .Empty and self.response_type != .Text) return DOMError.InvalidState; @@ -499,6 +649,7 @@ pub fn testExecFn( .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=UTF-8" }, .{ .src = "req.getAllResponseHeaders().length > 1024", .ex = "true" }, .{ .src = "req.responseText.length > 1024", .ex = "true" }, + .{ .src = "req.response", .ex = "" }, }; try checkCases(js_env, &send); } From 704f12f0395474aa5aac29f8394e0c0c0971d471 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 12 Feb 2024 17:57:05 +0100 Subject: [PATCH 37/46] xhr: fix json response --- src/xhr/xhr.zig | 40 +++++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 2e7b3b30..447c6e95 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -57,6 +57,8 @@ pub const XMLHttpRequest = struct { }; // TODO use std.json.Value instead, but it causes comptime error. + // blocked by https://github.com/lightpanda-io/jsruntime-lib/issues/204 + // const JSONValue = std.json.Value; const JSONValue = u8; const Response = union(ResponseType) { @@ -466,7 +468,7 @@ pub const XMLHttpRequest = struct { } // https://xhr.spec.whatwg.org/#the-response-attribute - pub fn get_response(self: *XMLHttpRequest) !?Response { + pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { if (self.response_type == .Empty or self.response_type == .Text) { if (self.state == LOADING or self.state == DONE) return .{ .Text = "" }; return .{ .Text = try self.get_responseText() }; @@ -502,7 +504,7 @@ pub const XMLHttpRequest = struct { // Otherwise, if this’s response type is "document", set a // document response for this. if (self.response_type == .Document) { - self.setResponseObjDocument(); + self.setResponseObjDocument(alloc); } if (self.response_type == .JSON) { @@ -511,6 +513,7 @@ pub const XMLHttpRequest = struct { // TODO Let jsonObject be the result of running parse JSON from bytes // on this’s received bytes. If that threw an exception, then return // null. + self.setResponseObjJSON(alloc); } if (self.response_obj) |obj| { @@ -529,7 +532,7 @@ pub const XMLHttpRequest = struct { // If the par sing fails, a Failure is stored in response_obj. // TODO parse XML. // https://xhr.spec.whatwg.org/#response-object - fn setResponseObjDocument(self: *XMLHttpRequest) void { + fn setResponseObjDocument(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { const isHTML = self.response_mime.eql(Mime.HTML); // TODO If finalMIME is not an HTML MIME type or an XML MIME type, then @@ -538,11 +541,11 @@ pub const XMLHttpRequest = struct { if (self.response_type == .Empty) return; - const ccharset = self.alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch { + const ccharset = alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch { self.response_obj = .{ .Failure = true }; return; }; - defer self.alloc.free(ccharset); + defer alloc.free(ccharset); var fbs = std.io.fixedBufferStream(self.response_bytes.?); const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { @@ -557,8 +560,16 @@ pub const XMLHttpRequest = struct { } // setResponseObjJSON parses the received bytes as a std.json.Value. - fn setResponseObjJSON(self: *XMLHttpRequest) void { - const p = std.json.parseFromSlice(JSONValue, self.alloc, self.response_bytes, .{}) catch { + fn setResponseObjJSON(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + // TODO should we use parseFromSliceLeaky if we expect the allocator is + // already an arena? + const p = std.json.parseFromSlice( + JSONValue, + alloc, + self.response_bytes.?, + .{}, + ) catch |e| { + log.err("parse JSON: {}", .{e}); self.response_obj = .{ .Failure = true }; return; }; @@ -652,4 +663,19 @@ pub fn testExecFn( .{ .src = "req.response", .ex = "" }, }; try checkCases(js_env, &send); + + var json = [_]Case{ + .{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" }, + .{ .src = "req2.open('GET', 'http://httpbin.io/json')", .ex = "undefined" }, + .{ .src = "req2.responseType = 'json'", .ex = "json" }, + + .{ .src = "req2.send()", .ex = "undefined" }, + + // Each case executed waits for all loop callaback calls. + // So the url has been retrieved. + .{ .src = "req2.status", .ex = "200" }, + .{ .src = "req2.statusText", .ex = "OK" }, + .{ .src = "req2.response", .ex = "" }, + }; + try checkCases(js_env, &json); } From d5e37621792c8eaf006f67dfacdfd6281bb692a4 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 12 Feb 2024 21:34:32 +0100 Subject: [PATCH 38/46] xhr: comment json and add a document test --- src/xhr/xhr.zig | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 447c6e95..26d875d1 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -637,7 +637,7 @@ pub fn testExecFn( // .{ .src = "req.onload", .ex = "function cbk(event) { nb ++; evt = event; }" }, //.{ .src = "req.onload = cbk", .ex = "function cbk(event) { nb ++; evt = event; }" }, - .{ .src = "req.open('GET', 'https://w3.org')", .ex = "undefined" }, + .{ .src = "req.open('GET', 'http://httpbin.io/html')", .ex = "undefined" }, .{ .src = "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", .ex = "undefined" }, // ensure open resets values @@ -657,17 +657,17 @@ pub fn testExecFn( .{ .src = "evt instanceof ProgressEvent", .ex = "true" }, .{ .src = "req.status", .ex = "200" }, .{ .src = "req.statusText", .ex = "OK" }, - .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=UTF-8" }, - .{ .src = "req.getAllResponseHeaders().length > 1024", .ex = "true" }, - .{ .src = "req.responseText.length > 1024", .ex = "true" }, + .{ .src = "req.getResponseHeader('Content-Type')", .ex = "text/html; charset=utf-8" }, + .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" }, + .{ .src = "req.responseText.length > 64", .ex = "true" }, .{ .src = "req.response", .ex = "" }, }; try checkCases(js_env, &send); - var json = [_]Case{ + var document = [_]Case{ .{ .src = "const req2 = new XMLHttpRequest()", .ex = "undefined" }, - .{ .src = "req2.open('GET', 'http://httpbin.io/json')", .ex = "undefined" }, - .{ .src = "req2.responseType = 'json'", .ex = "json" }, + .{ .src = "req2.open('GET', 'http://httpbin.io/html')", .ex = "undefined" }, + .{ .src = "req2.responseType = 'document'", .ex = "document" }, .{ .src = "req2.send()", .ex = "undefined" }, @@ -675,7 +675,22 @@ pub fn testExecFn( // So the url has been retrieved. .{ .src = "req2.status", .ex = "200" }, .{ .src = "req2.statusText", .ex = "OK" }, - .{ .src = "req2.response", .ex = "" }, + .{ .src = "req2.response instanceof HTMLDocument", .ex = "true" }, }; - try checkCases(js_env, &json); + try checkCases(js_env, &document); + + // var json = [_]Case{ + // .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" }, + // .{ .src = "req3.open('GET', 'http://httpbin.io/json')", .ex = "undefined" }, + // .{ .src = "req3.responseType = 'json'", .ex = "json" }, + + // .{ .src = "req3.send()", .ex = "undefined" }, + + // // Each case executed waits for all loop callaback calls. + // // So the url has been retrieved. + // .{ .src = "req3.status", .ex = "200" }, + // .{ .src = "req3.statusText", .ex = "OK" }, + // .{ .src = "req3.response", .ex = "" }, + // }; + // try checkCases(js_env, &json); } From 34015b8f577ae880b97da132909d7db6e1ba44c7 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 12 Feb 2024 21:43:18 +0100 Subject: [PATCH 39/46] xhr: add reponseXML --- src/xhr/xhr.zig | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 26d875d1..7398360e 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -467,6 +467,34 @@ pub const XMLHttpRequest = struct { } } + pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { + if (self.response_type != .Empty and self.response_type != .Document) { + return DOMError.InvalidState; + } + + if (self.state != DONE) return null; + + // fastpath if response is previously parsed. + if (self.response_obj) |obj| { + return switch (obj) { + .Failure => null, + .Document => |v| .{ .Document = v }, + .JSON => null, + }; + } + + self.setResponseObjDocument(alloc); + + if (self.response_obj) |obj| { + return switch (obj) { + .Failure => null, + .Document => |v| .{ .Document = v }, + .JSON => null, + }; + } + return null; + } + // https://xhr.spec.whatwg.org/#the-response-attribute pub fn get_response(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { if (self.response_type == .Empty or self.response_type == .Text) { @@ -474,8 +502,6 @@ pub const XMLHttpRequest = struct { return .{ .Text = try self.get_responseText() }; } - if (self.state != DONE) return null; - // fastpath if response is previously parsed. if (self.response_obj) |obj| { return switch (obj) { @@ -539,8 +565,6 @@ pub const XMLHttpRequest = struct { // return. if (!isHTML) return; - if (self.response_type == .Empty) return; - const ccharset = alloc.dupeZ(u8, self.response_mime.charset orelse "utf-8") catch { self.response_obj = .{ .Failure = true }; return; @@ -661,6 +685,7 @@ pub fn testExecFn( .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" }, .{ .src = "req.responseText.length > 64", .ex = "true" }, .{ .src = "req.response", .ex = "" }, + .{ .src = "req.responseXML instanceof HTMLDocument", .ex = "true" }, }; try checkCases(js_env, &send); @@ -676,6 +701,7 @@ pub fn testExecFn( .{ .src = "req2.status", .ex = "200" }, .{ .src = "req2.statusText", .ex = "OK" }, .{ .src = "req2.response instanceof HTMLDocument", .ex = "true" }, + .{ .src = "req2.responseXML instanceof HTMLDocument", .ex = "true" }, }; try checkCases(js_env, &document); From ff754fc666b040bfaade5a6295186c809c990aec Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 12 Feb 2024 21:50:01 +0100 Subject: [PATCH 40/46] xhr: implement responseURL --- src/xhr/xhr.zig | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 7398360e..2ad43027 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -467,6 +467,11 @@ pub const XMLHttpRequest = struct { } } + // TODO retrieve the redirected url + pub fn get_responseURL(self: *XMLHttpRequest) ?[]const u8 { + return self.url; + } + pub fn get_responseXML(self: *XMLHttpRequest, alloc: std.mem.Allocator) !?Response { if (self.response_type != .Empty and self.response_type != .Document) { return DOMError.InvalidState; From 54a807bb36a454b42577d4a4341d1883a03b112b Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Mon, 12 Feb 2024 21:59:59 +0100 Subject: [PATCH 41/46] xhr: add abort func --- src/xhr/xhr.zig | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 2ad43027..85346770 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -427,6 +427,10 @@ pub const XMLHttpRequest = struct { self.dispatchProgressEvent("loadend", .{}); } + pub fn _abort(self: *XMLHttpRequest) void { + self.onErr(DOMError.Abort); + } + pub fn get_responseType(self: *XMLHttpRequest) []const u8 { return switch (self.response_type) { .Empty => "", From d58fbe07e3704cb915e2a866a7e5ebdab1efbbf9 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 13 Feb 2024 09:07:15 +0100 Subject: [PATCH 42/46] xhr: return DOM document instead of HTML document --- src/xhr/xhr.zig | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 85346770..2d1cc18b 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -66,7 +66,7 @@ pub const XMLHttpRequest = struct { Text: []const u8, ArrayBuffer: void, Blob: void, - Document: *parser.DocumentHTML, + Document: *parser.Document, JSON: JSONValue, }; @@ -76,13 +76,16 @@ pub const XMLHttpRequest = struct { JSON, }; const ResponseObj = union(ResponseObjTag) { - Document: *parser.DocumentHTML, + Document: *parser.Document, Failure: bool, JSON: std.json.Parsed(JSONValue), fn deinit(self: ResponseObj) void { return switch (self) { - .Document => |d| parser.documentHTMLClose(d) catch {}, + .Document => |d| { + const doc = @as(*parser.DocumentHTML, @ptrCast(d)); + parser.documentHTMLClose(doc) catch {}; + }, .JSON => |p| p.deinit(), .Failure => {}, }; @@ -589,7 +592,9 @@ pub const XMLHttpRequest = struct { // TODO Set document’s URL to xhr’s response’s URL. // TODO Set document’s origin to xhr’s relevant settings object’s origin. - self.response_obj = .{ .Document = doc }; + self.response_obj = .{ + .Document = parser.documentHTMLToDocument(doc), + }; } // setResponseObjJSON parses the received bytes as a std.json.Value. @@ -694,7 +699,7 @@ pub fn testExecFn( .{ .src = "req.getAllResponseHeaders().length > 64", .ex = "true" }, .{ .src = "req.responseText.length > 64", .ex = "true" }, .{ .src = "req.response", .ex = "" }, - .{ .src = "req.responseXML instanceof HTMLDocument", .ex = "true" }, + .{ .src = "req.responseXML instanceof Document", .ex = "true" }, }; try checkCases(js_env, &send); @@ -709,8 +714,8 @@ pub fn testExecFn( // So the url has been retrieved. .{ .src = "req2.status", .ex = "200" }, .{ .src = "req2.statusText", .ex = "OK" }, - .{ .src = "req2.response instanceof HTMLDocument", .ex = "true" }, - .{ .src = "req2.responseXML instanceof HTMLDocument", .ex = "true" }, + .{ .src = "req2.response instanceof Document", .ex = "true" }, + .{ .src = "req2.responseXML instanceof Document", .ex = "true" }, }; try checkCases(js_env, &document); From d062d0f1b6313311a80d1c0ea1b06dcf86b8fc55 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Tue, 13 Feb 2024 12:08:35 +0100 Subject: [PATCH 43/46] xhr: implement basic send data from string --- src/xhr/xhr.zig | 157 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 124 insertions(+), 33 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 2d1cc18b..5a34566d 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -36,6 +36,44 @@ pub const XMLHttpRequestUpload = struct { proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, }; +pub const XMLHttpRequestBodyInitTag = enum { + Blob, + BufferSource, + FormData, + URLSearchParams, + String, +}; + +pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) { + Blob: []const u8, + BufferSource: []const u8, + FormData: []const u8, + URLSearchParams: []const u8, + String: []const u8, + + fn contentType(self: XMLHttpRequestBodyInit) ![]const u8 { + return switch (self) { + .Blob => error.NotImplemented, + .BufferSource => error.NotImplemented, + .FormData => "multipart/form-data; boundary=TODO", + .URLSearchParams => "application/x-www-form-urlencoded;charset=UTF-8", + .String => "text/plain;charset=UTF-8", + }; + } + + // Duplicate the body content. + // The caller owns the allocated string. + fn dupe(self: XMLHttpRequestBodyInit, alloc: std.mem.Allocator) ![]const u8 { + return switch (self) { + .Blob => error.NotImplemented, + .BufferSource => error.NotImplemented, + .FormData => error.NotImplemented, + .URLSearchParams => error.NotImplemented, + .String => |v| try alloc.dupe(u8, v), + }; + } +}; + pub const XMLHttpRequest = struct { pub const prototype = *XMLHttpRequestEventTarget; pub const mem_guarantied = true; @@ -92,7 +130,7 @@ pub const XMLHttpRequest = struct { } }; - const PrivState = enum { new, open, send, finish, wait, done }; + const PrivState = enum { new, open, send, write, finish, wait, done }; proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, alloc: std.mem.Allocator, @@ -110,7 +148,12 @@ pub const XMLHttpRequest = struct { sync: bool = true, err: ?anyerror = null, - upload: ?XMLHttpRequestUpload = null, + // TODO uncomment this field causes casting issue with + // XMLHttpRequestEventTarget. I think it's dueto an alignement issue, but + // not sure. see + // https://lightpanda.slack.com/archives/C05TRU6RBM1/p1707819010681019 + // upload: ?XMLHttpRequestUpload = null, + timeout: u32 = 0, withCredentials: bool = false, // TODO: response readonly attribute any response; @@ -123,6 +166,8 @@ pub const XMLHttpRequest = struct { response_obj: ?ResponseObj = null, send_flag: bool = false, + payload: ?[]const u8 = null, + pub fn constructor(alloc: std.mem.Allocator, loop: *Loop) !XMLHttpRequest { return .{ .alloc = alloc, @@ -138,17 +183,42 @@ pub const XMLHttpRequest = struct { }; } - pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { - self.proto.deinit(alloc); - self.headers.deinit(); - self.response_headers.deinit(); + pub fn reset(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { if (self.url) |v| alloc.free(v); - if (self.response_bytes) |v| alloc.free(v); - if (self.response_headers) |v| alloc.free(v); + self.url = null; + 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(); - if (self.req) |*r| r.deinit(); + self.response_obj = null; + self.response_mime = Mime.Empty; + self.response_type = .Empty; + + // TODO should we clearRetainingCapacity instead? + self.headers.clearAndFree(); + self.response_headers.clearAndFree(); + self.response_status = 0; + + self.send_flag = false; + + self.priv_state = .new; + + if (self.req) |*r| { + r.deinit(); + self.req = null; + } + } + + pub fn deinit(self: *XMLHttpRequest, alloc: std.mem.Allocator) void { + self.reset(); + self.headers.deinit(); + self.response_headers.deinit(); + + self.proto.deinit(alloc); + // TODO the client must be shared between requests. self.cli.deinit(); } @@ -198,31 +268,13 @@ pub const XMLHttpRequest = struct { self.method = try validMethod(method); + self.reset(alloc); + self.url = try alloc.dupe(u8, url); self.uri = std.Uri.parse(self.url.?) catch return DOMError.Syntax; self.sync = if (asyn) |b| !b else false; - self.send_flag = false; - - // TODO should we clearRetainingCapacity instead? - self.headers.clearAndFree(); - self.response_headers.clearAndFree(); - self.response_status = 0; - - if (self.response_obj) |v| v.deinit(); - self.response_obj = null; - - self.response_mime = Mime.Empty; - - self.response_type = .Empty; - if (self.response_bytes) |v| alloc.free(v); self.state = OPENED; - self.priv_state = .new; - if (self.req) |*r| { - r.deinit(); - self.req = null; - } - self.dispatchEvt("readystatechange"); } @@ -302,16 +354,30 @@ pub const XMLHttpRequest = struct { return try self.headers.append(name, value); } - // TODO body can be either a string or a document - pub fn _send(self: *XMLHttpRequest, body: ?[]const u8) !void { + // TODO body can be either a XMLHttpRequestBodyInit or a document + pub fn _send(self: *XMLHttpRequest, alloc: std.mem.Allocator, body: ?[]const u8) !void { if (self.state != OPENED) return DOMError.InvalidState; if (self.send_flag) return DOMError.InvalidState; // The body argument provides the request body, if any, and is ignored // if the request method is GET or HEAD. // https://xhr.spec.whatwg.org/#the-send()-method - _ = body; - // TODO set Content-Type header according to the given body. + // var used_body: ?XMLHttpRequestBodyInit = null; + if (body != null and self.method != .GET and self.method != .HEAD) { + // TODO If body is a Document, then set this’s request body to body, serialized, converted, and UTF-8 encoded. + + const body_init = XMLHttpRequestBodyInit{ .String = body.? }; + + // keep the user content type from request headers. + if (self.headers.getFirstEntry("Content-Type") == null) { + // https://fetch.spec.whatwg.org/#bodyinit-safely-extract + try self.headers.append("Content-Type", try body_init.contentType()); + } + + // copy the payload + if (self.payload) |v| alloc.free(v); + self.payload = try body_init.dupe(alloc); + } self.send_flag = true; self.impl.yield(self); @@ -330,10 +396,22 @@ pub const XMLHttpRequest = struct { self.req = self.cli.open(self.method, self.uri, self.headers, .{}) catch |e| return self.onErr(e); }, .open => { + // prepare payload transfert. + if (self.payload) |v| self.req.?.transfer_encoding = .{ .content_length = v.len }; + self.priv_state = .send; self.req.?.send(.{}) catch |e| return self.onErr(e); }, .send => { + if (self.payload) |payload| { + self.priv_state = .write; + self.req.?.writeAll(payload) catch |e| return self.onErr(e); + } else { + self.priv_state = .finish; + self.req.?.finish() catch |e| return self.onErr(e); + } + }, + .write => { self.priv_state = .finish; self.req.?.finish() catch |e| return self.onErr(e); }, @@ -733,4 +811,17 @@ pub fn testExecFn( // .{ .src = "req3.response", .ex = "" }, // }; // try checkCases(js_env, &json); + // + var post = [_]Case{ + .{ .src = "const req3 = new XMLHttpRequest()", .ex = "undefined" }, + .{ .src = "req3.open('POST', 'http://httpbin.io/post')", .ex = "undefined" }, + .{ .src = "req3.send('foo')", .ex = "undefined" }, + + // Each case executed waits for all loop callaback calls. + // So the url has been retrieved. + .{ .src = "req3.status", .ex = "200" }, + .{ .src = "req3.statusText", .ex = "OK" }, + .{ .src = "req3.responseText.length > 64", .ex = "true" }, + }; + try checkCases(js_env, &post); } From 4c19dbc34f9a819a3014bb75c1a39132a86db6c2 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 14 Feb 2024 11:28:32 +0100 Subject: [PATCH 44/46] xhr: fix content-type header typo --- src/xhr/xhr.zig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index 5a34566d..b6119c06 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -56,8 +56,8 @@ pub const XMLHttpRequestBodyInit = union(XMLHttpRequestBodyInitTag) { .Blob => error.NotImplemented, .BufferSource => error.NotImplemented, .FormData => "multipart/form-data; boundary=TODO", - .URLSearchParams => "application/x-www-form-urlencoded;charset=UTF-8", - .String => "text/plain;charset=UTF-8", + .URLSearchParams => "application/x-www-form-urlencoded; charset=UTF-8", + .String => "text/plain; charset=UTF-8", }; } From e9277436324c5bbbc0f4859970d57ffb9d1b0820 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 14 Feb 2024 15:50:31 +0100 Subject: [PATCH 45/46] browser: add log info on error --- src/browser/browser.zig | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 00dbc9c2..8de182b8 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -176,7 +176,15 @@ pub const Page = struct { log.info("GET {any} {d}", .{ self.uri, req.response.status }); // TODO handle redirection - if (req.response.status != .ok) return error.BadStatusCode; + if (req.response.status != .ok) { + log.debug("{?} {d} {s}\n{any}", .{ + req.response.version, + req.response.status, + req.response.reason, + req.response.headers, + }); + return error.BadStatusCode; + } // TODO handle charset // https://html.spec.whatwg.org/#content-type From 2508dc7e9a9cbf1a2c46e6a99a91bbfdcc30d902 Mon Sep 17 00:00:00 2001 From: Pierre Tachoire Date: Wed, 14 Feb 2024 15:51:14 +0100 Subject: [PATCH 46/46] xhr: add some logs --- src/xhr/xhr.zig | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/xhr/xhr.zig b/src/xhr/xhr.zig index b6119c06..f3840d66 100644 --- a/src/xhr/xhr.zig +++ b/src/xhr/xhr.zig @@ -379,6 +379,8 @@ pub const XMLHttpRequest = struct { self.payload = try body_init.dupe(alloc); } + log.debug("{any} {any}", .{ self.method, self.uri }); + self.send_flag = true; self.impl.yield(self); } @@ -420,6 +422,8 @@ pub const XMLHttpRequest = struct { self.req.?.wait() catch |e| return self.onErr(e); }, .wait => { + log.info("{any} {any} {d}", .{ self.method, self.uri, self.req.?.response.status }); + self.priv_state = .done; self.response_headers = self.req.?.response.headers.clone(self.response_headers.allocator) catch |e| return self.onErr(e); @@ -506,6 +510,8 @@ pub const XMLHttpRequest = struct { self.dispatchEvt("readystatechange"); self.dispatchProgressEvent("error", .{}); self.dispatchProgressEvent("loadend", .{}); + + log.debug("{any} {any} {any}", .{ self.method, self.uri, self.err }); } pub fn _abort(self: *XMLHttpRequest) void {