Header case insensitive

This commit is contained in:
Karl Seguin
2025-12-03 09:59:55 +08:00
parent c0da6994da
commit 2de0d4bc48
4 changed files with 183 additions and 19 deletions

View File

@@ -143,7 +143,6 @@
// innerText does NOT parse HTML (unlike innerHTML)
d1.innerText = 'hello <div>world</div><b>!!</b>';
testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerText);
console.warn(d1.innerHTML);
testing.expectEqual('hello &lt;div&gt;world&lt;/div&gt;&lt;b&gt;!!&lt;/b&gt;', d1.innerHTML);
// Setting empty string clears children

View File

@@ -17,15 +17,145 @@
testing.expectEqual(null, headers.get('Content-Type'));
testing.expectEqual(false, headers.has('Content-Type'));
}
</script>
<script id=case-insensitive>
// Headers should be case-insensitive per HTTP spec
{
const headers = new Headers();
// Set with one case, get with another
headers.set('Content-Type', 'application/json');
testing.expectEqual('application/json', headers.get('content-type'));
testing.expectEqual('application/json', headers.get('CONTENT-TYPE'));
testing.expectEqual('application/json', headers.get('Content-Type'));
// has should be case-insensitive
testing.expectEqual(true, headers.has('content-type'));
testing.expectEqual(true, headers.has('CONTENT-TYPE'));
testing.expectEqual(true, headers.has('Content-Type'));
// delete should be case-insensitive
headers.delete('CONTENT-TYPE');
testing.expectEqual(false, headers.has('content-type'));
testing.expectEqual(false, headers.has('Content-Type'));
}
{
const headers = new Headers();
// Append with different cases - should all be treated as same header
headers.append('Accept', 'application/json');
headers.append('ACCEPT', 'text/html');
headers.append('accept', 'text/plain');
// Verify all values are present using iteration
const values = Array.from(headers.values());
testing.expectEqual(3, values.length);
testing.expectEqual('application/json', values[0]);
testing.expectEqual('text/html', values[1]);
testing.expectEqual('text/plain', values[2]);
}
{
const headers = new Headers();
// Set should replace regardless of case
headers.set('Authorization', 'Bearer token1');
headers.set('AUTHORIZATION', 'Bearer token2');
testing.expectEqual('Bearer token2', headers.get('authorization'));
// Should only have one entry after set replaces
const entries = Array.from(headers.entries());
testing.expectEqual(1, entries.length);
testing.expectEqual('authorization', entries[0][0]);
testing.expectEqual('Bearer token2', entries[0][1]);
}
</script>
<script id=iterators>
// Test keys(), values(), entries() iterators
{
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', 'Bearer token123');
headers.set('X-Custom', 'test-value');
// Test keys()
const keys = Array.from(headers.keys());
testing.expectEqual(3, keys.length);
testing.expectEqual('content-type', keys[0]);
testing.expectEqual('authorization', keys[1]);
testing.expectEqual('x-custom', keys[2]);
// Test values()
const values = Array.from(headers.values());
testing.expectEqual(3, values.length);
testing.expectEqual('application/json', values[0]);
testing.expectEqual('Bearer token123', values[1]);
testing.expectEqual('test-value', values[2]);
// Test entries()
const entries = Array.from(headers.entries());
testing.expectEqual(3, entries.length);
testing.expectEqual('content-type', entries[0][0]);
testing.expectEqual('application/json', entries[0][1]);
testing.expectEqual('authorization', entries[1][0]);
testing.expectEqual('Bearer token123', entries[1][1]);
testing.expectEqual('x-custom', entries[2][0]);
testing.expectEqual('test-value', entries[2][1]);
}
// Test forEach()
{
const headers = new Headers();
headers.set('Content-Type', 'application/json');
headers.set('Authorization', 'Bearer token123');
const collected = [];
headers.forEach(function(value, name, headersObj) {
collected.push([name, value]);
testing.expectEqual(headers, headersObj);
});
testing.expectEqual(2, collected.length);
testing.expectEqual('content-type', collected[0][0]);
testing.expectEqual('application/json', collected[0][1]);
testing.expectEqual('authorization', collected[1][0]);
testing.expectEqual('Bearer token123', collected[1][1]);
}
// Test forEach with thisArg
{
const headers = new Headers();
headers.set('X-Test', 'value');
const context = { count: 0 };
headers.forEach(function() {
this.count++;
}, context);
testing.expectEqual(1, context.count);
}
// Test multiple values for same header using append
{
const headers = new Headers();
headers.append('Accept', 'application/json');
headers.append('Accept', 'text/html');
headers.append('Accept', 'text/plain');
const all = headers.getAll('Accept');
testing.expectEqual(2, all.length);
testing.expectEqual('application/json', all[0]);
testing.expectEqual('text/html', all[1]);
const values = [];
headers.forEach((value, name) => {
if (name === 'accept') {
values.push(value);
}
});
testing.expectEqual(3, values.length);
testing.expectEqual('application/json', values[0]);
testing.expectEqual('text/html', values[1]);
testing.expectEqual('text/plain', values[2]);
}
</script>

