From 25962326d2d134029831b680e7331a640ed87572 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Thu, 18 Sep 2025 22:25:49 -0700 Subject: [PATCH] add support for Response.type --- src/browser/fetch/Request.zig | 78 ++++++++++++++++++++++++---------- src/browser/fetch/Response.zig | 71 ++++++++++++++++++++++--------- src/browser/fetch/fetch.zig | 28 ++++++++++++ src/runtime/js.zig | 3 +- src/tests/fetch/response.html | 1 + 5 files changed, 136 insertions(+), 45 deletions(-) diff --git a/src/browser/fetch/Request.zig b/src/browser/fetch/Request.zig index 7cfc77c2..0674bcea 100644 --- a/src/browser/fetch/Request.zig +++ b/src/browser/fetch/Request.zig @@ -80,6 +80,27 @@ pub const RequestCredentials = enum { } }; +pub const RequestMode = enum { + cors, + @"no-cors", + @"same-origin", + navigate, + + pub fn fromString(str: []const u8) ?RequestMode { + for (std.enums.values(RequestMode)) |cache| { + if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) { + return cache; + } + } else { + return null; + } + } + + pub fn toString(self: RequestMode) []const u8 { + return @tagName(self); + } +}; + // https://developer.mozilla.org/en-US/docs/Web/API/RequestInit pub const RequestInit = struct { body: ?[]const u8 = null, @@ -88,6 +109,7 @@ pub const RequestInit = struct { headers: ?HeadersInit = null, integrity: ?[]const u8 = null, method: ?[]const u8 = null, + mode: ?[]const u8 = null, }; // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request @@ -97,6 +119,8 @@ method: Http.Method, url: [:0]const u8, cache: RequestCache, credentials: RequestCredentials, +// no-cors is default is not built with constructor. +mode: RequestMode = .@"no-cors", headers: Headers, body: ?[]const u8, body_used: bool = false, @@ -115,11 +139,11 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re }, }; - const body = if (options.body) |body| try arena.dupe(u8, body) else null; const cache = (if (options.cache) |cache| RequestCache.fromString(cache) else null) orelse RequestCache.default; const credentials = (if (options.credentials) |creds| RequestCredentials.fromString(creds) else null) orelse RequestCredentials.@"same-origin"; const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else ""; const headers: Headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else .{}; + const mode = (if (options.mode) |mode| RequestMode.fromString(mode) else null) orelse RequestMode.cors; const method: Http.Method = blk: { if (options.method) |given_method| { @@ -135,11 +159,19 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re } }; + // Can't have a body on .GET or .HEAD. + const body: ?[]const u8 = blk: { + if (method == .GET or method == .HEAD) { + break :blk null; + } else break :blk if (options.body) |body| try arena.dupe(u8, body) else null; + }; + return .{ .method = method, .url = url, .cache = cache, .credentials = credentials, + .mode = mode, .headers = headers, .body = body, .integrity = integrity, @@ -181,6 +213,10 @@ pub fn get_method(self: *const Request) []const u8 { return @tagName(self.method); } +pub fn get_mode(self: *const Request) RequestMode { + return self.mode; +} + pub fn get_url(self: *const Request) []const u8 { return self.url; } @@ -210,10 +246,7 @@ pub fn _bytes(self: *Response, page: *Page) !Env.Promise { return error.TypeError; } - const resolver = Env.PromiseResolver{ - .js_context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; + const resolver = page.main_context.createPromiseResolver(); try resolver.resolve(self.body); self.body_used = true; @@ -225,22 +258,24 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise { return error.TypeError; } - const resolver = Env.PromiseResolver{ - .js_context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; + const resolver = page.main_context.createPromiseResolver(); - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - self.body, - .{}, - ) catch |e| { - log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); - return error.SyntaxError; - }; + if (self.body) |body| { + const p = std.json.parseFromSliceLeaky( + std.json.Value, + page.call_arena, + body, + .{}, + ) catch |e| { + log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); + return error.SyntaxError; + }; + + try resolver.resolve(p); + } else { + try resolver.resolve(null); + } - try resolver.resolve(p); self.body_used = true; return resolver.promise(); } @@ -250,10 +285,7 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise { return error.TypeError; } - const resolver = Env.PromiseResolver{ - .js_context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; + const resolver = page.main_context.createPromiseResolver(); try resolver.resolve(self.body); self.body_used = true; diff --git a/src/browser/fetch/Response.zig b/src/browser/fetch/Response.zig index af1b615e..cf67b462 100644 --- a/src/browser/fetch/Response.zig +++ b/src/browser/fetch/Response.zig @@ -41,9 +41,10 @@ status_text: []const u8 = "", headers: Headers, mime: ?Mime = null, url: []const u8 = "", -body: []const u8 = "", +body: ?[]const u8 = null, body_used: bool = false, redirected: bool = false, +type: ResponseType = .basic, const ResponseBody = union(enum) { string: []const u8, @@ -55,6 +56,28 @@ const ResponseOptions = struct { headers: ?HeadersInit = null, }; +pub const ResponseType = enum { + basic, + cors, + @"error", + @"opaque", + opaqueredirect, + + pub fn fromString(str: []const u8) ?ResponseType { + for (std.enums.values(ResponseType)) |cache| { + if (std.ascii.eqlIgnoreCase(str, @tagName(cache))) { + return cache; + } + } else { + return null; + } + } + + pub fn toString(self: ResponseType) []const u8 { + return @tagName(self); + } +}; + pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response { const arena = page.arena; @@ -68,7 +91,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag }, } } else { - break :blk ""; + break :blk null; } }; @@ -85,7 +108,9 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag pub fn get_body(self: *const Response, page: *Page) !*ReadableStream { const stream = try ReadableStream.constructor(null, null, page); - try stream.queue.append(page.arena, self.body); + if (self.body) |body| { + try stream.queue.append(page.arena, body); + } return stream; } @@ -113,6 +138,10 @@ pub fn get_statusText(self: *const Response) []const u8 { return self.status_text; } +pub fn get_type(self: *const Response) ResponseType { + return self.type; +} + pub fn get_url(self: *const Response) []const u8 { return self.url; } @@ -132,6 +161,7 @@ pub fn _clone(self: *const Response) !Response { .redirected = self.redirected, .status = self.status, .url = self.url, + .type = self.type, }; } @@ -155,22 +185,24 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise { return error.TypeError; } - const resolver = Env.PromiseResolver{ - .js_context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; + const resolver = page.main_context.createPromiseResolver(); - const p = std.json.parseFromSliceLeaky( - std.json.Value, - page.call_arena, - self.body, - .{}, - ) catch |e| { - log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); - return error.SyntaxError; - }; + if (self.body) |body| { + const p = std.json.parseFromSliceLeaky( + std.json.Value, + page.call_arena, + body, + .{}, + ) catch |e| { + log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); + return error.SyntaxError; + }; + + try resolver.resolve(p); + } else { + try resolver.resolve(null); + } - try resolver.resolve(p); self.body_used = true; return resolver.promise(); } @@ -180,10 +212,7 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise { return error.TypeError; } - const resolver = Env.PromiseResolver{ - .js_context = page.main_context, - .resolver = v8.PromiseResolver.init(page.main_context.v8_context), - }; + const resolver = page.main_context.createPromiseResolver(); try resolver.resolve(self.body); self.body_used = true; diff --git a/src/browser/fetch/fetch.zig b/src/browser/fetch/fetch.zig index ee8162ae..288c74e9 100644 --- a/src/browser/fetch/fetch.zig +++ b/src/browser/fetch/fetch.zig @@ -53,6 +53,7 @@ pub const FetchContext = struct { headers: std.ArrayListUnmanaged([]const u8) = .empty, status: u16 = 0, mime: ?Mime = null, + mode: Request.RequestMode, transfer: ?*HttpClient.Transfer = null, /// This effectively takes ownership of the FetchContext. @@ -62,6 +63,19 @@ pub const FetchContext = struct { pub fn toResponse(self: *const FetchContext) !Response { var headers: Headers = .{}; + // If the mode is "no-cors", we need to return this opaque/stripped Response. + // https://developer.mozilla.org/en-US/docs/Web/API/Response/type + if (self.mode == .@"no-cors") { + return Response{ + .status = 0, + .headers = headers, + .mime = self.mime, + .body = null, + .url = self.url, + .type = .@"opaque", + }; + } + // convert into Headers for (self.headers.items) |hdr| { var iter = std.mem.splitScalar(u8, hdr, ':'); @@ -70,12 +84,25 @@ pub const FetchContext = struct { try headers.append(name, value, self.arena); } + const resp_type: Response.ResponseType = blk: { + if (std.mem.startsWith(u8, self.url, "data:")) { + break :blk .basic; + } + + break :blk switch (self.mode) { + .cors => .cors, + .@"same-origin", .navigate => .basic, + .@"no-cors" => unreachable, + }; + }; + return Response{ .status = self.status, .headers = headers, .mime = self.mime, .body = self.body.items, .url = self.url, + .type = resp_type, }; } }; @@ -110,6 +137,7 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promi .promise_resolver = resolver, .method = req.method, .url = req.url, + .mode = req.mode, }; try page.http_client.request(.{ diff --git a/src/runtime/js.zig b/src/runtime/js.zig index 7dbc1d9c..d8208891 100644 --- a/src/runtime/js.zig +++ b/src/runtime/js.zig @@ -2843,7 +2843,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type { const T = @TypeOf(value); switch (@typeInfo(T)) { - .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum" => { + .void, .bool, .int, .comptime_int, .float, .comptime_float, .@"enum", .null => { // Need to do this to keep the compiler happy // simpleZigValueToJs handles all of these cases. unreachable; @@ -3634,6 +3634,7 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T { fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value { switch (@typeInfo(@TypeOf(value))) { .void => return v8.initUndefined(isolate).toValue(), + .null => return v8.initNull(isolate).toValue(), .bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)), .int => |n| switch (n.signedness) { .signed => { diff --git a/src/tests/fetch/response.html b/src/tests/fetch/response.html index 01b4c8e7..f65a2fea 100644 --- a/src/tests/fetch/response.html +++ b/src/tests/fetch/response.html @@ -25,6 +25,7 @@ testing.expectEqual("no-cache", response2.headers.get("cache-control")); let response3 = new Response("Created", { status: 201, statusText: "Created" }); + testing.expectEqual("basic", response3.type); testing.expectEqual(201, response3.status); testing.expectEqual("Created", response3.statusText); testing.expectEqual(true, response3.ok);