Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
56c6e8b
remove polyfill and add req/resp
mookums Aug 18, 2025
df0b6d5
initial fetch in zig
mookums Aug 22, 2025
f9014bb
request url as null terminated
mookums Aug 22, 2025
dc2addb
fetch callback logging
mookums Aug 22, 2025
a133a71
proper fetch method and body setting
mookums Aug 25, 2025
814e411
basic readable stream working
mookums Aug 26, 2025
c6e82d5
add json response method
mookums Sep 1, 2025
1b3707a
add fetch to cdp domain
mookums Sep 3, 2025
5ca41b5
more Headers compatibility
mookums Sep 3, 2025
752e75e
add bodyUsed checks on Request and Response
mookums Sep 3, 2025
6d88438
move fetch() into fetch.zig
mookums Sep 3, 2025
38b922d
remove debug logging in ReadableStream
mookums Sep 3, 2025
8dcba37
expand Headers interface
mookums Sep 3, 2025
6225cb3
expand Request/Response interfaces
mookums Sep 3, 2025
8743841
use proper Headers in fetch()
mookums Sep 3, 2025
949479a
cleaning up various Headers routines
mookums Sep 3, 2025
744b0bf
TypeError when Stream is locked
mookums Sep 3, 2025
51ee313
working Header iterators
mookums Sep 5, 2025
463440b
implement remaining ReadableStream functionality
mookums Sep 5, 2025
f68f184
jsValueToZig for fixed sized arrays
sjorsdonkers Sep 5, 2025
cf8f76b
remove length check of fixed size
sjorsdonkers Sep 5, 2025
a7848f4
avoid explicit memcpy
sjorsdonkers Sep 5, 2025
7766892
retain value, avoid str alloc
sjorsdonkers Sep 5, 2025
2659043
add logging on fetch error callback
mookums Sep 8, 2025
9251180
support object as HeadersInit
mookums Sep 8, 2025
20463a6
use destructor callback for FetchContext
mookums Sep 8, 2025
9bbd06c
headers iterators should not allocate
mookums Sep 8, 2025
618fff0
simplify Headers
mookums Sep 8, 2025
bb2595e
use call arena for json in Req/Resp
mookums Sep 8, 2025
fe89c2f
simplify cloning of Req/Resp
mookums Sep 8, 2025
af75ce7
deinit persistent promise resolver
mookums Sep 9, 2025
76dae43
properly handle closed for ReadableStream
mookums Sep 9, 2025
969bfb4
migrate fetch tests to htmlRunner
mookums Sep 9, 2025
a875ce4
copy our Request headers into the HTTP client
mookums Sep 12, 2025
ed11eab
use content length to reserve body size
mookums Sep 12, 2025
cd763a7
fix arena, add fetch test
karlseguin Sep 16, 2025
24330a7
remove meaningless text from test
karlseguin Sep 16, 2025
c553a2c
use Env.PersistentPromiseResolver
mookums Sep 16, 2025
2a0964f
htmlRunner for ReadableStream tests, fix ReadableStream enqueue
mookums Sep 16, 2025
2a969f9
stop using destructor callback for fetch
mookums Sep 16, 2025
f22ee54
use fetch logging scope, clean some comments
mookums Sep 16, 2025
f6f0e14
PeristentPromiseResolver with page lifetime
mookums Sep 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/browser/env.zig
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ const WebApis = struct {
@import("xhr/form_data.zig").Interfaces,
@import("xhr/File.zig"),
@import("xmlserializer/xmlserializer.zig").Interfaces,
@import("fetch/fetch.zig").Interfaces,
@import("streams/streams.zig").Interfaces,
});
};

Expand Down
227 changes: 227 additions & 0 deletions src/browser/fetch/Headers.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const log = @import("../../log.zig");
const URL = @import("../../url.zig").URL;
const Page = @import("../page.zig").Page;

const iterator = @import("../iterator/iterator.zig");

const v8 = @import("v8");
const Env = @import("../env.zig").Env;

// https://developer.mozilla.org/en-US/docs/Web/API/Headers
const Headers = @This();

// Case-Insensitive String HashMap.
// This allows us to avoid having to allocate lowercase keys all the time.
const HeaderHashMap = std.HashMapUnmanaged([]const u8, []const u8, struct {
pub fn hash(_: @This(), s: []const u8) u64 {
var buf: [64]u8 = undefined;
var hasher = std.hash.Wyhash.init(s.len);

var key = s;
while (key.len >= 64) {
const lower = std.ascii.lowerString(buf[0..], key[0..64]);
hasher.update(lower);
key = key[64..];
}

if (key.len > 0) {
const lower = std.ascii.lowerString(buf[0..key.len], key);
hasher.update(lower);
}

return hasher.final();
}

pub fn eql(_: @This(), a: []const u8, b: []const u8) bool {
return std.ascii.eqlIgnoreCase(a, b);
}
}, 80);

