
Security News
Feross on the 10 Minutes or Less Podcast: Nobody Reads the Code
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.
cf-worker-utils
Advanced tools
This is a simple package for composing Cloudflare Workers, focused on the use case of having an upstream server, and wanting to conditionally manipulate requests and responses.
npm i cf-worker-utils from your Worker directory.handleFetch and provide an array of request/response
handlers, and/or route-limited request/response handlers.Example:
import handleFetch from 'cf-worker-utils';
handleFetch({
request: requestHandlers, // Run on every request.
response: responseHandler, // Run on every response.
routes: (router) => {
router.get('/test', {
request: requestHandlers, // Run on matching requests.
response: responseHanders, // Run on responses from matching requests.
});
router.get('/posts/:id', {
request: requestHandlers, // Run on matching requests.
response: responseHandlers, // Run on responses from matching requests.
});
},
});
Top level request and response handlers will be run on every route, before any route-specific handlers.
For all places where you specify handlers, you can provide one handler, an array of handlers, or no handlers (null, or empty array).
It goes like this:
Request is received.Request loops through all request handlers (global, and then route).Response wasn't received, the resulting Request object is
fetched (from the cache or the server).Response object is passed through the response handlers
(global, and then route). ┌──────────────────┐
│ Incoming Request │
│ to your Worker │
└──────────────────┘
│
▼
.───────────────.
( Matches route? )───Yes─┐
`───────────────' │
│ ▼
│ ┌───────────────────────┐
No │ Append route handlers │
│ │ to global handlers │
│ └───────────────────────┘
│ │
└───────┬────────┘
│
▼
┌─────────────────┐
│ Run next │
┌────────▶│ request handler │
│ └─────────────────┘
│ │
│ ▼
│ .─────────────────────────────.
│ ( Handler returned a Response? )───┐
│ `─────────────────────────────' │
│ │ Yes
│ No │
Yes │ │
│ ▼ │
│ .───────────────. │
└─────────( More handlers? ) │
`───────────────' │
│ │
No │
│ │
▼ │
.─────────────────────. │
┌─────( Request in CF cache? )────┐ │
│ `─────────────────────' │ │
Yes No │
│ ┌────────────┐ ┌────────────┐ │ │
│ │ Fetch from │ │ Fetch from │ │ │
└─▶│ cache │ │ server │◀─┘ │
└────────────┘ └────────────┘ │
│ │ │
└───────┬───────┘ │
│ │
▼ │
┌──────────┐ │
│ Response │◀─────────────┘
└──────────┘
│
▼
┌──────────────────┐
│ Run next │
┌──▶│ response handler │
│ └──────────────────┘
│ │
Yes ▼
│ .───────────────.
└────( More handlers? )
`───────────────'
│
No
│
▼
┌────────────────┐
│ Final Response │
└────────────────┘
The router has functions for all HTTP methods, plus router.any() which matches
any method. e.g. router.get(path, handlers), router.post(path, handlers).
The path argument uses the path-to-regexp library, which has good support for positional path parameters. Here's what various routes would yield for a given request:
/posts/:id
/posts/123 ➡️ {id: "123"}/posts/123/ ➡️ {id: "123"}/posts/hello ➡️ {id: "hello"}/posts/123/more/posts//posts/posts/:id(\\d+)
/posts/123 ➡️ {id: "123"}/posts/word/posts/:id?
/posts/123 ➡️ {id: "123"}/posts/hello ➡️ {id: "hello"}/posts/ ➡️ {}/posts => {}/posts/123/more/posts/:id+
/posts/123 ➡️ {id: ["123"]}/posts/123/456 ➡️ {id: ["123", "456"]}/posts/123/hello ➡️ {id: ["123", "hello"]}/posts//posts/posts/:id(\\d+)+
/posts/123 ➡️ {id: ["123"]}/posts/123/456 ➡️ {id: ["123", "456"]}/posts/123/hello/posts//posts/bread/:meat+/bread
/bread/turkey/bread ➡️ {meat: ["turkey"]}/bread/peanut-butter/jelly/bread ➡️ {meat: ["peanut-butter", "jelly"]}/bread/bread/mother{-:type}?
/mother ➡️ {}/mother-in-law ➡️ {type: "in-law"}/mothersIf you want to match a path prefix and everything after it, just use a wildcard
matcher: /prefix/:any*.
Go read the path-to-regex documentation for more information.
You can also limit your routes to a specific host, like so:
import handleFetch, { forbidden, setRequestHeaders } from 'cf-worker-utils';
handleFetch({
routes: (router) => {
router.host('example.com', (router) => {
router.get('/', setRequestHeaders({ 'x-foo': 'bar' }));
});
router.host('*.blogs.example.com', (router) => {
router.any('/xmlrpc.php', forbidden);
});
},
});
This makes it trivial to set up a Worker that services multiple subdomains and routes, instead of having to maintain a bunch of independent Workers.
Handlers are functions (preferably async functions). They are passed an object
that contains:
{
addRequestHandler(),
addResponseHandler(),
addCfPropertiesHandler(),
originalRequest,
request,
response,
current,
params,
phase,
}
addRequestHandler(handler, options) — dynamically adds another request
handler (pass {immediate: true} to add it as the first or next handler).addResponseHandler(handler, options) — dynamically adds another response
handler (pass {immediate: true} to add it as the first or next handler).addCfPropertiesHandler(handler) — adds a callback that receives and returns
new properties to pass to fetch() on the cf key (see Cloudflare
documentation)request — A Request object representing the current state of the request.originalRequest — The original Request object (might be different if other
handlers returned a new request).response — A Response object with the current state of the response. —
current — During the request phase, this will equal request. During the
response phrase, this will equal response. This is mostly used for
conditions. For instance, the header condition works on either requests or
responses, as both have headers. Thus it looks at { current: { headers } }.params — An object containing any param matches from the route.phase — One of "request" or "response".Request handlers can return three things:
Request object — this will get passed on to the rest of the request
handlers.Response object — this will skip the rest of the request handlers and get
passed through the response handlers.Response handlers can return two things:
Response object — this will get passed on to the rest of the request
handlers.The following handlers are included:
setUrl(url: string)setHost(host: string)setPath(path: string)setProtocol(protocol: string)setHttps()setHttp()forbidden()setRequestHeaders([header: string, value: string][] | {[header: string]: string})appendRequestHeaders([header: string, value: string][] | {[header: string]: string})removeRequestHeaders(headers: string[])setResponseHeaders([header: string, value: string][] | {[header: string]: string})appendResponseHeaders([header: string, value: string][] | {[header: string]: string})removeResponseHeaders(headers: string[])copyResponseHeader(from: string, to: string)lazyLoadImages()prependPath(pathPrefix: string)removePathPrefix(pathPrefix: string)redirect(status: number)redirectHttps()redirectHttp()requireCookieOrParam(param: string, forbiddenMessage: string)Instead of bundling logic into custom handlers, you can also use
addHandlerIf(condition, ...handlers) together with the any(), all() and
none() gates to specify the logic outside of the handler. Here's an example:
import {
handleFetch,
addHandlerIf,
contains,
header,
forbidden,
} from 'cf-worker-utils';
handleFetch({
request: [
addHandlerIf(
any(
header('user-agent', contains('Googlebot')),
header('user-agent', contains('Yahoo! Slurp')),
),
forbidden(),
someCustomHandler(),
),
],
});
addHandlerIf() takes a single condition as its first argument, but you can
nest any(), all() and none() as much as you like to compose a more complex
condition.
As hinted above, there are several built-in conditions for you to use:
header(headerName: string, matcher: ValueMatcher)contentType(matcher: ValueMatcher)isHtml()hasParam(paramName: string)hasRouteParam(paramName: string)param(paramName: string, matcher: ValueMatcher)routeParam(paramName: string, matcher: ValueMatcher)isHttps()isHttps()The ones that take a string (or nothing) are straightforward, but what's up with
ValueMatcher?
A ValueMatcher is flexible. It can be:
string — will match if the string === the value.string[] — will match if any of the strings === the value.ValueMatchingFunction — a function that takes the value and returns a
boolean that decides the match.ValueMatchingFunction[] — an array of functions that take the value, any of
which can return true and decide the match.The following ValueMatchingFunctions are available:
contains(value: string | NegatedString | CaseInsensitiveString | NegatedCaseInsensitiveString)startsWith(value: string | NegatedString | CaseInsensitiveString | NegatedCaseInsensitiveString)endsWith(value: string | NegatedString | CaseInsensitiveString | NegatedCaseInsensitiveString)These functions can also accept insensitive strings and negated strings with the
text('yourtext').i and text('yourtext).not helpers.
addHandlerIf(
header('User-Agent', startsWith(text('WordPress').not.i)),
forbidden(),
);
Note that you can use logic functions to compose value matchers! So the example from the Logic section could be rewritten like this:
import {
handleFetch,
addHandlerIf,
contains,
header,
forbidden,
} from 'cf-worker-utils';
handleFetch({
request: [
addHandlerIf(
header(
'user-agent',
any(contains('Googlebot'), contains('Yahoo! Slurp')),
),
forbidden(),
someCustomHandler(),
),
],
});
Two more points:
const userAgent = header('user-agent');
Now, userAgent is a function that accepts a ValueMatcher.
You could take this further and do:
const isGoogle = userAgent(startsWith('Googlebot'));
Now you could just add a handler like:
handleFetch({
request: [addHandlerIf(isGoogle, forbiddden)],
});
current. So if you run
them as a request handler, header inspection will look at the request. As a
response handler, it'll look at response. But you can also use the raw
conditionals while creating your own handlers. For instance, in a response
handler you might want to look at the request that went to the server, or the
originalRequest that came to Cloudflare.import forbidden from 'cf-worker-utils';
import { hasParam } from 'cf-worker-utils/conditions';
export default async function forbiddenIfFooParam({ request }) {
if (hasParam('foo', request)) {
return forbidden();
}
}
In most cases you will not be reaching into the request from the response. A better way to handle this is to have a request handler that conditionally adds a response handler. But if you want to, you can, and you can use those "raw" conditions to help. Note that the raw conditions will not be curried, and you'll have to pass a request or response to them as their last argument.
MIT License
Copyright © 2020–2021 Mark Jaquith
This software incorporates work covered by the following copyright and permission notices:
tsndr/cloudflare-worker-router
Copyright © 2021 Tobias Schneider
(MIT License)
pillarjs/path-to-regexp
Copyright © 2014 Blake Embrey
(MIT LICENSE)
FAQs
Utilities for writing Cloudflare Workers
We found that cf-worker-utils demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Socket CEO Feross Aboukhadijeh joins 10 Minutes or Less, a podcast by Ali Rohde, to discuss the recent surge in open source supply chain attacks.

Research
/Security News
Campaign of 108 extensions harvests identities, steals sessions, and adds backdoors to browsers, all tied to the same C2 infrastructure.

Security News
OpenAI rotated macOS signing certificates after a malicious Axios package reached its CI pipeline in a broader software supply chain attack.