The ScramjetServiceWorker is the core component that intercepts all network requests and proxies them through the configured transport. This page explains how it works.
Overview
Service Workers provide a programmable network proxy that runs in the browser. Scramjet leverages this to intercept fetch requests, decode URLs, fetch content, rewrite it, and return the modified response.
Service Workers run in a separate context from your web pages. They persist across page loads and can intercept requests from multiple clients (tabs/windows).
Class structure
The ScramjetServiceWorker class is defined in src/worker/index.ts:
export class ScramjetServiceWorker extends EventTarget {
client : BareClient ; // Transport for making proxied requests
config : ScramjetConfig ; // Current configuration
syncPool : Record < number , ( val ?: any ) => void > = {}; // Message queue
synctoken : number = 0 ; // Current sync token
cookieStore : CookieStore ; // Cookie emulation
serviceWorkers : FakeServiceWorker []; // Fake SW registrations
}
Initialization
When the Service Worker starts, it creates a ScramjetServiceWorker instance:
const scramjet = new ScramjetServiceWorker ();
self . addEventListener ( "fetch" , async ( event ) => {
if ( scramjet . route ( event )) {
event . respondWith ( scramjet . fetch ( event ));
}
});
The constructor initializes:
BareClient for transport
CookieStore for cookie emulation
Message listeners for client communication
IndexedDB to load saved cookies
Loading configuration
The Service Worker loads its configuration from IndexedDB:
await scramjet . loadConfig ();
This is called automatically by scramjet.fetch() if the config hasn’t been loaded yet. The config contains:
URL prefix for routing
Feature flags
Codec functions for URL encoding/decoding
File paths for injected scripts
Configuration can be dynamically updated at runtime via messages from the controller. Changes are persisted to IndexedDB.
Request routing
The route() method determines if a request should be handled by Scramjet:
route ({ request }: FetchEvent ): boolean {
if ( request . url . startsWith ( location . origin + this . config . prefix ))
return true ;
else if ( request . url . startsWith ( location . origin + this . config . files . wasm ))
return true ;
else return false ;
}
Requests are routed if they:
Start with the configured prefix (e.g., /scramjet/)
Request the WASM rewriter file
Request handling
The fetch() method orchestrates the entire proxy flow:
async fetch ({ request , clientId }: FetchEvent ): Promise < Response > {
if (!this.config) await this. loadConfig ();
const client = await self . clients . get ( clientId );
return handleFetch.call( this , request , client);
}
The actual logic is in handleFetch() from src/worker/fetch.ts.
Fetch flow
Parse query parameters
Extract metadata from the URL’s query string: const extraParams : Record < string , string > = {};
for ( const [ param , value ] of requestUrl . searchParams . entries ()) {
switch ( param ) {
case "type" : // Script type (module, classic)
scriptType = value ;
break ;
case "topFrame" : // Top frame name
topFrameName = value ;
break ;
case "parentFrame" : // Parent frame name
parentFrameName = value ;
break ;
default :
extraParams [ param ] = value ; // Form parameters
break ;
}
requestUrl . searchParams . delete ( param );
}
Decode URL
Convert the proxied URL back to the real destination: const url = new URL ( unrewriteUrl ( requestUrl ));
// Add back any extra params (e.g., from forms)
for ( const [ param , value ] of Object . entries ( extraParams )) {
url . searchParams . set ( param , value );
}
Build metadata context
Create the URLMeta object for rewriters: const meta : URLMeta = {
origin: url ,
base: url ,
topFrameName ,
parentFrameName ,
};
Handle special URLs
Blob and data URLs are handled directly: if ( requestUrl . pathname . startsWith ( ` ${ this . config . prefix } blob:` ) ||
requestUrl . pathname . startsWith ( ` ${ this . config . prefix } data:` )) {
let dataUrl = requestUrl . pathname . substring ( this . config . prefix . length );
if ( dataUrl . startsWith ( "blob:" )) {
dataUrl = unrewriteBlob ( dataUrl );
}
const response = await fetch ( dataUrl );
// Rewrite body and return
}
Process request headers
Rewrite headers for the real request: const headers = new ScramjetHeaders ();
for ( const [ key , value ] of request . headers . entries ()) {
headers . set ( key , value );
}
// Set correct Origin and Referer
if ( client && new URL ( client . url ). pathname . startsWith ( config . prefix )) {
const clientURL = new URL ( unrewriteUrl ( client . url ));
headers . set ( "Referer" , clientURL . href );
headers . set ( "Origin" , clientURL . origin );
}
// Add cookies from the cookie store
const cookies = this . cookieStore . getCookies ( url , false );
if ( cookies . length ) {
headers . set ( "Cookie" , cookies );
}
Dispatch request event
Allow external code to modify the request: const ev = new ScramjetRequestEvent (
url ,
headers . headers ,
request . body ,
request . method ,
request . destination ,
client
);
this . dispatchEvent ( ev );
Fetch via transport
Use BareClient to make the actual request: const response = await this . client . fetch ( ev . url , {
method: ev . method ,
body: ev . body ,
headers: ev . requestHeaders ,
credentials: "omit" ,
mode: request . mode === "cors" ? request . mode : "same-origin" ,
cache: request . cache ,
redirect: "manual" ,
duplex: "half" ,
}) as BareResponseFetch ;
Process response
Rewrite headers, handle cookies, and rewrite the body based on content type.
Response handling
The handleResponse() function processes the fetched response:
const responseHeaders = await rewriteHeaders (
response . rawHeaders ,
meta ,
bareClient ,
{ get: getReferrerPolicy , set: storeReferrerPolicy }
);
This:
Rewrites Location headers for redirects
Processes Set-Cookie headers
Removes Permissions-Policy headers that might block Scramjet features
Handles CORS headers
Cookie handling
const maybeHeaders = responseHeaders [ "set-cookie" ] || [];
// Send cookies to clients
for ( const cookie in maybeHeaders ) {
if ( client ) {
const promise = swtarget . dispatch ( client , {
scramjet$type: "cookie" ,
cookie ,
url: url . href ,
});
}
}
// Store in the Service Worker's cookie jar
await cookieStore . setCookies (
maybeHeaders instanceof Array ? maybeHeaders : [ maybeHeaders ],
url
);
Body rewriting
Based on the request destination, the response body is rewritten:
async function rewriteBody (
response : BareResponseFetch ,
meta : URLMeta ,
destination : RequestDestination ,
workertype : string ,
cookieStore : CookieStore
) : Promise < BodyType > {
switch ( destination ) {
case "iframe" :
case "document" :
if ( response . headers . get ( "content-type" )?. startsWith ( "text/html" )) {
return rewriteHtml ( await response . text (), cookieStore , meta , true );
}
return response . body ;
case "script" :
return rewriteJs (
new Uint8Array ( await response . arrayBuffer ()),
response . finalURL ,
meta ,
workertype === "module"
);
case "style" :
return rewriteCss ( await response . text (), meta );
case "sharedworker" :
case "worker" :
return rewriteWorkers (
new Uint8Array ( await response . arrayBuffer ()),
workertype ,
response . finalURL ,
meta
);
default :
return response . body ;
}
}
HTML rewriting injects Scramjet’s client scripts into the <head> and rewrites:
URL attributes (src, href, action, etc.)
Inline <script> and <style> tags
Event handler attributes (onclick, etc.)
Import maps
<base> tags
Meta refresh tags
CSP meta tags (removed)
JavaScript rewriting details
JavaScript is rewritten using an oxc-based WASM rewriter that:
Wraps function calls to intercept APIs
Rewrites property accesses
Injects runtime helpers
Generates source maps for debugging
Handles both classic scripts and ES modules
Download interception
When the interceptDownloads flag is enabled, Scramjet can intercept file downloads:
if ( isDownload ( responseHeaders , destination ) && ! isRedirect ( response )) {
if ( flagEnabled ( "interceptDownloads" , url )) {
const download : ScramjetDownload = {
filename ,
url: url . href ,
type: responseHeaders [ "content-type" ],
body: response . body ,
length: Number ( length ),
};
// Send to controller client
clis [ 0 ]. postMessage ({
scramjet$type: "download" ,
download ,
}, [ response . body ]);
// Prevent the download from completing
await new Promise (() => {});
}
}
Message passing
The Service Worker communicates with clients via postMessage:
Receiving messages
addEventListener ( "message" , async ({ data } : { data : MessageC2W }) => {
if ( ! ( "scramjet$type" in data )) return ;
if ( "scramjet$token" in data ) {
// Acknowledge message
const cb = this . syncPool [ data . scramjet$token ];
delete this . syncPool [ data . scramjet$token ];
cb ( data );
return ;
}
if ( data . scramjet$type === "registerServiceWorker" ) {
this . serviceWorkers . push ( new FakeServiceWorker ( data . port , data . origin ));
}
if ( data . scramjet$type === "cookie" ) {
this . cookieStore . setCookies ([ data . cookie ], new URL ( data . url ));
const db = await openDB < ScramjetDB >( "$scramjet" , 1 );
await db . put ( "cookies" , JSON . parse ( this . cookieStore . dump ()), "cookies" );
}
if ( data . scramjet$type === "loadConfig" ) {
this . config = data . config ;
}
});
Dispatching messages
The dispatch() method sends messages and waits for responses:
async dispatch ( client : Client , data : MessageW2C ): Promise < MessageC2W > {
const token = this . synctoken ++ ;
let cb : ( val : MessageC2W ) => void ;
const promise : Promise < MessageC2W > = new Promise (( r ) => ( cb = r ));
this . syncPool [ token ] = cb ;
data . scramjet$token = token ;
client . postMessage ( data );
return await promise ;
}
Cookie emulation
Scramjet emulates cookies using the CookieStore class because:
Service Workers cannot access document.cookie
Real cookies would leak the proxy origin
Cookie attributes (domain, path, etc.) need special handling
Cookies are:
Stored in the Service Worker’s CookieStore instance
Persisted to IndexedDB
Synced to clients via postMessage
Added to outgoing requests via the Cookie header
Events
The Service Worker dispatches custom events:
ScramjetRequestEvent
Fired before making a request, allowing modification:
scramjet . addEventListener ( "request" , ( event ) => {
console . log ( "Requesting:" , event . url . href );
// Modify event.requestHeaders, event.body, etc.
});
ScramjetHandleResponseEvent
Fired after receiving a response:
scramjet . addEventListener ( "handleResponse" , ( event ) => {
console . log ( "Received:" , event . url . href , event . status );
// Modify event.responseHeaders, event.responseBody, etc.
});
Best practices
Never modify the Service Worker config directly. Always use the controller’s modifyConfig() method to ensure changes are persisted.
Use the request event to add custom headers or authentication to proxied requests.
Next steps
URL rewriting Learn how URLs are encoded, decoded, and rewritten
Configuration Explore Service Worker configuration options