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
| Responsibility | Detail |
|---|---|
| Intercept outgoing fetches | Monkey-patches window.fetch to route POST requests through the middleware chain |
| Intercept outgoing beacons | Monkey-patches navigator.sendBeacon and reads Blob payloads to notify Sentinel collectors |
| Inject sentinel context | Attaches { token, signals } to the request body for protected routes |
| Encrypt/decrypt | Applies CryptoKit middleware for AES-GCM encryption on eligible requests |
| Feed collectors | Notifies 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:
- HTTP method is
POST Content-Typeheader isapplication/jsonx-domainheader 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
| Flag | Meaning |
|---|---|
isSentinelResource | URL starts with /api/sen/ or /api/dfp/ |
shouldProtectResource | ConfigManager.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
ConfigManagerpath 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 + pathnameTrackingCollector.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