Custom Protocols in Electron haben zwei Anwendungsfälle: erstens interne Schemes wie app:// als sichere Alternative zu file:// für lokale Inhalte, zweitens externe Schemes wie myapp://login für Deep-Links aus dem Browser oder anderen Apps. Beide nutzen das protocol-Modul.

Internes Protocol — protocol.handle

Statt file://-Pfade direkt zu laden, ist es heute Standard, ein eigenes app:// zu registrieren — sicherer (CSP einfacher) und sauberer.

JavaScript main.js
import { app, protocol, net } from 'electron';
import path from 'node:path';
import { fileURLToPath } from 'node:url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));

// VOR app.ready: Privilegien deklarieren
protocol.registerSchemesAsPrivileged([
    { scheme: 'app', privileges: { secure: true, standard: true, supportFetchAPI: true } }
]);

app.whenReady().then(() => {
    // Handler registrieren
    protocol.handle('app', (request) => {
        const url = new URL(request.url);
        const filePath = path.join(__dirname, 'dist', url.pathname);
        return net.fetch('file://' + filePath);
    });

    const win = new BrowserWindow({ /* ... */ });
    win.loadURL('app://./index.html');
});

Was passiert:

  1. registerSchemesAsPrivileged muss vor app.ready aufgerufen werden — sagt Chromium, dass app:// als sicheres, Standard-fähiges Schema gelten soll
  2. protocol.handle('app', handler) registriert den Request-Handler
  3. Der Handler bekommt einen Web-Standard-Request und gibt eine Response zurück (oder ein Promise darauf)
  4. Window lädt app://./index.html statt file://...

net.fetch ist Electrons spezielle Fetch-Funktion, die auch file://-URLs versteht.

Warum nicht file://?

file:// hat in Web-Kontexten viele Eigenheiten:

  • CORS verhält sich seltsam
  • CSP kann nicht streng konfiguriert werden
  • Service-Worker funktionieren nicht
  • Manche modernen Web-APIs verweigern auf file://

Mit app:// als „secure" und „standard" registriert hast du einen vollwertigen Origin — alle Web-Features funktionieren wie über HTTPS.

Custom Protocol mit dynamischen Daten

Du kannst auch dynamische Inhalte liefern — z. B. Bilder aus einer DB:

JavaScript
protocol.handle('media', async (request) => {
    const url = new URL(request.url);
    const id = url.pathname.replace('/', '');

    const media = await db.getMedia(id);
    if (!media) {
        return new Response('Not found', { status: 404 });
    }

    return new Response(media.buffer, {
        status: 200,
        headers: { 'Content-Type': media.mimeType }
    });
});

// im Renderer: <img src="media://./image-42">

Damit kann der Renderer Bilder per URL anfordern, ohne dass die ganzen Bytes per IPC laufen müssen — Browser-Standard, mit Caching, Range-Requests etc.

Damit myapp://login?code=abc aus dem Browser deine App startet:

JavaScript main.js
if (process.defaultApp) {
    // Während Dev: argv[1] ist der App-Pfad
    if (process.argv.length >= 2) {
        app.setAsDefaultProtocolClient('myapp', process.execPath, [path.resolve(process.argv[1])]);
    }
} else {
    app.setAsDefaultProtocolClient('myapp');
}

// macOS: Deep-Link kommt als open-url Event
app.on('open-url', (event, url) => {
    event.preventDefault();
    handleDeepLink(url);
});

// Windows/Linux: Deep-Link kommt via second-instance args
app.on('second-instance', (_event, argv) => {
    const url = argv.find(a => a.startsWith('myapp://'));
    if (url) handleDeepLink(url);
});

function handleDeepLink(url) {
    const parsed = new URL(url);
    // myapp://login?code=abc
    if (parsed.host === 'login') {
        const code = parsed.searchParams.get('code');
        completeLogin(code);
    }
}

Klassischer Use-Case: OAuth-Callback. User klickt „Login mit GitHub", Browser öffnet sich, GitHub redirect zurück zu myapp://login?code=xxx, OS startet deine App mit dem Code.

Plattform-Eigenheiten

PlattformWo wird's registriert
macOSInfo.plist CFBundleURLTypes (electron-builder generiert)
WindowsRegistry (per setAsDefaultProtocolClient zur Laufzeit)
Linux.desktop-Datei mit MimeType=x-scheme-handler/myapp

In electron-builder.yml:

YAML
protocols:
  - name: My App Protocol
    schemes:
      - myapp

Damit wird's bei der Distribution automatisch konfiguriert.

Pattern: bei zweitem Start nicht eine zweite Instanz starten, sondern den Link an die laufende Instanz weiterreichen:

JavaScript
const gotLock = app.requestSingleInstanceLock();

if (!gotLock) {
    app.quit();
} else {
    app.on('second-instance', (_event, argv) => {
        if (mainWindow) {
            if (mainWindow.isMinimized()) mainWindow.restore();
            mainWindow.focus();
        }
        const url = argv.find(a => a.startsWith('myapp://'));
        if (url) handleDeepLink(url);
    });

    app.whenReady().then(createWindow);
}

Ohne diesen Lock startet jeder Deep-Link eine neue Instanz — das nervt User und macht Login-Flows kaputt.

Besonderheiten

app:// statt file:// für lokale Inhalte.

Modern empfohlen — vollwertiger Origin, CSP funktioniert sauber, Service-Worker und moderne Web-APIs sind verfügbar. Die alte file://-Variante hat zu viele Browser-Eigenheiten.

registerSchemesAsPrivileged VOR app.ready.

Sonst werden die Privilegien nicht gesetzt. Das ist eine der wenigen Funktionen, die VOR app.ready stehen muss — direkt nach dem ersten Setup-Code in main.js.

Plattform-Trennung bei Deep-Links: open-url vs. second-instance.

macOS: Event open-url. Windows/Linux: kommt via Command-Line-Argumente, also second-instance mit argv parsen. Beides parallel implementieren — nicht eines weglassen.

Single-Instance-Lock ist Pflicht für saubere Deep-Links.

Ohne Lock startet jeder Klick eine neue App-Instanz. Mit Lock: bestehende Instanz fokussiert, Deep-Link wird dort verarbeitet.

Beim Distribuieren: electron-builder konfiguriert das Protocol.

macOS Info.plist und Linux .desktop-Datei werden bei Distribution gebraucht — manuell zu pflegen ist fehleranfällig. electron-builder hat dafür ein eigenes protocols-Feld.

Custom Protocols auch für Sicherheit nutzen.

Wer im Renderer Bilder aus dem Filesystem zeigt: app:// oder eigenes media://-Schema, statt file://-URLs. Verhindert Path-Traversal-Angriffe und ist mit CSP kompatibel.

Weiterführende Ressourcen

Externe Quellen

/ Weiter

Zurück zu Native APIs & Filesystem

Zur Übersicht