View File

@@ -75,7 +75,7 @@ pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
var result: js.Function.Result = undefined;
cb.tryCall(void, .{ next.value, i, self }, &result) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack });
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "nodelist" });
return;
};
}

View File

@@ -1,5 +1,6 @@
const std = @import("std");
const js = @import("../../js/js.zig");
const log = @import("../../../log.zig");
const Page = @import("../../Page.zig");
const KeyValueList = @import("../KeyValueList.zig");
@@ -15,27 +16,58 @@ pub fn init(page: *Page) !*Headers {
}
pub fn append(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
try self._list.append(page.arena, name, value);
const normalized_name = normalizeHeaderName(name, page);
try self._list.append(page.arena, normalized_name, value);
}
pub fn delete(self: *Headers, name: []const u8) void {
self._list.delete(name, null);
pub fn delete(self: *Headers, name: []const u8, page: *Page) void {
const normalized_name = normalizeHeaderName(name, page);
self._list.delete(normalized_name, null);
}
pub fn get(self: *const Headers, name: []const u8) ?[]const u8 {
return self._list.get(name);
pub fn get(self: *const Headers, name: []const u8, page: *Page) ?[]const u8 {
const normalized_name = normalizeHeaderName(name, page);
return self._list.get(normalized_name);
}
pub fn getAll(self: *const Headers, name: []const u8, page: *Page) ![]const []const u8 {
return self._list.getAll(name, page);
}
pub fn has(self: *const Headers, name: []const u8) bool {
return self._list.has(name);
pub fn has(self: *const Headers, name: []const u8, page: *Page) bool {
const normalized_name = normalizeHeaderName(name, page);
return self._list.has(normalized_name);
}
pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void {
try self._list.set(page.arena, name, value);
const normalized_name = normalizeHeaderName(name, page);
try self._list.set(page.arena, normalized_name, value);
}
pub fn keys(self: *Headers, page: *Page) !*KeyValueList.KeyIterator {
return KeyValueList.KeyIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn values(self: *Headers, page: *Page) !*KeyValueList.ValueIterator {
return KeyValueList.ValueIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn entries(self: *Headers, page: *Page) !*KeyValueList.EntryIterator {
return KeyValueList.EntryIterator.init(.{ .list = self, .kv = &self._list }, page);
}
pub fn forEach(self: *Headers, cb_: js.Function, js_this_: ?js.Object) !void {
const cb = if (js_this_) |js_this| try cb_.withThis(js_this) else cb_;
for (self._list._entries.items) |entry| {
var result: js.Function.Result = undefined;
cb.tryCall(void, .{ entry.value.str(), entry.name.str(), self }, &result) catch {
log.debug(.js, "forEach callback", .{ .err = result.exception, .stack = result.stack, .source = "headers" });
};
}
}
fn normalizeHeaderName(name: []const u8, page: *Page) []const u8 {
if (name.len > page.buf.len) {
return name;
}
return std.ascii.lowerString(&page.buf, name);
}
pub const JsApi = struct {
@@ -51,9 +83,12 @@ pub const JsApi = struct {
pub const append = bridge.function(Headers.append, .{});
pub const delete = bridge.function(Headers.delete, .{});
pub const get = bridge.function(Headers.get, .{});
pub const getAll = bridge.function(Headers.getAll, .{});
pub const has = bridge.function(Headers.has, .{});
pub const set = bridge.function(Headers.set, .{});
pub const keys = bridge.function(Headers.keys, .{});
pub const values = bridge.function(Headers.values, .{});
pub const entries = bridge.function(Headers.entries, .{});
pub const forEach = bridge.function(Headers.forEach, .{});
};
const testing = @import("../../../testing.zig");