Version: 1.2.0-build2
Last updated: 2026-07-30
Language: English |
APIs that let your application take over the browser
UI a page asks for:
JavaScript alert(), file selection, and certificate error
decisions — all
with your own dialogs and policies.
This page is written from measurements of the actual engine
implementation.
The wiring status of each handler was re-checked against 1.2.0-build2.
The handlers differ in maturity, so we start with their
status:
| Handler | Current status |
|---|---|
| JavaScript dialogs | Works (limits: only one dialog at a time, beforeunload never delivered) |
| File chooser | Works (limit: no directory upload) |
| Certificate errors | Works (synchronous response only, called for every failing request) |
| Fullscreen (Fullscreen API) | Works (limit: state sync is one-way) |
| Downloads | Not wired up — can be registered but is never called (status and workaround) |
| HTTP authentication | Not wired up — can be registered but is never called (status and workaround) |
Corrections from the previous edition (2026-07-29): the defaults table
said "Download → saved to~/Downloads/" and "Certificate error → logged
and allowed". Both were wrong. In reality: downloads are not saved, and
certificate errors are blocked. The table below is the measured truth.
NULL
as the callback unregisters.user_data is your responsibility (there is
noGDestroyNotify parameter — re-registering or unregistering
does notmessage,
accept_types, …) becomeg_strdup() if you| Page request | Default behavior |
|---|---|
alert() |
Not shown; the script simply continues |
confirm() |
Not shown; returns false |
prompt() |
Not shown; returns null (not the empty string) |
beforeunload |
Navigation always allowed (setting a handler does not change this) |
File chooser (<input type="file">) |
Cancelled |
| Download | Not saved (cancelled internally right after starting; no notification) |
| HTTP auth (401 / 407) | Cancelled (the server's 401 response body is displayed) |
| Certificate error | Blocked (the page goes blank and
load-failed fires) |
Fullscreen request (requestFullscreen()) |
Treated as success on the page side (the element expands within the view) but the window is not fullscreened — a half-way state; implementing the handler is recommended |
| Goal | API | Status |
|---|---|---|
| Own UI for alert / confirm / prompt | blink_web_view_set_javascript_dialog_handler() |
works |
| Own UI for file selection | blink_web_view_set_file_chooser_handler() +
blink_web_view_file_chooser_response() |
works |
| Decide certificate errors | blink_web_view_set_certificate_error_handler() |
works |
| Answer fullscreen requests | blink_web_view_set_fullscreen_handler() +
exit_fullscreen() / is_fullscreen() |
works |
| Choose download destinations | blink_web_view_set_download_handler() |
not wired |
| Answer HTTP auth | blink_web_view_set_auth_handler() +
blink_web_view_auth_response() |
not wired |
Receives alert() / confirm() /
prompt() requests.
Signature:
typedef enum {
BLINK_JS_DIALOG_ALERT = 0,
BLINK_JS_DIALOG_CONFIRM = 1,
BLINK_JS_DIALOG_PROMPT = 2,
BLINK_JS_DIALOG_BEFORE_UNLOAD = 3, /* reserved — never delivered (see below) */
} BlinkJSDialogType;
typedef void (*BlinkJSDialogResponseCallback)(gboolean success,
const char* input_text,
gpointer user_data);
typedef gboolean (*BlinkJSDialogRequestCallback)(
BlinkWebView* web_view,
BlinkJSDialogType dialog_type,
const char* message,
const char* default_prompt,
BlinkJSDialogResponseCallback response_callback,
gpointer response_user_data,
gpointer user_data);
void blink_web_view_set_javascript_dialog_handler(
BlinkWebView* web_view,
BlinkJSDialogRequestCallback callback,
gpointer user_data);The handler shows its own dialog and, once the user has
answered,
must call response_callback. This may
happen asynchronously —
return TRUE from the handler, show the dialog, and
call
response_callback when a button is pressed.
success: TRUE for OK, FALSE
for cancelinput_text: the prompt input string (NULL
is fine otherwise)FALSE from the handler falls back to the
defaults aboveNULL as callback unregisters| Aspect | Behavior |
|---|---|
| Thread | GTK main thread; opening GTK dialogs directly is fine |
| How the page waits | alert() etc. are synchronous IPC. The page's
JavaScript is completely stopped until you respond |
| If you never respond | The page hangs forever; there is no rescue timeout. In addition, other WebViews in the same renderer process stop accepting input |
Contents of message |
Line breaks are normalized to \n (\r\n /
\r become \n) |
| Identifying the caller | The requesting frame is not passed. An alert()
from an ad iframe is indistinguishable from the main page's — a
custom UI that doesn't show a site name can be abused for spoofing |
| beforeunload | Never reaches the handler. The engine always
answers "leave" immediately, so a "Leave this page?" confirmation cannot
currently be implemented (BLINK_JS_DIALOG_BEFORE_UNLOAD is
reserved for the future) |
| Dialog dismissal | If the page navigates away, the tab is destroyed, or the renderer
crashes, no "you may close the dialog now" notification
arrives. Your dialog stays up — closing it from a
load-changed signal handler is a sensible safety net |
The response target behind response_callback is a
single process-wide
slot. If a second dialog request arrives before you answer the
first
(from another WebView, or from another iframe), the first dialog's
response
target is lost and that page hangs forever. Calling an
old
response_callback you kept around answers the
newer dialog instead.
response_user_data is unused by the current
implementation (whateverresponse_callback). Carry context in
your owntypedef struct {
BlinkJSDialogResponseCallback respond;
gpointer respond_data;
} ConfirmCtx;
static void on_confirm_choice(GObject *src, GAsyncResult *res, gpointer data) {
ConfirmCtx *ctx = data;
int btn = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(src), res, NULL);
ctx->respond(btn == 1, NULL, ctx->respond_data);
g_free(ctx);
}
static gboolean on_js_dialog(BlinkWebView *view, BlinkJSDialogType type,
const char *message, const char *default_prompt,
BlinkJSDialogResponseCallback respond,
gpointer respond_data, gpointer user_data) {
if (type != BLINK_JS_DIALOG_CONFIRM)
return FALSE; /* defaults for alert / prompt */
GtkAlertDialog *dlg = gtk_alert_dialog_new("%s", message);
const char *buttons[] = { "Cancel", "OK", NULL };
gtk_alert_dialog_set_buttons(dlg, buttons);
gtk_alert_dialog_set_default_button(dlg, 1);
gtk_alert_dialog_set_cancel_button(dlg, 0);
ConfirmCtx *ctx = g_new0(ConfirmCtx, 1);
ctx->respond = respond;
ctx->respond_data = respond_data;
gtk_alert_dialog_choose(dlg, GTK_WINDOW(user_data), NULL,
on_confirm_choice, ctx);
g_object_unref(dlg);
return TRUE;
}GtkAlertDialog copies message at
construction time, so the display stays
valid after your handler returns. If you show it in your own widget,
copy it
with g_strdup(message) first.
Receives <input type="file"> requests.
Signature:
typedef gboolean (*BlinkFileChooserRequestCallback)(
BlinkWebView* web_view,
gboolean allow_multiple,
const char* accept_types,
gpointer user_data);
void blink_web_view_set_file_chooser_handler(
BlinkWebView* web_view,
BlinkFileChooserRequestCallback callback,
gpointer user_data);Open a GtkFileDialog (or similar) in the handler, then
call
blink_web_view_file_chooser_response() once the selection
is made.
The contents of the accept attribute arrive as a
comma-separated string,
reordered to MIME types first, then extensions, all
lowercased. The
original attribute order is not preserved.
<input type="file" accept=".PNG, image/jpeg, .txt">→ the handler receives: "image/jpeg,.png,.txt"
Without an accept attribute you get the empty
string "" (never
NULL). Invalid entries (neither a MIME type nor a
.ext form) are removed
before delivery.
Signature:
void blink_web_view_file_chooser_response(BlinkWebView* web_view,
const char** file_paths);file_paths is a NULL-terminated array of
absolute paths. The array and
its strings are copied during the call; you may free them
afterwards.
const char *paths[] = { "/home/user/photo.jpg", NULL };
blink_web_view_file_chooser_response(view, paths);NULL (or an array
whose first element is NULL).cancel event and any previously
selected files are| Aspect | Behavior |
|---|---|
| Path validation | None. A nonexistent path still fires
change; failure only surfaces when the page tries to read
the file. Relative paths effectively don't work — always pass absolute
paths |
| Security | The page is granted read access to every path you respond with. Verifying that the file matches what the page asked for — and that the user really chose it — is your application's responsibility |
| Multiple selection | If allow_multiple is FALSE and you return
several paths, the supposedly single-file <input>
ends up in an invalid multi-file state. Trim to one path
yourself |
Directories (webkitdirectory) |
Not supported. Directory requests arrive
indistinguishable from a single-file request
(allow_multiple = FALSE), and even if you respond, the
page's webkitRelativePath stays empty, so directory upload
does not function |
When the handler returns FALSE |
The request is cancelled. Do not call
file_chooser_response() afterwards (it would
double-answer an already-answered request; behavior is undefined) |
| If you never respond | Every subsequent file chooser on that WebView is instantly
cancelled, and window.open() gets blocked too.
Make sure every path out of your dialog — cancel button, Esc, window
close — calls the response function |
| Responding after navigation | A pending request expires internally when the page navigates away; a later response is silently ignored (no crash) |
static void on_file_open(GObject *src, GAsyncResult *res, gpointer data) {
BlinkWebView *view = BLINK_WEB_VIEW(data);
GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(src), res, NULL);
if (file) {
char *path = g_file_get_path(file);
const char *paths[] = { path, NULL };
blink_web_view_file_chooser_response(view, paths);
g_free(path);
g_object_unref(file);
} else {
blink_web_view_file_chooser_response(view, NULL); /* cancel */
}
}
static gboolean on_file_chooser(BlinkWebView *view, gboolean allow_multiple,
const char *accept_types, gpointer user_data) {
GtkFileDialog *dlg = gtk_file_dialog_new();
gtk_file_dialog_open(dlg, GTK_WINDOW(user_data), NULL, on_file_open, view);
g_object_unref(dlg);
return TRUE;
}Note that the cancel path (file == NULL) still calls the
response function.
Decides whether to proceed past an SSL certificate error.
Signature:
typedef gboolean (*BlinkCertErrorCallback)(
BlinkWebView* web_view,
const char* url,
const char* error_description,
gpointer user_data);
void blink_web_view_set_certificate_error_handler(
BlinkWebView* web_view,
BlinkCertErrorCallback callback,
gpointer user_data);TRUE proceeds, FALSE blocks.
The default (no handler) is to block everything. Set
a handler returning
TRUE only when you need to display intranet sites or
development servers
with self-signed certificates. When showing arbitrary external sites,
the
default is the safe choice.
| Aspect | Behavior |
|---|---|
| Response model | Synchronous only. The decision is taken from the return value; there is no deferred-response API. The UI thread is stopped while your handler runs, so you cannot show a dialog and ask the user — use an instant policy check such as a host allowlist |
| Call frequency | Called for every failing request; decisions are not remembered. A page loading 20 images from a bad-certificate host can invoke the handler 20 times |
url |
The URL of the individual failing resource — not necessarily the main page. For a subresource (image, fetch) it is that subresource's URL. There is currently no way to tell main-frame from subresource |
error_description |
A Chromium net error identifier such as
net::ERR_CERT_AUTHORITY_INVALID — not a human-readable
sentence. The certificate itself, its expiry and issuer are not passed;
your practical decision input is the URL's host name |
| The blocked page | No Chrome-style warning page appears — the page is simply
blank. A load-failed signal fires at the
same time (error_code is a negative value in the -200
range); build your own error screen there |
What TRUE does |
Proceeds even on HSTS hosts (where Chrome would refuse).
Keep your TRUE conditions minimal |
| Cases that never arrive | Certificate errors on Service Worker requests never reach the handler and are always blocked. While DevTools is attached, the tools may take over the decision |
static gboolean on_cert_error(BlinkWebView *view, const char *url,
const char *error, gpointer user_data) {
/* Allow only the self-signed intranet host; decide synchronously */
gboolean allow = g_str_has_prefix(url, "https://intranet.example.jp/");
g_message("cert error [%s] %s -> %s", error, url,
allow ? "continue" : "block");
return allow;
}
/* The blocked-page screen is provided via load-failed */
static void on_load_failed(BlinkWebView *view, int error_code,
const char *failing_uri, gpointer user_data) {
if (error_code <= -200 && error_code > -300) { /* net cert error range */
blink_web_view_load_uri(view, "app://error/cert.html");
}
}The app:// error page can be served with the App Integration API's custom
schemes. Note that load-failed's parameters
come in (error_code, failing_uri) order (Signals API).
Receives the page's requestFullscreen() / exit
requests.
Signature:
typedef void (*BlinkFullscreenCallback)(BlinkWebView* web_view,
gboolean enter_fullscreen,
gpointer user_data);
void blink_web_view_set_fullscreen_handler(BlinkWebView* web_view,
BlinkFullscreenCallback callback,
gpointer user_data);
void blink_web_view_exit_fullscreen(BlinkWebView* web_view);
gboolean blink_web_view_is_fullscreen(BlinkWebView* web_view);| Aspect | Behavior |
|---|---|
| Without a handler | The page's requestFullscreen() is treated as
success (not rejected). The element fills the view, but
the GTK window is not fullscreened — to avoid this
half-way state, apps showing video etc. should implement the
handler |
| The handler's job | Call gtk_window_fullscreen() /
gtk_window_unfullscreen() according to
enter_fullscreen |
| State sync is one-way | Engine-to-app notification only. If the user leaves fullscreen
through the window manager, the engine is not told.
When exiting from the app side, don't just call
gtk_window_unfullscreen() — always call
blink_web_view_exit_fullscreen() (this exits
Blink's fullscreen and delivers enter_fullscreen=FALSE to
your handler) |
| ESC key | Not handled by the engine. Implement it in the app
(a capture-phase key controller that calls
exit_fullscreen() on ESC) |
| Identifying the requester | Whether the request came from an iframe is not passed |
static void on_fullscreen(BlinkWebView *view, gboolean enter,
gpointer user_data) {
GtkWindow *win = GTK_WINDOW(user_data);
if (enter)
gtk_window_fullscreen(win);
else
gtk_window_unfullscreen(win);
}
/* Exit on ESC (the engine does not handle ESC; the app must) */
static gboolean on_key(GtkEventControllerKey *c, guint keyval, guint code,
GdkModifierType state, gpointer user_data) {
BlinkWebView *view = BLINK_WEB_VIEW(user_data);
if (keyval == GDK_KEY_Escape && blink_web_view_is_fullscreen(view)) {
blink_web_view_exit_fullscreen(view); /* exit properly via Blink */
return TRUE;
}
return FALSE;
}
blink_web_view_set_fullscreen_handler(BLINK_WEB_VIEW(view), on_fullscreen, win);This handler is still not wired up; registering it has no effect.
typedef char* (*BlinkDownloadRequestCallback)(
BlinkWebView* web_view,
const char* url,
const char* suggested_filename,
const char* mime_type,
gpointer user_data);
void blink_web_view_set_download_handler(
BlinkWebView* web_view,
BlinkDownloadRequestCallback callback,
gpointer user_data);Actual behaviour:
<a download>,Content-Disposition: attachment, …), it is
cancelled internally rightNULL to save to~/Downloads/" describe a future design; that behavior does
not existWiring this up is tracked as a known engine work item. The signature
is
expected to remain as-is, so registering a handler now is harmless (it
just
won't be called).
Where saving files matters, fetch in page JS and hand the bytes to
the app
(using the App Integration API
JS bridge):
/* Page side: fetch the file and send it to C (for modest sizes) */
const buf = await (await fetch(fileUrl)).arrayBuffer();
window.blinkgtk.postMessage(buf); /* ArrayBuffers arrive as raw binary */The C side receives the bytes via message-received and
writes them out
with GFile etc. — destination choice, overwrite
confirmation and progress
UI are all fully under your control.
This handler is also still not wired up; registering it has
no
effect. blink_web_view_auth_response() currently
always does nothing.
typedef gboolean (*BlinkAuthRequestCallback)(
BlinkWebView* web_view,
const char* url,
const char* realm,
gboolean is_proxy,
gpointer user_data);
void blink_web_view_set_auth_handler(
BlinkWebView* web_view,
BlinkAuthRequestCallback callback,
gpointer user_data);
void blink_web_view_auth_response(BlinkWebView* web_view,
const char* username,
const char* password);Actual behaviour: HTTP 401 / proxy 407 challenges are
always
cancelled immediately, and the server's 401 response body is
displayed.
Workarounds are limited:
Authorizationfetch() yourself (no handler needed)Wiring this up is tracked, together with downloads, as a known engine
work
item.
A complete example combining only the three working handlers.
#include <blink_gtk/blink_gtk.h>
#include <gtk/gtk.h>
typedef struct {
BlinkJSDialogResponseCallback respond;
gpointer respond_data;
} ConfirmCtx;
static void on_confirm_choice(GObject *src, GAsyncResult *res, gpointer data) {
ConfirmCtx *ctx = data;
int btn = gtk_alert_dialog_choose_finish(GTK_ALERT_DIALOG(src), res, NULL);
ctx->respond(btn == 1, NULL, ctx->respond_data);
g_free(ctx);
}
static gboolean on_js_dialog(BlinkWebView *view, BlinkJSDialogType type,
const char *message, const char *default_prompt,
BlinkJSDialogResponseCallback respond,
gpointer respond_data, gpointer user_data) {
if (type != BLINK_JS_DIALOG_CONFIRM)
return FALSE; /* defaults for alert / prompt */
GtkAlertDialog *dlg = gtk_alert_dialog_new("%s", message);
const char *buttons[] = { "Cancel", "OK", NULL };
gtk_alert_dialog_set_buttons(dlg, buttons);
gtk_alert_dialog_set_default_button(dlg, 1);
gtk_alert_dialog_set_cancel_button(dlg, 0);
ConfirmCtx *ctx = g_new0(ConfirmCtx, 1);
ctx->respond = respond;
ctx->respond_data = respond_data;
gtk_alert_dialog_choose(dlg, GTK_WINDOW(user_data), NULL,
on_confirm_choice, ctx);
g_object_unref(dlg);
return TRUE;
}
static void on_file_open(GObject *src, GAsyncResult *res, gpointer data) {
BlinkWebView *view = BLINK_WEB_VIEW(data);
GFile *file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(src), res, NULL);
if (file) {
char *path = g_file_get_path(file);
const char *paths[] = { path, NULL };
blink_web_view_file_chooser_response(view, paths);
g_free(path);
g_object_unref(file);
} else {
blink_web_view_file_chooser_response(view, NULL);
}
}
static gboolean on_file_chooser(BlinkWebView *view, gboolean allow_multiple,
const char *accept_types, gpointer user_data) {
GtkFileDialog *dlg = gtk_file_dialog_new();
gtk_file_dialog_open(dlg, GTK_WINDOW(user_data), NULL, on_file_open, view);
g_object_unref(dlg);
return TRUE;
}
static gboolean on_cert_error(BlinkWebView *view, const char *url,
const char *error, gpointer user_data) {
return g_str_has_prefix(url, "https://intranet.example.jp/");
}
int main(int argc, char **argv) {
blink_gtk_init(&argc, &argv);
GtkWidget *win = gtk_window_new();
gtk_window_set_default_size(GTK_WINDOW(win), 1024, 768);
GtkWidget *view = blink_web_view_new();
gtk_window_set_child(GTK_WINDOW(win), view);
blink_web_view_set_javascript_dialog_handler(BLINK_WEB_VIEW(view),
on_js_dialog, win);
blink_web_view_set_file_chooser_handler(BLINK_WEB_VIEW(view),
on_file_chooser, win);
blink_web_view_set_certificate_error_handler(BLINK_WEB_VIEW(view),
on_cert_error, NULL);
gtk_window_present(GTK_WINDOW(win));
blink_web_view_load_uri(BLINK_WEB_VIEW(view), "https://example.com/");
return blink_gtk_run_main_loop();
}Build:
cc -o handlers-demo handlers-demo.c $(pkg-config --cflags --libs blinkgtk-0.1)| Item | Status |
|---|---|
| Downloads | Not wired up. Downloads are cancelled without notice |
| HTTP authentication | Not wired up. 401/407 always cancelled; auth_response()
has no effect |
| beforeunload | Never reaches the handler; leaving is always allowed |
| Concurrent JS dialogs | One per process. A second request hangs the first page |
| Dialog dismissal notification | None. Your dialog stays up across navigations |
| Directory upload | Not supported (webkitRelativePath ends up empty) |
| Async certificate decisions | Not possible (synchronous return value only); no user-confirmation dialog |
| Remembering certificate decisions | Not done; the handler runs for every failing request |
| Identifying the requesting frame | Not possible (for JS dialogs and certificate errors alike, main frame vs. iframe is not passed) |
user_data destroy notification |
None (no GDestroyNotify parameter) |
| GObject signal variants | None (the five handlers on this page are C function pointers only;
from language bindings, only the permission-request signal
is available, for permissions) |
permission-request signal (Signals API) — you| Version | Changes |
|---|---|
| v1.1.0 (2026-07-30 addendum) | Added the Fullscreen API handler section (previously undocumented anywhere) |
| v1.1.0 (2026-07-30) | Complete revision based on measured engine behavior. Corrected the defaults table (certificate error = blocked / download = not saved / prompt = null). Documented the unwired download/auth handlers with workarounds, the one-dialog-at-a-time constraint, unimplemented beforeunload, the real accept_types format, and the consequences of never responding |
| v1.1.0 (2026-07-29) | First edition of this page |