Skip to content

Repository files navigation

cf-worker-proxy

A Cloudflare Workers port of echo-proxy — a multi-host reverse proxy that routes by Host header, modifies request/response headers, rewrites paths, and supports conditional/ordered routing per host.

One Worker script can be attached to many domains at once (Cloudflare Custom Domains or Routes); the Worker uses the incoming Host to pick the right config entry per request.

Setup

npm install
cp .dev.vars.example .dev.vars   # then fill in a real base64 PROXY_CONFIG for local dev
npm run dev                       # wrangler dev, reads .dev.vars

Config

PROXY_CONFIG is a base64-encoded JSON object keyed by hostname:

{
  "example.com": {
    "upstream": "https://backend.example.com",
    "request_headers": { "X-Custom-Header": "MyValue" },
    "response_headers": { "X-Response-Header": "ResponseValue" },
    "remove_headers": ["Server", "Set-Cookie"]
  },
  "*": {
    "static_index_file": "/index.html"
  }
}

Encode it with: base64 -w 0 < config.json

Set it for local dev in .dev.vars (gitignored). For production, use a secret so it isn't stored in plaintext:

wrangler secret put PROXY_CONFIG

Conditional proxying

{
  "condition": { "header": "X-Api-Key", "value": "secret-key" },
  "fallback_behavior": "404"
}

condition supports header, query_param (both matched by equality against value), or path_prefix (matched by prefix). fallback_behavior is one of fallback_upstream (requires fallback_upstream to also be set), 404, bad_gateway, or defaults to 403.

Multiple upstreams per host (routes)

{
  "routes": [
    { "condition": { "path_prefix": "/files/" }, "upstream": "https://files.example.com" },
    { "condition": { "header": "target-app", "value": "b" }, "upstream": "https://b.example.com" },
    { "upstream": "https://default.example.com" }
  ]
}

Evaluated in order; the first matching route wins. A route with no condition is a catch-all and should go last. If no route matches, fallback_behavior applies.

Wildcard host fallback

A "*" entry handles requests whose Host matches nothing else — either upstream (proxied like any other host) or static_index_file (served via the ASSETS binding). DEFAULT_UPSTREAM / DEFAULT_STATIC_INDEX_FILE vars populate this automatically when PROXY_CONFIG doesn't define an explicit "*" entry.

Path rewriting

{
  "path_rewrite_regex": "^/v1/(.*)",
  "path_rewrite_replacement": "/api/$1"
}

Static asset edge caching

{
  "cache": { "extensions": [".js", ".css", ".png", ".woff2"], "ttl_seconds": 3600 }
}

Responses to GET/HEAD requests whose path ends in one of extensions are cached at the edge (Workers Cache API) for ttl_seconds, served on subsequent matching requests without hitting the upstream. Only successful (2xx) responses are cached. This reduces load on your upstream and improves latency — it does not reduce the request count billed by Cloudflare, since the Worker still runs on every request whether it's a cache hit or not.

Attaching multiple domains

Add each domain under the Worker's Settings → Domains & Routes in the Cloudflare dashboard (or via Wrangler). Every attached domain invokes this same Worker; the Worker picks the right config entry from the incoming Host.

Known limitation: no host_override

Unlike the Go version, this port has no way to send an upstream a Host header different from the upstream's own hostname. Cloudflare's fetch() always derives the Host header from the outgoing request's URL; the only way to decouple them (cf.resolveOverride) only works when both hosts are on your own Cloudflare account/zone. Since this proxy's purpose is reaching domains outside Cloudflare, that mechanism doesn't apply, so the config field was dropped rather than shipped half-working.

Commands

  • npm run dev — run locally with Wrangler
  • npm test — run the Vitest suite (runs inside workerd, outbound fetch() mocked)
  • npm run typecheck — tsc --noEmit
  • npm run deploy — deploy with Wrangler

Manual verification after deploy

wrangler deploy
curl -i https://<your-attached-domain>/some/path

Confirm: the response comes from the configured upstream, configured request/response headers are present, remove_headers are absent, and an unmatched host either proxies to the wildcard upstream, serves the static fallback page, or returns 502.

License

MIT with the Commons Clause condition — see LICENSE. Free to use, modify, and self-host. Offering this software (or a product substantially derived from it) as a paid Software-as-a-Service or Platform-as-a-Service product to third parties requires a separate commercial license — see LICENSE-COMMERCIAL.md.

About

Cloudflare Workers port of echo-proxy

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages