Reroute #
A small, auditable browser extension for client-side URL rewrites in Firefox and Chrome. One codebase, no dependencies, no build step, no analytics, and no network access except an optional per-rule health probe against the instances you configure (see Health-checked pools).
Ships with editable presets:
| What | Example |
|---|---|
| Twitter / X → nitter pool (rule) | https://x.com/user/status/1 → a live instance from an 8-front-end pool (xcancel.com, nitter.net, …) |
| Bluesky → impro.social (rule) | https://bsky.app/profile/foo → https://impro.social/profile/foo |
| Reddit image links load directly (rule) | opening https://i.redd.it/abc.jpeg shows the image, not Reddit's JS viewer |
Reddit /media viewer skipped (rule) |
https://www.reddit.com/media?url=… → the decoded image URL |
| Reddit preview → original image (rule, off by default) | https://preview.redd.it/abc.jpeg?width=… → https://i.redd.it/abc.jpeg |
You can disable, edit, delete or reorder any rule, and add your own rules of any of the four types below.
The Reddit behaviors (now just ordinary rules) #
Reroute 1.2.0 turned the two Reddit-specific hacks of earlier versions into user-configurable rule types, so the exact same behavior works for any website. The Reddit rules are simply seeded defaults you can edit or delete.
Header fix (rule type accept). Reddit's image hosts (i.redd.it,
preview.redd.it, cf.preview.redd.it, external-preview.redd.it) return an
HTML page with a JavaScript viewer when the browser navigates to them,
because the navigation's Accept header prefers text/html. The same server
returns the raw image when asked for an image. The seeded "Reddit image links
load directly" rule is a declarative modifyHeaders DNR rule that sets the
Accept header of top-level navigations to those four hosts to
image/avif,image/webp,*/*;q=0.8 — the URL never changes, Reddit's own server
just serves the file. This is the same approach as
load-reddit-images-directly,
reimplemented on Manifest V3 / declarativeNetRequest so it works identically in
Firefox and Chrome. Being a normal rule, you can point it at any host.
?url= redirect (rule type param-redirect). Reddit links images through
www.reddit.com/media?url=<percent-encoded URL>. Declarative rules can't
URL-decode, so this is implemented by a content script. The seeded
"Skip the reddit.com/media viewer" rule matches *://*.reddit.com/media*,
decodes the url parameter, and location.replace()s to it before the page
renders. For safety it only redirects to hosts on the rule's allowlist
(*.redd.it, *.redditmedia.com). As a normal rule, you can point it at any
site that uses ?url=-style links (e.g. a non-Reddit image viewer).
How it works (and why you can trust it) #
The extension uses the browsers' declarativeNetRequest API: DNR rules are
compiled once into declarative redirect rules that the browser's own network
layer applies before a request is sent. param-redirect rules (which need to
decode a query parameter, which DNR can't) use a single dynamically-registered
content script. Consequences:
- For URL rewrites, the original site is never contacted — the redirect happens pre-flight.
- Extension code never sees, touches, or logs your traffic. There are no
webRequestlisteners. - The one content script (
param-redirect.js) is registered only for the union of your enabledparam-redirectrules' match patterns — it never runs on every page. It decodes the parameter, checks the destination against the rule's allowlist, and only then redirects. If anything fails the check it does nothing and the page renders normally. - Health probes (see below) only ever hit the root of an instance you
configured, only when a health-checked pool rule matches, and only from the
local
go.htmlpage. Nothing about the source or target URL is sent with a probe, and nothing is logged. - The only JavaScript that runs is: (a)
background.js, which recompiles rules whenever you edit them, (b)param-redirect.json URLs you configured, (c)go.js/probe.json the local rotor page for pool rules, and (d) the options/popup pages while open. - Rules are stored in
storage.localon your machine. Nothing syncs or phones home. The whole thing is a handful of small files you can read in ten minutes.
host_permissions: <all_urls> is required by Chrome for redirect actions —
it grants the ability to redirect any URL your rules match, not the ability to
read pages. It also lets the local go.html page probe instances you
configured in a health-checked pool.
Auditability. Health checking is the one place the extension makes a network request, and it is fully opt-in: it is off by default, only applies to rules with 2+ destinations, only fires when one of those rules matches, is bounded to at most 2 probes per redirect, and only contacts the instance root (never the source or target URL). Results are cached locally for a few minutes so a healthy pool isn't re-probed on every visit. The extension collects nothing itself — the instances see ordinary requests, exactly what a normal visit to them would produce.
Install #
Firefox (temporary, for testing) #
- Open
about:debugging#/runtime/this-firefox - Click Load Temporary Add-on… and select
manifest.json(or the.zip).
Temporary add-ons are removed when Firefox closes. For a permanent unsigned
install use Firefox Developer Edition or ESR: set
xpinstall.signatures.required to false in about:config, then install the
zip via about:addons → gear icon → Install Add-On From File…. (Regular
release Firefox requires the add-on to be signed; you can self-sign for free
by uploading the zip to addons.mozilla.org as an unlisted add-on.)
Note for reinstalls: the add-on ID changed in 1.2.0 to
reroute@7778777.online(the localbuild ID only applied to temporary installs). Rules are tied to the add-on ID, so export your rules before reinstalling and import them after.
Chrome / Chromium / Edge / Brave #
- Unzip the folder somewhere permanent.
- Open
chrome://extensions, enable Developer mode (top right). - Click Load unpacked and select the folder.
Rule types #
Domain swap — replace the host, keep everything else. List one or more
source domains (subdomains are matched automatically) and one or more
destination hosts. The scheme is forced to https.
Giving a domain swap several destinations turns it into a round-robin
pool: each visit uses the next host in the list, e.g.
twitter.com, x.com → xcancel.com, nitter.net. Because a
declarativeNetRequest rule is static (one fixed destination), pool rules
redirect through the extension's own local go.html page, which picks the
next instance (per-rule cursor in storage.local) and immediately
location.replace()s — an imperceptible local hop, no extra permissions, and
still nothing observing your traffic. Single-destination rules keep the direct
zero-hop transform. Pool bookkeeping details: the cursor lives under a separate
storage key so rotating never triggers a rule recompile; on any failure
go.html shows the original URL instead of auto-navigating, which is what
makes redirect loops impossible; and pools apply to page navigations only (an
extension page can't stand in for an embedded image or script). Editing the
pool is just editing the comma-separated list — remove a dead instance and the
rotation instantly adjusts.
Health-checked pools #
A host rule with 2+ destinations can turn on healthCheck (the
"Health-check instances before redirecting" toggle in the rule editor). With
it on, the rotor probes the next candidate before redirecting:
- A probe is a plain
GETto the instance root (https://<host>/) with a 4-second timeout. 2xx/3xx means up; a network error, timeout, 4xx or 5xx means down. The source/target URL is never part of a probe — only the rewritten URL is passed to the subsequentlocation.replace(), exactly like clicking a normal link. - Results are cached in
storage.local: an up result is trusted for ~10 minutes; a down result gets a ~30-minute cooldown before it can be probed again, so a dead instance isn't hammered on every visit. - Probing is bounded: at most 2 probes per redirect. The rotor tries the cursor's candidate, then the next one; once the budget is spent it stops.
- If every candidate is down (or the budget is spent with no healthy host),
go.htmlshows the original URL and does not auto-navigate — the same loop-free fallback as any other failure.
healthCheck is off by default and only applies to pools; a single
destination ignores the flag. The seeded Twitter preset ships with it on,
because nitter instances come and go. Because probes are opt-in, per-rule, and
bounded, the default "no network access" story is unchanged for rules that
don't opt in.
Why direct probing instead of a status service. We evaluated the public
nitter status API at https://status.d420.de/api/v1/instances (it is alive and
current — it returns a {hosts:[…]} list with per-instance healthy flags and
15-minute check history). It is a useful snapshot, but we deliberately chose
direct probing as the only source: a central status service is a third
party that would see every instance query the extension makes, its view can be
stale or from a different network than yours, and a healthy global status
doesn't guarantee the instance is reachable from your machine. Direct probing
is more accurate (you test what you'll actually load) and more private (the
only observers are the instances you configured, seeing requests like any
normal visit). A status service was not wired in, and is not a dependency.
Regex rewrite — a regular expression matched against the full URL
(case-insensitive), plus a substitution template where \1–\9 insert
capture groups and \0 inserts the whole match. Patterns must be
RE2-compatible (no lookaheads or
backreferences); the browser validates each pattern and the options page shows
exactly why a rule was rejected, without silently disabling the others.
Header fix (accept) — set a request header on navigations to a list of
hosts (subdomains matched automatically). The default, an image-friendly
Accept header, makes servers that otherwise serve a JS viewer return the raw
file. The URL is unchanged; the server just answers with what you asked for.
Defaults to page navigations only.
?url= redirect (param-redirect) — for sites that route links through a
viewer page carrying a percent-encoded destination in a query parameter. Give
a content-script match pattern (e.g. *://*.reddit.com/media*), the parameter
name (default url), and an allowlist of destination hosts. The script
decodes the parameter and, only if the destination is an http/https URL on an
allowed host, goes straight to it. Anything else is left alone. The allowed-host
check matches on label boundaries, so *.redd.it matches i.redd.it but not
evilredd.it — this stays airtight because it's the redirect primitive.
By default DNR rules apply to page navigations only (main_frame), so a
rewrite of an image host can't break sites that embed content from it. Each
rule has a checkbox to also cover embedded requests (iframes, images, scripts,
XHR) if you want that. (?url= redirects follow the page's own links, so the
checkbox doesn't apply to them.)
First matching rule wins, top to bottom — use ↑/↓ to reorder.
Extras #
- Try a URL box in options: paste any URL and see which rule fires and
what it becomes (including, for
?url=rules, whether the decoded destination passes the allowed-host check), before you rely on it. - Export / Import rules as JSON (do this before uninstalling — the browser deletes extension storage on uninstall).
- Toolbar popup with a global kill switch and per-rule toggles.
Notes on the presets #
- The Reddit preview → original rule (off by default) additionally maps
preview.redd.it/<file>toi.redd.it/<file>, which serves the un-resized original. A small number of previews (converted GIFs, external previews) have no original oni.redd.itand may 404 — that's why it's opt-in; the Accept-header rule already makes preview URLs display fine as-is. - Nitter instances come and go, so the Twitter preset is a health-checked round-robin pool of 8 front-ends. The rotor probes each candidate before redirecting and skips dead ones, so you don't land on a broken instance. You can still edit the list or turn health checking off per rule.
File map #
manifest.json permissions + cross-browser wiring (MV3)
background.js storage → DNR rules + registers the param-redirect script
common.js shared data + migration + pure security/test helpers
probe.js pure health-check pool logic + the fetch wrapper (testable)
param-redirect.js the one content script (?url= rules), registered on demand
go.html/.js local round-robin rotor for pool rules (health-checked)
options.html/.css/.js rule editor, import/export, URL tester, health-check UI
popup.html/.js toolbar quick toggles
tests/ node --test suite for the security validator + probe logic
icons/ generated PNGs
Tests #
The security-critical param-redirect validation (allowlist suffix matching,
scheme checks, decode/parse edge cases), the v1→v2 migration, and the
health-check pool logic (candidate selection with a cursor, cache TTL +
down-cooldown, bounded probe count, all-down fallback) are covered by a
zero-dependency node:test suite. The fetch itself is a thin wrapper around
the pure logic and is not exercised in the test environment:
node --test tests/*.test.js
License: MIT. Do whatever you like with it.