Debugging a web proxy like Scramjet requires understanding both the proxy internals and how browsers execute proxied code. This guide covers debugging techniques, logging utilities, and common pitfalls.
Logging system
Scramjet includes a custom logging utility in src/log.ts that provides formatted console output with stack traces.
Basic usage
import dbg from "@/log" ;
// Standard log levels
dbg . log ( "Initialization complete" );
dbg . warn ( "Deprecated API used" );
dbg . error ( "Failed to fetch resource" , error );
dbg . debug ( "Variable state:" , { foo: "bar" });
The logger automatically formats messages with:
Function name from call stack
Severity-based styling (colors, padding)
Caller context for debugging
// Output example:
// [rewriteJs] Rewriting script: https://example.com/app.js
// ↑ function name ↑ message
Track execution time for operations:
import dbg from "@/log" ;
const before = performance . now ();
const result = rewriteJs ( code , url , meta , false );
dbg . time ( meta , before , "JavaScript rewrite" );
// Output: [time] JavaScript rewrite was decent speed (23.45ms)
The time() method categorizes performance:
< 1ms : “BLAZINGLY FAST”
< 500ms : “decent speed”
≥ 500ms : “really slow”
Performance timing is only active when the rewriterLogs flag is enabled.
Feature flags
Scramjet uses feature flags to control debugging and experimental features.
Available flags
interface ScramjetFlags {
// Debugging
rewriterLogs : boolean ; // Enable rewriter debug logs
cleanErrors : boolean ; // Clean stack traces
captureErrors : boolean ; // Capture and log errors
sourcemaps : boolean ; // Generate sourcemaps
// Features
serviceworkers : boolean ; // Enable SW registration
syncxhr : boolean ; // Synchronous XHR support
interceptDownloads : boolean ; // Intercept file downloads
// Error handling
allowInvalidJs : boolean ; // Allow malformed JS
allowFailedIntercepts : boolean ; // Continue on hook failures
}
Enabling flags
const { ScramjetController } = $scramjetLoadController ();
const controller = new ScramjetController ({
prefix: "/service/" ,
flags: {
rewriterLogs: true , // Enable for debugging
sourcemaps: true , // Enable sourcemaps
cleanErrors: true , // Clean stack traces
},
});
Site-specific flags
Override flags for specific domains:
const controller = new ScramjetController ({
flags: {
rewriterLogs: false , // Default off
},
siteFlags: {
"example \\ .com" : {
rewriterLogs: true , // Enable for example.com
},
},
});
Checking flags at runtime
import { flagEnabled } from "@/shared" ;
const url = new URL ( "https://example.com" );
if ( flagEnabled ( "rewriterLogs" , url )) {
console . log ( "Debug logging enabled for this site" );
}
if ( flagEnabled ( "allowInvalidJs" , url )) {
console . warn ( "Invalid JavaScript will not cause errors" );
}
Error handling
Client-side error capture
Scramjet can capture and analyze errors from proxied sites:
// In client hooks (src/client/shared/err.ts)
if ( flagEnabled ( "captureErrors" , client . url )) {
self . addEventListener ( "error" , ( event ) => {
console . error ( "Captured error:" , {
message: event . message ,
filename: event . filename ,
lineno: event . lineno ,
colno: event . colno ,
error: event . error ,
});
});
self . addEventListener ( "unhandledrejection" , ( event ) => {
console . error ( "Unhandled rejection:" , event . reason );
});
}
Service worker error handling
The service worker catches and logs fetch errors:
// From src/worker/fetch.ts
try {
return await handleFetch . call ( this , request , client );
} catch ( err ) {
const errorDetails = {
message: err . message ,
url: request . url ,
destination: request . destination ,
};
if ( err . stack ) {
errorDetails . stack = err . stack ;
}
console . error ( "ERROR FROM SERVICE WORKER FETCH:" , errorDetails );
// Return error page for documents
if ([ "document" , "iframe" ]. includes ( request . destination )) {
return renderError (
Object . entries ( errorDetails )
. map (([ key , value ]) => ` ${ key } : ${ value } ` )
. join ( " \n\n " ),
unrewriteUrl ( request . url )
);
}
return new Response ( undefined , { status: 500 });
}
Scramjet generates user-friendly error pages: // Simplified from src/worker/error.ts
export function renderError ( error : string , url : string ) {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Scramjet Error</title>
<style>
body { font-family: monospace; padding: 20px; }
.error { background: #fee; padding: 10px; border: 1px solid #fcc; }
</style>
</head>
<body>
<h1>Scramjet encountered an error</h1>
<div class="error">
<pre> ${ error } </pre>
</div>
<p>URL: <code> ${ url } </code></p>
</body>
</html>
` ;
return new Response ( html , {
status: 500 ,
headers: { "Content-Type" : "text/html" },
});
}
Stack trace cleaning
Scramjet can clean stack traces to hide proxy internals:
import { flagEnabled } from "@/shared" ;
if ( flagEnabled ( "cleanErrors" , client . url )) {
const originalPrepare = Error . prepareStackTrace ;
Error . prepareStackTrace = ( err , stack ) => {
// Filter out Scramjet internal frames
const filtered = stack . filter ( frame => {
const filename = frame . getFileName ();
return filename && ! filename . includes ( "scramjet" );
});
return filtered
. map ( frame => ` at ${ frame . toString () } ` )
. join ( " \n " );
};
}
Sourcemap debugging
Sourcemaps allow debugging rewritten code as if it were the original.
Enabling sourcemaps
const controller = new ScramjetController ({
flags: {
sourcemaps: true ,
},
});
How sourcemaps work
When enabled, Scramjet injects sourcemap data into rewritten scripts:
// Original code:
fetch ( "/api" );
// Rewritten with sourcemap:
__scramjet$pushsourcemap ([ /* binary sourcemap data */ ], "tag-12345" );
scramjet$fetch ( "/api" );
The client maintains a sourcemap registry:
// From src/client/shared/sourcemaps.ts
const sourcemaps = new Map < string , SourceMap >();
globalThis [ config . globals . pushsourcemapfn ] = ( map : number [], tag : string ) => {
const bytes = new Uint8Array ( map );
const decoded = new TextDecoder (). decode ( bytes );
const parsed = JSON . parse ( decoded );
sourcemaps . set ( tag , parsed );
};
With sourcemaps enabled:
Open Chrome DevTools
Enable “Enable JavaScript source maps” in Settings
Set breakpoints in the original code
Stack traces show original line numbers
Sourcemaps add overhead to rewriting. Only enable them during development.
Inspecting service worker
Open chrome://serviceworker-internals/
Find your Scramjet service worker
Click “Inspect” to open dedicated DevTools
View console logs, network requests, and sources
Network inspection
Monitor proxied requests:
Open DevTools → Network tab
Filter by “Fetch/XHR” or “WS” for WebSockets
Inspect request/response headers
Check “Preserve log” to track redirects
Scramjet requests appear as same-origin to DevTools since they’re routed through the service worker.
Console filtering
Filter Scramjet logs:
// Show only Scramjet logs
console . log = new Proxy ( console . log , {
apply ( target , thisArg , args ) {
if ( args [ 0 ]?. includes ?.( "scramjet" )) {
target . apply ( thisArg , args );
}
},
});
Common debugging patterns
Tracking URL rewriting
import { rewriteUrl , unrewriteUrl } from "@rewriters/url" ;
const original = "https://example.com/page" ;
const rewritten = rewriteUrl ( original , meta );
const restored = unrewriteUrl ( rewritten );
console . log ( "Original:" , original );
console . log ( "Rewritten:" , rewritten );
console . log ( "Restored:" , restored );
console . log ( "Match:" , original === restored );
Debugging client hooks
client . Proxy ( "fetch" , {
apply ( ctx ) {
console . log ( "fetch() intercepted:" , {
url: ctx . args [ 0 ],
init: ctx . args [ 1 ],
});
// Continue with normal behavior
},
});
Monitoring rewriter pool
import { getRewriter } from "@rewriters/wasm" ;
const [ rewriter , release ] = getRewriter ( meta );
console . log ( "Rewriter pool size:" , rewriters . length );
console . log ( "In-use rewriters:" , rewriters . filter ( r => r . inUse ). length );
try {
const result = rewriter . rewrite_js ( code , base , url , module );
} finally {
release ();
}
const metrics = {
rewriteTime: 0 ,
fetchTime: 0 ,
totalTime: 0 ,
};
const start = performance . now ();
// Fetch
const fetchStart = performance . now ();
const response = await fetch ( url );
metrics . fetchTime = performance . now () - fetchStart ;
// Rewrite
const rewriteStart = performance . now ();
const body = await rewriteBody ( response , meta , destination );
metrics . rewriteTime = performance . now () - rewriteStart ;
metrics . totalTime = performance . now () - start ;
console . table ( metrics );
Common pitfalls
Forgetting to release rewriters
// BAD: Rewriter never released
const [ rewriter , release ] = getRewriter ( meta );
const result = rewriter . rewrite_js ( code , base , url , module );
// Missing: release();
// GOOD: Use try/finally
const [ rewriter , release ] = getRewriter ( meta );
try {
const result = rewriter . rewrite_js ( code , base , url , module );
return result ;
} finally {
release ();
}
// BAD: Missing base URL
const meta : URLMeta = {
origin: new URL ( "https://example.com/page" ),
base: undefined , // Error!
};
// GOOD: Always provide base
const meta : URLMeta = {
origin: new URL ( "https://example.com/page" ),
base: new URL ( "https://example.com/page" ),
};
Rewriting before WASM loads
// BAD: WASM not loaded
const result = rewriteJs ( code , url , meta , false );
// Error: rewriter wasm not found
// GOOD: Wait for WASM
await scramjet . loadConfig ();
// -> calls asyncSetWasm() internally
const result = rewriteJs ( code , url , meta , false );
Not handling edge cases
// BAD: Assumes string input
const rewritten = rewriteJs ( code , url , meta , false );
return rewritten . toUpperCase (); // Error if Uint8Array!
// GOOD: Handle both types
const rewritten = rewriteJs ( code , url , meta , false );
const str = typeof rewritten === "string"
? rewritten
: new TextDecoder (). decode ( rewritten );
return str . toUpperCase ();
Circular rewriting
// BAD: Rewriting already-rewritten URLs
const url1 = rewriteUrl ( originalUrl , meta );
const url2 = rewriteUrl ( url1 , meta ); // Double-encoded!
// GOOD: Check if already rewritten
const url = originalUrl . startsWith ( config . prefix )
? originalUrl
: rewriteUrl ( originalUrl , meta );
Debugging checklist
When encountering issues:
Check service worker
Verify the service worker is active: navigator . serviceWorker . ready . then ( reg => {
console . log ( "SW active:" , reg . active ?. state );
});
Enable debug flags
Turn on logging: flags : {
rewriterLogs : true ,
captureErrors : true ,
}
Inspect network
Open DevTools → Network and filter by type (JS, CSS, Fetch/XHR).
Check console
Look for Scramjet errors, warnings, or debug messages.
Verify transport
Ensure bare-mux transport is configured: await BareClient . SetTransport ( "/bare/" , "bare" );
Test with simple page
Try proxying a basic HTML page to isolate the issue.
Getting help
If you’re still stuck:
Check GitHub issues : Search for similar problems
Enable all debug flags : Gather comprehensive logs
Create minimal reproduction : Isolate the issue to a small example
Share logs : Include console output, network traces, and error messages
When reporting issues, always include your Scramjet version, browser version, and transport configuration.