headers: HeaderHashMap = .empty,

// They can either be:
//
// 1. An array of string pairs.
// 2. An object with string keys to string values.
// 3. Another Headers object.
pub const HeadersInit = union(enum) {
// List of Pairs of []const u8
strings: []const [2][]const u8,
// Headers
headers: *Headers,
// Mappings
object: Env.JsObject,
};

pub fn constructor(_init: ?HeadersInit, page: *Page) !Headers {
const arena = page.arena;
var headers: HeaderHashMap = .empty;

if (_init) |init| {
switch (init) {
.strings => |kvs| {
for (kvs) |pair| {
const key = try arena.dupe(u8, pair[0]);
const value = try arena.dupe(u8, pair[1]);

try headers.put(arena, key, value);
}
},
.headers => |hdrs| {
var iter = hdrs.headers.iterator();
while (iter.next()) |entry| {
try headers.put(arena, entry.key_ptr.*, entry.value_ptr.*);
}
},
.object => |obj| {
var iter = obj.nameIterator();
while (try iter.next()) |name_value| {
const name = try name_value.toString(arena);
const value = try obj.get(name);
const value_string = try value.toString(arena);

try headers.put(arena, name, value_string);
}
},
}
}

return .{
.headers = headers,
};
}

pub fn append(self: *Headers, name: []const u8, value: []const u8, allocator: std.mem.Allocator) !void {
const key = try allocator.dupe(u8, name);
const gop = try self.headers.getOrPut(allocator, key);

if (gop.found_existing) {
// If we found it, append the value.
const new_value = try std.fmt.allocPrint(allocator, "{s}, {s}", .{ gop.value_ptr.*, value });
gop.value_ptr.* = new_value;
} else {
// Otherwise, we should just put it in.
gop.value_ptr.* = try allocator.dupe(u8, value);
}
}

pub fn _append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;
try self.append(name, value, arena);
}

pub fn _delete(self: *Headers, name: []const u8) void {
_ = self.headers.remove(name);
}

pub const HeadersEntryIterator = struct {
slot: [2][]const u8,
iter: HeaderHashMap.Iterator,

// TODO: these SHOULD be in lexigraphical order but I'm not sure how actually
// important that is.
pub fn _next(self: *HeadersEntryIterator) ?[2][]const u8 {
if (self.iter.next()) |entry| {
self.slot[0] = entry.key_ptr.*;
self.slot[1] = entry.value_ptr.*;
return self.slot;
} else {
return null;
}
}
};

pub fn _entries(self: *const Headers) HeadersEntryIterable {
return .{
.inner = .{
.slot = undefined,
.iter = self.headers.iterator(),
},
};
}

pub fn _forEach(self: *Headers, callback_fn: Env.Function, this_arg: ?Env.JsObject) !void {
var iter = self.headers.iterator();

const cb = if (this_arg) |this| try callback_fn.withThis(this) else callback_fn;

while (iter.next()) |entry| {
try cb.call(void, .{ entry.key_ptr.*, entry.value_ptr.*, self });
}
}

pub fn _get(self: *const Headers, name: []const u8) ?[]const u8 {
return self.headers.get(name);
}

pub fn _has(self: *const Headers, name: []const u8) bool {
return self.headers.contains(name);
}

pub const HeadersKeyIterator = struct {
iter: HeaderHashMap.KeyIterator,

pub fn _next(self: *HeadersKeyIterator) ?[]const u8 {
if (self.iter.next()) |key| {
return key.*;
} else {
return null;
}
}
};

pub fn _keys(self: *const Headers) HeadersKeyIterable {
return .{ .inner = .{ .iter = self.headers.keyIterator() } };
}

pub fn _set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
const arena = page.arena;

const key = try arena.dupe(u8, name);
const gop = try self.headers.getOrPut(arena, key);
gop.value_ptr.* = try arena.dupe(u8, value);
}

pub const HeadersValueIterator = struct {
iter: HeaderHashMap.ValueIterator,

pub fn _next(self: *HeadersValueIterator) ?[]const u8 {
if (self.iter.next()) |value| {
return value.*;
} else {
return null;
}
}
};

pub fn _values(self: *const Headers) HeadersValueIterable {
return .{ .inner = .{ .iter = self.headers.valueIterator() } };
}

pub const HeadersKeyIterable = iterator.Iterable(HeadersKeyIterator, "HeadersKeyIterator");
pub const HeadersValueIterable = iterator.Iterable(HeadersValueIterator, "HeadersValueIterator");
pub const HeadersEntryIterable = iterator.Iterable(HeadersEntryIterator, "HeadersEntryIterator");

const testing = @import("../../testing.zig");
test "fetch: Headers" {
try testing.htmlRunner("fetch/headers.html");
}
Loading