mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 16:28:58 +00:00
fetch/request/response improvement (legacy)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
196
src/browser/tests/net/fetch.html
Normal file
196
src/browser/tests/net/fetch.html
Normal 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>
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user