Skip to main content

FetchInterceptor

FetchInterceptor replaces window.fetch with a custom implementation that applies a middleware pipeline to outgoing requests. It also monkey-patches navigator.sendBeacon to funnel eligible beacon payloads into Sentinel's collection flow. It is the integration point between Sentinel's signal collection, sentinel context injection, and CryptoKit encryption.

Responsibilities

ResponsibilityDetail
Intercept outgoing fetchesMonkey-patches window.fetch to route POST requests through the middleware chain
Intercept outgoing beaconsMonkey-patches navigator.sendBeacon and reads Blob payloads to notify Sentinel collectors
Inject sentinel contextAttaches { token, signals } to the request body for protected routes
Encrypt/decryptApplies CryptoKit middleware for AES-GCM encryption on eligible requests
Feed collectorsNotifies FetchCollector and TrackingCollector of outgoing calls

Interception Criteria

Not all fetch calls are intercepted. The middleware pipeline only runs when all of these are true:

  1. HTTP method is POST
  2. Content-Type header is application/json
  3. x-domain header is present

Other requests pass through to the original fetch unmodified.

Beacon Interception

navigator.sendBeacon is patched to observe Blob payloads only. When the payload is a Blob, the interceptor calls await blob.text() to obtain the JSON string, then routes it through onFetch with a synthetic RequestInit:

  • method: 'POST'
  • body: <payload string>
  • headers: { 'Content-Type': 'application/json', 'x-domain': <from URL query> }

If the beacon URL does not include x-domain, or the payload is not a Blob, the interceptor leaves it untouched (beyond calling the original sendBeacon).

Middleware Pipeline

The interceptor maintains an ordered array of middlewares:

Request → SentinelContextMiddleware → CryptoKitMiddleware → fetch()
Response ← CryptoKitMiddleware ← SentinelContextMiddleware ←

Each middleware implements FetchInterceptorMiddleware:

interface FetchInterceptorMiddleware {
onRequest?: (
context: RequestContext
) => Promise<RequestContext> | RequestContext;
onResponse?: (
context: ResponseContext
) => Promise<ResponseContext> | ResponseContext;
}

Request middlewares are applied in order. Response middlewares are applied in reverse order (stack unwinding).

Request/Response Context

interface RequestContext {
input: RequestInfo | URL;
init?: RequestInit;
url: string;
state: {
isSentinelResource: boolean;
shouldProtectResource: boolean;
[key: string]: unknown; // middlewares can attach extra state
};
}

interface ResponseContext extends RequestContext {
response: Response;
}

State Flags

FlagMeaning
isSentinelResourceURL starts with /api/sen/ or /api/dfp/
shouldProtectResourceConfigManager.shouldProtectResource(url) returns true
requestSecret(set by CryptoKit middleware) — encryption key for the response decryption

Sentinel Context Middleware

This built-in middleware injects the sentinel payload into the request body for protected non-sentinel resources:

{
// original request body …
"sentinel": {
"token": "abc123",
"signals": [
/* recent context signals */
]
}
}

Conditions to inject:

  • URL is not a Sentinel internal resource (/api/sen/…, /api/dfp/…)
  • URL is a protected resource (per ConfigManager path matching)
  • A valid token exists

Collector Notification

Before middleware runs, the interceptor notifies the Sentinel orchestrator via onFetch(input, init) for non-sentinel resources. This feeds:

  • FetchCollector.onFetchCall(input) — captures host + pathname
  • TrackingCollector.onFetchCall(input, init) — extracts tracking event names if the URL matches the tracking API path

Header Normalisation

All headers are normalised to lowercase keys before processing, regardless of whether they arrive as a Headers object, array of tuples, or plain object.

SSR Safety

initialize() is a no-op when typeof window === 'undefined'window.fetch is not patched during server-side rendering.

Options

interface FetchInterceptorOptions {
getSentinelContext: () => { token: string; signals: Array<Signal> };
onFetch: (input: RequestInfo | URL, init?: RequestInit) => void;
shouldProtectResource: (url: string) => boolean;
}

Last updated: 2026-02-25