Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions src/bridge/root.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,33 @@ const std = @import("std");
const json = @import("json");
const security = @import("../security/root.zig");

pub const max_message_bytes: usize = 1024 * 1024;
pub const max_response_bytes: usize = 1024 * 1024;
pub const max_result_bytes: usize = 1024 * 1024;
// Requests may carry base64-encoded image/video bytes (annex.media.write), so
// the request cap matches the response caps. At 1 MB, any write of a file whose
// base64 exceeded ~750 KB was rejected as "too large" — and that rejection
// carries an empty id, so the JS promise never resolves and the call hangs.
pub const max_message_bytes: usize = 48 * 1024 * 1024;
// Responses/results may carry base64-encoded HTTP bodies (e.g. images) from the
// native net.fetch bridge, so these are sized generously (48 MB). Buffers of this

@vercel vercel Bot Jun 20, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A net.fetch response whose JSON fills the result buffer to within ~envelope-size of 48 MB causes writeSuccessResponse to overflow and return an empty slice, which the macOS host turns into _complete({}) (no id), leaving the originating JS promise pending forever.

Fix on Vercel

// size must never live on the stack — see the shared static buffers below.
pub const max_response_bytes: usize = 48 * 1024 * 1024;
pub const max_result_bytes: usize = 48 * 1024 * 1024;
pub const max_id_bytes: usize = 64;
pub const max_command_bytes: usize = 128;

// Bridge dispatch runs on the single main event-loop thread, so these large
// buffers are shared statics rather than per-call stack arrays (a 48 MB stack
// allocation would overflow the thread stack).
var shared_response_buffer: [max_response_bytes]u8 = undefined;
var shared_result_buffer: [max_result_bytes]u8 = undefined;

pub fn sharedResponseBuffer() []u8 {
return &shared_response_buffer;
}

pub fn sharedResultBuffer() []u8 {
return &shared_result_buffer;
}

const null_json = "null";

pub const ErrorCode = enum {
Expand Down Expand Up @@ -95,13 +116,11 @@ pub const AsyncResponder = struct {
}

pub fn success(self: AsyncResponder, id: []const u8, result: []const u8) anyerror!void {
var buffer: [max_response_bytes]u8 = undefined;
try self.respond(writeSuccessResponse(&buffer, id, result));
try self.respond(writeSuccessResponse(sharedResponseBuffer(), id, result));
}

pub fn fail(self: AsyncResponder, id: []const u8, code: ErrorCode, message: []const u8) anyerror!void {
var buffer: [max_response_bytes]u8 = undefined;
try self.respond(writeErrorResponse(&buffer, id, code, message));
try self.respond(writeErrorResponse(sharedResponseBuffer(), id, code, message));
}
};

Expand Down Expand Up @@ -155,8 +174,7 @@ pub const Dispatcher = struct {
return writeErrorResponse(output, request.id, .unknown_command, "Bridge command is not registered");
};

var result_buffer: [max_result_bytes]u8 = undefined;
const result = handler.invoke_fn(handler.context, .{ .request = request, .source = source }, &result_buffer) catch |err| {
const result = handler.invoke_fn(handler.context, .{ .request = request, .source = source }, sharedResultBuffer()) catch |err| {
return writeErrorResponse(output, request.id, .handler_failed, @errorName(err));
};
return writeSuccessResponse(output, request.id, if (result.len == 0) null_json else result);
Expand Down
4 changes: 4 additions & 0 deletions src/platform/macos/appkit_host.h
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ typedef struct {
size_t tertiary_button_len;
} zero_native_appkit_message_dialog_opts_t;

typedef void (*zero_native_appkit_http_fetch_callback_t)(void *ctx, int status, const char *json_utf8, size_t json_len);

void zero_native_appkit_http_fetch_async(zero_native_appkit_host_t *host, const char *url, size_t url_len, zero_native_appkit_http_fetch_callback_t callback, void *ctx);

typedef void (*zero_native_appkit_tray_callback_t)(void *context, uint32_t item_id);

zero_native_appkit_open_dialog_result_t zero_native_appkit_show_open_dialog(zero_native_appkit_host_t *host, const zero_native_appkit_open_dialog_opts_t *opts, char *buffer, size_t buffer_len);
Expand Down
Loading