Merge pull request #1069 from lightpanda-io/response-gettype

Adds `Response.type`
This commit is contained in:
Karl Seguin
2025-09-19 15:12:10 +08:00
committed by GitHub
5 changed files with 136 additions and 45 deletions

View File

@@ -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 // https://developer.mozilla.org/en-US/docs/Web/API/RequestInit
pub const RequestInit = struct { pub const RequestInit = struct {
body: ?[]const u8 = null, body: ?[]const u8 = null,
@@ -88,6 +109,7 @@ pub const RequestInit = struct {
headers: ?HeadersInit = null, headers: ?HeadersInit = null,
integrity: ?[]const u8 = null, integrity: ?[]const u8 = null,
method: ?[]const u8 = null, method: ?[]const u8 = null,
mode: ?[]const u8 = null,
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
@@ -97,6 +119,8 @@ method: Http.Method,
url: [:0]const u8, url: [:0]const u8,
cache: RequestCache, cache: RequestCache,
credentials: RequestCredentials, credentials: RequestCredentials,
// no-cors is default is not built with constructor.
mode: RequestMode = .@"no-cors",
headers: Headers, headers: Headers,
body: ?[]const u8, body: ?[]const u8,
body_used: bool = false, 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 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 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 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 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: { const method: Http.Method = blk: {
if (options.method) |given_method| { 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 .{ return .{
.method = method, .method = method,
.url = url, .url = url,
.cache = cache, .cache = cache,
.credentials = credentials, .credentials = credentials,
.mode = mode,
.headers = headers, .headers = headers,
.body = body, .body = body,
.integrity = integrity, .integrity = integrity,
@@ -181,6 +213,10 @@ pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method); return @tagName(self.method);
} }
pub fn get_mode(self: *const Request) RequestMode {
return self.mode;
}
pub fn get_url(self: *const Request) []const u8 { pub fn get_url(self: *const Request) []const u8 {
return self.url; return self.url;
} }
@@ -210,10 +246,7 @@ pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{ const resolver = page.main_context.createPromiseResolver();
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body); try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;
@@ -225,22 +258,24 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{ const resolver = page.main_context.createPromiseResolver();
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
const p = std.json.parseFromSliceLeaky( if (self.body) |body| {
std.json.Value, const p = std.json.parseFromSliceLeaky(
page.call_arena, std.json.Value,
self.body, page.call_arena,
.{}, body,
) catch |e| { .{},
log.info(.browser, "invalid json", .{ .err = e, .source = "Request" }); ) catch |e| {
return error.SyntaxError; 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; self.body_used = true;
return resolver.promise(); return resolver.promise();
} }
@@ -250,10 +285,7 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{ const resolver = page.main_context.createPromiseResolver();
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body); try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;

View File

@@ -41,9 +41,10 @@ status_text: []const u8 = "",
headers: Headers, headers: Headers,
mime: ?Mime = null, mime: ?Mime = null,
url: []const u8 = "", url: []const u8 = "",
body: []const u8 = "", body: ?[]const u8 = null,
body_used: bool = false, body_used: bool = false,
redirected: bool = false, redirected: bool = false,
type: ResponseType = .basic,
const ResponseBody = union(enum) { const ResponseBody = union(enum) {
string: []const u8, string: []const u8,
@@ -55,6 +56,28 @@ const ResponseOptions = struct {
headers: ?HeadersInit = null, 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 { pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Page) !Response {
const arena = page.arena; const arena = page.arena;
@@ -68,7 +91,7 @@ pub fn constructor(_input: ?ResponseBody, _options: ?ResponseOptions, page: *Pag
}, },
} }
} else { } 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 { pub fn get_body(self: *const Response, page: *Page) !*ReadableStream {
const stream = try ReadableStream.constructor(null, null, page); 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; return stream;
} }
@@ -113,6 +138,10 @@ pub fn get_statusText(self: *const Response) []const u8 {
return self.status_text; return self.status_text;
} }
pub fn get_type(self: *const Response) ResponseType {
return self.type;
}
pub fn get_url(self: *const Response) []const u8 { pub fn get_url(self: *const Response) []const u8 {
return self.url; return self.url;
} }
@@ -132,6 +161,7 @@ pub fn _clone(self: *const Response) !Response {
.redirected = self.redirected, .redirected = self.redirected,
.status = self.status, .status = self.status,
.url = self.url, .url = self.url,
.type = self.type,
}; };
} }
@@ -155,22 +185,24 @@ pub fn _json(self: *Response, page: *Page) !Env.Promise {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{ const resolver = page.main_context.createPromiseResolver();
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
const p = std.json.parseFromSliceLeaky( if (self.body) |body| {
std.json.Value, const p = std.json.parseFromSliceLeaky(
page.call_arena, std.json.Value,
self.body, page.call_arena,
.{}, body,
) catch |e| { .{},
log.info(.browser, "invalid json", .{ .err = e, .source = "Response" }); ) catch |e| {
return error.SyntaxError; 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; self.body_used = true;
return resolver.promise(); return resolver.promise();
} }
@@ -180,10 +212,7 @@ pub fn _text(self: *Response, page: *Page) !Env.Promise {
return error.TypeError; return error.TypeError;
} }
const resolver = Env.PromiseResolver{ const resolver = page.main_context.createPromiseResolver();
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body); try resolver.resolve(self.body);
self.body_used = true; self.body_used = true;

View File

@@ -53,6 +53,7 @@ pub const FetchContext = struct {
headers: std.ArrayListUnmanaged([]const u8) = .empty, headers: std.ArrayListUnmanaged([]const u8) = .empty,
status: u16 = 0, status: u16 = 0,
mime: ?Mime = null, mime: ?Mime = null,
mode: Request.RequestMode,
transfer: ?*HttpClient.Transfer = null, transfer: ?*HttpClient.Transfer = null,
/// This effectively takes ownership of the FetchContext. /// This effectively takes ownership of the FetchContext.
@@ -62,6 +63,19 @@ pub const FetchContext = struct {
pub fn toResponse(self: *const FetchContext) !Response { pub fn toResponse(self: *const FetchContext) !Response {
var headers: Headers = .{}; 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 // convert into Headers
for (self.headers.items) |hdr| { for (self.headers.items) |hdr| {
var iter = std.mem.splitScalar(u8, hdr, ':'); var iter = std.mem.splitScalar(u8, hdr, ':');
@@ -70,12 +84,25 @@ pub const FetchContext = struct {
try headers.append(name, value, self.arena); 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{ return Response{
.status = self.status, .status = self.status,
.headers = headers, .headers = headers,
.mime = self.mime, .mime = self.mime,
.body = self.body.items, .body = self.body.items,
.url = self.url, .url = self.url,
.type = resp_type,
}; };
} }
}; };
@@ -110,6 +137,7 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promi
.promise_resolver = resolver, .promise_resolver = resolver,
.method = req.method, .method = req.method,
.url = req.url, .url = req.url,
.mode = req.mode,
}; };
try page.http_client.request(.{ try page.http_client.request(.{

View File

@@ -2843,7 +2843,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
const T = @TypeOf(value); const T = @TypeOf(value);
switch (@typeInfo(T)) { 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 // Need to do this to keep the compiler happy
// simpleZigValueToJs handles all of these cases. // simpleZigValueToJs handles all of these cases.
unreachable; 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 { fn simpleZigValueToJs(isolate: v8.Isolate, value: anytype, comptime fail: bool) if (fail) v8.Value else ?v8.Value {
switch (@typeInfo(@TypeOf(value))) { switch (@typeInfo(@TypeOf(value))) {
.void => return v8.initUndefined(isolate).toValue(), .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)), .bool => return v8.getValue(if (value) v8.initTrue(isolate) else v8.initFalse(isolate)),
.int => |n| switch (n.signedness) { .int => |n| switch (n.signedness) {
.signed => { .signed => {

View File

@@ -25,6 +25,7 @@
testing.expectEqual("no-cache", response2.headers.get("cache-control")); testing.expectEqual("no-cache", response2.headers.get("cache-control"));
let response3 = new Response("Created", { status: 201, statusText: "Created" }); let response3 = new Response("Created", { status: 201, statusText: "Created" });
testing.expectEqual("basic", response3.type);
testing.expectEqual(201, response3.status); testing.expectEqual(201, response3.status);
testing.expectEqual("Created", response3.statusText); testing.expectEqual("Created", response3.statusText);
testing.expectEqual(true, response3.ok); testing.expectEqual(true, response3.ok);