BlinkGTK UI Handlers API Reference — Dialogs, File Chooser, Certificate Errors

Version: 1.2.0-build2
Last updated: 2026-07-30
Language: English |

日本語


About this page

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.

Three facts to know first

  1. All handlers run on the GTK main thread. You may call GTK APIs
    directly from them. This thread is also the engine's browser UI thread,
    so blocking inside a handler for a long time freezes painting, input and
    networking for every WebView.
  2. Handlers are per-WebView; passing NULL as the callback unregisters.
    The lifetime of user_data is your responsibility (there is no
    GDestroyNotify parameter — re-registering or unregistering does not
    notify you).
  3. Strings passed to a handler (message, accept_types, …) become
    invalid once the handler returns.
    Copy them with g_strdup() if you
    need them asynchronously.

Defaults when no handler is set (measured)

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

Overview

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

JavaScript dialogs

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.

Behavior details

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

Constraint: only one dialog at a time

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.

Example: answering confirm() with GTK4's async dialog

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;
}

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.


File chooser

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 actual format of accept_types

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);

Behavior details and cautions

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)

Example: GtkFileDialog

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.


Certificate errors

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.

Behavior details

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

Example: host allowlist plus your own error screen

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).


Fullscreen (Fullscreen 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);

Behavior details

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

Example

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);

Downloads (currently not supported)

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:

Wiring 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).

Workaround: implement downloads as an app-managed operation

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.


HTTP authentication (currently not supported)

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:

Wiring this up is tracked, together with downloads, as a known engine work
item.


Complete example: taking over confirm, file selection and certificate policy

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)

Known limitations

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)

Changelog

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