๐ Reflare is a lightweight and scalable reverse proxy and load balancing library built for Cloudflare Workers. It sits in front of web servers (e.g. web application, storage platform, or RESTful API), forwards HTTP requests or WebSocket traffics from clients to upstream servers, and transforms responses with several optimizations to improve page loading time.
- โก Serverless: Publish instantly to the auto-scaling serverless platform built by Cloudflare. There's no need to manage virtual machines or containers.
โ๏ธ Load Balancing: Distribute incoming traffics among different upstream services.- โ๏ธ Hackable: Deliver unique content based on visitor attributes, conduct A/B testing, or build middlewares to hook into the lifecycle. (Experimental)
- ๐ณ๏ธ Dynamic Route Definition (Experimental): Store and update route definitions with Workers KV to avoid redundant redeployment.
- Generate a new project from reflare-template and install the dependencies.
npm init cloudflare reflare-app https://github.com/xiaoyang-sde/reflare-template
cd reflare-app
npm install
- Authenticate
wrangler
with a Cloudflare account.
npx wrangler login
-
Edit or add route definitions in
src/index.ts
. Please read the examples and the documentation below for more details. -
Run
npm run dev
to preview Reflare with a local development server. -
Run
npm run publish
to publish Reflare on Cloudflare Workers.
- Install the
reflare
package.
npm install reflare
Import useReflare
from reflare
. useReflare
accepts these options:
provider
: The location of the list of route definitions. (optional, defaults tostatic
)static
: Reflare loads the route definitions fromrouteList
.kv
: Reflare loads the route definitions from Workers KV. (Experimental)
routeList
: The initial list of route definitions. (optional, defaults to[]
, ignored ifprovider
is notstatic
)namespace
: The Workers KV namespace that stores the list of route definitions. (required ifprovider
iskv
)
useReflare
returns an object with the handle
method and push
method.
- The
handle
method takes the inbound Request to the Worker and returns the Response fetched from the upstream service. - The
push
method takes a route and appends it torouteList
.
import useReflare from 'reflare';
export default {
async fetch(
request: Request,
): Promise<Response> {
const reflare = await useReflare();
reflare.push({
path: '/*',
upstream: {
domain: 'httpbin.org',
protocol: 'https',
},
});
return reflare.handle(request);
},
};
Edit the route definition to change the behavior of Reflare. For example, the route definition below lets Reflare add the access-control-allow-origin: *
header to each response from the upstream service.
{
path: '/*',
upstream: {
domain: 'httpbin.org',
protocol: 'https',
},
cors: {
origin: '*',
},
}
Set up a reverse proxy for MDN Web Docs:
{
path: '/*',
upstream: {
domain: 'developer.mozilla.org',
protocol: 'https',
},
}
Set up a reverse proxy for wss://echo.websocket.org:
{
path: '/*',
upstream: {
domain: 'echo.websocket.org',
protocol: 'https',
},
}
Set up a reverse proxy for https://example.s3.amazonaws.com and add custom headers to the response:
{
path: '/*',
upstream: {
domain: 'example.s3.amazonaws.com',
protocol: 'https',
},
headers: {
response: {
'x-response-header': 'Hello from Reflare',
},
},
cors: {
origin: ['https://www.example.com'],
methods: ['GET', 'POST'],
credentials: true,
},
}
Reflare implements express-like route matching. Reflare matches the path and HTTP method of each incoming request with the list of route definitions and forwards the request to the first matched route.
domain
(string | string[]
): The domain name or the list of domain names that matches the domain name in the incoming request's URLpath
(string | string[]
): The path or the list of paths that matches the routemethods
(string[]
): The list of HTTP methods that match the route
// Matches all requests
reflare.push({
path: '/*',
/* ... */
});
// Matches all requests that are accessing the Cloudflare Workers instance
// through `template.reflare.workers.dev`
reflare.push({
domain: 'template.reflare.workers.dev',
path: '/*',
/* ... */
});
// Matches GET and POST requests with path `/api`
reflare.push({
path: '/api',
methods: ['GET', 'POST'],
});
// Matches GET requests with path ending with `.json` or `.yaml` in `/data`
reflare.push({
path: ['/data/*.json', '/data/*.yaml'],
methods: ['GET'],
});
domain
(string
): The domain name of the upstream serverprotocol
(string
): The protocol scheme of the upstream server (optional, defaults to'https'
)port
(number
): The port of the upstream server (optional, defaults to80
or443
based onprotocol
)timeout
(number
): The maximum wait time on a request to the upstream server (optional, defaults to10000
)weight
(number
): The weight of the server that will be accounted for as part of the load balancing decision (optional, defaults to1
)onRequest(request: Request, url: string)
: The callback function that will be called before sending the request to upstreamonResponse(response: Response, url: string)
: The callback function that will be called after receiving the response from upstream
reflare.push({
path: '/*',
upstream: {
domain: 'httpbin.org',
protocol: 'https',
port: 443,
timeout: 10000,
weight: 1,
},
/* ... */
});
The onRequest
and onResponse
fields accept callback functions that could change the content of the request or response. For example, the following example replaces the URL of the request and sets the cache-control
header of the response based on its URL. These fields accept either a standalone function or a list of functions. The function could be either async
or non-async
.
reflare.push({
path: '/*',
upstream: {
domain: 'httpbin.org',
protocol: 'https',
port: 443,
timeout: 10000,
weight: 1,
onRequest: (request: Request, url: string): Request => {
// Modifies the URL of the request
return new Request(url.replace('/original/request/path', ''), request);
},
onResponse: (response: Response, url: string): Response => {
// If the URL ends with `.html` or `/`, sets the `cache-control` header
if (url.endsWith('.html') || url.endsWith('/')) {
response.headers.set('cache-control', 'public, max-age=240, s-maxage=60');
}
return response;
}
},
/* ... */
});
To load balance HTTP traffic to a group of servers, pass an array of server configurations to upstream
. The load balancer will forward the request to an upstream server based on the loadBalancing.policy
option.
random
: The load balancer will select a random upstream server from the server group. The optionalweight
parameter in the server configuration could influence the load balancing algorithm.ip-hash
: The client's IP address is used as a hashing key to select the upstream server from the server group. It ensures that the requests from the same client will always be directed to the same server.
reflare.push({
path: '/*',
loadBalancing: {
policy: 'random',
},
upstream: [
{
domain: 's1.example.com',
protocol: 'https',
weight: 20,
},
{
domain: 's2.example.com',
protocol: 'https',
weight: 30,
},
{
domain: 's3.example.com',
protocol: 'https',
weight: 50,
},
],
/* ... */
});
Each incoming request is inspected against the firewall rules defined in the firewall
property of the options object. The request will be blocked if it matches at least one firewall rule.
field
: The property of the incoming request to be inspectedasn
: The ASN number of the incoming request (number
)ip
: The IP address of the incoming request, e.g.1.1.1.1
(string
)hostname
: The content of thehost
header, e.g.github.com
(string | undefined
)user-agent
: The content of theuser-agent
header, e.g.Mozilla/5.0
(string | undefined
)country
: The two-letter country code in the request, e.g.US
(string | undefined
)continent
: The continent of the incoming request, e.g.NA
(string | undefined
)
value
(string | string[] | number | number[] | RegExp
): The value of the firewall ruleoperator
: The operator to be used to determine if the request is blockedequal
: Block the request iffield
is equal tovalue
not equal
: Block the request iffield
is not equal tovalue
match
: Block the request ifvalue
matchesfield
(Expectfield
to bestring
andvalue
to beRegExp
)not match
: Block the request ifvalue
doesn't matchfield
(Expectfield
to bestring
andvalue
to beRegExp
)in
: Block the request iffield
is invalue
(Expectvalue
to beArray
)not in
: Block the request iffield
is not invalue
(Expectvalue
to beArray
)contain
: Block the request iffield
containsvalue
(Expectfield
andvalue
to bestring
)not contain
: Block the request iffield
doesn't containvalue
(Expectfield
andvalue
to bestring
)greater
: Block the request iffield
is greater thanvalue
(Expectfield
andvalue
to benumber
)less
: Block the request iffield
is less thanvalue
(Expectfield
andvalue
to benumber
)
reflare.push('/', {
path: '/*',
/* ... */
firewall: [
{
field: 'ip',
operator: 'in',
value: ['1.1.1.1', '1.0.0.1'],
},
{
field: 'user-agent',
operator: 'match',
value: /Chrome/,
}
],
});
request
(Record<string, string>
): Sets request header going upstream to the backend. Accepts an object. (optional, defaults to{}
)response
(Record<string, string>
): Sets response header coming downstream to the client. Accepts an object. (optional, defaults to{}
)
reflare.push({
path: '/*',
/* ... */
headers: {
request: {
'x-example-header': 'hello server',
},
response: {
'x-example-header': 'hello client',
},
},
});
-
origin
: Configures theAccess-Control-Allow-Origin
CORS header. (optional, defaults tofalse
)boolean
: set totrue
to reflect the origin of the request, or set tofalse
to disable CORS.string[]
: an array of acceptable origins.*
: allow any origin to access the resource.
-
methods
(string[]
): Configures theAccess-Control-Allow-Methods
CORS header. Expect an array of valid HTTP methods or*
. (optional, defaults to reflecting the method specified in the requestโsAccess-Control-Request-Method
header) -
allowedHeaders
(string[]
): Configures theAccess-Control-Allow-Headers
CORS header. Expect an array of HTTP headers or *. (optional, defaults to reflecting the headers specified in the requestโsAccess-Control-Request-Headers
header.) -
exposedHeaders
(string[]
): Configures theAccess-Control-Expose-Headers
CORS header. Expect an array of HTTP headers or*
. (optional, defaults to[]
) -
credentials
(boolean
): Configures theAccess-Control-Allow-Credentials
CORS header. Set to true to pass the header, or it is omitted. (optional, defaults tofalse
) -
maxAge
(number
): Configures theAccess-Control-Max-Age
CORS header. Set to an integer to pass the header, or it is omitted. (optional)
reflare.push({
path: '/*',
/* ... */
cors: {
origin: true,
methods: [
'GET',
'POST',
],
allowedHeaders: [
'Example-Header',
],
exposedHeaders: [
'Example-Header',
],
credentials: true,
maxAge: 86400,
},
});
Reflare could load the route definitions from Workers KV. Set the provider
to kv
and namespace
to a Workers KV namespace (e.g. REFLARE
) that binds to the current Worker. Reflare fetches the route definitions from namespace
and handles each incoming request with the latest route definitions.
import useReflare from 'reflare';
declare const REFLARE: KVNamespace;
export default {
async fetch(
request: Request,
): Promise<Response> {
const reflare = await useReflare({
provider: 'kv',
namespace: REFLARE,
});
return reflare.handle(request);
},
};
The route definitions should be stored as a JSON array in the route-list
key of namespace
. The KV namespace could be modified with wrangler
or Cloudflare API.
wrangler kv:key put --binding=[namespace] 'route-list' '[{"path":"/*","upstream":{"domain":"httpbin.org","protocol":"https"}}]'