Microsoft EdgeでService Workerがfile://で登録できない原因と解決策|localhost・HTTPS・スコープ設定・デバッグ完全ガイド

Microsoft Edge で Service Worker が登録できない――とくにローカルの HTML をダブルクリックして file:// で開いた時に発生するエラーは、多くのフロントエンド開発者が最初に踏む落とし穴です。本稿では「なぜ登録できないのか」「最短で直すには」「HTTPS 前提の開発へどう移行するか」を、具体的なコマンド・コード・デバッグ手順・チェックリストまで含めて網羅的に解説します。

目次

Microsoft Edgeで Service Workerが登録できない問題の本質

Edge のコンソールに次のメッセージが出て Service Worker が登録できないことがあります。

Uncaught (in promise) TypeError: Failed to register a ServiceWorker: 
The URL protocol of the current origin ('file://') is not supported.

これはバグではなく仕様です。Service Worker はセキュリティ上の理由から「安全なコンテキスト(Secure Context)」でしか動作しないため、file:// のようにオリジンが保証されないスキームでは登録そのものが禁止されています。Edge(Chromium)だけでなく、Chrome や Firefox を含むモダンブラウザ共通の挙動です。

プロトコル別:登録の可否早見表

アクセス方法例Service Worker 登録備考
HTTPShttps://localhost:8443 / https://example.test◯本番は必ずこれ。ローカルも推奨。
HTTP(localhost)http://localhost:3000 / http://127.0.0.1:5173◯開発特例として許可済み。
HTTP(LAN・外部ホスト)http://192.168.0.10 / http://dev.myhost×原則 HTTPS が必要。
FILEfile:///C:/site/index.html×ダブルクリックでの起動は不可。
DATA / BLOB の一部data: / blob:×オリジンが制御できないため不可。

最短で解決する手順(ローカル Web サーバーを立てる)

解決のカギは「file:// をやめて localhost の HTTP/HTTPS で配信する」ことです。すぐに試せる代表例をいくつか挙げます。

Node.js がある場合

# プロジェクトフォルダで実行
npx http-server            # 既定ポート 8080
# または
npx serve -l 3000

ブラウザで http://localhost:8080(または指定したポート)にアクセスします。

Python がある場合

# Python 3 系
python -m http.server 8000

ブラウザで http://localhost:8000 にアクセスします。

VS Code の拡張を使う場合

  • Live Server をインストールし、HTML を右クリックして「Open with Live Server」。
  • ホットリロード付きで素早く検証できます。

PowerShell(Windows 標準のみ)

# Windows PowerShell 5.1+
# カレントディレクトリを配信(自己責任・開発用途のみ)
Add-Type -AssemblyName System.Net.HttpListener
$h = New-Object System.Net.HttpListener
$h.Prefixes.Add("http://localhost:8888/")
$h.Start()
Write-Host "Serving http://localhost:8888/ (Ctrl+C to stop)"
while ($h.IsListening) {
  $ctx = $h.GetContext()
  $path = Join-Path (Get-Location) ($ctx.Request.Url.LocalPath.TrimStart('/'))
  if (Test-Path $path -PathType Leaf) { $bytes = [IO.File]::ReadAllBytes($path) }
  else { $path = "index.html"; $bytes = [IO.File]::ReadAllBytes($path) }
  $ctx.Response.OutputStream.Write($bytes,0,$bytes.Length)
  $ctx.Response.OutputStream.Close()
}

簡易実装なので、通常は前述の http-server/Live Server の利用を推奨します。

HTTPS 前提のローカル開発に移行する

本番はもちろん開発でも HTTPS での検証を勧めます。以下の選択肢があります。

自己署名証明書を許可して検証

  1. Edge のアドレスバーで edge://flags を開く。
  2. Allow invalid certificates for resources loaded from localhost(#allow-insecure-localhost)を「Enabled」にする。
  3. 開発用サーバーを https://localhost:<port> で起動する。

この方法は localhost 限定の緩和です。LAN 内の別ホストやカスタムドメインでは有効になりません。

開発機で本物の「信頼できる」ローカル証明書を発行する

ローカルに開発者用 CA を作成し、localhost や任意の開発用ドメイン(例:dev.example.test)に署名した証明書を発行します。代表的な手順例:

# 例:開発者用 CA を作成して OS に登録
mkcert -install

# 例:localhost/127.0.0.1/::1 向けの証明書を作る

mkcert localhost 127.0.0.1 ::1

# 生成された cert/key をローカルサーバーに設定

npx http-server --ssl --cert cert.pem --key key.pem -p 8443 

.NET/Node/Go など各フレームワークにも開発証明書の仕組みがあり、dotnet dev-certs https --trust のように一発で準備できる場合もあります。

最低限の登録コードと配置の原則

Service Worker は「スコープ(scope)」という配下 URL の概念があります。原則として、Service Worker ファイル(例:/sw.js)は、制御したいパス配下に置きます。サイト全体を制御したいならルート直下が安全です。

登録コード(最小例)

&lt;script&gt;
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () =&gt; {
    try {
      const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
      console.log('SW registered', reg.scope);
    } catch (e) {
      console.error('SW registration failed:', e);
    }
  });
}
&lt;/script&gt;

