add bodyUsed checks on Request and Response

This commit is contained in:
Muki Kiboigo
2025-09-02 20:25:24 -07:00
parent 4ceca6b90b
commit 91899912d8
2 changed files with 128 additions and 133 deletions

View File

@@ -25,21 +25,24 @@ const Page = @import("../page.zig").Page;
const Response = @import("./Response.zig"); const Response = @import("./Response.zig");
const Http = @import("../../http/Http.zig"); const Http = @import("../../http/Http.zig");
const HttpClient = @import("../../http/Client.zig");
const Mime = @import("../mime.zig").Mime;
const v8 = @import("v8"); const v8 = @import("v8");
const Env = @import("../env.zig").Env; const Env = @import("../env.zig").Env;
const Headers = @import("Headers.zig");
const HeadersInit = @import("Headers.zig").HeadersInit;
pub const RequestInput = union(enum) { pub const RequestInput = union(enum) {
string: []const u8, string: []const u8,
request: Request, request: *Request,
}; };
// 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 {
method: ?[]const u8 = null, method: ?[]const u8 = null,
body: ?[]const u8 = null, body: ?[]const u8 = null,
integrity: ?[]const u8 = null,
headers: ?HeadersInit = null,
}; };
// https://developer.mozilla.org/en-US/docs/Web/API/Request/Request // https://developer.mozilla.org/en-US/docs/Web/API/Request/Request
@@ -47,7 +50,10 @@ const Request = @This();
method: Http.Method, method: Http.Method,
url: [:0]const u8, url: [:0]const u8,
headers: Headers,
body: ?[]const u8, body: ?[]const u8,
body_used: bool = false,
integrity: []const u8,
pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request { pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Request {
const arena = page.arena; const arena = page.arena;
@@ -77,165 +83,115 @@ pub fn constructor(input: RequestInput, _options: ?RequestInit, page: *Page) !Re
}; };
const body = if (options.body) |body| try arena.dupe(u8, body) else null; const body = if (options.body) |body| try arena.dupe(u8, body) else null;
const integrity = if (options.integrity) |integ| try arena.dupe(u8, integ) else "";
const headers = if (options.headers) |hdrs| try Headers.constructor(hdrs, page) else Headers{};
return .{ return .{
.method = method, .method = method,
.url = url, .url = url,
.headers = headers,
.body = body, .body = body,
.integrity = integrity,
}; };
} }
pub fn get_url(self: *const Request) []const u8 {
return self.url;
}
pub fn get_method(self: *const Request) []const u8 {
return @tagName(self.method);
}
// pub fn get_body(self: *const Request) ?[]const u8 { // pub fn get_body(self: *const Request) ?[]const u8 {
// return self.body; // return self.body;
// } // }
const FetchContext = struct { pub fn get_bodyUsed(self: *const Request) bool {
arena: std.mem.Allocator, return self.body_used;
js_ctx: *Env.JsContext, }
promise_resolver: v8.Persistent(v8.PromiseResolver),
pub fn get_headers(self: *Request) *Headers {
method: Http.Method, return &self.headers;
url: []const u8, }
body: std.ArrayListUnmanaged(u8) = .empty,
headers: std.ArrayListUnmanaged([]const u8) = .empty, pub fn get_integrity(self: *const Request) []const u8 {
status: u16 = 0, return self.integrity;
mime: ?Mime = null, }
transfer: ?*HttpClient.Transfer = null,
// TODO: If we ever support the Navigation API, we need isHistoryNavigation
/// This effectively takes ownership of the FetchContext. // https://developer.mozilla.org/en-US/docs/Web/API/Request/isHistoryNavigation
///
/// We just return the underlying slices used for `headers` pub fn get_method(self: *const Request) []const u8 {
/// and for `body` here to avoid an allocation. return @tagName(self.method);
pub fn toResponse(self: *const FetchContext) !Response { }
return Response{
.status = self.status, pub fn get_url(self: *const Request) []const u8 {
.headers = self.headers.items, return self.url;
.mime = self.mime, }
.body = self.body.items,
}; pub fn _clone(self: *Request, page: *Page) !Request {
// Not allowed to clone if the body was used.
if (self.body_used) {
return error.TypeError;
} }
};
// https://developer.mozilla.org/en-US/docs/Web/API/Window/fetch
pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promise {
const arena = page.arena; const arena = page.arena;
const req = try Request.constructor(input, options, page); return Request{
.body = if (self.body) |body| try arena.dupe(u8, body) else null,
.body_used = self.body_used,
.headers = try self.headers.clone(arena),
.method = self.method,
.integrity = try arena.dupe(u8, self.integrity),
.url = try arena.dupeZ(u8, self.url),
};
}
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{ const resolver = Env.PromiseResolver{
.js_context = page.main_context, .js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context), .resolver = v8.PromiseResolver.init(page.main_context.v8_context),
}; };
var headers = try Http.Headers.init(); try resolver.resolve(self.body);
try page.requestCookie(.{}).headersForRequest(arena, req.url, &headers); self.body_used = true;
return resolver.promise();
}
const fetch_ctx = try arena.create(FetchContext); pub fn _json(self: *Response, page: *Page) !Env.Promise {
fetch_ctx.* = .{ if (self.body_used) {
.arena = arena, return error.TypeError;
.js_ctx = page.main_context, }
.promise_resolver = v8.Persistent(v8.PromiseResolver).init(
page.main_context.isolate, const resolver = Env.PromiseResolver{
resolver.resolver, .js_context = page.main_context,
), .resolver = v8.PromiseResolver.init(page.main_context.v8_context),
.method = req.method,
.url = req.url,
}; };
try page.http_client.request(.{ const p = std.json.parseFromSliceLeaky(
.ctx = @ptrCast(fetch_ctx), std.json.Value,
.url = req.url, page.arena,
.method = req.method, self.body,
.headers = headers, .{},
.body = req.body, ) catch |e| {
.cookie_jar = page.cookie_jar, log.warn(.browser, "invalid json", .{ .err = e, .source = "Request" });
.resource_type = .fetch, return error.SyntaxError;
.start_callback = struct {
fn startCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
log.debug(.http, "request start", .{ .method = self.method, .url = self.url, .source = "fetch" });
self.transfer = transfer;
}
}.startCallback,
.header_callback = struct {
fn headerCallback(transfer: *HttpClient.Transfer) !void {
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
const header = &transfer.response_header.?;
log.debug(.http, "request header", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = header.status,
});
if (header.contentType()) |ct| {
self.mime = Mime.parse(ct) catch {
return error.MimeParsing;
};
}
var it = transfer.responseHeaderIterator();
while (it.next()) |hdr| {
const joined = try std.fmt.allocPrint(self.arena, "{s}: {s}", .{ hdr.name, hdr.value });
try self.headers.append(self.arena, joined);
}
self.status = header.status;
}
}.headerCallback,
.data_callback = struct {
fn dataCallback(transfer: *HttpClient.Transfer, data: []const u8) !void {
const self: *FetchContext = @alignCast(@ptrCast(transfer.ctx));
try self.body.appendSlice(self.arena, data);
}
}.dataCallback,
.done_callback = struct {
fn doneCallback(ctx: *anyopaque) !void {
const self: *FetchContext = @alignCast(@ptrCast(ctx));
log.info(.http, "request complete", .{
.source = "fetch",
.method = self.method,
.url = self.url,
.status = self.status,
});
const response = try self.toResponse();
const promise_resolver: Env.PromiseResolver = .{
.js_context = self.js_ctx,
.resolver = self.promise_resolver.castToPromiseResolver(),
}; };
try promise_resolver.resolve(response); try resolver.resolve(p);
self.body_used = true;
return resolver.promise();
} }
}.doneCallback,
.error_callback = struct {
fn errorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *FetchContext = @alignCast(@ptrCast(ctx));
self.transfer = null; pub fn _text(self: *Response, page: *Page) !Env.Promise {
const promise_resolver: Env.PromiseResolver = .{ if (self.body_used) {
.js_context = self.js_ctx, return error.TypeError;
.resolver = self.promise_resolver.castToPromiseResolver(), }
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
}; };
promise_resolver.reject(@errorName(err)) catch unreachable; try resolver.resolve(self.body);
} self.body_used = true;
}.errorCallback,
});
return resolver.promise(); return resolver.promise();
} }

