# ERR_LOCAL_NETWORK_PERMISSION_MISSING 排查

> 区分 Chrome 中的本地网络权限拒绝、地址空间不匹配、CORS、混合内容和设备不可达等失败。

公共页面请求本地或 loopback 目标时，如果 Chrome 无法通过权限和地址空间检查，就会产生 Local Network Access 错误。页面 JavaScript 通常只收到被拒绝的 `fetch()` Promise，DevTools 才可能显示更具体的 Chromium 网络错误。

Chrome 142 上线 Local Network Access 权限提示。Chrome 公告说明首个版本覆盖 `fetch()`、子资源加载和子框架导航；规范定义安全上下文、权限、Permissions Policy 和目标地址空间检查。BCD 对 `Request.targetAddressSpace` 的支持记录包括 Chrome 142 及镜像的 Chromium 派生浏览器；完整支持范围见[兼容性数据](/zh/compatibility/local-network-access/)。BCD 未记录 Firefox 或 Safari 支持。

## 工作原理

在 Chrome 首个里程碑中，从公共地址空间发往 `local` 或 `loopback` 目标的请求属于本地网络请求。规范要求客户端处于安全上下文，存在关联文档，Permissions Policy 允许访问，并且页面取得 `local-network` 或 `loopback-network` 授权。如果权限状态为 `prompt`，用户代理会询问用户，只有同意后才继续。

Fetch API 不会把 Chromium 内部网络错误码作为结构化 JavaScript 值暴露出来。因此，`fetch()` 被拒绝只是诊断起点，不能证明用户拒绝了权限。还要查看 DevTools、请求 URL、页面安全上下文、响应的 CORS 标头，以及目标设备是否响应。

| 信号 | 来源给出的含义 | 下一项检查 |
|---|---|---|
| `net::ERR_LOCAL_NETWORK_PERMISSION_MISSING` | Chromium 因缺少本地网络权限而阻止请求 | 请用户检查该站点的本地网络权限 |
| `net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` | Chromium 在 Local Network Access 检查中阻止连接，并把这个错误码留给 CORS 错误路径 | 比较声明与解析后的地址空间，并检查策略 |
| `fetch()` 在 JavaScript 中抛出 `TypeError` | Fetch 以网络错误结束 | 配合 DevTools 和服务端日志，区分权限、CORS、混合内容、DNS、拒绝连接和超时 |
| 没有出现权限提示 | 请求可能在提示前失败，或文档没有提示资格 | 检查安全上下文、关联文档和 Permissions Policy |

`targetAddressSpace: "local"` 或 `"loopback"` 用来声明目标。规范允许这项声明绕过对应的混合内容升级步骤，连接后再比较解析地址与声明；两者不匹配时返回网络错误。它既不会授予权限，也不会替设备补上 CORS 响应标头，只是一项声明和约束。

worker 需要额外处理。当前规范在请求没有关联 `Document` 时返回网络错误，旁注明确指出 Service Worker 会受影响。Chromium issue 404887282 跟踪 Service Worker 的 Local Network Access 行为。把网络任务移入 worker 前，应先从文档流程取得授权，并保留文档侧回退。

| 失败边界 | 应调整的内容 |
|---|---|
| 权限被拒绝或缺失 | 说明连接用途，让用户修改站点权限后重试 |
| Permissions Policy 阻止 iframe | 向目标 frame 委派 `local-network`、`loopback-network`，或 Chromium 的合并别名 |
| 地址空间声明与 DNS 结果不符 | 修正 `targetAddressSpace` 或主机名；不要为了访问公共目标而放宽声明 |
| 设备没有允许 CORS | 配置设备 HTTP 响应，允许发起请求的源 |
| 设备离线或拒绝连接 | 提供设备设置和连接检查 |
| 浏览器没有 `targetAddressSpace` | 使用 HTTPS 端点或浏览器支持的设备能力，不要假定 Chrome 路径存在 |

:::caution
Chromium 的 [`net_error_list.h`](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h)把 `ERR_LOCAL_NETWORK_PERMISSION_MISSING` 定义为 `-36`，含义是请求因缺少本地网络权限而被阻止。同一源码把 `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` 区分为 CORS 错误所用的代码。JavaScript 不应解析控制台文本；这些字符串只适合在 DevTools 中诊断。
:::