最小の sw.js(App Shell をキャッシュ)

const CACHE_VERSION = 'v1';
const APP_SHELL = [
  '/',
  '/index.html',
  '/styles.css',
  '/app.js',
];

self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_VERSION).then((cache) => cache.addAll(APP_SHELL))
);
self.skipWaiting(); // 速やかに新 SW をアクティブ化
});

self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) => Promise.all(
keys.filter((k) => k !== CACHE_VERSION).map((k) => caches.delete(k))
))
);
self.clients.claim(); // 既存ページも即制御
});

self.addEventListener('fetch', (event) => {
const req = event.request;
event.respondWith(
caches.match(req).then((cached) => {
const fetchPromise = fetch(req).then((res) => {
const copy = res.clone();
caches.open(CACHE_VERSION).then((cache) => cache.put(req, copy));
return res;
}).catch(() => cached);
return cached || fetchPromise;
})
);
}); 

Edge でのデバッグ手順(確実版)

  1. Edge を開き、F12(DevTools)→ Application → Service Workers を選択。
  2. Update / Unregister ボタンで更新・解除を明示的に実行。
  3. Clear storage(左メニュー)で Unregister service workers とキャッシュ・IndexedDB を一括クリア。
  4. ネットワークパネルで「Disable cache(開発者ツールを開いている間)」にチェック。
  5. アドレスバーで ハードリロード(Windows: Ctrl+F5)。

古い Service Worker が残っていると挙動が分かりにくくなります。テスト前に一度「Unregister → Clear storage → Hard Reload」を行うと再現性が上がります。

よくある落とし穴と対処

症状原因対処
file:// では登録できないSecure Context ではないローカルサーバー(localhost)で配信。できれば HTTPS。
登録は成功するが fetch が効かないスコープ外のリクエスト/sw.js をサイトルートに置く。scope を見直す。
更新しても古いキャッシュが返るキャッシュキーのバージョニング不足CACHE_VERSION を上げ、activate で古いキャッシュを削除。
ネットワークエラー時に白画面フォールバック未実装オフライン用 HTML/画像を用意して返す。
スコープが意図せずサブディレクトリのみService Worker を /app/sw.js に配置サイト全体を制御するなら /sw.js に移動。
本番だけ動かないHTTPS 未対応 / Mixed Content / CSPHTTPS を有効化。HTTP リソースを排除。CSP を調整。
ビルドツール使用時に 404出力先に sw.js がコピーされないビルド設定で sw.js を公開ディレクトリ直下へ出力。
dev サブパスで不整合ベースパス設定(例:Vite の base)の不一致ローカルと本番で同一のベースパスを維持。

オフラインフォールバックの実装例

「ネットワークにもキャッシュにも無い」場合に専用ページを返して UX を向上させます。

// sw.js の一部
const OFFLINE_URL = '/offline.html';

self.addEventListener('install', (e) => {
e.waitUntil(
caches.open(CACHE_VERSION).then((c) => c.addAll([OFFLINE_URL]))
);
});

self.addEventListener('fetch', (e) => {
e.respondWith((async () => {
try {
const res = await fetch(e.request);
const copy = res.clone();
const cache = await caches.open(CACHE_VERSION);
cache.put(e.request, copy);
return res;
} catch {
const cached = await caches.match(e.request);
return cached || caches.match(OFFLINE_URL);
}
})());
}); 

更新制御:ユーザーに「新しいバージョンがあります」を出す

Service Worker の更新は自動ですが、即時反映させるにはユーザー介入や UI が必要な場合があります。

// sw.js
self.addEventListener('install', () => self.skipWaiting()); // 即時アクティブ化の準備
self.addEventListener('activate', () => self.clients.claim());

// ページ側(main.js など)
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
// ここで「新しいバージョンに更新します」ダイアログを出してリロードするなど
console.log('New SW took control. Reload to apply.');
});
} 

