Skip to main content
URL rewriting is fundamental to how Scramjet works. Every URL that passes through the proxy must be encoded to route through the Service Worker, and every URL in proxied content must be rewritten to maintain the proxy.

URL encoding and decoding

The codec system

Scramjet uses a configurable codec to encode and decode URLs. The codec is a pair of functions defined in your configuration:
The default codec uses encodeURIComponent and decodeURIComponent, but you can use any encoding scheme (base64, custom obfuscation, etc.).

How encoding works

When you navigate to a URL through Scramjet:
The controller’s encodeUrl() method transforms it:
1

Parse the URL

The URL is parsed into a URL object.
2

Extract and encode hash

The hash fragment is separated and encoded independently:
3

Encode the URL

The main URL (without hash) is encoded:
4

Result

Note that the hash is preserved separately so browser navigation works correctly.

How decoding works

When the Service Worker intercepts a request, it decodes the URL:
The unrewriteUrl() function in src/shared/rewriters/url.ts reverses the process:
1

Check for special protocols

Handle special URL types first:
2

Handle blob/data URLs

3

Decode the main URL

URL rewriting in content

Once content is fetched, all URLs within it must be rewritten to point through the proxy.

The rewriteUrl function

The rewriteUrl() function is the core rewriter, defined in src/shared/rewriters/url.ts:
It takes:
  • url: The URL to rewrite (absolute or relative)
  • meta: Context about the current page (origin, base URL, frame names)
And returns the proxied URL.

URLMeta context

The URLMeta object provides context for rewriting:
The base URL can differ from origin if the page contains a <base> tag. This is updated dynamically during HTML rewriting.

Special URL handling

rewriteUrl() handles different URL schemes:
JavaScript URLs are rewritten by rewriting the JavaScript code:
Blob URLs are prefixed with the proxy origin:
This routes blob fetches through the Service Worker so they can be rewritten.
Data URLs are also prefixed:
These are returned unchanged:
Regular URLs are resolved against the base, then encoded:

Relative URL resolution

Relative URLs are resolved against meta.base:
This uses the browser’s native URL parser:
If URL parsing fails (e.g., for invalid URLs), the original URL is returned unchanged. This prevents breaking pages with malformed URLs.

Where URLs are rewritten

Scramjet rewrites URLs in multiple places throughout the stack:

HTML rewriting

In src/shared/rewriters/html.ts, URLs in HTML attributes are rewritten:
The htmlRules array (from src/shared/htmlRules.ts) defines which attributes to rewrite:

Special HTML cases

JavaScript rewriting

JavaScript rewriting is more complex. It uses an oxc-based WASM rewriter to:
  1. Parse the JavaScript AST
  2. Identify API calls that accept URLs
  3. Wrap those calls with runtime functions that rewrite URLs
For example, this code:
Is rewritten to:
The $scramjet$rewrite function calls rewriteUrl() at runtime with the current page’s metadata.

CSS rewriting

In src/shared/rewriters/css.ts, URLs in CSS are rewritten:
This handles:
  • background-image: url(...)
  • @import url(...)
  • @font-face { src: url(...) }
  • etc.

Client-side interception

In the ScramjetClient, DOM APIs are intercepted to rewrite URLs at runtime:
This ensures that even programmatic URL manipulation is proxied.

URL preservation

Scramjet preserves original URLs in HTML using scramjet-attr-* attributes:
This allows:
  • Debugging and inspection
  • Restoring original URLs when needed
  • Compatibility with scripts that read attributes directly

Hash handling

Hash fragments require special handling because:
  1. They’re not sent to the server
  2. They’re used for client-side routing
  3. They need to work with browser navigation APIs
Scramjet encodes hashes separately:
The hash is encoded but kept as a real hash fragment so:
  • window.location.hash works correctly
  • Hash-based routers work
  • The browser’s back/forward buttons work

Performance considerations

URL rewriting can be expensive for large documents. The HTML rewriter uses htmlparser2 for performance, and the JS rewriter is written in Rust/WASM.
Use the rewriterLogs flag during development to see timing information:

Common pitfalls

Always use ScramjetFrame for iframes. Direct <iframe> elements won’t have proper frame tracking, breaking nested iframe URL resolution.
Be careful with custom URL encoders. They must:
  • Be deterministic (same input = same output)
  • Not produce URLs with characters that need escaping in URLs
  • Be reversible (encode and decode must be inverses)

Next steps

Configuration

Learn about codec configuration and other options

Service Worker

See how URLs are decoded in the Service Worker