Manifest V3 · local-only · open source (MIT)

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.

A recent desktop Google Chrome (Manifest V3) is required. We recommend installing from Chrome Web Store, or building from source.

No telemetry No backend change Network-layer fast path HAR · cURL · JSON EN / 中文 UI 6 themes
FAT page · fat.appfetch · XMLHttpRequest · WebSocket
Simple rule → DNR redirectnetwork layer · zero JS per request
Complex rule → SW channeloverride · mock · delay · block · retry · ws
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.

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)
Channel B · background worker
Complex ruleoverride · mock · delay · block · retry · WebSocket · method filter
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.

03

Add one rule

  1. Turn the proxy switch on in the popup.
  2. Wildcard rule: https://fat-api.example.com/*https://uat-api.example.com.
  3. 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”.

The long version of this comparison — what each approach is actually good at, and the six situations where you should not use this extension.

Requirements and development

Runtime requirements

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.

TF

Transfer Any File

Offline format conversion

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.