View File

@@ -35,6 +35,8 @@ status: u16 = 0,
headers: []const []const u8, headers: []const []const u8,
mime: ?Mime = null, mime: ?Mime = null,
body: []const u8, body: []const u8,
body_used: bool = false,
redirected: bool = false,
const ResponseInput = union(enum) { const ResponseInput = union(enum) {
string: []const u8, string: []const u8,
@@ -72,17 +74,38 @@ pub fn get_ok(self: *const Response) bool {
return self.status >= 200 and self.status <= 299; return self.status >= 200 and self.status <= 299;
} }
pub fn _text(self: *const Response, page: *Page) !Env.Promise { pub fn get_bodyUsed(self: *const Response) bool {
return self.body_used;
}
pub fn get_redirected(self: *const Response) bool {
return self.redirected;
}
pub fn get_status(self: *const Response) u16 {
return self.status;
}
pub fn _bytes(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{ const resolver = Env.PromiseResolver{
.js_context = page.main_context, .js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context), .resolver = v8.PromiseResolver.init(page.main_context.v8_context),
}; };
try resolver.resolve(self.body); try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise(); return resolver.promise();
} }
pub fn _json(self: *const Response, page: *Page) !Env.Promise { pub fn _json(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{ const resolver = Env.PromiseResolver{
.js_context = page.main_context, .js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context), .resolver = v8.PromiseResolver.init(page.main_context.v8_context),
@@ -99,6 +122,22 @@ pub fn _json(self: *const Response, page: *Page) !Env.Promise {
}; };
try resolver.resolve(p); try resolver.resolve(p);
self.body_used = true;
return resolver.promise();
}
pub fn _text(self: *Response, page: *Page) !Env.Promise {
if (self.body_used) {
return error.TypeError;
}
const resolver = Env.PromiseResolver{
.js_context = page.main_context,
.resolver = v8.PromiseResolver.init(page.main_context.v8_context),
};
try resolver.resolve(self.body);
self.body_used = true;
return resolver.promise(); return resolver.promise();
} }