Skip to content
Workers
Visit Workers on GitHub
Set theme to dark (⇧+D)

Examples

Shows how to restrict access using the HTTP "Basic" schema.

/** * Shows how to restrict access using the HTTP "Basic" schema. * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication * @see https://tools.ietf.org/html/rfc7617 * * The user and password MUST NOT contain any control characters. * @see https://tools.ietf.org/html/rfc5234#appendix-B.1 * * A user-id containing a colon (":") character is invalid, as the * first colon in a user-pass string separates user and password. */const BASIC_USER = 'admin'const BASIC_PASS = 'admin'/** * Receives a HTTP request and replies with a response. * @param {Request} request * @returns {Promise<Response>} */async function handleRequest(request) {  const { protocol, pathname } = new URL(request.url)
  // In the case of a "Basic" authentication, the exchange   // MUST happen over an HTTPS (TLS) connection to be secure.  if ('https:' !== protocol || 'https' !== request.headers.get('x-forwarded-proto')) {    throw new BadRequestException('Please use a HTTPS connection.')  }
  switch (pathname) {    case '/':      return new Response('Anyone can access the homepage.')
    case '/logout':      // Invalidate the "Authorization" header by returning a HTTP 401.      // We do not send a "WWW-Authenticate" header, as this would trigger      // a popup in the browser, immediately asking for credentials again.      return new Response('Logged out.', { status: 401 })
    case '/admin': {      // The "Authorization" header is sent when authenticated.      if (request.headers.has('Authorization')) {        // Throws exception when authorization fails.        const { user, pass } = basicAuthentication(request)        verifyCredentials(user, pass)
        // Only returns this response when no exception is thrown.        return new Response('You have private access.', {          status: 200,          headers: {            'Cache-Control': 'no-store'          }        })      }
      // Not authenticated.      return new Response('You need to login.', {        status: 401,        headers: {          // Prompts the user for credentials.          'WWW-Authenticate': 'Basic realm="my scope", charset="UTF-8"'        }      })    }
    case '/favicon.ico':    case '/robots.txt':      return new Response(null, { status: 204 })  }
  return new Response('Not Found.', { status: 404 })}/** * Throws exception on verification failure. * @param {string} user * @param {string} pass * @throws {UnauthorizedException} */function verifyCredentials(user, pass) {  if (BASIC_USER !== user) {    throw new UnauthorizedException('Invalid username.')  }
  if (BASIC_PASS !== pass) {    throw new UnauthorizedException('Invalid password.')  }}/** * Parse HTTP Basic Authorization value. * @param {Request} request * @throws {BadRequestException} * @returns {{ user: string, pass: string }} */function basicAuthentication(request) {  const Authorization = request.headers.get('Authorization')
  const [scheme, encoded] = Authorization.split(' ')
  // The Authorization header must look like "Basic user:encoded".  if (scheme !== 'Basic') throw new BadRequestException('Malformed authorization header.')
  // Decode the base64 value.  const decoded = atob(encoded)
  // The username & password are split by the first colon.  const seperatorPosition = decoded.indexOf(':')    // NOTE: Without `.normalize()` unicode characters could fail verification.  // @see https://dev.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
  return {     // The username is the value before the first colon.    user: decoded.substring(0, seperatorPosition).normalize(),    // The password is everything after the first colon.    pass: decoded.substring(seperatorPosition + 1).normalize(),  }}
function UnauthorizedException(reason) {  this.status = 401  this.statusText = 'Unauthorized'  this.reason = reason}
function BadRequestException(reason) {  this.status = 400  this.statusText = 'Bad Request'  this.reason = reason}
addEventListener('fetch', event => {  event.respondWith(    handleRequest(event.request).catch(err => {      const message = err.reason || err.stack || 'Unknown Error'
      return new Response(message, {        status: err.status || 500,        statusText: err.statusText || null,        headers: {          'Content-Type': 'text/plain;charset=UTF-8',          // Disables caching by default.          'Cache-Control': 'no-store',          // Returns the "Content-Length" header for HTTP HEAD requests.          'Content-Length': message.length,        }      })    })  )})

Cache POST requests using the Cache API.

async function sha256(message) {  // encode as UTF-8  const msgBuffer = new TextEncoder().encode(message)
  // hash the message  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer)
  // convert ArrayBuffer to Array  const hashArray = Array.from(new Uint8Array(hashBuffer))
  // convert bytes to hex string  const hashHex = hashArray.map(b => ("00" + b.toString(16)).slice(-2)).join("")  return hashHex}
async function handlePostRequest(event) {  const request = event.request  const body = await request.clone().text()
  // Hash the request body to use it as a part of the cache key  const hash = await sha256(body)  const cacheUrl = new URL(request.url)
  // Store the URL in cache by prepending the body's hash  cacheUrl.pathname = "/posts" + cacheUrl.pathname + hash
  // Convert to a GET to be able to cache  const cacheKey = new Request(cacheUrl.toString(), {    headers: request.headers,    method: "GET",  })
  const cache = caches.default
  // Find the cache key in the cache  let response = await cache.match(cacheKey)
  // Otherwise, fetch response to POST request from origin  if (!response) {    response = await fetch(request)    event.waitUntil(cache.put(cacheKey, response.clone()))  }  return response}
addEventListener("fetch", event => {  try {    const request = event.request    if (request.method.toUpperCase() === "POST")      return event.respondWith(handlePostRequest(event))    return event.respondWith(fetch(request))  } catch (e) {    return event.respondWith(new Response("Error thrown " + e.message))  }})

Return a response based on the incoming request's URL, HTTP method, User Agent, IP address, ASN or device type.

const BLOCKED_HOSTNAMES = ["nope.mywebsite.com", "bye.website.com"]
async function handleRequest(request) {  // Return a new Response based on a URL's hostname  const url = new URL(request.url)
  if (BLOCKED_HOSTNAMES.includes(url.hostname)) {    return new Response("Blocked Host", { status: 403 })  }
  // Block paths ending in .doc or .xml based on the URL's file extension  const forbiddenExtRegExp = new RegExp(/\.(doc|xml)$/)
  if (forbiddenExtRegExp.test(url.pathname)) {    return new Response("Blocked Extension", { status: 403 })  }
  // On HTTP method  if (request.method === "POST") {    return new Response("Response for POST")  }
  // On User Agent  const userAgent = request.headers.get("User-Agent") || ""  if (userAgent.includes("bot")) {    return new Response("Block User Agent containing bot", { status: 403 })  }
  // On Client's IP address  const clientIP = request.headers.get("CF-Connecting-IP")  if (clientIP === "1.2.3.4") {    return new Response("Block the IP 1.2.3.4", { status: 403 })  }
  // On ASN  if (request.cf && request.cf.asn == 64512) {    return new Response("Block the ASN 64512 response")  }
  // On Device Type  // Requires Enterprise "CF-Device-Type Header" zone setting or  // Page Rule with "Cache By Device Type" setting applied.  const device = request.headers.get("CF-Device-Type")  if (device === "mobile") {    return Response.redirect("https://mobile.example.com")  }
  console.error(    "Getting Client's IP address, device type, and ASN are not supported in playground. Must test on a live worker",  )  return fetch(request)}
addEventListener("fetch", event => {  event.respondWith(handleRequest(event.request))})

Add the necessary CORS headers to a third party API response.

// We support the GET, POST, HEAD, and OPTIONS methods from any origin,// and allow any header on requests. These headers must be present// on all responses to all CORS preflight requests. In practice, this means// all responses to OPTIONS requests.const corsHeaders = {  "Access-Control-Allow-Origin": "*",  "Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS",  "Access-Control-Max-Age": "86400",}
// The URL for the remote third party API you want to fetch from// but does not implement CORSconst API_URL = "https://examples.cloudflareworkers.com/demos/demoapi"
// The endpoint you want the CORS reverse proxy to be onconst PROXY_ENDPOINT = "/corsproxy/"
// The rest of this snippet for the demo pageasync function rawHtmlResponse(html) {  return new Response(html, {    headers: {      "content-type": "text/html;charset=UTF-8",    },  })}
const DEMO_PAGE = `  <!DOCTYPE html>  <html>  <body>    <h1>API GET without CORS Proxy</h1>    <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#Checking_that_the_fetch_was_successful">Shows TypeError: Failed to fetch since CORS is misconfigured</a>    <p id="noproxy-status"/>    <code id="noproxy">Waiting</code>    <h1>API GET with CORS Proxy</h1>    <p id="proxy-status"/>    <code id="proxy">Waiting</code>    <h1>API POST with CORS Proxy + Preflight</h1>    <p id="proxypreflight-status"/>    <code id="proxypreflight">Waiting</code>    <script>    let reqs = {};    reqs.noproxy = async () => {      let response = await fetch("${API_URL}")      return await response.json()    }    reqs.proxy = async () => {      let response = await fetch(window.location.origin + "${PROXY_ENDPOINT}?apiurl=${API_URL}")      return await response.json()    }    reqs.proxypreflight = async () => {      const reqBody = {        msg: "Hello world!"      }      let response = await fetch(window.location.origin + "${PROXY_ENDPOINT}?apiurl=${API_URL}", {        method: "POST",        headers: {          "Content-Type": "application/json"        },        body: JSON.stringify(reqBody),      })      return await response.json()    }    (async () => {      for (const [reqName, req] of Object.entries(reqs)) {        try {          let data = await req()          document.getElementById(reqName).innerHTML = JSON.stringify(data)        } catch (e) {          document.getElementById(reqName).innerHTML = e        }      }    })()    </script>  </body>  </html>`
async function handleRequest(request) {  const url = new URL(request.url)  let apiUrl = url.searchParams.get("apiurl")
  if (apiUrl == null) {    apiUrl = API_URL  }
  // Rewrite request to point to API url. This also makes the request mutable  // so we can add the correct Origin header to make the API server think  // that this request isn't cross-site.  request = new Request(apiUrl, request)  request.headers.set("Origin", new URL(apiUrl).origin)  let response = await fetch(request)
  // Recreate the response so we can modify the headers  response = new Response(response.body, response)
  // Set CORS headers  response.headers.set("Access-Control-Allow-Origin", url.origin)
  // Append to/Add Vary header so browser will cache response correctly  response.headers.append("Vary", "Origin")
  return response}
function handleOptions(request) {  // Make sure the necessary headers are present  // for this to be a valid pre-flight request  let headers = request.headers;  if (    headers.get("Origin") !== null &&    headers.get("Access-Control-Request-Method") !== null &&    headers.get("Access-Control-Request-Headers") !== null  ){    // Handle CORS pre-flight request.    // If you want to check or reject the requested method + headers    // you can do that here.    let respHeaders = {      ...corsHeaders,    // Allow all future content Request headers to go back to browser    // such as Authorization (Bearer) or X-Client-Name-Version      "Access-Control-Allow-Headers": request.headers.get("Access-Control-Request-Headers"),    }
    return new Response(null, {      headers: respHeaders,    })  }  else {    // Handle standard OPTIONS request.    // If you want to allow other HTTP Methods, you can do that here.    return new Response(null, {      headers: {        Allow: "GET, HEAD, POST, OPTIONS",      },    })  }}
addEventListener("fetch", event => {  const request = event.request  const url = new URL(request.url)  if(url.pathname.startsWith(PROXY_ENDPOINT)){    if (request.method === "OPTIONS") {      // Handle CORS preflight requests      event.respondWith(handleOptions(request))    }    else if(      request.method === "GET" ||      request.method === "HEAD" ||      request.method === "POST"    ){      // Handle requests to the API server      event.respondWith(handleRequest(request))    }    else {      event.respondWith(        new Response(null, {          status: 405,          statusText: "Method Not Allowed",        }),      )    }  }  else {    // Serve demo page    event.respondWith(rawHtmlResponse(DEMO_PAGE))  }})

Protect sensitive data to prevent data loss, and send alerts to a webhooks server in the event of a data breach.

const DEBUG = trueconst SOME_HOOK_SERVER = "https://webhook.flow-wolf.io/hook"/** * Alert a data breach by posting to a webhook server */async function postDataBreach(request) {  const trueClientIp = request.headers.get("cf-connecting-ip")  const epoch = new Date().getTime()  const body = {    ip: trueClientIp,    time: epoch,    request: request  }  const init = {    body: JSON.stringify(body),    method: "POST",    headers: {      "content-type": "application/json;charset=UTF-8"    },  }  return await fetch(SOME_HOOK_SERVER, init)}/** * Define personal data with regular expressions. * Respond with block if credit card data, and strip * emails and phone numbers from the response. * Execution will be limited to MIME type "text/*". */async function handleRequest(request) {  const response = await fetch(request)
  // Return origin response, if response wasn’t text  const contentType = response.headers.get("content-type") || ""  if (!contentType.toLowerCase().includes("text/")) {    return response  }
  let text = await response.text()
  // When debugging replace the response  // from the origin with an email  text = DEBUG    ? text.replace("You may use this", "me@example.com may use this")    : text
  const sensitiveRegexsMap = {    creditCard: String.raw`\b(?:4[0-9]{12}(?:[0-9]{3})?|(?:5[1-5][0-9]{2}|222[1-9]|22[3-9][0-9]|2[3-6][0-9]{2}|27[01][0-9]|2720)[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|6(?:011|5[0-9]{2})[0-9]{12}|(?:2131|1800|35\d{3})\d{11})\b`,    email: String.raw`\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b`,    phone: String.raw`\b07\d{9}\b`  }
  for (const kind in sensitiveRegexsMap) {    const sensitiveRegex = new RegExp(sensitiveRegexsMap[kind], "ig")    const match = await sensitiveRegex.test(text)    if (match) {      // Alert a data breach      await postDataBreach(request)
      // Respond with a block if credit card,      // otherwise replace sensitive text with `*`s      return kind === "creditCard"        ? new Response(kind + " found\nForbidden\n", {            status: 403,            statusText: "Forbidden"          })        : new Response(text.replace(sensitiveRegex, "**********"), response)    }  }  return new Response(text, response)}
addEventListener("fetch", event => {  event.respondWith(handleRequest(event.request))})

Fetch weather data from an API using the user's geolocation data.

addEventListener('fetch', event => {  event.respondWith(handleRequest(event.request))})
async function handleRequest(request) {  let endpoint = "https://api.waqi.info/feed/geo:"  const token = "" //Use a token from https://aqicn.org/api/  let html_style = `body{padding:6em; font-family: sans-serif;} h1{color:#f6821f}`    let html_content = "<h1>Weather 🌦</h1>"
  latitude = request.cf.latitude  longitude = request.cf.longitude  endpoint+= `${latitude};${longitude}/?token=${token}`  const init = {    headers: {      "content-type": "application/json;charset=UTF-8",    },  }
  const response = await fetch(endpoint,init)  const content = await response.json()
  html_content += `<p>This is a demo using Workers geolocation data. </p>`  html_content += `You are located at: ${latitude},${longitude}.</p>`  html_content += `<p>Based off sensor data from <a href="${content.data.city.url}">${content.data.city.name}</a>:</p>`  html_content += `<p>The AQI level is: ${content.data.aqi}.</p>`  html_content += `<p>The N02 level is: ${content.data.iaqi.no2.v}.</p>`  html_content += `<p>The O3 level is: ${content.data.iaqi.o3.v}.</p>`  html_content += `<p>The temperature is: ${content.data.iaqi.t.v}°C.</p>`
  let html = `<!DOCTYPE html><head>  <title>Geolocation: Weather</title></head><body>  <style>${html_style}</style>  <div id="container">  ${html_content}  </div></body>`
  return new Response(html, {    headers: {      "content-type": "text/html;charset=UTF-8",    },})}

Personalize website styling based on localized user time.

addEventListener('fetch', event => {  event.respondWith(handleRequest(event.request))})
let grads = [  [{color:"00000c",position:0},{color:"00000c",position:0}],  [{color:"020111",position:85},{color:"191621",position:100}],  [{color:"020111",position:60},{color:"20202c",position:100}],  [{color:"020111",position:10},{color:"3a3a52",position:100}],  [{color:"20202c",position:0},{color:"515175",position:100}],  [{color:"40405c",position:0},{color:"6f71aa",position:80},{color:"8a76ab",position:100}],  [{color:"4a4969",position:0},{color:"7072ab",position:50},{color:"cd82a0",position:100}],  [{color:"757abf",position:0},{color:"8583be",position:60},{color:"eab0d1",position:100}],  [{color:"82addb",position:0},{color:"ebb2b1",position:100}],  [{color:"94c5f8",position:1},{color:"a6e6ff",position:70},{color:"b1b5ea",position:100}],  [{color:"b7eaff",position:0},{color:"94dfff",position:100}],  [{color:"9be2fe",position:0},{color:"67d1fb",position:100}],  [{color:"90dffe",position:0},{color:"38a3d1",position:100}],  [{color:"57c1eb",position:0},{color:"246fa8",position:100}],  [{color:"2d91c2",position:0},{color:"1e528e",position:100}],  [{color:"2473ab",position:0},{color:"1e528e",position:70},{color:"5b7983",position:100}],  [{color:"1e528e",position:0},{color:"265889",position:50},{color:"9da671",position:100}],  [{color:"1e528e",position:0},{color:"728a7c",position:50},{color:"e9ce5d",position:100}],  [{color:"154277",position:0},{color:"576e71",position:30},{color:"e1c45e",position:70},{color:"b26339",position:100}],  [{color:"163C52",position:0},{color:"4F4F47",position:30},{color:"C5752D",position:60},{color:"B7490F",position:80},{color:"2F1107",position:100}],  [{color:"071B26",position:0},{color:"071B26",position:30},{color:"8A3B12",position:80},{color:"240E03",position:100}],  [{color:"010A10",position:30},{color:"59230B",position:80},{color:"2F1107",position:100}],  [{color:"090401",position:50},{color:"4B1D06",position:100}],  [{color:"00000c",position:80},{color:"150800",position:100}],];
function toCSSGradient(hour){   var css = "linear-gradient(to bottom, ";  var data = grads[hour]  var len = data.length;     for (var i=0;i<len;i++)  {      var item = data[i];     css+= " #" + item.color + " " + item.position + "%";     if ( i<len-1 ) css += ",";  }  return css + ")"; }
async function handleRequest(request) {  let html_content = ""  let html_style = `  html{width:100vw; height:100vh;}  body{padding:0; margin:0 !important;height:100%;}  #container {    display: flex;    flex-direction:column;    align-items: center;    justify-content: center;    height: 100%;    color:white;    font-family:sans-serif;  }`
  const timezone = request.cf.timezone  let localized_date = new Date(new Date().toLocaleString('en-US', {timeZone: timezone}))  let hour = localized_date.getHours()  let minutes = localized_date.getMinutes()
  html_content += "<h1>" + hour + ":"+ minutes + "</h1>"  html_content += "<p>" + timezone + "<br/></p>"  html_style += "body{background:"+toCSSGradient(hour)+";}"
  let html = `<!DOCTYPE html><head>  <title>Geolocation: Customized Design</title></head><body>  <style> ${html_style}</style>  <div id="container">    ${html_content}  </div></body>`
  return new Response(html, {    headers: {      "content-type": "text/html;charset=UTF-8",    },})}

Create a modified request with edited properties based off of an incoming request.

/** * Example someHost is set up to return raw JSON * @param {string} someUrl the URL to send the request to, since we are setting hostname too only path is applied * @param {string} someHost the host the request will resolve too */const someHost = "example.com"const someUrl = "https://foo.example.com/api.js"
async function handleRequest(request) {  /**   * The best practice is to only assign new properties on the request   * object (i.e. RequestInit props) using either a method or the constructor   */  const newRequestInit = {    // Change method    method: "POST",    // Change body    body: JSON.stringify({ bar: "foo" }),    // Change the redirect mode.    redirect: "follow",    // Change headers, note this method will erase existing headers    headers: {      "Content-Type": "application/json",    },    // Change a Cloudflare feature on the outbound response    cf: { apps: false },  }
  // Change just the host  const url = new URL(someUrl)
  url.hostname = someHost
  // Best practice is to always use the original request to construct the new request  // to clone all the attributes. Applying the URL also requires a constructor  // since once a Request has been constructed, its URL is immutable.  const newRequest = new Request(    url.toString(),    new Request(request, newRequestInit),  )
  // Set headers using method  newRequest.headers.set("X-Example", "bar")  newRequest.headers.set("Content-Type", "application/json")  try {    return await fetch(newRequest)  } catch (e) {    return new Response(JSON.stringify({ error: e.message }), { status: 500 })  }}
addEventListener("fetch", event => {  event.respondWith(handleRequest(event.request))})

Serve an HTML form, then read POST requests. Use also to read JSON or POST data from an incoming request.

/** * rawHtmlResponse returns HTML inputted directly * into the worker script * @param {string} html */function rawHtmlResponse(html) {  const init = {    headers: {      "content-type": "text/html;charset=UTF-8",    },  }  return new Response(html, init)}/** * readRequestBody reads in the incoming request body * Use await readRequestBody(..) in an async function to get the string * @param {Request} request the incoming request to read from */async function readRequestBody(request) {  const { headers } = request  const contentType = headers.get("content-type") || ""
  if (contentType.includes("application/json")) {    return JSON.stringify(await request.json())  }  else if (contentType.includes("application/text")) {    return await request.text()  }  else if (contentType.includes("text/html")) {    return await request.text()  }  else if (contentType.includes("form")) {    const formData = await request.formData()    const body = {}    for (const entry of formData.entries()) {      body[entry[0]] = entry[1]    }    return JSON.stringify(body)  }  else {    const myBlob = await request.blob()    const objectURL = URL.createObjectURL(myBlob)    return objectURL  }}
const someForm = `  <!DOCTYPE html>  <html>  <body>  <h1>Hello World</h1>  <p>This is all generated using a Worker</p>  <form action="/demos/requests" method="post">    <div>      <label for="say">What  do you want to say?</label>      <input name="say" id="say" value="Hi">    </div>    <div>      <label for="to">To who?</label>      <input name="to" id="to" value="Mom">    </div>    <div>      <button>Send my greetings</button>    </div>  </form>  </body>  </html>  `
async function handleRequest(request) {  const reqBody = await readRequestBody(request)  const retBody = `The request body sent in was ${reqBody}`  return new Response(retBody)}
addEventListener("fetch", event => {  const { request } = event  const { url } = request
  if (url.includes("form")) {    return event.respondWith(rawHtmlResponse(someForm))  }  if (request.method === "POST") {    return event.respondWith(handleRequest(request))  }  else if (request.method === "GET") {    return event.respondWith(new Response(`The request was a GET`))  }})

Sign and verify a request using the HMAC and SHA-256 algorithms or return a 403.

// We will need some super-secret data to use as a symmetric key.const encoder = new TextEncoder()const secretKeyData = encoder.encode("my secret symmetric key")
// Convert a ByteString (a string whose code units are all in the range// [0, 255]), to a Uint8Array. If you pass in a string with code units larger// than 255, their values will overflow!function byteStringToUint8Array(byteString) {  const ui = new Uint8Array(byteString.length)  for (let i = 0; i < byteString.length; ++i) {    ui[i] = byteString.charCodeAt(i)  }  return ui}
async function verifyAndFetch(request) {  const url = new URL(request.url)
  // If the path does not begin with our protected prefix, just pass the request  // through.  if (!url.pathname.startsWith("/verify/")) {    return fetch(request)  }
  // Make sure we have the minimum necessary query parameters.  if (!url.searchParams.has("mac") || !url.searchParams.has("expiry")) {    return new Response("Missing query parameter", { status: 403 })  }
  const key = await crypto.subtle.importKey(    "raw",    secretKeyData,    { name: "HMAC", hash: "SHA-256" },    false,    ["verify"],  )
  // Extract the query parameters we need and run the HMAC algorithm on the  // parts of the request we are authenticating: the path and the expiration  // timestamp.  const expiry = Number(url.searchParams.get("expiry"))  const dataToAuthenticate = url.pathname + expiry
  // The received MAC is Base64-encoded, so we have to go to some trouble to  // get it into a buffer type that crypto.subtle.verify() can read.  const receivedMacBase64 = url.searchParams.get("mac")  const receivedMac = byteStringToUint8Array(atob(receivedMacBase64))
  // Use crypto.subtle.verify() to guard against timing attacks. Since HMACs use  // symmetric keys, we could implement this by calling crypto.subtle.sign() and  // then doing a string comparison -- this is insecure, as string comparisons  // bail out on the first mismatch, which leaks information to potential  // attackers.  const verified = await crypto.subtle.verify(    "HMAC",    key,    receivedMac,    encoder.encode(dataToAuthenticate),  )
  if (!verified) {    const body = "Invalid MAC"    return new Response(body, { status: 403 })  }
  if (Date.now() > expiry) {    const body = `URL expired at ${new Date(expiry)}`    return new Response(body, { status: 403 })  }
  // We have verified the MAC and expiration time; we are good to pass the request  // through.  return fetch(request)}
addEventListener("fetch", event => {  event.respondWith(verifyAndFetch(event.request))})