This is the SpeedWorkers origin-request Lambda@Edge (force-fail variant):
// SpeedWorkers origin-request Lambda@Edge (force-fail variant)
//
// The cache behavior targets an origin group (primary = SpeedWorkers,
// secondary = your true origin). This lambda runs on cache misses and decides
// whether the request may reach SpeedWorkers:
// - bot GET/HEAD page request => leave the origin group untouched, so
// SpeedWorkers serves it and CloudFront fails over to your origin on error
// - anything else (users, POST/PUT/DELETE/..., websockets, ignored paths)
// => generate a 403 response, which matches the origin group failover
// criteria: CloudFront retries the SECONDARY origin (your true origin)
// and this lambda runs a second time, passing the request through
//
// Unlike the redirect variant (origin.mjs), this lambda carries NO
// configuration of your origin: the fallback target is the origin group's
// secondary origin, defined in the CloudFront distribution. The same lambda
// works for any origin. The price is a second lambda invocation for every
// non-bot request that misses the cache.
//
// The behavior itself should only allow GET/HEAD (CloudFront doesn't offer
// the all-methods option on origin-group behaviors); note that a forced 403
// on a non-GET/HEAD request would NOT fail over (CloudFront only fails over
// for GET/HEAD/OPTIONS), so those methods must never reach this behavior.
//
// Requirements: a currently supported Node.js runtime, deployed in us-east-1,
// timeout 1 second (the code only manipulates headers).
// Configure this
const swPrimaryOriginDomain = "xxx.speedworkers.com"; // Set the same domain as in the primary origin of the origin group
// Optional
const swAllowedUrls = []; // Optional. Set something like https://mysubdomain.mydomain.com/mypages (in lower case) to restrict SW to urls starting with these values
const swRewriteOrigin = ""; // Optional. Set something like https://www.mydomain.com
// Don't touch this
const swAgent = "CloudFrontInterceptorFF/2.0";
const swIgnored = [
"/graphql",
"/css/",
"/fonts/",
"/images/",
"/js/",
"/scss/",
".bmp",
".css",
".csv",
".doc",
".docx",
".eot",
".gif",
".ico",
".ief",
".jpe",
".jpeg",
".jpg",
".js",
".json",
".less",
".map",
".m1v",
".mov",
".mp2",
".mp3",
".mp4",
".mpa",
".mpe",
".mpeg",
".mpg",
".otf",
".ott",
".ogv",
".pbm",
".pdf",
".pgm",
".png",
".ppm",
".pnm",
".ppt",
".pps",
".ps",
".qt",
".ras",
".rdf",
".rgb",
".rss",
".svg",
".swf",
".tiff",
".tif",
".tsv",
".ttf",
".txt",
".vcf",
".wav",
".webm",
".woff",
".woff2",
".xbm",
".xlm",
".xls",
".xml",
".xpdl",
".xpm",
".xwd",
]; // in lower case
export const handler = async (event) => {
const request = event.Records[0].cf.request;
try {
return route(request);
} catch (e) {
// Never let a routing bug send a request to SpeedWorkers: force the
// failover to your origin instead
return forceFallback();
}
};
function route(request) {
// If the request origin is not the SW custom origin, then this is the
// second lambda call targeting the secondary origin after a failover
// (native, or forced by this lambda). We don't have anything to do
if (
request.origin.custom === undefined ||
request.origin.custom.domainName !== swPrimaryOriginDomain
) {
return request;
}
// Restore user-agent header (to replace Amazon CloudFront user-agent...)
if (request.headers["x-sw-user-agent"] !== undefined) {
request.headers["user-agent"] = [
{ key: "User-Agent", value: request.headers["x-sw-user-agent"][0].value },
];
}
// Ignore web sockets
if (
request.headers["upgrade"] !== undefined &&
request.headers["upgrade"][0].value.toLowerCase() === "websocket"
) {
return forceFallback();
}
// SpeedWorkers only handles GET and HEAD. The behavior should only allow
// these methods anyway: a forced failover does not work for other methods
if (request.method !== "GET" && request.method !== "HEAD") {
return forceFallback();
}
// The viewer-request lambda classifies every request; its decision travels
// in x-sw-request-type, which the origin request policy must forward.
// If the header is missing (viewer lambda not associated, or policy not
// forwarding it) or doesn't mark a bot, never let the request reach
// SpeedWorkers.
if (
request.headers["x-sw-request-type"] === undefined ||
!request.headers["x-sw-request-type"][0].value.startsWith("bot")
) {
return forceFallback();
}
// We need the host from the Viewer Request Lambda, or at least a rewrite origin value!
if (
request.headers["x-sw-host"] === undefined &&
(swRewriteOrigin === undefined || swRewriteOrigin === "")
) {
return forceFallback();
}
// Retrieve original host set by the viewer request lambda
let originalHost = "";
if (request.headers["x-sw-host"] !== undefined) {
originalHost = request.headers["x-sw-host"][0].value;
}
// Rebuild the complete uri
let url = "https://" + originalHost;
if (swRewriteOrigin !== undefined && swRewriteOrigin !== "") {
url = swRewriteOrigin;
}
url += request.uri;
if (request.querystring !== "") {
url += "?" + request.querystring;
}
// Ignore requests which url doesn't start with one of the url SW is enabled
if (!isUrlAllowed(url)) {
return forceFallback();
}
// Ignore resources that are not handled by SW
if (ignorePath(url)) {
return forceFallback();
}
// The request comes from a bot, let it go to the origin group with SW as primary origin
request.origin.custom.customHeaders["x-sw-uri"] = [
{ key: "x-sw-uri", value: url },
];
request.headers["x-sw-agent"] = [{ key: "x-sw-agent", value: swAgent }];
request.headers["x-sw-client-ip"] = [
{ key: "x-sw-client-ip", value: request.clientIp },
];
request.headers["host"] = [{ key: "host", value: swPrimaryOriginDomain }];
return request;
}
// Generate a 403 response matching the origin group failover criteria, so
// CloudFront retries the secondary origin (your true origin). Viewers never
// see this response unless the failover itself fails.
function forceFallback() {
return {
status: "403",
statusDescription: "Forbidden",
headers: {},
body:
"Forcing fallback to secondary origin!\n" +
" If you see this message, the secondary origin is not reachable and CloudFront returned the origin request lambda response.\n" +
" Make sure CloudFront can connect to your origin. Could be a headers issue (original Host header forwarded?)\n",
};
}
// Returns true if the url contains a path sw should ignore
function ignorePath(url) {
for (let i = 0; i < swIgnored.length; i++) {
if (url.toLowerCase().includes(swIgnored[i])) return true;
}
return false;
}
// Returns true if the url belongs to (starts with) an allowed url
function isUrlAllowed(url) {
if (swAllowedUrls.length === 0) return true;
for (let i = 0; i < swAllowedUrls.length; i++) {
if (url.toLowerCase().startsWith(swAllowedUrls[i])) return true;
}
return false;
}
