fetch/request/response improvement (legacy)

This commit is contained in:
Karl Seguin
2025-12-16 17:54:05 +08:00
parent e47091f9a1
commit 8a2641d213
7 changed files with 303 additions and 28 deletions

View File

@@ -2006,7 +2006,7 @@ const IdleNotification = union(enum) {
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const URLRaw = @import("URL.zig");
const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false;
const current_origin = (try URLRaw.getOrigin(self.call_arena, self.url)) orelse return false;
return std.mem.startsWith(u8, url, current_origin);
}

View File

@@ -240,29 +240,66 @@ pub fn getHash(raw: [:0]const u8) []const u8 {
}
pub fn getOrigin(allocator: Allocator, raw: [:0]const u8) !?[]const u8 {
const port = getPort(raw);
const protocol = getProtocol(raw);
const hostname = getHostname(raw);
const scheme_end = std.mem.indexOf(u8, raw, "://") orelse return null;
const p = std.meta.stringToEnum(KnownProtocol, getProtocol(raw)) orelse return null;
const include_port = blk: {
if (port.len == 0) {
break :blk false;
}
if (p == .@"https:" and std.mem.eql(u8, port, "443")) {
break :blk false;
}
if (p == .@"http:" and std.mem.eql(u8, port, "80")) {
break :blk false;
}
break :blk true;
};
if (include_port) {
return try std.fmt.allocPrint(allocator, "{s}//{s}:{s}", .{ protocol, hostname, port });
// Only HTTP and HTTPS schemes have origins
const protocol = raw[0..scheme_end + 1];
if (!std.mem.eql(u8, protocol, "http:") and !std.mem.eql(u8, protocol, "https:")) {
return null;
}
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname });
var authority_start = scheme_end + 3;
const has_user_info = if (std.mem.indexOf(u8, raw[authority_start..], "@")) |pos| blk: {
authority_start += pos + 1;
break :blk true;
} else false;
// Find end of authority (start of path/query/fragment or end of string)
const authority_end_relative = std.mem.indexOfAny(u8, raw[authority_start..], "/?#");
const authority_end = if (authority_end_relative) |end|
authority_start + end
else
raw.len;
// Check for port in the host:port section
const host_part = raw[authority_start..authority_end];
if (std.mem.lastIndexOfScalar(u8, host_part, ':')) |colon_pos_in_host| {
const port = host_part[colon_pos_in_host + 1..];
// Validate it's actually a port (all digits)
for (port) |c| {
if (c < '0' or c > '9') {
// Not a port (probably IPv6)
if (has_user_info) {
// Need to allocate to exclude user info
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ raw[0 .. scheme_end + 1], host_part });
}
// Can return a slice
return raw[0..authority_end];
}
}
// Check if it's a default port that should be excluded from origin
const is_default =
(std.mem.eql(u8, protocol, "http:") and std.mem.eql(u8, port, "80")) or
(std.mem.eql(u8, protocol, "https:") and std.mem.eql(u8, port, "443"));
if (is_default or has_user_info) {
// Need to allocate to build origin without default port and/or user info
const hostname = host_part[0..colon_pos_in_host];
if (is_default) {
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, hostname });
} else {
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ protocol, host_part });
}
}
} else if (has_user_info) {
// No port, but has user info - need to allocate
return try std.fmt.allocPrint(allocator, "{s}//{s}", .{ raw[0 .. scheme_end + 1], host_part });
}
// Common case: no user info, no default port - return slice (zero allocation!)
return raw[0..authority_end];
}
fn getUserInfo(raw: [:0]const u8) ?[]const u8 {

View File

@@ -3,7 +3,7 @@
const promise1 = new Promise((resolve) => {
fetch('http://127.0.0.1:9589/xhr/json')
.then((res) => {
testing.expectEqual('basic', res.type);
testing.expectEqual('cors', res.type);
return res.json()
})
.then((json) => {

View File

@@ -0,0 +1,196 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=fetch_basic>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr');
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
testing.expectEqual('basic', response.type);
testing.expectEqual('http://127.0.0.1:9582/xhr', response.url);
testing.expectEqual(false, response.redirected);
// Check headers
const headers = response.headers;
testing.expectEqual('text/html; charset=utf-8', headers.get('Content-Type'));
testing.expectEqual('100', headers.get('content-length'));
// Check text response
const text = await response.text();
testing.expectEqual(100, text.length);
});
</script>
<script id=fetch_json>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr/json');
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
testing.expectEqual('basic', response.type);
testing.expectEqual(false, response.redirected);
const json = await response.json();
testing.expectEqual('9000!!!', json.over);
});
</script>
<script id=fetch_post>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr', {
method: 'POST',
body: 'foo'
});
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
const text = await response.text();
testing.expectEqual(true, text.length > 64);
});
</script>
<script id=fetch_redirect>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr/redirect');
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
testing.expectEqual('http://127.0.0.1:9582/xhr', response.url);
testing.expectEqual(true, response.redirected);
const text = await response.text();
testing.expectEqual(100, text.length);
});
</script>
<script id=fetch_404>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr/404');
restore();
testing.expectEqual(404, response.status);
testing.expectEqual(false, response.ok);
const text = await response.text();
testing.expectEqual('Not Found', text);
});
</script>
<script id=fetch_500>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr/500');
restore();
testing.expectEqual(500, response.status);
testing.expectEqual(false, response.ok);
const text = await response.text();
testing.expectEqual('Internal Server Error', text);
});
</script>
<script id=fetch_request_object>
testing.async(async (restore) => {
const request = new Request('http://127.0.0.1:9582/xhr', {
method: 'GET'
});
testing.expectEqual('http://127.0.0.1:9582/xhr', request.url);
testing.expectEqual('GET', request.method);
const response = await fetch(request);
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
const text = await response.text();
testing.expectEqual(100, text.length);
});
</script>
<script id=fetch_request_with_headers>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr', {
method: 'GET',
headers: {
'X-Custom-Header': 'test-value'
}
});
restore();
testing.expectEqual(200, response.status);
testing.expectEqual(true, response.ok);
});
</script>
<script id=fetch_request_with_method_variations>
for (const method of ['GET', 'POST', 'PUT', 'DELETE', 'PATCH']) {
const request = new Request('http://127.0.0.1:9582/xhr', {
method: method
});
testing.expectEqual(method, request.method);
}
</script>
<script id=fetch_cache_credentials>
{
const request = new Request('http://127.0.0.1:9582/xhr', {
cache: 'no-cache',
credentials: 'same-origin'
});
testing.expectEqual('no-cache', request.cache);
testing.expectEqual('same-origin', request.credentials);
}
</script>
<script id=response_constructor>
testing.async(async (restore) => {
const response1 = new Response(null);
testing.expectEqual(200, response1.status);
testing.expectEqual(true, response1.ok);
const response2 = new Response('Hello', {
status: 201,
});
testing.expectEqual(201, response2.status);
testing.expectEqual(true, response2.ok);
const text = await response2.text();
restore();
testing.expectEqual('Hello', text);
});
</script>
<script id=response_body_stream>
testing.async(async (restore) => {
const response = await fetch('http://127.0.0.1:9582/xhr');
restore();
testing.expectEqual(true, response.body !== null);
testing.expectEqual(true, response.body instanceof ReadableStream);
});
</script>
<script id=response_empty_body>
testing.async(async (restore) => {
const response = new Response('');
testing.expectEqual(200, response.status);
const text = await response.text();
restore();
testing.expectEqual('', text);
// Empty body should still create a valid stream
testing.expectEqual(true, response.body !== null);
});
</script>