registration.update() をページロード時に明示的に呼ぶと、バックグラウンド更新の頻度を高められます。

Edge でのトラブルシューティング・コマンド

コンソールから状態を確認できます。

// すべての登録を取得
navigator.serviceWorker.getRegistrations().then(rs => console.table(rs.map(r => ({
  scope: r.scope,
  installing: r.installing?.state,
  waiting: r.waiting?.state,
  active: r.active?.state
}))));

// 手動で更新を要求
navigator.serviceWorker.getRegistration().then(r => r?.update());

// 待機中の SW を即アクティブ化(ページ起点)
navigator.serviceWorker.getRegistration().then(r => r?.waiting?.postMessage({type:'SKIP_WAITING'})); 

上記の postMessage に対応するため、sw.js 側に次のハンドラーを仕込んでおくと便利です。

self.addEventListener('message', (e) =&gt; {
  if (e.data&amp;&amp;e.data.type === 'SKIP_WAITING') { self.skipWaiting(); }
});

本番デプロイ前のチェックリスト

  • 配信は HTTPS になっている。
  • sw.js はサイトルートに存在し、期待する scope で登録される。
  • キャッシュ名にバージョンを付け、activate で古いキャッシュを消す。
  • オフラインフォールバック(HTML/画像)を持つ。
  • Mixed Content を含まない(HTTP リソースを読み込んでいない)。
  • アセットの長期キャッシュ戦略(ファイル名ハッシュ or キャッシュヘッダー)が整備されている。
  • DevTools の Application → Service Workers で Update/Unregister が期待通り動く。

セキュリティ背景:なぜ file:// がダメなのか

Service Worker はネットワークを横取り(プロキシ)できる強力な仕組みで、XSS や改ざんが発生すると被害が大きくなります。そのため「同一オリジン」「HTTPS による暗号化・完全性保証」といった条件を満たす必要があります。file:// はユーザーごと・ファイルごとにオリジンが安定せず、意図せぬ別ファイルへのアクセスや権限昇格の危険があるため、仕様として禁止されているのです。

開発フレームワーク別のヒント

  • Vite/Next.js/CRA など:開発サーバーは既定で http://localhost を使用します。Service Worker をルートへ出力する設定(例:public/sw.js)を確認し、base(サブパス配信)を使う場合は登録パスも合わせます。
  • Workbox:プリキャッシュとランタイムキャッシュの両輪で、更新時の競合が減ります。生成物の sw.js がルート直下へ出るように設定します。
  • ASP.NET / IIS Express:dotnet dev-certs https --trust を実行し、プロジェクトを HTTPS で起動します。静的ファイルのキャッシュヘッダーはミドルウェアで制御できます。

「再現から修復」までの最短フロー(保存版)

  1. HTML を ダブルクリックで開かない。必ずローカルサーバーで配信。
  2. 最短テスト:npx http-server → http://localhost:8080。
  3. Edge DevTools → Application → Service Workers で状態確認。
  4. 必要に応じて Unregister → Clear storage → Hard Reload。
  5. HTTPS が必要なら #allow-insecure-localhost を有効化、または開発証明書を導入。
  6. sw.js をサイトルートへ、scope:'/' で登録。
  7. バージョニングとオフラインフォールバックを実装。

トラブル事例:サブディレクトリ配信

例として、アプリを https://example.com/app/ 配下にデプロイする場合を考えます。

  • sw.js は /app/sw.js に置く(/ 直下に置くと /app/ 以外も制御するため注意)。
  • 登録コードは navigator.serviceWorker.register('/app/sw.js', { scope: '/app/' })。
  • ビルド時に APP_SHELL の各パスを /app/... へ合わせる。

逆にサイト全体を制御したいなら、/sw.js と scope:'/' に統一します。スコープのミスマッチは「登録は成功するがキャッシュされない」典型要因です。

ネットワーク戦略の実践例

API は Network First、静的アセットは Stale-While-Revalidate など、用途に応じた戦略を混在させると体験が安定します。

// sw.js の一部
const RUNTIME = 'runtime-v1';

self.addEventListener('fetch', (e) => {
const url = new URL(e.request.url);

// API は Network First
if (url.pathname.startsWith('/api/')) {
e.respondWith((async () => {
try {
const res = await fetch(e.request);
const copy = res.clone();
const cache = await caches.open(RUNTIME);
cache.put(e.request, copy);
return res;
} catch {
return caches.match(e.request);
}
})());
return;
}

// CSS/JS/画像は Stale-While-Revalidate
if (/.(css|js|png|jpg|svg)$/.test(url.pathname)) {
e.respondWith((async () => {
const cache = await caches.open(RUNTIME);
const cached = await cache.match(e.request);
const network = fetch(e.request).then((res) => {
cache.put(e.request, res.clone());
return res;
}).catch(() => cached);
return cached || network;
})());
}
}); 

