WebSocket 連本機服務怎麼過 mixed content?用 targetAddressSpace
從 HTTPS 網站連到開發機上的 WebSocket 服務時,常見的錯誤不是 WebSocket server 沒啟動,而是瀏覽器把 ws:// 視為 mixed content。Chrome 154 beta 的 release notes 提供了一個新的 constructor options:用 targetAddressSpace 明確表示目標是 local 或 loopback address space。
直接答案是:在支援的 Chrome 版本中,從 secure context 建立 WebSocket 時傳入 targetAddressSpace: "local";但仍要讓 hostname 解析到本機網路、取得 Local Network Access permission,並保留 wss:// 或代理回退。這個選項不是 DNS 修改器,也不是繞過權限的開關。
本文依 Chrome 154 beta 官方資料與 Local Network Access draft 整理,沒有在本機服務上執行連線。這項 API 仍不是跨瀏覽器穩定基線,正式產品應把瀏覽器版本、權限結果和 fallback 都列入測試。
為什麼 HTTPS 頁面連不到 ws://?
假設你的網站是:
https://app.example.com而本機開發服務是:
ws://local-server.example:8765一般情況下,HTTPS 頁面建立明碼 ws:// 連線,可能先被 mixed content 規則擋下。若 local-server.example 解析到 local address,瀏覽器還要處理 Local Network Access 的權限與 address-space 判定;只改成 http://、把 port 換掉或在 server 加 CORS header,都不能自動解決這兩層限制。
Local Network Access 把目標分成 public、local 和 loopback。公開網站要連到本機網路或本機程序,應由使用者授權;這個設計是為了降低網站探測路由器、印表機或本機服務的風險。
最小的 targetAddressSpace 用法
Chrome 154 beta 的範例是把 options dictionary 放在 WebSocket constructor 的第二個參數:
const socket = new WebSocket("ws://local-server.example:8765", { targetAddressSpace: "local",});
socket.addEventListener("open", () => { socket.send(JSON.stringify({ type: "ping" }));});
socket.addEventListener("message", (event) => { console.log("local service:", event.data);});
socket.addEventListener("error", () => { console.error("WebSocket 連線失敗");});
socket.addEventListener("close", (event) => { console.log("WebSocket closed:", event.code, event.reason);});這個選項表達的是「我預期這個 hostname 最後會連到 local address」。它不會把公開 IP 變成本機 IP,也不會把名稱解析到錯誤的網段時硬送封包。如果實際解析結果不符合指定的 address space,連線應該失敗。
四個條件缺一不可
| 條件 | 要確認什麼 | 沒滿足時的結果 |
|---|---|---|
| Secure context | 頁面從 HTTPS 或其他可信任來源載入 | 瀏覽器可能先以 mixed content/secure context 拒絕 |
| 正確的 hostname | hostname 解析到你指定的 local address | 與 targetAddressSpace 不一致時連線失敗 |
| Local Network Access permission | 使用者允許該網站存取 local network;loopback 另有對應的 permission 邊界 | open 不會發生,通常只看見 error/close |
| 瀏覽器實作 | Chrome 154 beta 或含此功能的後續版本 | 舊瀏覽器可能不接受 options object,或仍被原本的 mixed content 規則擋下 |
如果這段程式放在 iframe,還要檢查 iframe 的 Permissions Policy 是否允許相關 local-network 或 loopback-network capability。不要把「主頁面是 HTTPS」理解成所有嵌入文件自動有相同權限。
local 和 loopback 不要混用
local 指的是同一個網路內可達、但不同網路可能指向不同裝置的 address,例如家用或公司網路中的私有服務。loopback 指的是目前這台裝置本身的服務,例如 127.0.0.1。
先依 server 綁定的位置選擇目標:
// 同一個區域網路上的開發盒或印表機服務const localSocket = new WebSocket("ws://devbox.example:8765", { targetAddressSpace: "local",});
// 目前這台電腦的 localhost 服務const loopbackSocket = new WebSocket("ws://localhost:8765", { targetAddressSpace: "loopback",});這不是把兩種值都試一遍就好。若 hostname 實際解析到 local 而不是 loopback,使用錯誤的值會讓 address-space 檢查失敗;反過來也一樣。對 localhost、127.0.0.1 或 .local 名稱,仍要依瀏覽器當下的 Local Network Access 實作和權限提示驗證,不要只靠字面判斷。
把連線失敗分成三層
WebSocket 的 error 事件通常不會告訴你完整的政策原因,因此應在自己的啟動流程記錄可安全公開的診斷資訊:
- 建立階段:constructor 是否因第二個參數或不支援的 options 直接丟出例外。
- 網路階段:hostname、port、DNS 解析和本機 server 是否真的可達。
- 政策階段:頁面是否為 secure context、使用者是否拒絕 Local Network Access、iframe 是否受 Permissions Policy 限制。
可以用一個小 wrapper 把第一層和 open/close 狀態留下來:
function connectLocalSocket(url, addressSpace) { let socket;
try { socket = new WebSocket(url, { targetAddressSpace: addressSpace }); } catch (error) { return { socket: null, reason: "constructor-unsupported", error, }; }
socket.addEventListener("open", () => { console.info("local WebSocket opened", { url, addressSpace }); });
socket.addEventListener("close", (event) => { console.warn("local WebSocket closed", { code: event.code, reason: event.reason, }); });
return { socket, reason: "connecting" };}constructor 沒有丟例外,也不代表功能已被支援;options dictionary 可能被忽略,真正的支援結果要以 open 和實際權限流程確認。不要用 User-Agent 字串取代能力與連線測試。
正式產品仍應優先使用 wss://
targetAddressSpace 的主要價值是處理「公開 HTTPS 網站需要連到沒有 HTTPS 的本機服務」這個過渡情境。它不會替本機服務提供 TLS,也不會讓未授權的網站直接取得內網存取權。
長期方案通常是:
- 讓本機服務提供可被裝置信任的 TLS,前端使用
wss://。 - 在本機安裝受控的 secure reverse proxy,把瀏覽器連線轉到內部明碼 WebSocket。
- 只在開發或受控桌面應用情境使用
ws://加targetAddressSpace,並把 permission、hostname 和 port 固定在文件與測試矩陣中。 - 提供清楚的錯誤訊息,告訴使用者需要允許哪個網站存取本機網路,而不是提示「請關閉瀏覽器安全性」。
如果你的產品只支援少數 Chrome 版本,仍應保留 wss:// fallback 或直接顯示不支援;不要把 beta channel 的 API 當成所有瀏覽器都能執行的基線。
上線前的測試矩陣
至少測試以下組合:
| 測試 | 應觀察的結果 |
|---|---|
| HTTPS 頁面 + hostname 解析到 local + permission 允許 | WebSocket 進入 open,可完成最小 ping/pong |
| HTTPS 頁面 + permission 拒絕 | 不應把錯誤誤顯示成 server 一定離線 |
hostname 解析到 public,但指定 local | 應失敗,不能讓 targetAddressSpace 變成任意 mixed-content bypass |
| 舊版或不支援 options 的瀏覽器 | constructor/連線失敗後進入 wss:// 或可理解的 fallback |
| iframe 或企業政策環境 | 額外驗證 Permissions Policy、Local Network Access policy 和使用者提示 |
若你還要處理連線逾時與頁面卸載時的清理,可搭配 fetch 逾時與 AbortSignal 的取消方式 的錯誤分類概念;WebSocket 本身則要另外處理 close、重連與 server 端冪等。
參考資料:
回報錯字、失效連結,或告訴我你想看的延伸主題。