## 示例

下面两个示例都从安全文档开始。它们返回应用状态，不会声称一个 JavaScript `TypeError` 就能识别具体网络原因。

### 声明本地目标并保留拒绝结果

特性检测让不支持的路径不使用 Chrome 专有请求成员。catch 分支记录错误名称，并告诉界面仍需进一步诊断。

```html
<label>
  设备状态 URL
  <input id="device-url" type="url" value="http://192.168.1.1/status">
</label>
<button id="read-device" type="button">读取状态</button>
<output id="read-result" aria-live="polite"></output>

<script>
async function readDeviceStatus(url = 'http://192.168.1.1/status') {
  if (!window.isSecureContext) {
    return { ok: false, reason: 'secure-context-required' };
  }
  if (!('targetAddressSpace' in Request.prototype)) {
    return { ok: false, reason: 'address-space-option-unsupported' };
  }
  try {
    const response = await fetch(url, {
      targetAddressSpace: 'local',
    });
    if (!response.ok) return { ok: false, reason: `http-${response.status}` };
    return { ok: true, data: await response.json() };
  } catch (error) {
    return { ok: false, reason: 'network-error', errorName: error.name };
  }
}

const readButton = document.querySelector('#read-device');
const deviceUrl = document.querySelector('#device-url');
const readResult = document.querySelector('#read-result');

if (readButton && deviceUrl && readResult) {
  readButton.addEventListener('click', async () => {
    readButton.disabled = true;
    const result = await readDeviceStatus(deviceUrl.value);
    readResult.textContent = JSON.stringify(result);
    readButton.disabled = false;
  });
}
</script>
```

界面现在可以把配置问题和 HTTP 响应分开。若支持人员需要区分权限与地址空间检查，仍需从 DevTools 或受管遥测中取得 Chromium 网络错误码。

### worker 失败后从文档重试

按当前规范旁注，Service Worker 没有可靠的提示界面。下面的消息处理器把控制权交还文档，用户点击后由文档发出请求，Chrome 因而有机会显示权限 UI。

```html
<button id="retry-device" type="button" hidden>重试设备连接</button>
<output id="device-result" aria-live="polite"></output>

<script>
const retryButton = document.querySelector('#retry-device');
const deviceResult = document.querySelector('#device-result');

function renderDeviceResult(result) {
  if (!deviceResult) return;
  deviceResult.textContent = result.ok
    ? JSON.stringify(result.data)
    : `设备请求已停止：${result.reason}`;
}

if (retryButton) {
  retryButton.addEventListener('click', async () => {
    if (retryButton.disabled) return;
    retryButton.disabled = true;
    const result = await readDeviceStatus();
    renderDeviceResult(result);
    retryButton.hidden = result.ok;
    retryButton.disabled = false;
  });
}

navigator.serviceWorker.addEventListener('message', (event) => {
  if (event.data?.type !== 'local-network-retry-required') return;
  if (!retryButton) return;
  retryButton.hidden = false;
});

async function requestFromWorker() {
  const controller = navigator.serviceWorker.controller;
  if (!controller) return readDeviceStatus();
  controller.postMessage({ type: 'read-local-device' });
  return { ok: false, reason: 'delegated-to-worker' };
}

requestFromWorker().then(renderDeviceResult);
</script>
```

worker 的请求被拒绝后，应发送 `local-network-retry-required`。文档不能保证重试成功；CORS、地址不匹配或设备不可达仍会产生网络错误。[Web Bluetooth 兼容性数据](/zh/compatibility/web-bluetooth/)展示了另一条需要权限的设备连接路径，适用于提供蓝牙而非 HTTP 服务的设备。

## 另请参阅

- [Local Network Access](https://wicg.github.io/local-network-access/)（wicg.github.io）
- [Local Network Access 新权限提示](https://developer.chrome.com/blog/local-network-access)（developer.chrome.com）
- [Local Network Access](/zh/reference/capabilities/local-network-access/)
- [Permissions API](/zh/reference/capabilities/permissions-api/)
- [Web Bluetooth API](/zh/reference/capabilities/web-bluetooth/)