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) // innerText does NOT parse HTML (unlike innerHTML)
d1.innerText = 'hello <div>world</div><b>!!</b>'; d1.innerText = 'hello <div>world</div><b>!!</b>';
testing.expectEqual('hello <div>world</div><b>!!</b>', d1.innerText); 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); testing.expectEqual('hello &lt;div&gt;world&lt;/div&gt;&lt;b&gt;!!&lt;/b&gt;', d1.innerHTML);
// Setting empty string clears children // Setting empty string clears children

View File

@@ -17,15 +17,145 @@
testing.expectEqual(null, headers.get('Content-Type')); testing.expectEqual(null, headers.get('Content-Type'));
testing.expectEqual(false, headers.has('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(); const headers = new Headers();
headers.append('Accept', 'application/json'); headers.append('Accept', 'application/json');
headers.append('Accept', 'text/html'); headers.append('Accept', 'text/html');
headers.append('Accept', 'text/plain');
const all = headers.getAll('Accept'); const values = [];
testing.expectEqual(2, all.length); headers.forEach((value, name) => {
testing.expectEqual('application/json', all[0]); if (name === 'accept') {
testing.expectEqual('text/html', all[1]); 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> </script>

View File

@@ -75,7 +75,7 @@ pub fn forEach(self: *NodeList, cb: js.Function, page: *Page) !void {
var result: js.Function.Result = undefined; var result: js.Function.Result = undefined;
cb.tryCall(void, .{ next.value, i, self }, &result) catch { 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; return;
}; };
} }

View File

@@ -1,5 +1,6 @@
const std = @import("std"); const std = @import("std");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const log = @import("../../../log.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const KeyValueList = @import("../KeyValueList.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 { 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 { pub fn delete(self: *Headers, name: []const u8, page: *Page) void {
self._list.delete(name, null); const normalized_name = normalizeHeaderName(name, page);
self._list.delete(normalized_name, null);
} }
pub fn get(self: *const Headers, name: []const u8) ?[]const u8 { pub fn get(self: *const Headers, name: []const u8, page: *Page) ?[]const u8 {
return self._list.get(name); 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 { pub fn has(self: *const Headers, name: []const u8, page: *Page) bool {
return self._list.getAll(name, page); const normalized_name = normalizeHeaderName(name, page);
} return self._list.has(normalized_name);
pub fn has(self: *const Headers, name: []const u8) bool {
return self._list.has(name);
} }
pub fn set(self: *Headers, name: []const u8, value: []const u8, page: *Page) !void { 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 { pub const JsApi = struct {
@@ -51,9 +83,12 @@ pub const JsApi = struct {
pub const append = bridge.function(Headers.append, .{}); pub const append = bridge.function(Headers.append, .{});
pub const delete = bridge.function(Headers.delete, .{}); pub const delete = bridge.function(Headers.delete, .{});
pub const get = bridge.function(Headers.get, .{}); pub const get = bridge.function(Headers.get, .{});
pub const getAll = bridge.function(Headers.getAll, .{});
pub const has = bridge.function(Headers.has, .{}); pub const has = bridge.function(Headers.has, .{});
pub const set = bridge.function(Headers.set, .{}); 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"); const testing = @import("../../../testing.zig");