View File

@@ -23,6 +23,7 @@ const Http = @import("../../../http/Http.zig");
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const URL = @import("../../URL.zig");
const Headers = @import("Headers.zig");
const Request = @import("Request.zig");
@@ -96,7 +97,30 @@ fn httpHeaderDoneCallback(transfer: *Http.Transfer) !void {
}
const res = self._response;
res._status = transfer.response_header.?.status;
const header = transfer.response_header.?;
res._status = header.status;
res._url = try self._page.arena.dupeZ(u8, std.mem.span(header.url));
res._is_redirected = header.redirect_count > 0;
// Determine response type based on origin comparison
const page_origin = URL.getOrigin(self._page.call_arena, self._page.url) catch null;
const response_origin = URL.getOrigin(self._page.call_arena, res._url) catch null;
if (page_origin) |po| {
if (response_origin) |ro| {
if (std.mem.eql(u8, po, ro)) {
res._type = .basic; // Same-origin
} else {
res._type = .cors; // Cross-origin (for simplicity, assume CORS passed)
}
} else {
res._type = .basic;
}
} else {
res._type = .basic;
}
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
try res._headers.append(hdr.name, hdr.value, self._page);
@@ -116,5 +140,12 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx));
self._response._type = .@"error"; // Set type to error for network failures
self._resolver.reject("fetch error", @errorName(err));
}
const testing = @import("../../../testing.zig");
test "WebApi: fetch" {
try testing.htmlRunner("net/fetch.html", .{});
}

View File

@@ -40,6 +40,8 @@ _headers: *Headers,
_body: ?[]const u8,
_type: Type,
_status_text: []const u8,
_url: [:0]const u8,
_is_redirected: bool,
const InitOpts = struct {
status: u16 = 200,
@@ -58,8 +60,10 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
._arena = page.arena,
._status = opts.status,
._status_text = status_text,
._url = "",
._body = body,
._type = .basic,
._is_redirected = false,
._headers = try Headers.init(opts.headers, page),
});
}
@@ -69,18 +73,19 @@ pub fn getStatus(self: *const Response) u16 {
}
pub fn getStatusText(self: *const Response) []const u8 {
// @TODO
// This property is meant to actually capture the response status text, not
// just return the text representation of self._status. If we do,
// new Response(null, {status: 200}).statusText, we should get empty string.
return self._status_text;
}
pub fn getURL(_: *const Response) []const u8 {
return "";
pub fn getURL(self: *const Response) []const u8 {
return self._url;
}
pub fn isRedirected(_: *const Response) bool {
return false;
pub fn isRedirected(self: *const Response) bool {
return self._is_redirected;
}
pub fn getHeaders(self: *const Response) *Headers {

View File

@@ -781,9 +781,13 @@ pub const Transfer = struct {
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_RESPONSE_CODE, &status));
}
var redirect_count: c_long = undefined;
try errorCheck(c.curl_easy_getinfo(easy, c.CURLINFO_REDIRECT_COUNT, &redirect_count));
self.response_header = .{
.url = url,
.status = @intCast(status),
.redirect_count = @intCast(redirect_count),
};
if (getResponseHeader(easy, "content-type", 0)) |ct| {
@@ -1122,6 +1126,7 @@ pub const Transfer = struct {
transfer.response_header = .{
.status = status,
.url = req.url,
.redirect_count = 0,
._injected_headers = headers,
};
for (headers) |hdr| {
@@ -1177,6 +1182,7 @@ pub const ResponseHeader = struct {
status: u16,
url: [*c]const u8,
redirect_count: u32,
_content_type_len: usize = 0,
_content_type: [MAX_CONTENT_TYPE_LEN]u8 = undefined,
// this is normally an empty list, but if the response is being injected