Switch XHR to new http client

get puppeteer/cdp.js working again

make test are all passing
This commit is contained in:
Karl Seguin
2025-07-30 17:13:23 +08:00
parent b0fe5d60ab
commit 54ab1326e5
12 changed files with 330 additions and 386 deletions

View File

@@ -38,6 +38,10 @@ page: *Page,
// Only once this is true can deferred scripts be run // Only once this is true can deferred scripts be run
static_scripts_done: bool, static_scripts_done: bool,
// when async_count == 0 and static_script_done == true, the document is completed
// loading (i.e. page.documentIsComplete should be called).
async_count: usize,
// Normal scripts (non-deffered & non-async). These must be executed ni order // Normal scripts (non-deffered & non-async). These must be executed ni order
scripts: OrderList, scripts: OrderList,
@@ -58,6 +62,7 @@ pub fn init(app: *App, page: *Page) ScriptManager {
.page = page, .page = page,
.scripts = .{}, .scripts = .{},
.deferred = .{}, .deferred = .{},
.async_count = 0,
.allocator = allocator, .allocator = allocator,
.client = app.http_client, .client = app.http_client,
.static_scripts_done = false, .static_scripts_done = false,
@@ -183,7 +188,7 @@ pub fn addFromElement(self: *ScriptManager, element: *parser.Element) !void {
.ctx = pending_script, .ctx = pending_script,
.method = .GET, .method = .GET,
.start_callback = startCallback, .start_callback = startCallback,
.header_callback = headerCallback, .header_done_callback = headerCallback,
.data_callback = dataCallback, .data_callback = dataCallback,
.done_callback = doneCallback, .done_callback = doneCallback,
.error_callback = errorCallback, .error_callback = errorCallback,
@@ -229,7 +234,31 @@ fn evaluate(self: *ScriptManager) void {
pending_script.script.eval(page); pending_script.script.eval(page);
} }
// When all scripts (normal and deferred) are done loading, the document
// state changes (this ultimately triggers the DOMContentLoaded event)
page.documentIsLoaded(); page.documentIsLoaded();
if (self.async_count == 0) {
// if we're here, then its like `asyncDone`
// 1 - there are no async scripts pending
// 2 - we checkecked static_scripts_done == true above
// 3 - we drained self.scripts above
// 4 - we drained self.deferred above
page.documentIsComplete();
}
}
fn asyncDone(self: *ScriptManager) void {
self.async_count -= 1;
if (
self.async_count == 0 and // there are no more async scripts
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
self.scripts.first == null and // and there are no more <script src=> to wait for
self.deferred.first == null // and there are no more <script defer src=> to wait for
) {
// then the document is considered complete
self.page.documentIsComplete();
}
} }
fn getList(self: *ScriptManager, script: *const Script) ?*OrderList { fn getList(self: *ScriptManager, script: *const Script) ?*OrderList {
@@ -334,10 +363,13 @@ const PendingScript = struct {
fn doneCallback(self: *PendingScript, transfer: *http.Transfer) void { fn doneCallback(self: *PendingScript, transfer: *http.Transfer) void {
log.debug(.http, "script fetch complete", .{ .req = transfer }); log.debug(.http, "script fetch complete", .{ .req = transfer });
const manager = self.manager;
if (self.script.is_async) { if (self.script.is_async) {
// async script can be evaluated immediately // async script can be evaluated immediately
defer self.deinit(); defer self.deinit();
self.script.eval(self.manager.page); self.script.eval(self.manager.page);
manager.asyncDone();
} else { } else {
self.complete = true; self.complete = true;
self.manager.evaluate(); self.manager.evaluate();
@@ -348,6 +380,7 @@ const PendingScript = struct {
log.warn(.http, "script fetch error", .{ .req = transfer, .err = err }); log.warn(.http, "script fetch error", .{ .req = transfer, .err = err });
self.deinit(); self.deinit();
} }
}; };
const Script = struct { const Script = struct {

View File

@@ -96,12 +96,23 @@ pub const Mime = struct {
break; break;
} }
var attribute_value = value; var attribute_value = value;
if (value[0] == '"' and value[value.len - 1] == '"') { if (value[0] == '"') {
if (value.len < 3 or value[value.len - 1] != '"') {
return error.Invalid;
}
attribute_value = value[1 .. value.len - 1]; attribute_value = value[1 .. value.len - 1];
} }
if (std.ascii.eqlIgnoreCase(attribute_value, "utf-8")) { if (std.ascii.eqlIgnoreCase(attribute_value, "utf-8")) {
charset = "UTF-8"; charset = "UTF-8";
} else {
// we only care about null (which we default to UTF-8)
// or UTF-8. If this is actually set (i.e. not null)
// and isn't UTF-8, we'll just put a dummy value. If
// we want to capture the actual value, we'll need to
// dupe/allocate it. Since, for now, we don't need that
// we can avoid the allocation.
charset = "lightpanda:UNSUPPORTED";
} }
}, },
} }
@@ -271,7 +282,7 @@ pub const Mime = struct {
}; };
const testing = @import("../testing.zig"); const testing = @import("../testing.zig");
test "Mime: invalid " { test "Mime: invalid" {
defer testing.reset(); defer testing.reset();
const invalids = [_][]const u8{ const invalids = [_][]const u8{
@@ -289,7 +300,6 @@ test "Mime: invalid " {
"text/html; charset=\"\"", "text/html; charset=\"\"",
"text/html; charset=\"", "text/html; charset=\"",
"text/html; charset=\"\\", "text/html; charset=\"\\",
"text/html; charset=\"\\a\"", // invalid to escape non special characters
}; };
for (invalids) |invalid| { for (invalids) |invalid| {
@@ -351,19 +361,19 @@ test "Mime: parse charset" {
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "utf-8", .charset = "UTF-8",
.params = "charset=utf-8", .params = "charset=utf-8",
}, "text/xml; charset=utf-8"); }, "text/xml; charset=utf-8");
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "utf-8", .charset = "UTF-8",
.params = "charset=\"utf-8\"", .params = "charset=\"utf-8\"",
}, "text/xml;charset=\"utf-8\""); }, "text/xml;charset=\"utf-8\"");
try expect(.{ try expect(.{
.content_type = .{ .text_xml = {} }, .content_type = .{ .text_xml = {} },
.charset = "\\ \" ", .charset = "lightpanda:UNSUPPORTED",
.params = "charset=\"\\\\ \\\" \"", .params = "charset=\"\\\\ \\\" \"",
}, "text/xml;charset=\"\\\\ \\\" \" "); }, "text/xml;charset=\"\\\\ \\\" \" ");
} }

