# Local Network Access

> Chrome 142 asks permission before a public page may request a local-network or loopback address. Address spaces, permissions, targetAddressSpace, prompt text.

Local Network Access is the permission a browser asks for before a page served from the public internet may send a request to a device on the user's local network (a router, a printer, a dev server) or to the machine itself. The page calls no API to request it: its first `fetch()`, subresource load, or iframe navigation to such an address triggers the prompt, and a denied prompt fails the request. The one script-visible surface is the `targetAddressSpace` option on `fetch()` and `Request`, which declares where a request is going so that the browser can exempt it from mixed-content blocking.

Chrome 142 shows the prompt by default; from Chrome 138 it was opt-in through `chrome://flags/#local-network-access-check` set to "Enabled (Blocking)". `Request.targetAddressSpace` exists from Chrome 142 and in no version of Firefox, Safari, or Android WebView (BCD `api.Request.targetAddressSpace`). The design replaced Private Network Access, which gated the same requests on a CORS preflight that the local device had to answer and which Chrome put on hold.

## How it works

Every IP address belongs to one of three address spaces defined by the specification: `loopback` (127.0.0.0/8, ::1/128, 0.0.0.0/32, ::/128, 198.18.0.0/15), `local` (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 100.64.0.0/10 carrier-grade NAT, 169.254.0.0/16 link-local, fc00::/7, fe80::/10, fec0::/10, 0.0.0.0/8, and the IPv6 documentation prefixes 2001:db8::/32 and 3fff::/20), and `public` for everything else. An IPv4-mapped IPv6 address (::ffff:0:0/96) takes the space of its embedded IPv4 address. A local network request is one whose target is in a less public space than the requesting document: public to local, public to loopback, and local to loopback.

Two policy-controlled features gate those requests, each with a default allowlist of `'self'`: `local-network` for requests to a local address and `loopback-network` for requests to a loopback address. The older single name `local-network-access` remains in Chromium as an alias for both, including in `Permissions-Policy` headers and iframe `allow` attributes. A cross-origin iframe that needs the capability therefore has to be embedded with `allow="local-network"` (or `allow="local-network-access"`); without delegation its requests fail without a prompt. Only secure contexts can hold the permission, so a page on `http://` is blocked outright.

Because most local devices still serve plain HTTP, the specification exempts a request from mixed-content checks when the browser can tell before DNS resolution that it is a local network request: the hostname is a private IP literal (`http://192.168.0.1/`), the hostname ends in `.local`, or the call carries `targetAddressSpace: "local"` or `"loopback"`. The declaration is checked after the connection is made; if the resolved address is not in the declared space, the request fails rather than silently becoming a public request. The exemption applies to the connection, not to the response: a public hostname that happens to resolve to 192.168.0.1 is still mixed content unless annotated.

