Skip to main content
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" });

Formatted output

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

Performance timing

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);
};

Using sourcemaps in DevTools

With sourcemaps enabled:
  1. Open Chrome DevTools
  2. Enable “Enable JavaScript source maps” in Settings
  3. Set breakpoints in the original code
  4. Stack traces show original line numbers
Sourcemaps add overhead to rewriting. Only enable them during development.

Browser DevTools integration

Inspecting service worker

  1. Open chrome://serviceworker-internals/
  2. Find your Scramjet service worker
  3. Click “Inspect” to open dedicated DevTools
  4. View console logs, network requests, and sources

Network inspection

Monitor proxied requests:
  1. Open DevTools → Network tab
  2. Filter by “Fetch/XHR” or “WS” for WebSockets
  3. Inspect request/response headers
  4. 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();
}

Analyzing performance

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();
}

Incorrect URL metadata

// 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:
1

Check service worker

Verify the service worker is active:
navigator.serviceWorker.ready.then(reg => {
  console.log("SW active:", reg.active?.state);
});
2

Enable debug flags

Turn on logging:
flags: {
  rewriterLogs: true,
  captureErrors: true,
}
3

Inspect network

Open DevTools → Network and filter by type (JS, CSS, Fetch/XHR).
4

Check console

Look for Scramjet errors, warnings, or debug messages.
5

Verify transport

Ensure bare-mux transport is configured:
await BareClient.SetTransport("/bare/", "bare");
6

Test with simple page

Try proxying a basic HTML page to isolate the issue.

Getting help

If you’re still stuck:
  1. Check GitHub issues: Search for similar problems
  2. Enable all debug flags: Gather comprehensive logs
  3. Create minimal reproduction: Isolate the issue to a small example
  4. Share logs: Include console output, network traces, and error messages
When reporting issues, always include your Scramjet version, browser version, and transport configuration.

Build docs developers (and LLMs) love