---
title: Network blocker
description: Hold browser fetch and XMLHttpRequest calls to tracking domains in
  a SvelteKit app until their consent category is allowed, with the
  networkBlocker option.
group: frameworks
lastModified: "2026-10-10T16:01:45+01:00"
---
## When to use the network blocker

The network blocker stops `fetch` and `XMLHttpRequest` calls to domains you
list until their consent category is allowed. Use it as a backstop for
tracking calls from code that is already on the page, such as your own
analytics wrapper or an SDK you import. It is off until you configure it.

Load vendor SDKs through the provider's `scripts` prop first; see
[scripts](./scripts). A script that never loads sends nothing, which is
stronger than blocking its requests one by one.

## Configure the rules

Write the configuration in its own module:

```ts title="src/lib/network-blocker.ts"
import type { UseNetworkBlockerOptions } from '@c15t/svelte';

// Pass as `networkBlocker={networkBlocker}` on ConsentProvider.
export const networkBlocker: UseNetworkBlockerOptions = {
	onRequestBlocked: ({ method, url }) => {
		console.info('Blocked until consent', method, url);
	},
	rules: [
		{
			category: 'measurement',
			domain: 'google-analytics.com',
			id: 'google-analytics',
		},
		{
			category: 'marketing',
			domain: 'connect.facebook.net',
			id: 'meta-pixel',
			pathIncludes: '/signals',
		},
	],
};
```

Pass it to the provider:

```svelte
<!-- Svelte: src/App.svelte -->
<ConsentProvider mode={manifest()} {scripts} {networkBlocker}>

<!-- SvelteKit: src/routes/+layout.svelte -->
<ConsentRoot state={data.consent} {scripts} {networkBlocker}>
```

The provider reads `networkBlocker` once, when it is created. Remount the
provider to change the rules.

## Match requests with rules

Each rule names a `domain` and the consent `category` a request needs. The
domain also matches its subdomains: `google-analytics.com` covers
`www.google-analytics.com`. `pathIncludes` narrows the rule to paths that
contain a substring, and `methods` narrows it to HTTP methods. A request is
blocked when a matching rule's condition is not met by the visitor's
effective permissions.

`category` takes the same conditions as scripts:

```ts
{ category: 'measurement' }
{ category: { and: ['measurement', 'marketing'] } }
{ category: { or: ['measurement', 'marketing'] } }
```

Add `vendor` to also block the request while the visitor has turned that
vendor off. IAB TCF rules use `vendorId` and the `iab*` purpose fields
instead.

|Option|Default|Purpose|
|--|--|--|
|`rules`|required|Rules described above|
|`enabled`|`true`|Set `false` to keep the rules but stop blocking|
|`logBlockedRequests`|`true`|Log each blocked request with `console.warn`|
|`onRequestBlocked`|none|Called with `{ method, url, rule }` for each blocked request|

## What a blocked request looks like

A blocked `fetch` resolves to a response with status `451` and the status
text `Request blocked by consent`, and nothing is sent. A blocked
`XMLHttpRequest` is aborted and fires an `error` event. Requests that match no
rule are not delayed. With `logBlockedRequests` on, the default, each blocked
request is logged with `console.warn`; `onRequestBlocked` receives
`{ method, url, rule }`.

## When blocking starts

The provider starts holding matching requests when it is created in the
browser, before its children run their own code. The blocker module loads
when the provider mounts and then decides each held request:

* While the policy is still loading, a matching request waits instead of
  failing. When the policy arrives, with the visitor's stored choice applied,
  the request is sent if its category is allowed and blocked otherwise.
* If the policy fails to load, optional categories stay denied and the
  waiting requests are blocked.
* A synchronous XHR cannot wait. Before the blocker module loads, a matching
  one throws a `NetworkError` from `send()`.

When the visitor allows a category later, new requests to its domains go
through. Requests blocked earlier are not replayed.

## What it cannot stop

The blocker sees only `fetch` and `XMLHttpRequest` calls made after the
provider is created in the browser. It cannot stop:

* Scripts that run before the provider, such as tags in `index.html` or
  `app.html` and code at the top level of modules that load first.
* Code that kept its own reference to `fetch` or `XMLHttpRequest` from before
  the provider was created.
* `navigator.sendBeacon`, `WebSocket`, `EventSource`, and requests made by
  `<img>`, `<script>` and `<iframe>` elements, web workers and service
  workers.
* Requests your server makes, such as from a SvelteKit `load` or endpoint.

Keep tracking calls out of that window. Send them from event handlers or
effects, not at a module's top level, and check
`getConsentManager().has('measurement')` before you call a vendor from your
own code.

## Verify the blocker

Open DevTools, clear site data for your origin and reload:

1. Before you choose, requests matching a rule do not reach the network, and
   the console logs each blocked request.
2. Allow the rule's category. New matching requests appear in the Network
   panel.
3. Reject, reload and confirm they stay blocked.

`fetch('https://www.google-analytics.com/g/collect')` in the console returns a
response with status `451` while measurement is denied.

## Preload the blocker

The blocker shares a separate chunk with the script loader. The chunk loads
only on pages with `networkBlocker` rules or `scripts`, and matching requests
wait until it has loaded. `consentManifest()` lets `c15tHandle` link the
chunk from the page's `<head>`, so the browser fetches it with the app's own
code.
[Preload the script loader](./scripts#preload-the-script-loader) shows the
setup.

## Server requests are not blocked

The network blocker runs in the browser. A `fetch` in a SvelteKit `load`,
`+server.ts` or `hooks.server.ts` is never blocked, even when the visitor has
denied the category. If server code sends data to a vendor, check the
visitor's stored choice yourself before it does; `event.locals.c15t.config`
from [`c15tHandle`](./server-api#c15thandle) holds it.
