> ## Documentation Index
> Fetch the complete documentation index at: https://webview.js.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Custom URL Protocol Handlers for Local File Serving

> Register custom URL schemes like app:// to serve local assets, route requests, and build offline-capable desktop apps without running an HTTP server.

Custom protocols let you handle URL schemes such as `app://` directly inside Node.js without starting an HTTP server. The webview loads content by scheme name — `app://localhost/index.html`, for example — and your handler resolves every request. This is the recommended approach for serving local assets, and it is required on Windows when the page needs to use `window.ipc.postMessage()`, because IPC does not fire on `file:` URLs on that platform.

***

## Registering a protocol

Call `win.registerProtocol(scheme, handler)` **before** `win.createWebview()`. The handler receives a standard Fetch `Request` and must return a `Response` (or a `Promise<Response>`).

```js theme={null}
import { readFile } from 'node:fs/promises';
import { extname, join } from 'node:path';
import { Application } from '@webviewjs/webview';

const MIME = {
  '.html': 'text/html; charset=utf-8',
  '.js':   'application/javascript; charset=utf-8',
  '.css':  'text/css',
};

const app = new Application();
const win = app.createBrowserWindow({ title: 'My App' });

win.registerProtocol('app', async (request) => {
  const url = new URL(request.url);
  const path = join(process.cwd(), 'dist', url.pathname);

  try {
    return new Response(await readFile(path), {
      headers: {
        'Content-Type': MIME[extname(path)] ?? 'application/octet-stream',
      },
    });
  } catch {
    return new Response(`Not found: ${url.pathname}`, {
      status: 404,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    });
  }
});

const webview = win.createWebview({ url: 'app://localhost/index.html' });
app.run();
```

<Warning>
  You must call `win.registerProtocol()` **before** `win.createWebview()`.
  Registering a scheme after the webview is created has no effect on the
  existing webview instance.
</Warning>

***

## Fetch API interface

The protocol handler receives a standard global Fetch API `Request` object and must return a standard `Response`. This means anything that works with the Fetch API — headers, status codes, streaming bodies — works here too.

```js theme={null}
win.registerProtocol('api', async (request) => {
  const body = JSON.stringify({ status: 'ok', ts: Date.now() });

  return new Response(body, {
    status: 200,
    headers: {
      'Content-Type': 'application/json',
    },
  });
});
```

If the handler throws or returns a rejected `Promise`, WebviewJS delivers a `500 text/plain` response to the webview.

***

## Routing with Hono

Because the handler receives a standard `Request` and must return a standard `Response`, you can pass the request directly to a Hono router. No HTTP server is required — Hono's `fetch` method acts as the handler.

```js theme={null}
import { Hono } from 'hono';
import { Application } from '@webviewjs/webview';

const router = new Hono();

router.get('/', (c) => c.html('<h1>Home</h1>'));
router.get('/about', (c) => c.html('<h1>About</h1>'));
router.get('/*', (c) => {
  return c.html(`<h1>Current page: ${c.req.path}</h1>`);
});

const app = new Application();
const win = app.createBrowserWindow({ title: 'Hono App', width: 900, height: 600 });

win.registerProtocol('app', (request) => router.fetch(request));
const webview = win.createWebview({ url: 'app://localhost/' });

app.on('application-close-requested', () => app.exit());
app.run();
```

Any Fetch-compatible router works the same way.

***

## Multiple protocols

Register as many schemes as you need before calling `createWebview()`. Each scheme gets its own independent handler.

```js theme={null}
// Static file server for the UI
win.registerProtocol('app', async (request) => {
  // ... serve files from dist/
});

// Proxy API calls to a remote server
win.registerProtocol('api', async (request) => {
  const response = await fetch(
    `https://api.example.com${new URL(request.url).pathname}`
  );
  return response;
});

const webview = win.createWebview({ url: 'app://localhost/index.html' });
```

<Note>
  Protocol registrations are fixed at the moment the webview is created.
  Calling `registerProtocol()` after `createWebview()` does not change the
  routing for an existing webview.
</Note>

***

## CORS and cache headers

Set response headers when the page makes cross-protocol fetch calls or when you want to control caching behavior.

```js theme={null}
return new Response(JSON.stringify(data), {
  headers: {
    'Content-Type': 'application/json',
    'Access-Control-Allow-Origin': '*',
    'Cache-Control': 'no-store',
  },
});
```

***

## Security

<Warning>
  Never resolve a request path without verifying that it stays inside your
  intended asset directory. An attacker-controlled URL such as
  `app://localhost/../../etc/passwd` can escape your `dist/` folder with a
  naive `join()`.
</Warning>

Use `relative()` to detect path traversal attempts and return a `403` before touching the file system:

```js theme={null}
import { relative, resolve } from 'node:path';

const root = resolve(process.cwd(), 'dist');

win.registerProtocol('app', async (request) => {
  const url = new URL(request.url);
  const pathname = decodeURIComponent(url.pathname).replace(/^\/+/, '') || 'index.html';
  const filePath = resolve(root, pathname);

  // Reject any path that escapes the root directory
  if (relative(root, filePath).startsWith('..')) {
    return new Response('Forbidden', {
      status: 403,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    });
  }

  // Safe to read
  try {
    return new Response(await readFile(filePath), {
      headers: { 'Content-Type': MIME[extname(filePath)] ?? 'application/octet-stream' },
    });
  } catch {
    return new Response(`Not found: ${url.pathname}`, {
      status: 404,
      headers: { 'Content-Type': 'text/plain; charset=utf-8' },
    });
  }
});
```

***

## Legacy response format

As an alternative to returning a `Response` object, your handler can return a plain `CustomProtocolResponse` object. This format is supported for compatibility but the standard `Response` API is preferred.

```ts theme={null}
interface CustomProtocolResponse {
  body: Buffer;
  statusCode?: number; // default: 200
  mimeType?: string;   // default: application/octet-stream
  headers?: { key: string; value?: string }[];
}
```

Example usage:

```js theme={null}
win.registerProtocol('app', async (request) => {
  return {
    statusCode: 200,
    body: Buffer.from('<h1>Hello</h1>'),
    mimeType: 'text/html; charset=utf-8',
  };
});
```

***

## Related pages

* [IPC Messaging](/guides/ipc-messaging) — send messages between the page and Node.js (requires custom protocol on Windows)
* [Webview API reference](/api/webview) — `WebviewOptions`, `registerProtocol`, and webview creation