Chrome applies the check to `fetch()`, subresource loads (`<img>`, `<script>`, CSS), and iframe navigations, and to service and shared workers only once a document on the same origin has already been granted the permission (the worker cannot prompt; [crbug.com/404887282](https://crbug.com/404887282) tracks a fix). At the Chrome 138 opt-in stage WebSocket, WebTransport, and WebRTC connections were not yet gated ([crbug.com/421156866](https://crbug.com/421156866), [crbug.com/421216834](https://crbug.com/421216834), [crbug.com/421223919](https://crbug.com/421223919)). Chrome's first milestone gates only requests that start from a public address; a local origin such as `https://router.local` calling another local device, or calling loopback, is not yet prompted for, which the Chrome blog lists as a planned extension.

:::observed
In Chrome with the check active, a `fetch("http://192.168.1.1/status")` from an `https://` page shows a permission bubble reading `<origin> wants to access other devices on your local network`; a request to `http://127.0.0.1:8080/` instead reads `<origin> wants to access other apps and services on this device` (English UI). The site-settings fragments for the same two permissions are `Access other devices on your local network` and `Access other apps and services on this device`. A request refused because the permission is missing fails in the DevTools Network panel with `net::ERR_LOCAL_NETWORK_PERMISSION_MISSING` (-36); one blocked by the address-space check fails with `net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` (-385). Sources: [`permissions_strings.grdp`](https://chromium.googlesource.com/chromium/src/+/main/components/permissions_strings.grdp) (chromium.googlesource.com) and [`net_error_list.h`](https://chromium.googlesource.com/chromium/src/+/main/net/base/net_error_list.h) (chromium.googlesource.com).
:::

## Examples

Both examples run in an `https://` page. Because the prompt is tied to the request itself, each one treats a rejected `fetch()` as the expected outcome and shows the user a manual path instead of retrying.

### Reading a router status page from an installed PWA

`'targetAddressSpace' in Request.prototype` is the only runtime signal that the browser's fetch stack knows about address spaces; where it is absent (Firefox, Safari, Chrome before 142) the request is plain mixed content from an HTTPS page and the browser blocks it before any network activity. The fallback below opens the device's own page in a new tab, where no cross-origin request is involved.

```js
const ROUTER = 'http://192.168.1.1/status.json';

function browserGatesLocalNetwork() {
  return typeof Request !== 'undefined' && 'targetAddressSpace' in Request.prototype;
}

async function readRouterStatus() {
  if (!window.isSecureContext || !browserGatesLocalNetwork()) {
    return { ok: false, reason: 'unsupported' };
  }
  try {
    const res = await fetch(ROUTER); // IP literal: mixed content exempt, prompt shown once
    if (!res.ok) return { ok: false, reason: `http-${res.status}` };
    return { ok: true, status: await res.json() };
  } catch {
    return { ok: false, reason: 'blocked-or-offline' }; // denied prompt, CORS, or device down
  }
}

document.querySelector('#check-router').addEventListener('click', async () => {
  const result = await readRouterStatus();
  if (!result.ok) {
    window.open('http://192.168.1.1/', '_blank', 'noopener');
    return;
  }
  renderStatus(result.status);
});
```

A rejected promise does not say why: a denied prompt, a CORS failure on the device, and an unplugged router all surface as a `TypeError` from `fetch()`. Logging the reason is fine; branching on it is not possible from script.

### Declaring the address space for a hostname that resolves locally

A public hostname such as `devbox.example.com` that resolves to 10.0.0.5 is not exempt from mixed content on its own. Annotating the call with `targetAddressSpace: "local"` makes the browser allow the `http://` connection and then verify that the resolved address really is local. In browsers that lack the option the unknown `RequestInit` member is ignored, so the same call falls back to being blocked as mixed content; the code therefore offers an `https://` alternative when the option is missing.

```js
async function callDevBox(path) {
  const supportsOption = 'targetAddressSpace' in Request.prototype;
  const url = supportsOption
    ? `http://devbox.example.com${path}`
    : `https://devbox.example.com${path}`; // needs a valid certificate on the device

  const init = supportsOption ? { targetAddressSpace: 'local' } : {};
  const res = await fetch(url, init);
  return res.json();
}

callDevBox('/api/health').catch((err) => {
  document.querySelector('#devbox-note').textContent =
    `Could not reach the dev box (${err.name}). Check that you are on the office network.`;
});
```

If `devbox.example.com` later moves to a public IP, the annotated request fails with `ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS` rather than leaking the request to the internet, which is the point of the declaration.

## See also

- [WebRTC: peer-to-peer audio, video, and data](/reference/capabilities/webrtc/), one of the connection types not yet covered by the permission
- [WebTransport: HTTP/3 streams and datagrams](/reference/capabilities/webtransport/)
- [Web Bluetooth API](/reference/capabilities/web-bluetooth/), the alternative route to nearby devices that does not go through IP at all
- [Local Network Access: Permissions](https://wicg.github.io/local-network-access/#permission-prompt) (wicg.github.io)
- [New permission prompt for Local Network Access](https://developer.chrome.com/blog/local-network-access) (developer.chrome.com)
- [Request: targetAddressSpace property](https://developer.mozilla.org/en-US/docs/Web/API/Request/targetAddressSpace) (developer.mozilla.org)
- [Local Network Access explainer](https://github.com/WICG/local-network-access/blob/main/explainer.md) (github.com)