View File

@@ -131,6 +131,7 @@ pub const Page = struct {
.messageloop_node = .{ .func = messageLoopCallback }, .messageloop_node = .{ .func = messageLoopCallback },
.keydown_event_node = .{ .func = keydownCallback }, .keydown_event_node = .{ .func = keydownCallback },
.window_clicked_event_node = .{ .func = windowClicked }, .window_clicked_event_node = .{ .func = windowClicked },
// @newhttp
// .request_factory = browser.http_client.requestFactory(.{ // .request_factory = browser.http_client.requestFactory(.{
// .notification = browser.notification, // .notification = browser.notification,
// }), // }),
@@ -233,32 +234,32 @@ pub const Page = struct {
pub fn wait(self: *Page, wait_sec: usize) !void { pub fn wait(self: *Page, wait_sec: usize) !void {
switch (self.mode) { switch (self.mode) {
.pre, .html, .raw, .parsed => { .pre, .html, .raw, .parsed => {
// The HTML page was parsed. We're now either have JS scripts to // The HTML page was parsed. We now either have JS scripts to
// download, or timeouts to execute, or both. // download, or timeouts to execute, or both.
const cutoff = timestamp() + wait_sec; const cutoff = timestamp() + wait_sec;
var loop = self.session.browser.app.loop;
var try_catch: Env.TryCatch = undefined; var try_catch: Env.TryCatch = undefined;
try_catch.init(self.main_context); try_catch.init(self.main_context);
defer try_catch.deinit(); defer try_catch.deinit();
var http_client = self.http_client;
var loop = self.session.browser.app.loop;
// @newhttp Not sure about the timing / the order / any of this. // @newhttp Not sure about the timing / the order / any of this.
// I think I want to remove the loop. Implement our own timeouts // I think I want to remove the loop. Implement our own timeouts
// and switch the CDP server to blocking. For now, just try this.` // and switch the CDP server to blocking. For now, just try this.`
while (timestamp() < cutoff) { while (timestamp() < cutoff) {
const active = try self.http_client.tick(10); // 10ms const has_pending_timeouts = loop.hasPendingTimeout();
if (http_client.active > 0) {
try http_client.tick(10); // 10ms
} else if (self.loaded and self.loaded and !has_pending_timeouts) {
// we have no active HTTP requests, and no timeouts pending
return;
}
if (active == 0) { if (!has_pending_timeouts) {
if (!self.loaded) { continue;
// We have no pending HTTP requests and we haven't
// triggered the load event. Trigger the load event.
self.documentIsComplete();
} else if (loop.hasPendingTimeout() == false) {
// we have no active HTTP requests, and no timeouts pending
return;
}
} }
// 10ms // 10ms
@@ -308,11 +309,17 @@ pub const Page = struct {
.ctx = self, .ctx = self,
.url = owned_url, .url = owned_url,
.method = opts.method, .method = opts.method,
.header_callback = pageHeaderCallback, .header_done_callback = pageHeaderCallback,
.data_callback = pageDataCallback, .data_callback = pageDataCallback,
.done_callback = pageDoneCallback, .done_callback = pageDoneCallback,
.error_callback = pageErrorCallback, .error_callback = pageErrorCallback,
}); });
self.session.browser.notification.dispatch(.page_navigate, &.{
.opts = opts,
.url = owned_url,
.timestamp = timestamp(),
});
} }
pub fn documentIsLoaded(self: *Page) void { pub fn documentIsLoaded(self: *Page) void {
@@ -321,11 +328,18 @@ pub const Page = struct {
}; };
} }
fn documentIsComplete(self: *Page) void { pub fn documentIsComplete(self: *Page) void {
std.debug.assert(self.loaded == false);
self.loaded = true; self.loaded = true;
self._documentIsComplete() catch |err| { self._documentIsComplete() catch |err| {
log.err(.browser, "document is complete", .{ .err = err }); log.err(.browser, "document is complete", .{ .err = err });
}; };
self.session.browser.notification.dispatch(.page_navigated, &.{
.url = self.url.raw,
.timestamp = timestamp(),
});
} }
fn _documentIsComplete(self: *Page) !void { fn _documentIsComplete(self: *Page) !void {
try HTMLDocument.documentIsComplete(self.window.document, self); try HTMLDocument.documentIsComplete(self.window.document, self);

View File

@@ -31,7 +31,6 @@ const Mime = @import("../mime.zig").Mime;
const parser = @import("../netsurf.zig"); const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page; const Page = @import("../page.zig").Page;
const http = @import("../../http/client.zig"); const http = @import("../../http/client.zig");
const Loop = @import("../../runtime/loop.zig").Loop;
const CookieJar = @import("../storage/storage.zig").CookieJar; const CookieJar = @import("../storage/storage.zig").CookieJar;
// XHR interfaces // XHR interfaces
@@ -80,54 +79,29 @@ const XMLHttpRequestBodyInit = union(enum) {
pub const XMLHttpRequest = struct { pub const XMLHttpRequest = struct {
proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{}, proto: XMLHttpRequestEventTarget = XMLHttpRequestEventTarget{},
loop: *Loop,
arena: Allocator, arena: Allocator,
request: ?*http.Request = null, transfer: ?*http.Transfer = null,
cookie_jar: *CookieJar,
err: ?anyerror = null,
last_dispatch: i64 = 0,
send_flag: bool = false,
method: http.Method, method: http.Method,
state: State, state: State,
url: ?URL = null, url: ?[:0]const u8 = null,
origin_url: *const URL,
// request headers
headers: Headers,
sync: bool = true, sync: bool = true,
err: ?anyerror = null, withCredentials: bool = false,
last_dispatch: i64 = 0, headers: std.ArrayListUnmanaged([:0]const u8),
request_body: ?[]const u8 = null, request_body: ?[]const u8 = null,
cookie_jar: *CookieJar, response_status: u16 = 0,
// the URI of the page where this request is originating from
// 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,
// 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
// timeout: u32 = 0,
withCredentials: bool = false,
// TODO: response readonly attribute any response;
response_bytes: std.ArrayListUnmanaged(u8) = .{}, response_bytes: std.ArrayListUnmanaged(u8) = .{},
response_type: ResponseType = .Empty, response_type: ResponseType = .Empty,
response_headers: Headers, response_headers: std.ArrayListUnmanaged([]const u8) = .{},
response_status: u16 = 0,
// 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
// response_override_mime_type: ?[]const u8 = null,
response_mime: ?Mime = null, response_mime: ?Mime = null,
response_obj: ?ResponseObj = null, response_obj: ?ResponseObj = null,
send_flag: bool = false,
pub const prototype = *XMLHttpRequestEventTarget; pub const prototype = *XMLHttpRequestEventTarget;
@@ -158,68 +132,6 @@ pub const XMLHttpRequest = struct {
const JSONValue = std.json.Value; const JSONValue = std.json.Value;
const Headers = struct {
list: List,
arena: Allocator,
const List = std.ArrayListUnmanaged(std.http.Header);
fn init(arena: Allocator) Headers {
return .{
.arena = arena,
.list = .{},
};
}
fn append(self: *Headers, k: []const u8, v: []const u8) !void {
// duplicate strings
const kk = try self.arena.dupe(u8, k);
const vv = try self.arena.dupe(u8, v);
try self.list.append(self.arena, .{ .name = kk, .value = vv });
}
fn reset(self: *Headers) void {
self.list.clearRetainingCapacity();
}
fn has(self: Headers, k: []const u8) bool {
for (self.list.items) |h| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
return true;
}
}
return false;
}
fn getFirstValue(self: Headers, k: []const u8) ?[]const u8 {
for (self.list.items) |h| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
return h.value;
}
}
return null;
}
// replace any existing header with the same key
fn set(self: *Headers, k: []const u8, v: []const u8) !void {
for (self.list.items, 0..) |h, i| {
if (std.ascii.eqlIgnoreCase(k, h.name)) {
_ = self.list.swapRemove(i);
}
}
self.append(k, v);
}
// TODO
fn sort(_: *Headers) void {}
fn all(self: Headers) []std.http.Header {
return self.list.items;
}
};
const Response = union(ResponseType) { const Response = union(ResponseType) {
Empty: void, Empty: void,
Text: []const u8, Text: []const u8,
@@ -254,22 +166,18 @@ pub const XMLHttpRequest = struct {
return .{ return .{
.url = null, .url = null,
.arena = arena, .arena = arena,
.loop = page.loop, .headers = .{},
.headers = Headers.init(arena),
.response_headers = Headers.init(arena),
.method = undefined, .method = undefined,
.state = .unsent, .state = .unsent,
.origin_url = &page.url,
.cookie_jar = page.cookie_jar, .cookie_jar = page.cookie_jar,
}; };
} }
pub fn destructor(self: *XMLHttpRequest) void { pub fn destructor(self: *XMLHttpRequest) void {
// @newhttp if (self.transfer) |transfer| {
// if (self.request) |req| { transfer.abort();
// req.abort(); self.transfer = null;
self.request = null; }
// }
} }
pub fn reset(self: *XMLHttpRequest) void { pub fn reset(self: *XMLHttpRequest) void {
@@ -283,9 +191,8 @@ pub const XMLHttpRequest = struct {
self.response_type = .Empty; self.response_type = .Empty;
self.response_mime = null; self.response_mime = null;
// TODO should we clearRetainingCapacity instead? self.headers.clearRetainingCapacity();
self.headers.reset(); self.response_headers.clearRetainingCapacity();
self.response_headers.reset();
self.response_status = 0; self.response_status = 0;
self.send_flag = false; self.send_flag = false;
@@ -325,6 +232,7 @@ pub const XMLHttpRequest = struct {
asyn: ?bool, asyn: ?bool,
username: ?[]const u8, username: ?[]const u8,
password: ?[]const u8, password: ?[]const u8,
page: *Page,
) !void { ) !void {
_ = username; _ = username;
_ = password; _ = password;
@@ -335,9 +243,7 @@ pub const XMLHttpRequest = struct {
self.reset(); self.reset();
self.method = try validMethod(method); self.method = try validMethod(method);
const arena = self.arena; self.url = try URL.stitch(page.arena, url, page.url.raw, .{ .null_terminated = true });
self.url = try self.origin_url.resolve(arena, url);
self.sync = if (asyn) |b| !b else false; self.sync = if (asyn) |b| !b else false;
self.state = .opened; self.state = .opened;
@@ -438,9 +344,18 @@ pub const XMLHttpRequest = struct {
} }
pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void { pub fn _setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8) !void {
if (self.state != .opened) return DOMError.InvalidState; if (self.state != .opened) {
if (self.send_flag) return DOMError.InvalidState; return DOMError.InvalidState;
return try self.headers.append(name, value); }
if (self.send_flag) {
return DOMError.InvalidState;
}
return self.headers.append(
self.arena,
try std.fmt.allocPrintZ(self.arena, "{s}: {s}", .{name, value}),
);
} }
// TODO body can be either a XMLHttpRequestBodyInit or a document // TODO body can be either a XMLHttpRequestBodyInit or a document
@@ -455,113 +370,107 @@ pub const XMLHttpRequest = struct {
self.request_body = try self.arena.dupe(u8, b); self.request_body = try self.arena.dupe(u8, b);
} }
// @newhttp try page.http_client.request(.{
_ = page; .ctx = self,
// try page.request_factory.initAsync( .url = self.url.?,
// page.arena, .method = self.method,
// self.method, .start_callback = httpStartCallback,
// &self.url.?.uri, .header_callback = httpHeaderCallback,
// self, .header_done_callback = httpHeaderDoneCallback,
// onHttpRequestReady, .data_callback = httpDataCallback,
// ); .done_callback = httpDoneCallback,
.error_callback = httpErrorCallback,
});
} }
fn onHttpRequestReady(ctx: *anyopaque, request: *http.Request) !void { fn httpStartCallback(transfer: *http.Transfer) !void {
// on error, our caller will cleanup request const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
const self: *XMLHttpRequest = @alignCast(@ptrCast(ctx));
for (self.headers.list.items) |hdr| { for (self.headers.items) |hdr| {
try request.addHeader(hdr.name, hdr.value, .{}); try transfer.addHeader(hdr);
} }
{ // @newhttp
var arr: std.ArrayListUnmanaged(u8) = .{}; // {
try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{ // var arr: std.ArrayListUnmanaged(u8) = .{};
.navigation = false, // try self.cookie_jar.forRequest(&self.url.?.uri, arr.writer(self.arena), .{
.origin_uri = &self.origin_url.uri, // .navigation = false,
.is_http = true, // .origin_uri = &self.origin_url.uri,
}); // .is_http = true,
// });
if (arr.items.len > 0) { // if (arr.items.len > 0) {
try request.addHeader("Cookie", arr.items, .{}); // try request.addHeader("Cookie", arr.items, .{});
} // }
} // }
// 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
// var used_body: ?XMLHttpRequestBodyInit = null;
if (self.request_body) |b| { if (self.request_body) |b| {
if (self.method != .GET and self.method != .HEAD) { if (self.method != .GET and self.method != .HEAD) {
request.body = b; try transfer.setBody(b);
try request.addHeader("Content-Type", "text/plain; charset=UTF-8", .{}); try transfer.addHeader("Content-Type: text/plain; charset=UTF-8");
} }
} }
self.transfer = transfer;
try request.sendAsync(self, .{});
self.request = request;
} }
pub fn onHttpResponse(self: *XMLHttpRequest, progress_: anyerror!http.Progress) !void { fn httpHeaderCallback(transfer: *http.Transfer, header: []const u8) !void {
const progress = progress_ catch |err| { const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
// The request has been closed internally by the client, it isn't safe try self.response_headers.append(self.arena, try self.arena.dupe(u8, header));
// for us to keep it around. }
self.request = null;
self.onErr(err);
return err;
};
if (progress.first) { fn httpHeaderDoneCallback(transfer: *http.Transfer) !void {
const header = progress.header; const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
log.debug(.http, "request header", .{
.source = "xhr",
.url = self.url,
.status = header.status,
});
for (header.headers.items) |hdr| {
try self.response_headers.append(hdr.name, hdr.value);
}
// extract a mime type from headers. const header = &transfer.response_header.?;
if (header.get("content-type")) |ct| {
self.response_mime = Mime.parse(self.arena, ct) catch |e| {
return self.onErr(e);
};
}
// TODO handle override mime type log.debug(.http, "request header", .{
self.state = .headers_received; .source = "xhr",
self.dispatchEvt("readystatechange"); .url = self.url,
.status = header.status,
});
self.response_status = header.status; if (header.contentType()) |ct| {
self.response_mime = Mime.parse(ct) catch |e| {
// TODO correct total return self.onErr(e);
self.dispatchProgressEvent("loadstart", .{ .loaded = 0, .total = 0 }); };
self.state = .loading;
self.dispatchEvt("readystatechange");
try self.cookie_jar.populateFromResponse(self.request.?.request_uri, &header);
} }
if (progress.data) |data| { // TODO handle override mime type
try self.response_bytes.appendSlice(self.arena, data); self.state = .headers_received;
self.dispatchEvt("readystatechange");
self.response_status = header.status;
// TODO correct total
self.dispatchProgressEvent("loadstart", .{ .loaded = 0, .total = 0 });
self.state = .loading;
self.dispatchEvt("readystatechange");
// @newhttp
// try self.cookie_jar.populateFromResponse(self.request.?.request_uri, &header);
}
fn httpDataCallback(transfer: *http.Transfer, data: []const u8) !void {
const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
try self.response_bytes.appendSlice(self.arena, data);
const now = std.time.milliTimestamp();
if (now - self.last_dispatch < 50) {
// don't send this more than once every 50ms
return;
} }
const loaded = self.response_bytes.items.len; const loaded = self.response_bytes.items.len;
const now = std.time.milliTimestamp(); self.dispatchProgressEvent("progress", .{
if (now - self.last_dispatch > 50) { .total = loaded, // TODO, this is wrong? Need the content-type
// don't send this more than once every 50ms .loaded = loaded,
self.dispatchProgressEvent("progress", .{ });
.total = loaded, self.last_dispatch = now;
.loaded = loaded, }
});
self.last_dispatch = now;
}
if (progress.done == false) { fn httpDoneCallback(transfer: *http.Transfer) !void {
return; const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
}
log.info(.http, "request complete", .{ log.info(.http, "request complete", .{
.source = "xhr", .source = "xhr",
@@ -569,20 +478,34 @@ pub const XMLHttpRequest = struct {
.status = self.response_status, .status = self.response_status,
}); });
// Not that the request is done, the http/client will free the request // Not that the request is done, the http/client will free the transfer
// object. It isn't safe to keep it around. // object. It isn't safe to keep it around.
self.request = null; self.transfer = null;
self.state = .done; self.state = .done;
self.send_flag = false; self.send_flag = false;
self.dispatchEvt("readystatechange"); self.dispatchEvt("readystatechange");
const loaded = self.response_bytes.items.len;
// dispatch a progress event load. // dispatch a progress event load.
self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = loaded }); self.dispatchProgressEvent("load", .{ .loaded = loaded, .total = loaded });
// dispatch a progress event loadend. // dispatch a progress event loadend.
self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = loaded }); self.dispatchProgressEvent("loadend", .{ .loaded = loaded, .total = loaded });
} }
fn httpErrorCallback(transfer: *http.Transfer, err: anyerror) void {
const self: *XMLHttpRequest = @alignCast(@ptrCast(transfer.ctx));
// http client will close it after an error, it isn't safe to keep around
self.transfer = null;
self.onErr(err);
}
pub fn _abort(self: *XMLHttpRequest) void {
self.onErr(DOMError.Abort);
self.destructor();
}
fn onErr(self: *XMLHttpRequest, err: anyerror) void { fn onErr(self: *XMLHttpRequest, err: anyerror) void {
self.send_flag = false; self.send_flag = false;
@@ -610,15 +533,10 @@ pub const XMLHttpRequest = struct {
log.log(.http, level, "error", .{ log.log(.http, level, "error", .{
.url = self.url, .url = self.url,
.err = err, .err = err,
.source = "xhr", .source = "xhr.OnErr",
}); });
} }
pub fn _abort(self: *XMLHttpRequest) void {
self.onErr(DOMError.Abort);
self.destructor();
}
pub fn get_responseType(self: *XMLHttpRequest) []const u8 { pub fn get_responseType(self: *XMLHttpRequest) []const u8 {
return switch (self.response_type) { return switch (self.response_type) {
.Empty => "", .Empty => "",
@@ -660,9 +578,8 @@ pub const XMLHttpRequest = struct {
} }
// TODO retrieve the redirected url // TODO retrieve the redirected url
pub fn get_responseURL(self: *XMLHttpRequest) ?[]const u8 { pub fn get_responseURL(self: *XMLHttpRequest) ?[:0]const u8 {
const url = &(self.url orelse return null); return self.url;
return url.raw;
} }
pub fn get_responseXML(self: *XMLHttpRequest) !?Response { pub fn get_responseXML(self: *XMLHttpRequest) !?Response {
@@ -766,18 +683,8 @@ pub const XMLHttpRequest = struct {
return; return;
} }
var ccharset: [:0]const u8 = "utf-8";
if (mime.charset) |rc| {
if (std.mem.eql(u8, rc, "utf-8") == false) {
ccharset = self.arena.dupeZ(u8, rc) catch {
self.response_obj = .{ .Failure = {} };
return;
};
}
}
var fbs = std.io.fixedBufferStream(self.response_bytes.items); var fbs = std.io.fixedBufferStream(self.response_bytes.items);
const doc = parser.documentHTMLParse(fbs.reader(), ccharset) catch { const doc = parser.documentHTMLParse(fbs.reader(), mime.charset orelse "UTF-8") catch {
self.response_obj = .{ .Failure = {} }; self.response_obj = .{ .Failure = {} };
return; return;
}; };
@@ -814,26 +721,27 @@ pub const XMLHttpRequest = struct {
} }
pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 { pub fn _getResponseHeader(self: *XMLHttpRequest, name: []const u8) ?[]const u8 {
return self.response_headers.getFirstValue(name); for (self.response_headers.items) |entry| {
if (entry.len <= name.len) {
continue;
}
if (std.ascii.eqlIgnoreCase(name, entry[0..name.len]) == false) {
continue;
}
if (entry[name.len] != ':') {
continue;
}
return std.mem.trimLeft(u8, entry[name.len + 1..], " ");
}
return null;
} }
// 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) ![]const u8 { pub fn _getAllResponseHeaders(self: *XMLHttpRequest) ![]const u8 {
if (self.response_headers.list.items.len == 0) return "";
self.response_headers.sort();
var buf: std.ArrayListUnmanaged(u8) = .{}; var buf: std.ArrayListUnmanaged(u8) = .{};
const w = buf.writer(self.arena); const w = buf.writer(self.arena);
for (self.response_headers.list.items) |entry| { for (self.response_headers.items) |entry| {
if (entry.value.len == 0) continue; try w.writeAll(entry);
try w.writeAll(entry.name);
try w.writeAll(": ");
try w.writeAll(entry.value);
try w.writeAll("\r\n"); try w.writeAll("\r\n");
} }
@@ -865,8 +773,7 @@ test "Browser.XHR.XMLHttpRequest" {
.{ "req.onload", "function cbk(event) { nb ++; evt = event; }" }, .{ "req.onload", "function cbk(event) { nb ++; evt = event; }" },
.{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" }, .{ "req.onload = cbk", "function cbk(event) { nb ++; evt = event; }" },
.{ "req.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, .{ "req.open('GET', 'http://127.0.0.1:9582/xhr')", null },
.{ "req.setRequestHeader('User-Agent', 'lightpanda/1.0')", "undefined" },
// ensure open resets values // ensure open resets values
.{ "req.status ", "0" }, .{ "req.status ", "0" },
@@ -886,7 +793,12 @@ test "Browser.XHR.XMLHttpRequest" {
.{ "req.status", "200" }, .{ "req.status", "200" },
.{ "req.statusText", "OK" }, .{ "req.statusText", "OK" },
.{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" }, .{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" },
.{ "req.getAllResponseHeaders().length", "80" }, .{
"req.getAllResponseHeaders()",
"content-length: 100\r\n" ++
"Content-Type: text/html; charset=utf-8\r\n" ++
"Connection: Close\r\n"
},
.{ "req.responseText.length", "100" }, .{ "req.responseText.length", "100" },
.{ "req.response.length == req.responseText.length", "true" }, .{ "req.response.length == req.responseText.length", "true" },
.{ "req.responseXML instanceof Document", "true" }, .{ "req.responseXML instanceof Document", "true" },
@@ -894,7 +806,7 @@ test "Browser.XHR.XMLHttpRequest" {
try runner.testCases(&.{ try runner.testCases(&.{
.{ "const req2 = new XMLHttpRequest()", "undefined" }, .{ "const req2 = new XMLHttpRequest()", "undefined" },
.{ "req2.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, .{ "req2.open('GET', 'http://127.0.0.1:9582/xhr')", "undefined" },
.{ "req2.responseType = 'document'", "document" }, .{ "req2.responseType = 'document'", "document" },
.{ "req2.send()", "undefined" }, .{ "req2.send()", "undefined" },
@@ -909,7 +821,7 @@ test "Browser.XHR.XMLHttpRequest" {
try runner.testCases(&.{ try runner.testCases(&.{
.{ "const req3 = new XMLHttpRequest()", "undefined" }, .{ "const req3 = new XMLHttpRequest()", "undefined" },
.{ "req3.open('GET', 'https://127.0.0.1:9581/xhr/json')", "undefined" }, .{ "req3.open('GET', 'http://127.0.0.1:9582/xhr/json')", "undefined" },
.{ "req3.responseType = 'json'", "json" }, .{ "req3.responseType = 'json'", "json" },
.{ "req3.send()", "undefined" }, .{ "req3.send()", "undefined" },
@@ -923,7 +835,7 @@ test "Browser.XHR.XMLHttpRequest" {
try runner.testCases(&.{ try runner.testCases(&.{
.{ "const req4 = new XMLHttpRequest()", "undefined" }, .{ "const req4 = new XMLHttpRequest()", "undefined" },
.{ "req4.open('POST', 'https://127.0.0.1:9581/xhr')", "undefined" }, .{ "req4.open('POST', 'http://127.0.0.1:9582/xhr')", "undefined" },
.{ "req4.send('foo')", "undefined" }, .{ "req4.send('foo')", "undefined" },
// Each case executed waits for all loop callaback calls. // Each case executed waits for all loop callaback calls.
@@ -935,7 +847,7 @@ test "Browser.XHR.XMLHttpRequest" {
try runner.testCases(&.{ try runner.testCases(&.{
.{ "const req5 = new XMLHttpRequest()", "undefined" }, .{ "const req5 = new XMLHttpRequest()", "undefined" },
.{ "req5.open('GET', 'https://127.0.0.1:9581/xhr')", "undefined" }, .{ "req5.open('GET', 'http://127.0.0.1:9582/xhr')", "undefined" },
.{ "var status = 0; req5.onload = function () { status = this.status };", "function () { status = this.status }" }, .{ "var status = 0; req5.onload = function () { status = this.status };", "function () { status = this.status }" },
.{ "req5.send()", "undefined" }, .{ "req5.send()", "undefined" },
@@ -956,7 +868,7 @@ test "Browser.XHR.XMLHttpRequest" {
, ,
null, null,
}, },
.{ "req6.open('GET', 'https://127.0.0.1:9581/xhr')", null }, .{ "req6.open('GET', 'http://127.0.0.1:9582/xhr')", null },
.{ "req6.send()", null }, .{ "req6.send()", null },
.{ "readyStates.length", "4" }, .{ "readyStates.length", "4" },
.{ "readyStates[0] === XMLHttpRequest.OPENED", "true" }, .{ "readyStates[0] === XMLHttpRequest.OPENED", "true" },

View File

@@ -155,6 +155,7 @@ fn navigate(cmd: anytype) !void {
.reason = .address_bar, .reason = .address_bar,
.cdp_id = cmd.input.id, .cdp_id = cmd.input.id,
}); });
try page.wait(5);
} }
pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void { pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.PageNavigate) !void {
@@ -189,13 +190,13 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
.frameId = target_id, .frameId = target_id,
.delay = 0, .delay = 0,
.reason = reason, .reason = reason,
.url = event.url.raw, .url = event.url,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
try cdp.sendEvent("Page.frameRequestedNavigation", .{ try cdp.sendEvent("Page.frameRequestedNavigation", .{
.frameId = target_id, .frameId = target_id,
.reason = reason, .reason = reason,
.url = event.url.raw, .url = event.url,
.disposition = "currentTab", .disposition = "currentTab",
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
@@ -203,7 +204,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa
// frameStartedNavigating event // frameStartedNavigating event
try cdp.sendEvent("Page.frameStartedNavigating", .{ try cdp.sendEvent("Page.frameStartedNavigating", .{
.frameId = target_id, .frameId = target_id,
.url = event.url.raw, .url = event.url,
.loaderId = loader_id, .loaderId = loader_id,
.navigationType = "differentDocument", .navigationType = "differentDocument",
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
@@ -306,7 +307,7 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !voi
.type = "Navigation", .type = "Navigation",
.frame = Frame{ .frame = Frame{
.id = target_id, .id = target_id,
.url = event.url.raw, .url = event.url,
.loaderId = bc.loader_id, .loaderId = bc.loader_id,
.securityOrigin = bc.security_origin, .securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type, .secureContextType = bc.secure_context_type,

View File

@@ -23,6 +23,7 @@ pub const c = @cImport({
const ENABLE_DEBUG = false; const ENABLE_DEBUG = false;
const std = @import("std"); const std = @import("std");
const log = @import("../log.zig");
const builtin = @import("builtin"); const builtin = @import("builtin");
const errors = @import("errors.zig"); const errors = @import("errors.zig");
@@ -36,14 +37,11 @@ pub fn init() !void {
} }
} }
const LogFn = *const fn(ctx: []const u8, err: anyerror) void;
pub fn deinit() void { pub fn deinit() void {
c.curl_global_cleanup(); c.curl_global_cleanup();
} }
pub const Client = struct { pub const Client = struct {
log: LogFn,
active: usize, active: usize,
multi: *c.CURLM, multi: *c.CURLM,
handles: Handles, handles: Handles,
@@ -57,7 +55,6 @@ pub const Client = struct {
const RequestQueue = std.DoublyLinkedList(Request); const RequestQueue = std.DoublyLinkedList(Request);
const Opts = struct { const Opts = struct {
log: ?LogFn = null,
timeout_ms: u31 = 0, timeout_ms: u31 = 0,
max_redirects: u8 = 10, max_redirects: u8 = 10,
connect_timeout_ms: u31 = 5000, connect_timeout_ms: u31 = 5000,
@@ -80,7 +77,6 @@ pub const Client = struct {
errdefer _ = c.curl_multi_cleanup(multi); errdefer _ = c.curl_multi_cleanup(multi);
client.* = .{ client.* = .{
.log = opts.log orelse noopLog,
.queue = .{}, .queue = .{},
.active = 0, .active = 0,
.multi = multi, .multi = multi,
@@ -101,7 +97,7 @@ pub const Client = struct {
self.allocator.destroy(self); self.allocator.destroy(self);
} }
pub fn tick(self: *Client, timeout_ms: usize) !usize { pub fn tick(self: *Client, timeout_ms: usize) !void {
var handles = &self.handles.available; var handles = &self.handles.available;
while (true) { while (true) {
if (handles.first == null) { if (handles.first == null) {
@@ -116,7 +112,6 @@ pub const Client = struct {
} }
try self.perform(@intCast(timeout_ms)); try self.perform(@intCast(timeout_ms));
return self.active;
} }
pub fn request(self: *Client, req: Request) !void { pub fn request(self: *Client, req: Request) !void {
@@ -144,16 +139,11 @@ pub const Client = struct {
.OPTIONS => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "options")), .OPTIONS => try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_CUSTOMREQUEST, "options")),
} }
const header_list = c.curl_slist_append(null, "User-Agent: Lightpanda/1.0");
const header_list = c.curl_slist_append(null, "User-Agent: lightpanda/1");
errdefer c.curl_slist_free_all(header_list); errdefer c.curl_slist_free_all(header_list);
try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list)); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_HTTPHEADER, header_list));
//try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(STRING.len))));
//try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, STRING.ptr));
break :blk header_list; break :blk header_list;
}; };
@@ -345,8 +335,9 @@ pub const Request = struct {
// arbitrary data that can be associated with this request // arbitrary data that can be associated with this request
ctx: *anyopaque = undefined, ctx: *anyopaque = undefined,
start_callback: ?*const fn(req: *Transfer) anyerror!void = noopStart, start_callback: ?*const fn(req: *Transfer) anyerror!void = null,
header_callback: *const fn (req: *Transfer) anyerror!void, header_callback: ?*const fn (req: *Transfer, header: []const u8) anyerror!void = null ,
header_done_callback: *const fn (req: *Transfer) anyerror!void,
data_callback: *const fn(req: *Transfer, data: []const u8) anyerror!void, data_callback: *const fn(req: *Transfer, data: []const u8) anyerror!void,
done_callback: *const fn(req: *Transfer) anyerror!void, done_callback: *const fn(req: *Transfer) anyerror!void,
error_callback: *const fn(req: *Transfer, err: anyerror) void, error_callback: *const fn(req: *Transfer, err: anyerror) void,
@@ -366,6 +357,12 @@ pub const Transfer = struct {
// needs to be freed when we're done // needs to be freed when we're done
_request_header_list: ?*c.curl_slist = null, _request_header_list: ?*c.curl_slist = null,
fn deinit(self: *Transfer) void {
if (self._request_header_list) |list| {
c.curl_slist_free_all(list);
}
}
pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void { pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
const req = self.req; const req = self.req;
return writer.print("[{d}] {s} {s}", .{self.id, @tagName(req.method), req.url}); return writer.print("[{d}] {s} {s}", .{self.id, @tagName(req.method), req.url});
@@ -375,10 +372,23 @@ pub const Transfer = struct {
self.req.error_callback(self, err); self.req.error_callback(self, err);
} }
fn deinit(self: *Transfer) void { pub fn setBody(self: *Transfer, body: []const u8) !void {
if (self._request_header_list) |list| { const easy = self.handle.easy;
c.curl_slist_free_all(list); try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDSIZE, @as(c_long, @intCast(body.len))));
} try errorCheck(c.curl_easy_setopt(easy, c.CURLOPT_POSTFIELDS, body.ptr));
}
pub fn addHeader(self: *Transfer, value: [:0]const u8) !void {
self._request_header_list = c.curl_slist_append(self._request_header_list, value);
}
pub fn abort(self: *Transfer) void {
var client = self.handle.client;
errorMCheck(c.curl_multi_remove_handle(client.multi, self.handle.easy)) catch |err| {
log.err(.http, "Failed to abort", .{.err = err});
};
client.active -= 1;
self.deinit();
} }
fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) callconv(.c) usize { fn headerCallback(buffer: [*]const u8, header_count: usize, buf_len: usize, data: *anyopaque) callconv(.c) usize {
@@ -387,11 +397,13 @@ pub const Transfer = struct {
const handle: *Handle = @alignCast(@ptrCast(data)); const handle: *Handle = @alignCast(@ptrCast(data));
var transfer = fromEasy(handle.easy) catch |err| { var transfer = fromEasy(handle.easy) catch |err| {
handle.client.log("retrieve private info", err); log.err(.http, "retrive private info", .{.err = err});
return 0; return 0;
}; };
const header = buffer[0..buf_len]; std.debug.assert(std.mem.endsWith(u8, buffer[0..buf_len], "\r\n"));
const header = buffer[0..buf_len - 2];
if (transfer.response_header == null) { if (transfer.response_header == null) {
if (buf_len < 13 or std.mem.startsWith(u8, header, "HTTP/") == false) { if (buf_len < 13 or std.mem.startsWith(u8, header, "HTTP/") == false) {
@@ -439,8 +451,7 @@ pub const Transfer = struct {
if (buf_len > CONTENT_TYPE_LEN) { if (buf_len > CONTENT_TYPE_LEN) {
if (std.ascii.eqlIgnoreCase(header[0..CONTENT_TYPE_LEN], "content-type:")) { if (std.ascii.eqlIgnoreCase(header[0..CONTENT_TYPE_LEN], "content-type:")) {
const value = std.mem.trimLeft(u8, header[CONTENT_TYPE_LEN..], " "); const value = std.mem.trimLeft(u8, header[CONTENT_TYPE_LEN..], " ");
// -2 to trim the trailing \r\n const len = @min(value.len, hdr._content_type.len);
const len = @min(value.len - 2, hdr._content_type.len);
hdr._content_type_len = len; hdr._content_type_len = len;
@memcpy(hdr._content_type[0..len], value[0..len]); @memcpy(hdr._content_type[0..len], value[0..len]);
} }
@@ -448,10 +459,14 @@ pub const Transfer = struct {
} }
if (buf_len == 2) { if (buf_len == 2) {
transfer.req.header_callback(transfer) catch { transfer.req.header_done_callback(transfer) catch {
// returning < buf_len terminates the request // returning < buf_len terminates the request
return 0; return 0;
}; };
} else {
if (transfer.req.header_callback) |cb| {
cb(transfer, header) catch return 0;
}
} }
return buf_len; return buf_len;
} }
@@ -462,8 +477,8 @@ pub const Transfer = struct {
const handle: *Handle = @alignCast(@ptrCast(data)); const handle: *Handle = @alignCast(@ptrCast(data));
var transfer = fromEasy(handle.easy) catch |err| { var transfer = fromEasy(handle.easy) catch |err| {
handle.client.log("retrieve private info", err); log.err(.http, "retrive private info", .{.err = err});
return 0; return c.CURL_WRITEFUNC_ERROR;
}; };
if (transfer._redirecting) { if (transfer._redirecting) {
@@ -535,10 +550,3 @@ pub const ProxyAuth = union(enum) {
bearer: struct { token: []const u8 }, bearer: struct { token: []const u8 },
}; };
fn noopLog(ctx: []const u8, _: anyerror) void {
_ = ctx;
}
fn noopStart(transfer: *Transfer) !void {
_ = transfer;
}

View File

@@ -667,88 +667,42 @@ fn serveHTTP(address: std.net.Address) !void {
test_wg.finish(); test_wg.finish();
var read_buffer: [1024]u8 = undefined; var read_buffer: [1024]u8 = undefined;
ACCEPT: while (true) { while (true) {
defer _ = arena.reset(.{ .free_all = {} });
const aa = arena.allocator();
var conn = try listener.accept(); var conn = try listener.accept();
defer conn.stream.close(); defer conn.stream.close();
var http_server = std.http.Server.init(conn, &read_buffer); var http_server = std.http.Server.init(conn, &read_buffer);
var connect_headers: std.ArrayListUnmanaged(std.http.Header) = .{};
REQUEST: while (true) { var request = http_server.receiveHead() catch |err| switch (err) {
var request = http_server.receiveHead() catch |err| switch (err) { error.HttpConnectionClosing => continue,
error.HttpConnectionClosing => continue :ACCEPT, else => {
else => { std.debug.print("Test HTTP Server error: {}\n", .{err});
std.debug.print("Test HTTP Server error: {}\n", .{err}); return err;
return err; },
};
const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) {
try request.respond("Hello!", .{
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
});
} else if (std.mem.eql(u8, path, "/xhr")) {
try request.respond("1234567890" ** 10, .{
.extra_headers = &.{
.{ .name = "Content-Type", .value = "text/html; charset=utf-8" },
.{ .name = "Connection", .value = "Close" },
}, },
}; });
} else if (std.mem.eql(u8, path, "/xhr/json")) {
if (request.head.method == .CONNECT) { try request.respond("{\"over\":\"9000!!!\"}", .{
try request.respond("", .{ .status = .ok }); .extra_headers = &.{
.{ .name = "Content-Type", .value = "application/json" },
// Proxy headers and destination headers are separated in the case of a CONNECT proxy .{ .name = "Connection", .value = "Close" },
// We store the CONNECT headers, then continue with the request for the destination },
var it = request.iterateHeaders(); });
while (it.next()) |hdr| { } else {
try connect_headers.append(aa, .{ // should not have an unknown path
.name = try std.fmt.allocPrint(aa, "__{s}", .{hdr.name}), unreachable;
.value = try aa.dupe(u8, hdr.value),
});
}
continue :REQUEST;
}
const path = request.head.target;
if (std.mem.eql(u8, path, "/loader")) {
try request.respond("Hello!", .{
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
});
} else if (std.mem.eql(u8, path, "/http_client/simple")) {
try request.respond("", .{
.extra_headers = &.{.{ .name = "Connection", .value = "close" }},
});
} else if (std.mem.eql(u8, path, "/http_client/redirect")) {
try request.respond("", .{
.status = .moved_permanently,
.extra_headers = &.{
.{ .name = "Connection", .value = "close" },
.{ .name = "LOCATION", .value = "../http_client/echo" },
},
});
} else if (std.mem.eql(u8, path, "/http_client/redirect/secure")) {
try request.respond("", .{
.status = .moved_permanently,
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "LOCATION", .value = "https://127.0.0.1:9581/http_client/body" } },
});
} else if (std.mem.eql(u8, path, "/http_client/gzip")) {
const body = &.{ 0x1f, 0x8b, 0x08, 0x08, 0x01, 0xc6, 0x19, 0x68, 0x00, 0x03, 0x74, 0x65, 0x73, 0x74, 0x2e, 0x68, 0x74, 0x6d, 0x6c, 0x00, 0x73, 0x54, 0xc8, 0x4b, 0x2d, 0x57, 0x48, 0x2a, 0xca, 0x2f, 0x2f, 0x4e, 0x2d, 0x52, 0x48, 0x2a, 0xcd, 0xcc, 0x29, 0x51, 0x48, 0xcb, 0x2f, 0x52, 0xc8, 0x4d, 0x4c, 0xce, 0xc8, 0xcc, 0x4b, 0x2d, 0xe6, 0x02, 0x00, 0xe7, 0xc3, 0x4b, 0x27, 0x21, 0x00, 0x00, 0x00 };
try request.respond(body, .{
.extra_headers = &.{ .{ .name = "Connection", .value = "close" }, .{ .name = "Content-Encoding", .value = "gzip" } },
});
} else if (std.mem.eql(u8, path, "/http_client/echo")) {
var headers: std.ArrayListUnmanaged(std.http.Header) = .{};
var it = request.iterateHeaders();
while (it.next()) |hdr| {
try headers.append(aa, .{
.name = try std.fmt.allocPrint(aa, "_{s}", .{hdr.name}),
.value = hdr.value,
});
}
if (connect_headers.items.len > 0) {
try headers.appendSlice(aa, connect_headers.items);
connect_headers.clearRetainingCapacity();
}
try headers.append(aa, .{ .name = "Connection", .value = "Close" });
try request.respond("over 9000!", .{
.status = .created,
.extra_headers = headers.items,
});
}
continue :ACCEPT;
} }
} }
} }

View File

@@ -81,13 +81,13 @@ pub const Notification = struct {
pub const PageNavigate = struct { pub const PageNavigate = struct {
timestamp: u32, timestamp: u32,
url: *const URL, url: []const u8,
opts: page.NavigateOpts, opts: page.NavigateOpts,
}; };
pub const PageNavigated = struct { pub const PageNavigated = struct {
timestamp: u32, timestamp: u32,
url: *const URL, url: []const u8,
}; };
pub const RequestStart = struct { pub const RequestStart = struct {

View File

@@ -1052,7 +1052,15 @@ pub fn run(
// infinite loop on I/O events, either: // infinite loop on I/O events, either:
// - cmd from incoming connection on server socket // - cmd from incoming connection on server socket
// - JS callbacks events from scripts // - JS callbacks events from scripts
// var http_client = app.http_client;
while (true) { while (true) {
// // @newhttp
// // This is a temporary hack for the newhttp work. The issue is that we
// // now have 2 event loops.
// if (http_client.active > 0) {
// _ = try http_client.tick(10);
// }
try loop.io.run_for_ns(10 * std.time.ns_per_ms); try loop.io.run_for_ns(10 * std.time.ns_per_ms);
} }
} }

View File

@@ -35,7 +35,7 @@ pub const LightPanda = struct {
.thread = null, .thread = null,
.running = true, .running = true,
.allocator = allocator, .allocator = allocator,
.client = &app.http_client, .client = app.http_client,
.uri = std.Uri.parse(URL) catch unreachable, .uri = std.Uri.parse(URL) catch unreachable,
.node_pool = std.heap.MemoryPool(List.Node).init(allocator), .node_pool = std.heap.MemoryPool(List.Node).init(allocator),
}; };

View File

@@ -79,7 +79,7 @@ fn TelemetryT(comptime P: type) type {
const self: *Self = @alignCast(@ptrCast(ctx)); const self: *Self = @alignCast(@ptrCast(ctx));
self.record(.{ .navigate = .{ self.record(.{ .navigate = .{
.proxy = false, .proxy = false,
.tls = std.ascii.eqlIgnoreCase(data.url.scheme(), "https"), .tls = std.ascii.startsWithIgnoreCase(data.url, "https://"),
} }); } });
} }
}; };

View File

@@ -409,6 +409,10 @@ pub const JsRunner = struct {
const html_doc = try parser.documentHTMLParseFromStr(opts.html); const html_doc = try parser.documentHTMLParseFromStr(opts.html);
try page.setDocument(html_doc); try page.setDocument(html_doc);
// after the page is considered loaded, page.wait can exit early if
// there's no IO/timeouts. So setting this speeds up our tests
page.loaded = true;
return .{ return .{
.app = app, .app = app,
.page = page, .page = page,
@@ -441,7 +445,7 @@ pub const JsRunner = struct {
} }
return err; return err;
}; };
try self.page.loop.run(std.time.ns_per_ms * 200); try self.page.wait(1);
@import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start); @import("root").js_runner_duration += std.time.Instant.since(try std.time.Instant.now(), start);
if (case.@"1") |expected| { if (case.@"1") |expected| {