Point a FAT frontend at a UAT backend — one rule,
no code changes
Cross-Origin Proxy is an open-source Chrome extension (Manifest V3) that redirects the API requests a page
makes to another backend environment. Rewrite URLs, headers and responses, mock data with conditional
responses, inject latency, block requests, retry failures and forward WebSocket — configured in the
browser, stored on your machine.
Target environmentUAT · PROD · a mock server — one browser-wide rule set
One rule set, two forwarding channels — chosen per rule, not per project. If interception fails, the
page falls back to native fetch / XMLHttpRequest / WebSocket;
block rules are never replayed.
2forwarding channels
12per-rule capabilities
200rules max
0bytes uploaded
The problem it removes
Cross-environment debugging usually costs one of three things: a backend change, a config change in every
project, or a fake local build. This extension collapses all three into a single browser rule that applies
to every page you open.
Before and after, for the common "the UAT service has the fix, my FAT page does not" case
Without the extension
With one rule
Edit the devServer proxy table, then restart the dev server — per project, invisible to teammates.
Add https://fat-api.example.com/* → https://uat-api.example.com. Applies
immediately to every project in the browser.
Ask the backend to add your origin to Access-Control-Allow-Origin and redeploy.
Requests with any advanced capability are answered by the extension, so page CORS checks do not
apply.
Hardcode another environment's token in the source, and remember to revert it.
Inject the header per rule; switch the rule off when you are done.
Wait for an API that is still being built, or stub it inside the application.
Mock it in the rule — optionally a different body per URL, method or query parameter.
Record traffic in a capture tool, then rebuild the request by hand to reproduce a failure.
Import the HAR file or paste the cURL command and the rule is prefilled.
What a single rule can do
A rule always has a match pattern, a target URL and a priority. Everything else is optional and additive —
and each capability you switch on is also what decides which channel runs it.
Two channels, one rule set
A rule that only rewrites the URL becomes a declarativeNetRequest dynamic redirect,
resolved by the browser's network stack with no JavaScript per request — for XHR, navigations, iframes,
scripts, styles, images, fonts and media alike. Anything richer runs through the background service
worker: a MAIN-world interceptor patches fetch, XMLHttpRequest and
WebSocket, bridges through an isolated-world content script, and hands the response back to
the page.
Network-layer fast pathMain-world interception30 s per-request ceilingNative fallback
Full control over request and response
Rewrite the URL; inject or replace request headers; replace the request body; override the response
status code and response headers, or replace individual JSON fields by dot path such as
data.token. Header names are validated and CRLF inside values is refused, so a rule can
never smuggle a split request; mocked and overridden status codes are clamped to 200–599 so the page
always gets a response it can construct.
HBRMDXReWSBadges on every rule row
Wildcard, prefix and regex matching
Three match types — * for any text, a leading substring, or a full regular expression whose
capture groups the target URL can reference. Wildcard and prefix rewrite the same way on both channels;
a regex only does when it covers the whole URL, because the network layer replaces the URL while the
background channel replaces the matched part. Regexes are screened for nested-quantifier ReDoS patterns,
and anything heading for the network layer is validated against Chrome's RE2 engine first.
Mock with conditional responses
Return a crafted JSON / text / HTML / XML body and status without contacting any server — or attach a
list of conditions (URL pattern, method, query parameters) so the first match decides the response. One
rule can stand in for a whole endpoint group, in as many states as you need.
Delay, block and retry
Add 0–60000 ms of latency to exercise skeletons and timeouts; block a request so it fails like a network
error; or let the extension add 1–5 extra attempts after a failure or a 5xx, spaced by an interval you
set between 100 and 30000 ms.
Method filter and query injection
Restrict a rule to GET / POST / PUT / DELETE / PATCH / OPTIONS / HEAD — for example to send only writes
to the test backend — or append and override query parameters such as __env=uat and
gray-release tags on the final URL without rewriting it.
WebSocket forwarding
Point ws:// and wss:// connections at another environment by rewriting the
socket URL, so real-time traffic follows the same switch as REST calls. Patterns may be written with
http(s) or ws(s), and method filtering treats the handshake as
GET.
A rule workspace, not a config file
Start from a quick template while the list is empty, duplicate a rule, drag to reorder priority, batch
enable, disable or delete, and undo a deletion. Search by name, match pattern or target URL, then narrow
the list with the status and match-type filters. A rule shadowed by a higher-priority rule with the same
pattern is flagged while you edit, so a rule that can never fire does not go unnoticed. A rule that
Chrome's network layer will not apply at all — regex syntax RE2 does not support, or a capture reference
out of range — is tagged "Not applied" in the list.
Evidence, not guesswork
The URL match tester previews the matched rule, the rewritten URL, the channel, the extra actions and
which other rules match the same URL but lose to it, for any URL in real time. The request log keeps the
last 500 calls with method, status, duration and the rule that served them, filters by method, status
class, rule name or URL keyword, opens a detail view with the request and response headers and text
bodies (a binary response body is not stored), copies any entry as cURL using its original URL, and
reports hit counts for both channels.
Import, export and environment profiles
JSON config export — share mode is on by default, stripping Authorization, Cookie and similar headers
plus token-like query parameters — with replace or merge modes on import; HAR 1.2 export of captured
traffic and HAR import that turns recorded requests into rules (those rules arrive disabled until you
enable them); cURL paste that prefills the form; copy-as-cURL on every log entry, using its original
URL. Named profiles snapshot the whole rule set so FAT, UAT and PROD are one click apart, and batch
migration rewrites target domains across selected rules with a change preview.
The parts you touch every day
The popup holds the global switch, the active-rule count and today's request count (background channel
only), recent requests, quick rule toggles, the auto-off countdown and a current-page match preview with
“create a rule for this page”. A toolbar badge shows the proxy state, and auto-off (30 minutes to 4
hours, on chrome.alarms) closes the switch so a forgotten proxy never outlives the
debugging session.
Interface details that matter
English and Simplified Chinese UI, six colour themes with light / dark / follow-system modes synced
across popup, options and in-page surfaces, and keyboard shortcuts: ⌘⇧P toggle
proxy, N new rule, / focus search,
Esc close. The single-key shortcuts are deliberate — ⌘/Ctrl+N opens a browser
window and a page cannot capture it.
See the interface
Real screenshots from the current build. The options page holds the rule table, match tester, request logs
and import/export; the popup keeps the global switch and the current-page preview one click away.
Rule tableCapability badges (H · B · R · M · D · X · WS), drag-to-reorder priority, per-rule hit counts,
and batch enable, disable, delete and migrate.
Rule editorMatch, rewrite, method filter, query injection, header and body override, response override,
conditional mock, delay, block and retry — one form, with a live conflict warning.
URL match testerType any URL, optionally with a method, to preview the matched rule, the rewritten URL, the
forwarding channel, the extra actions and which other rules match the same URL but lose to
it.
Request logLast 500 calls with filters and adjustable auto-refresh, a full detail view, copy as cURL using
the original URL, HAR export, plus DNR and service-worker hit statistics.
PopupGlobal switch, active rules, today's requests (background channel only), recent calls, quick rule
toggles, auto-off countdown, current-page hit preview and “create rule for this page”.
Themes and languageSix colour themes with light, dark and follow-system modes, switchable between English and
Chinese — synced across popup, options and in-page UI.
How it works
Every enabled rule is classified on each config sync, straight from storage. That classification decides
whether the browser's network stack or the extension's background worker answers the request, and it is
the same classifier the match tester reports. The global switch sits above both channels: turning it off
removes the network-layer rules and makes the background worker pass requests through untouched.
Page issues a requestfetch · XMLHttpRequest · WebSocket
↓
One enabled rule matcheslowest priority number wins; no match means the request goes through
↓
The rule's capabilities route itclassified on every config sync, not guessed per request
↓
Channel A · browser network layer
Simple ruleURL rewrite only — none of the items below
↓
declarativeNetRequest redirectrewritten by the network stack · zero JS per request
↓
Target environment repliesstill a cross-origin request, so same-origin policy applies (see CORS below)
Page interceptor (main world)takes over fetch / XHR / WebSocket · postMessage to own origin only
↓
Bridge (isolated world)relays over chrome.runtime; non-string bodies stay on the native path
↓
Background worker sends and repliesholds host permissions · builds the response for the page
Both channels run off the same rule set — the only difference is who rewrites the request. Turning the
global switch off removes the network-layer rules and makes the background worker pass requests through
untouched.
A rule stops being “simple” as soon as it has any of
The exact conditions that switch a rule to the background channel, and why
Capability
Why the network layer cannot do it
Request header or body override
A body replacement needs a real request to be re-issued, not a redirect.
Response status code, header or JSON field override
The response has to be read and rebuilt.
Mock (with or without conditions), delay, block, retry
These change whether, when and how often a request happens at all.
HTTP method filter
Redirect rules cannot be conditioned on the request method.
Query parameter injection
Applied after rewriting, outside what redirect substitution can express.
WebSocket (ws:// / wss://)
declarativeNetRequest has no WebSocket resource type.
Wildcard pattern not ending in *, or an empty target URL
Substitution would silently drop trailing text or produce an invalid redirect value.
CORS, precisely
Background-channel requests are issued by the extension, which holds host permissions, and the page receives
a response the extension constructed — page CORS checks do not apply. A pure URL rewrite stays an ordinary
cross-origin request: the browser applies the same-origin policy to the redirected response and still needs
Access-Control-Allow-Origin to cover your origin. If a target environment does not allow your
origin, give the rule any capability (a response header override is the cheapest) and it changes channels.
If interception fails, the page falls back to its native fetch,
XMLHttpRequest or WebSocket, and non-string bodies (FormData, Blob, ArrayBuffer)
go down the native path rather than being silently dropped. Block rules are the deliberate exception: a
blocked request is never replayed, because that would send the very request you asked to suppress.
chrome.storage.local is the single source of truth. Service-worker caches — compiled regexes,
the rule list, hit counters — are rebuildable and are invalidated on every config change, so the extension
stays correct across worker restarts.
Get started in four steps
No account, no server, no certificate to install. From clone to first proxied request in about a minute.
01
Build
Requires Node.js 20+ and pnpm 10. No toolchain? Every tagged release attaches a prebuilt zip at
Releases
— once the first one exists, unzipping it is all the install needs.
git clone https://github.com/liaolongdong/cross-origin-proxy
cd cross-origin-proxy
pnpm install
pnpm build
02
Load
Open chrome://extensions, enable Developer mode, click Load unpacked and
select .output/chrome-mv3 — or the folder you just unzipped from the release. Pin the icon.
Set a priority (lower number matches first) and save.
04
Verify
Reload the page. A wildcard rewrite like this one runs in the browser's network layer, so it writes no
per-request log entry: preview the URL in the match tester and watch the rule's DNR hit count instead.
Only background-channel requests appear in the request log.
Updating an unpacked build: overwrite the files in the same folder. Loading from a
different folder makes Chrome treat it as a new installation with a different extension ID, and rules,
logs and profiles are stored per ID in chrome.storage.local — the old set would look empty.
Use cases, with the rule behind each one
The shortest path from a situation to a configuration. Every row below is the extension's actual
behaviour, not a workaround.
Common setups and the capability each one uses
Situation
Rule configuration
Channel
Verify a UAT-only fix from a FAT page
Wildcard rewrite of the API prefix to the UAT host.
DNR
Build UI before the backend exists
Mock response with a crafted body and status; add conditions to vary it per endpoint.
SW
Prove the loading and timeout paths work
Delay of 3000–60000 ms on the specific endpoint.
SW
Check the offline or 500 fallback UI
Block the request, or override the response status.
SW
Flaky upstream? Make reads resilient
Retry 2–5 times with a 1000 ms interval.
SW
Exercise a gray-release or A/B branch
Query parameter injection, e.g. __env=uat.
SW
Send only writes to the test backend
Method filter limited to POST / PUT / DELETE.
SW
Debug a real-time feature against another environment
WebSocket rewrite from the page's socket URL to the target socket.
SW
Keep a FAT page from breaking on a missing field
JSON field replacement, e.g. data.token → "mock-token".
SW
The target environment rejects your origin
Add any capability, e.g. a response header override, to leave the network layer.
SW
Hand the same setup to a teammate
Export JSON (share mode strips credentials by default), or share a HAR-derived rule set.
—
Environment rotated to a new domain
Batch migrate targets with a change preview.
—
How it compares to the alternatives
Columns are tool categories, not products: capabilities vary inside a category, so check the
details against what you already use.
Cross-environment debugging and request rewriting, the usual ways. Colours read from your side: green is
favourable for that row, orange is partial, grey is absent.
Approach
This extension
Dev-server proxy
Capture proxy tool
API client / header-modifier extension
Editing app config
Needs a backend or gateway change
No
Often, for CORS
No
No
No
Configured per project
No — browser-wide
Yes
System-wide
Per rule set
Yes
Works on any page origin (localhost, internal tools)
Yes
Only traffic to that dev server
Yes
Limited by the extension API
Yes
Rewrites responses (status, headers, JSON fields)
Yes
No
Yes
Usually not the body
No
Mock / delay / block / retry from the UI
Yes, conditional mock included
With extra plugins
Yes
Mock only, typically
No
Covers WebSocket traffic
Yes
Rarely
Yes
No
No
Needs a local CA certificate to read HTTPS
No — runs inside the browser
No
Yes
No
No
Rules shareable as a plain local file
Yes — JSON / HAR / cURL
Yes — in the repo
Session files
Often cloud-synced
Yes — in the repo
Can proxy server-to-server or non-browser traffic
No — browser-scoped
No
Yes
No
Depends
The trade-off is stated plainly: this is a browser tool. It cannot help a server-to-server call, it records
nothing to a cloud workspace, and its rules live in your Chrome profile rather than in a shared project file
— export JSON when you want a teammate to have them. Where a capture proxy is the right tool for system-wide
traffic, this extension is the right tool for “this page should be talking to that environment”.
A recent desktop Google Chrome (Manifest V3). We recommend installing from
Chrome Web Store, or building from source by loading the unpacked build from .output/chrome-mv3 — or, once
a tag has been published, the folder you unzipped from
Releases. Edge runs Chromium
extensions, so the same build normally works there; Firefox is not a supported target today because of
declarativeNetRequest differences. Chrome itself blocks every extension on
chrome:// pages, the Web Store and other extension pages.
Build requirements
Node.js 20 or newer and pnpm 10 (see packageManager). Built with WXT + Vue 3 + TypeScript +
Element Plus + Vite, with Element Plus imported per component so the bundle stays small; Vitest covers
the matching, rewriting and import/export logic.
Developer commands
pnpm dev # WXT dev server with HMR (port 8899)
pnpm build # production build → .output/chrome-mv3
pnpm build:zip # zip for store upload
pnpm test # vitest unit tests
pnpm typecheck # tsc --noEmit
pnpm lint # eslint
pnpm lint:style # stylelint
pnpm assets # regenerate store and landing images
Privacy and permissions
Everything is stored on your machine in chrome.storage.local. There is no account, no
analytics, no telemetry and no remote service: the only network traffic is the API traffic you ask it to
proxy. Logs and exports are produced locally — and because the log records what it proxied, remember that
headers may carry tokens before you share an exported file.
Read the privacy policy.
Every permission the manifest declares, and what it is used for
Permission
Why it is needed
storage
Persist rules, request logs, environment profiles and preferences.
declarativeNetRequest
Install the network-layer redirect rules that make simple rewrites cost zero JavaScript.
declarativeNetRequestFeedback
Read which redirect rules actually matched, for the hit statistics in the log drawer.
alarms
Keep the background worker alive while proxying, and run the auto-off countdown.
<all_urls> host permission
Proxying has to work on whatever origin your frontend runs on; the target environment is a
developer-controlled internal domain, so it cannot be enumerated in advance.
Limits, enforced in code
Hard ceilings you can rely on when planning a rule set
Constraint
Value
Rules
200 maximum when adding, batch-adding or merging; a replace-mode import writes the file as-is
Request log
Last 500 entries (ring buffer)
Request body
10 MB maximum
Injected delay
0 – 60000 ms
Retry
0 – 5 attempts, configurable interval
Proxied request timeout
30 s
Mocked or overridden status code
Clamped to 200 – 599
Auto-off countdown
30 minutes, 1 h, 2 h or 4 h
Frequently asked questions
Everything on this page is also machine-readable: the same questions and answers are published as
FAQPage structured data, and llms.txt summarises the product for AI
assistants.
Basics and matching
What does a cross-origin proxy for a browser do?
It intercepts the API requests a page makes and sends them somewhere else. In development that usually
means pointing a frontend running against one backend at a different environment, so you can test UAT
code from a FAT page without editing the app or redeploying.
Which requests can it intercept?
The page interceptor patches fetch, XMLHttpRequest and
WebSocket in the page's main world, and relative URLs are resolved to absolute ones before
matching. Rules that only rewrite the URL are compiled into
declarativeNetRequest redirects, which the browser applies to matching requests of every
resource type: main and sub frames, XHR, scripts, stylesheets, images, fonts, media and other.
How do wildcard, prefix and regex matching differ?
Wildcard treats * as any text and appends whatever the trailing * captured to
the target URL. Prefix matches the beginning of the URL and appends the remaining part. Regex must cover
the whole URL: the background channel replaces only the part the pattern matched, while the network
layer replaces the URL entirely, so anchor the pattern with ^ and capture the tail with
(.*)$ if you want both channels to agree. When several rules match, the lowest priority
number wins; ties keep the order shown in the rule table, so give each rule its own number.
Does it work on any website?
It is injected on all http and https pages, and rules match on request URL, so
it works on internal tools, localhost dev servers and staging domains alike. It does not
run on chrome:// pages, the Chrome Web Store, or other extension pages, which Chrome
restricts for every extension.
Channels, CORS and performance
Does this bypass CORS restrictions?
Rules that use any advanced capability (header override, body override, response override, mock, delay,
block, retry, method filter, query injection or WebSocket) are executed by the extension's background
context, which holds host permissions, and the page receives a response the extension constructed — so
page CORS checks do not apply. A pure URL rewrite is handled as a network-layer redirect instead, where
the browser still validates Access-Control-Allow-Origin on the redirected response. If a
target environment does not allow your origin, add any advanced capability (a response header override
is the cheapest) and the rule switches to the background channel.
Why do some rules use a different forwarding channel than I expected?
Because the network layer cannot express what you asked for. A wildcard that does not end in
*, an empty target URL, or any advanced capability is routed to the background channel on
purpose, since compiling it anyway would silently change the redirect result. The URL match tester shows
the channel for any URL before you rely on it.
Will it slow my page down?
Rules that only rewrite the URL cost no JavaScript per request — the browser resolves them in its
network stack. Background-channel requests add one extension hop plus whatever delay you configure, and
each proxied request has a 30 second ceiling. User regexes are checked for nested-quantifier ReDoS
patterns before use, and regular expressions heading for the network layer are validated against
Chrome's RE2 engine first, because one unsupported pattern would make the whole batch of rules fail to
install.
Why is there no per-request log entry for my simple rules?
Network-layer redirects never reach the extension, so there is nothing to log per request. Their
rule-level hit counts appear under DNR hits for the last five minutes; the quota Chrome allows for that
read is limited, so the panel refreshes on demand and quietly keeps the previous numbers when the quota
is exhausted. Background-channel hits are counted separately.
Mock and failure injection
What can mock mode do beyond returning a static body?
One rule can carry a list of conditions — a URL pattern, an HTTP method and query parameters — and the
first matching condition decides the body, status code and content type, falling back to the rule's
default body when none match. That lets a single mock stand in for several endpoints, or for several
states of one endpoint. Mocked requests never touch the network, and returned status codes are clamped
to 200–599.
What is the difference between blocking a request and forcing a 500?
A block rule makes the request fail the way a network failure does, which is what exercises offline and
retry handling. Overriding the response status instead gives you a real HTTP error code, status text and
headers, which is what exercises error rendering that depends on the code. Blocked requests are never
replayed by the native fallback — otherwise the extension would send exactly the request you asked to
suppress.
How does the retry capability work?
Retrying sits behind a per-rule switch and runs on the background channel. Switched on, a rule makes 1
to 5 extra attempts after the first, spaced by an interval you set between 100 and 30000 ms (default
1000). An attempt is retried when it throws — including the 30 second per-attempt timeout — or when it
returns a 5xx status while attempts remain; the last response or error reaches the page once the budget
is exhausted. Because retrying is a background-channel capability, a rule that uses it never becomes a
network-layer redirect.
Sharing, limits and safety
Can I share rules with my team?
Yes. Export the full configuration as JSON — share mode is on by default, stripping Authorization,
Cookie and similar request/response headers plus token-like query parameters, untick it for a verbatim
local backup — and teammates import it either as a replacement or merged into what they already have.
Export captured traffic as a HAR 1.2 file, or import a HAR recorded elsewhere to create rules
automatically. Anyone can paste a cURL command — DevTools “Copy as cURL” output — to prefill a rule.
Environment profiles store a named snapshot of the whole rule set.
What are the limits?
Up to 200 rules, the last 500 request log entries, a 10 MB request body ceiling, delays between 0 and
60000 ms, retry off by default and then 1 to 5 extra attempts at 100–30000 ms, a 30 second ceiling per
proxied request, and response status codes clamped to 200–599 so the page can always construct a valid
Response object.
What happens if the extension fails to handle a request?
The page falls back to the browser's native fetch, XMLHttpRequest or
WebSocket, so the request goes out as it normally would. Non-string bodies such as
FormData, Blob and ArrayBuffer cannot cross the message boundary and also fall back to native. The
single exception is a block rule: a blocked request is never replayed natively.
Privacy and compatibility
Is my data sent to a server?
No. Rules, logs, profiles and preferences live in chrome.storage.local on your machine. The
extension contains no analytics, no telemetry and no remote service of its own; the only network traffic
is the API traffic you ask it to proxy. Note that the request log records the headers and bodies it
proxies — including any tokens — locally, which is worth remembering before you share an exported HAR or
a copied cURL command.
Does it work in Firefox or Edge?
It is built and tested against Chrome (Manifest V3). Edge runs Chromium extensions, so the same build
normally works there; Firefox's Manifest V3 support differs around declarativeNetRequest,
so it is not a supported target today.
More extensions by this author
Same author, same rules: Manifest V3, open source, data stays on your machine, no service of our own. They
are often switched on together while debugging.
A local-first open-source password manager: Ctrl +
Shift + F logs you in (fill, tick, click), isolates
dev / test / staging / prod accounts by exact host, and ships TOTP, an offline security audit and a
generator — everything encrypted with AES-256-GCM on your machine. It answers "who am I in this
environment"; this extension answers "where do this environment's requests go".
14 formats converted in the browser, not one byte uploaded: Markdown, Word, PDF, Excel, CSV, JSON, HTML
and images all convert on your own machine, with mixed-format batches, automatic multi-step chains,
preview and inline editing, and ZIP packaging. No account, no upload, no network request.