ログと監視

  • 登録時・インストール時・アクティベート時に console.log を入れておく。
  • 致命エラーは console.error とともに self.registration.showNotification などで気づける仕組みを作る。
  • 致命的な例外で SW が停止していないか、DevTools の Errors を確認。

まとめ

file:// では Microsoft Edge を含むモダンブラウザの仕様として Service Worker は登録できません。ローカルでも localhost の HTTP/HTTPS で配信し、可能なら HTTPS 前提で開発するのが最短で確実な解決策です。sw.js の配置と scope を整え、キャッシュのバージョニングとデバッグ手順(Unregister / Clear storage / Hard Reload)を習慣化すれば、Edge でも安定してオフライン機能とキャッシュ戦略を活用できます。


付録:一気通貫の最小プロジェクト例

以下をそのまま保存して npx http-server で配信すれば、Edge での登録〜キャッシュ〜オフラインフォールバックまで確認できます。

index.html

&lt;!doctype html&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta name="viewport" content="width=device-width,initial-scale=1"&gt;
&lt;title&gt;Edge SW Sample&lt;/title&gt;
&lt;link rel="stylesheet" href="/styles.css"&gt;
&lt;h1&gt;Service Worker Sample&lt;/h1&gt;
&lt;p&gt;このページはオフラインでも表示されます。&lt;/p&gt;
&lt;script&gt;
if ('serviceWorker' in navigator) {
  window.addEventListener('load', async () =&gt; {
    try {
      const reg = await navigator.serviceWorker.register('/sw.js', { scope: '/' });
      await reg.update(); // 更新を積極的に確認
      console.log('registered:', reg.scope);
    } catch (e) {
      console.error(e);
    }
  });
}
&lt;/script&gt;

styles.css

body { font-family: system-ui, sans-serif; margin: 2rem; }
h1 { font-size: 1.8rem; }

offline.html

&lt;!doctype html&gt;
&lt;meta charset="utf-8"&gt;
&lt;meta name="viewport" content="width=device-width,initial-scale=1"&gt;
&lt;title&gt;Offline&lt;/title&gt;
&lt;h1&gt;オフラインです&lt;/h1&gt;
&lt;p&gt;ネットワークに接続できません。再接続後にリロードしてください。&lt;/p&gt;

sw.js

const STATIC_CACHE = 'static-v1';
const RUNTIME_CACHE = 'runtime-v1';
const STATIC_ASSETS = ['/', '/index.html', '/styles.css', '/offline.html'];

self.addEventListener('install', (e) => {
e.waitUntil(caches.open(STATIC_CACHE).then((c) => c.addAll(STATIC_ASSETS)));
self.skipWaiting();
});

self.addEventListener('activate', (e) => {
e.waitUntil(caches.keys().then((keys) => Promise.all(
keys.filter((k) => ![STATIC_CACHE, RUNTIME_CACHE].includes(k)).map((k) => caches.delete(k))
)));
self.clients.claim();
});

self.addEventListener('fetch', (e) => {
const { request } = e;
// ナビゲーションは Network First → Offline fallback
if (request.mode === 'navigate') {
e.respondWith((async () => {
try {
const res = await fetch(request);
const copy = res.clone();
const cache = await caches.open(RUNTIME_CACHE);
cache.put(request, copy);
return res;
} catch {
return (await caches.match('/offline.html')) || Response.error();
}
})());
return;
}
// 静的アセットは Stale-While-Revalidate
e.respondWith((async () => {
const cache = await caches.open(RUNTIME_CACHE);
const cached = await cache.match(request);
const network = fetch(request).then((res) => { cache.put(request, res.clone()); return res; })
.catch(() => cached);
return cached || network;
})());
});

self.addEventListener('message', (e) => {
if (e.data?.type === 'SKIP_WAITING') self.skipWaiting();
}); 

最後に

Edge で Service Worker が登録できない時は「file:// で開いていないか」をまず疑い、localhost 配信 → DevTools でクリーンアップ → スコープの見直し → HTTPS 対応という順序で進めると短時間で解決できます。本記事の手順とスニペットをそのまま開発テンプレートとして活用してください。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次