Casos de uso de funções do Edge
Os casos de uso a seguir são fornecidos apenas como exemplos e não se destinam à duplicação exata em seu ambiente.
Tenha cuidado ao testar qualquer um dos códigos a seguir, pois isso pode causar uma interrupção no serviço.
Teste A/B
É possível criar uma função do CIS Edge para controlar os testes A/B.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
const name = 'experiment-0'
let group // 'control' or 'test', set below
let isNew = false // is the group newly-assigned?
// Determine which group this request is in.
const cookie = request.headers.get('Cookie')
if (cookie && cookie.includes(`${name}=control`)) {
group = 'control'
} else if (cookie && cookie.includes(`${name}=test`)) {
group = 'test'
} else {
// 50/50 Split
group = Math.random() < 0.5 ? 'control' : 'test'
isNew = true
}
// We'll prefix the request path with the experiment name. This way,
// the origin server merely has to have two copies of the site under
// top-level directories named "control" and "test".
let url = new URL(request.url)
// Note that `url.pathname` always begins with a `/`, so we don't
// need to explicitly add one after `${group}`.
url.pathname = `/${group}${url.pathname}`
const modifiedRequest = new Request(url, {
method: request.method,
headers: request.headers
})
const response = await fetch(modifiedRequest)
if (isNew) {
// The experiment was newly-assigned, so add a Set-Cookie header
// to the response.
const newHeaders = new Headers(response.headers)
newHeaders.append('Set-Cookie', `${name}=${group}; path=/`)
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: newHeaders
})
} else {
// Return response unmodified.
return response
}
}
Incluindo um cabeçalho de resposta
Para modificar os cabeçalhos de resposta, primeiro faça uma cópia da resposta para que você possa torná-la mutável. Em seguida, você pode usar a interface Headers para adicionar, alterar ou remover cabeçalhos.
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
/**
* Set the `x-my-header` header
* @param {Request} request
*/
async function handleRequest(request) {
let response = await fetch(request);
// Make the headers mutable by re-constructing the Response.
response = new Response(response.body, response);
response.headers.set('x-my-header', 'custom value');
return response;
}
Agregando diversas solicitações
Este exemplo faz várias solicitações a diferentes endpoints de API, agrega as respostas e as envia de volta como uma única resposta.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
/**
* Make multiple requests,
* aggregate the responses and
* send it back as a single response
*/
async function fetchAndApply(request) {
const init = {
method: 'GET',
headers: {'Authorization': 'XXXXXX'}
}
const [btcResp, ethResp, ltcResp] = await Promise.all([
fetch('https://api.coinbase.com/v2/prices/BTC-USD/spot', init),
fetch('https://api.coinbase.com/v2/prices/ETH-USD/spot', init),
fetch('https://api.coinbase.com/v2/prices/LTC-USD/spot', init)
])
const btc = await btcResp.json()
const eth = await ethResp.json()
const ltc = await ltcResp.json()
let combined = {}
combined['btc'] = btc['data'].amount
combined['ltc'] = ltc['data'].amount
combined['eth'] = eth['data'].amount
const responseInit = {
headers: {'Content-Type': 'application/json'}
}
return new Response(JSON.stringify(combined), responseInit)
}
Roteamento condicional
A maneira mais fácil de fornecer conteúdo diferente com base no dispositivo que está sendo usado é reescrever o site URL da solicitação com base na condição que lhe interessa. Consulte os exemplos a seguir.
Tipo de dispositivo
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
let uaSuffix = ''
const ua = request.headers.get('user-agent')
if (ua.match(/iphone/i) || ua.match(/ipod/i)) {
uaSuffix = '/mobile'
} else if (ua.match(/ipad/i)) {
uaSuffix = '/tablet'
}
return fetch(request.url + uaSuffix, request)
}
Cabeçalhos customizados
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
let suffix = ''
//Assuming that the client is sending a custom header
const cryptoCurrency = request.headers.get('X-Crypto-Currency')
if (cryptoCurrency === 'BTC') {
suffix = '/btc'
} else if (cryptoCurrency === 'XRP') {
suffix = '/xrp'
} else if (cryptoCurrency === 'ETH') {
suffix = '/eth'
}
return fetch(request.url + suffix, request)
}
Proteção de hot-link
É possível usar as funções do CIS Edge para proteger os hot-links nas propriedades da web.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
/**
* If the browser is requesting an image and
* the referer does not match your host
* we redirect the request to your page
*/
async function fetchAndApply(request) {
// Fetch the response.
let response = await fetch(request)
// If it's an image, engage hotlink protection based on the
// Referer header.
let referer = request.headers.get('Referer')
let contentType = response.headers.get('Content-Type') || ''
if (referer && contentType.startsWith('image/')) {
// It's an image and there's a Referer. Verify that the
// hostnames match.
if (new URL(referer).hostname !==
new URL(request.url).hostname) {
// Hosts don't match. This is a hotlink. Redirect the
// user to our homepage.
return new Response('', {
status: 302,
headers: {
'Location': '/'
}
})
}
}
// Everything is fine, return the response normally.
return response
}
Respostas sem origem
É possível retornar respostas diretamente da borda. Não há necessidade de enviar uma solicitação para sua origem.
Ignorar solicitações de HTTP POST e PUT
Ignorar solicitações de POST e PUT de HTTP. Esse fragmento permite que todas as outras solicitações sejam transmitidas à origem.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
if (request.method === 'POST' || request.method === 'PUT') {
return new Response('Sorry, this page is not available.',
{ status: 403, statusText: 'Forbidden' })
}
return fetch(request)
}
Negar uma aranha ou um crawler
Proteja sua origem contra aranhas ou crawlers indesejados. Nesse caso, se o agente do usuário for um "robô inoportuno", a função do Edge retornará a resposta em vez de enviar a solicitação para a origem.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
if (request.headers.get('user-agent').includes('annoying_robot')) {
return new Response('Sorry, this page is not available.',
{ status: 403, statusText: 'Forbidden' })
}
return fetch(request)
}
Evitar a conexão de um IP específico
Inclua endereços IP na lista de bloqueios. Esse trecho de código impede que um IP específico (nesse caso, 225.0.0.1) se conecte à origem.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
async function fetchAndApply(request) {
if (request.headers.get('cf-connecting-ip') === '225.0.0.1') {
return new Response('Sorry, this page is not available.',
{ status: 403, statusText: 'Forbidden' })
}
return fetch(request)
}
Solicitações de Post
Lendo o conteúdo de uma solicitação de HTTP POST:
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
/**
* Making a curl request that looks like
* curl -X POST --data 'key=world' example.com
* or
* curl -X POST --form 'key=world' example.com
*/
async function fetchAndApply(request) {
try {
const postData = await request.formData();
return new Response(`hello ${postData.get('key')}`)
} catch (err) {
return new Response('could not unbundle post data')
}
}
Criando uma solicitação de HTTP POST por meio de uma função do Edge:
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
/**
* Create a POST request with body 'key=world'
* Here, we are assuming that example.com acknowledges the POST request with body key=world
*/
async function fetchAndApply(request) {
let content = 'key=world'
let headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
const init = {
method: 'POST',
headers: headers,
body: content
}
const response = await fetch('https://example.com', init)
console.log('Got response', response)
return response
}
Solicitações assinadas
Um método de autenticação de URL comum conhecido como assinatura de solicitação pode ser implementado em uma função do Edge com a ajuda da Web Crypto API.
No exemplo apresentado aqui, o site CIS autentica o caminho de um URL juntamente com um registro de data e hora de expiração que o acompanha usando um código de autenticação de mensagem baseado em hash (HMAC) com um algoritmo de digestão SHA-256. Para buscar com êxito um recurso autenticado, o agente do usuário precisa fornecer o caminho correto, o registro de data e hora de expiração e o HMAC usando os parâmetros de consulta Se algum desses três parâmetros for violado, a solicitação falhará.
A autenticidade do carimbo de data/hora de expiração é coberta pelo HMAC, o que significa que você pode confiar que o carimbo de data/hora fornecido pelo usuário estará correto se o HMAC estiver correto e quando o URL expirar. Você também pode determinar se um URL em sua posse está vencido.
Verificando solicitações assinadas
Este exemplo verifica o HMAC para qualquer solicitação URL em que o nome do caminho começa com /verify/.
Para conveniência de depuração, essa função do Edge retorna uma mensagem 403 se o URL ou o HMAC for inválido ou se o URL tiver expirado. Talvez você deseje retornar 404 em uma implementação real.
addEventListener('fetch', event => {
event.respondWith(verifyAndFetch(event.request))
})
async function verifyAndFetch(request) {
const url = new URL(request.url)
// If the path doesn't 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 })
}
// We'll need some super-secret data to use as a symmetric key.
const encoder = new TextEncoder()
const secretKeyData = encoder.encode("my secret symmetric key")
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're 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've verified the MAC and expiration time; we're good to pass the request
// through.
return fetch(request)
}
// 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 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
}
Gerando solicitações assinadas
Normalmente, as solicitações assinadas são entregues a você de alguma forma externa, como um e-mail, ou você mesmo gera uma se tiver a chave simétrica. Você também pode gerar as solicitações assinadas em uma função do Edge.
Para qualquer solicitação URL que comece com /generate/, CIS substitui /generate/ por /verify/, assina o caminho resultante com seu registro de data e hora e retorna o URL completo e assinado no corpo
da resposta.
addEventListener('fetch', event => {
const url = new URL(event.request.url)
const prefix = "/generate/"
if (url.pathname.startsWith(prefix)) {
// Replace the "/generate/" path prefix with "/verify/", which we
// use in the first example to recognize authenticated paths.
url.pathname = `/verify/${url.pathname.slice(prefix.length)}`
event.respondWith(generateSignedUrl(url))
} else {
event.respondWith(fetch(event.request))
}
})
async function generateSignedUrl(url) {
// We'll need some super-secret data to use as a symmetric key.
const encoder = new TextEncoder()
const secretKeyData = encoder.encode("my secret symmetric key")
const key = await crypto.subtle.importKey(
"raw", secretKeyData,
{ name: "HMAC", hash: "SHA-256" },
false, [ "sign" ]
)
// Signed requests expire after one minute. Note that you could choose
// expiration durations dynamically, depending on, e.g. the path or a query
// parameter.
const expirationMs = 60000
const expiry = Date.now() + expirationMs
const dataToAuthenticate = url.pathname + expiry
const mac = await crypto.subtle.sign(
"HMAC", key,
encoder.encode(dataToAuthenticate)
)
// `mac` is an ArrayBuffer, so we need to jump through a couple hoops to get
// it into a ByteString, then a Base64-encoded string.
const base64Mac = btoa(String.fromCharCode(...new Uint8Array(mac)))
url.searchParams.set("mac", base64Mac)
url.searchParams.set("expiry", expiry)
return new Response(url)
}
Transmitindo respostas
Um script de função do Edge não precisa preparar todo o seu corpo de resposta antes de enviar uma resposta para event.respondWith(). Usando um TransformStream,, você pode transmitir um corpo de resposta depois de enviar a parte
frontal da resposta (por exemplo, a linha de status HTTP e os cabeçalhos). Essa simplificação ajuda o site CIS a minimizar o tempo do visitante até o primeiro byte e a quantidade de buffering que deve ser feito no script da função do Edge.
A minimização do buffer é especialmente importante quando corpos de resposta maiores do que o limite de memória da função do Edge devem ser processados ou transformados. Nesses casos, a transmissão é a única estratégia de implementação viável.
O serviço CIS Edge Function já realiza transmissões por padrão sempre que possível. Essas APIs serão necessárias somente se você desejar modificar o corpo de resposta de alguma maneira, enquanto mantém o comportamento de fluxo. Se o script da função do Edge passar respostas de subsolicitação de volta para o cliente literalmente, sem ler seus corpos, a manipulação do corpo já será ideal.
Passagem de fluxo
Introdução ao exemplo de passagem mínima a seguir.
addEventListener("fetch", event => {
event.respondWith(fetchAndStream(event.request))
})
async function fetchAndStream(request) {
// Fetch from origin server.
let response = await fetch(request)
// Create an identity TransformStream (a.k.a. a pipe).
// The readable side becomes our new response body.
let { readable, writable } = new TransformStream()
// Start pumping the body. NOTE: No await!
streamBody(response.body, writable)
// ... and deliver our Response while that's running.
return new Response(readable, response)
}
async function streamBody(readable, writable) {
let reader = readable.getReader()
let writer = writable.getWriter()
while (true) {
const { done, value } = await reader.read()
if (done) break
// Optionally transform value's bytes here.
await writer.write(value)
}
await writer.close()
}
Alguns detalhes importantes a serem observados:
- Embora
streamBody()seja uma função assíncrona, você não deseja chamarawaitpara que ela não bloqueie o progresso da chamada da funçãofetchAndStream(). A função continua a ser executada de forma assíncrona para o período em que ela possui uma operaçãoreader.read()ouwriter.write()pendente. - Contrapressão:
awaita operação de leitura antes de chamar a operação de gravação. Da mesma forma,awaita operação de gravação antes de chamar a próxima operação de leitura. Seguir esse padrão propaga a contrapressão para a origem. - Conclusão: chame
writer.close()no final, o que sinaliza para o tempo de execução da função do Edge que você terminou de escrever esse corpo de resposta. Depois de ser chamado, o sitestreamBody()é encerrado - se esse comportamento for indesejável, passe sua promessa retornada paraFetchEvent.waitUntil(). Se o seu script nunca chamarwriter.close(), o corpo aparecerá truncado para o tempo de execução, embora ele possa continuar a funcionar como desejado.
Agregar e transmitir diversas solicitações
Esse caso de uso é semelhante à receita de agregação de várias solicitações, mas, dessa vez, você começa a escrever a resposta assim que verifica que cada sub-solicitação foi bem-sucedida, sem precisar esperar pelos corpos de resposta.
addEventListener('fetch', event => {
event.respondWith(fetchAndApply(event.request))
})
/**
* Make multiple requests,
* aggregate the responses and
* stream it back as a single response.
*/
async function fetchAndApply(request) {
const requestInit = {
headers: { "Authorization": "XXXXXX" }
}
const fetches = [
"https://api.coinbase.com/v2/prices/BTC-USD/spot",
"https://api.coinbase.com/v2/prices/ETH-USD/spot",
"https://api.coinbase.com/v2/prices/LTC-USD/spot"
].map(url => fetch(url, requestInit))
// Wait for each fetch() to complete.
let responses = await Promise.all(fetches)
// Make sure every subrequest succeeded.
if (!responses.every(r => r.ok)) {
return new Response(null, { status: 502 })
}
// Create a pipe and stream the response bodies out
// as a JSON array.
let { readable, writable } = new TransformStream()
streamJsonBodies(responses.map(r => r.body), writable)
return new Response(readable)
}
async function streamJsonBodies(bodies, writable) {
// We're presuming these bodies are JSON, so we
// concatenate them into a JSON array. Since we're
// streaming, we can't use JSON.stringify(), but must
// instead manually write an initial '[' before the
// bodies, interpolate ',' between them, and write a
// terminal ']' after them.
let writer = writable.getWriter()
let encoder = new TextEncoder()
await writer.write(encoder.encode("[\n"))
for (let i = 0; i < bodies.length; ++i) {
if (i > 0) {
await writer.write(encoder.encode(",\n"))
}
writer.releaseLock()
await bodies[i].pipeTo(writable, { preventClose: true })
writer = writable.getWriter()
}
await writer.write(encoder.encode("]"))
await writer.close()
}
O tempo de execução espera receber TypedArrays no lado legível de TransformStream. Portanto, você nunca passa uma sequência para writer.write(), apenas Uint 8Arrays.. Se precisar gravar uma sequência, use um TextEncoder.
Balanceador de carga customizado com funções do Edge
O balanceamento de carga ajuda a manter a escalabilidade e a confiabilidade dos sites que você hospeda. Você pode usar as funções do Edge para criar balanceadores de carga personalizados, projetados para atender às suas necessidades específicas.
const US_HOSTS = [
"0.us.example.com",
"1.us.example.com",
"2.us.example.com"
];
const IN_HOSTS = [
"0.in.example.com",
"1.in.example.com",
"2.in.example.com"
];
var COUNTRIES_MAP = {
IN: IN_HOSTS,
PK: IN_HOSTS,
BD: IN_HOSTS,
SL: IN_HOSTS,
NL: IN_HOSTS
}
addEventListener('fetch', event => {
var url = new URL(event.request.url);
var countryCode = event.request.headers.get('CF-IPCountry');
var hostnames = US_HOSTS;
if (COUNTRIES_MAP[countryCode]) {
hostnames = COUNTRIES_MAP[countryCode];
}
// Randomly pick the next host
var primary = hostnames[getRandomInt(hostnames.length)];
var primaryUrl = new URL(event.request.url);
primaryUrl.hostname = hostnames[primary];
// Fallback if there is no response within timeout
var timeoutId = setTimeout(function() {
var backup;
do {
// Naive solution to pick a backup host
backup = getRandomInt(hostnames.length);
} while(backup === primary);
var backupUrl = new URL(event.request.url);
backupUrl.hostname = hostnames[backup];
event.respondWith(fetch(backupUrl));
}, 2000 /* 2 seconds */);
fetch(primaryUrl)
.then(function(response) {
clearTimeout(timeoutId);
event.respondWith(response);
});
});
function getRandomInt(max) {
return Math.floor(Math.random() * max);
}
Armazenamento em cache usando busca
Determine como armazenar em cache um recurso, configurando TTLs, chaves de cache customizadas e cabeçalhos de cache em uma solicitação de busca.
async function handleRequest(request) {
const url = new URL(request.url)
// Only use the path for the cache key, removing query strings
// and always store using HTTPS, for example, https://www.example.com/file-uri-here
const someCustomKey = `https://${url.hostname}${url.pathname}`
let response = await fetch(request, {
cf: {
// Always cache this fetch regardless of content type
// for a max of 5 seconds before revalidating the resource
cacheTtl: 5,
cacheEverything: true,
//Enterprise only feature, see Cache API for other plans
cacheKey: someCustomKey,
},
})
// Reconstruct the Response object to make its headers mutable.
response = new Response(response.body, response)
//Set cache control headers to cache on browser for 25 minutes
response.headers.set("Cache-Control", "max-age=1500")
return response
}
addEventListener("fetch", event => {
return event.respondWith(handleRequest(event.request))
})
Armazenamento em cache de recursos HTML
// Force CIS to cache an asset
fetch(event.request, { cf: { cacheEverything: true } })
A configuração do nível de cache para Armazenar tudo em cache substitui a "capacidade de armazenamento em cache" padrão do ativo. Para TTL, o CIS ainda conta com cabeçalhos configurados pela origem.
Chaves de cache customizadas
Esse recurso está disponível apenas para clientes corporativos.
A chave de cache de uma solicitação é o que determina se duas solicitações são "iguais" para fins de cache. Se uma solicitação tiver a mesma chave de cache que alguma solicitação anterior, poderemos servir a mesma resposta em cache para ambas.
// Set cache key for this request to "some-string".
fetch(event.request, { cf: { cacheKey: "some-string" } })
CIS calcula a chave de cache para uma solicitação com base na solicitação URL, mas você pode querer que URLs diferentes sejam tratados como se fossem os mesmos para fins de cache. Por exemplo, se o conteúdo do seu site estiver hospedado em Amazon S3 e Google Cloud Storage (você tem o mesmo conteúdo em ambos os lugares), e você usar uma função de borda para equilibrar aleatoriamente entre os dois. No entanto, você não quer armazenar em cache duas cópias do seu conteúdo. Você pode usar chaves de cache personalizadas para armazenar em cache com base na solicitação original URL em vez da sub-solicitação URL.
addEventListener("fetch", (event) => {
let url = new URL(event.request.url)
if (Math.random() < 0.5) {
url.hostname = "example.s3.amazonaws.com"
}
else {
url.hostname = "example.storage.googleapis.com"
}
let request = new Request(url, event.request)
event.respondWith(
fetch(request, {
cf: { cacheKey: event.request.url },
})
)
})
Lembre-se de que as funções de borda que operam em nome de zonas diferentes não podem afetar o cache umas das outras. Você pode substituir as chaves de cache somente quando fizer solicitações dentro de sua própria zona (no exemplo anterior,
event.request.url era a chave armazenada) ou solicitações para hosts que não estejam em CIS. Quando você faz uma solicitação para outra zona CIS (por exemplo, uma zona que pertence a um cliente CIS diferente), essa zona controla
totalmente como seu próprio conteúdo é armazenado em cache em CIS; você não pode substituí-lo.
Substituir com base no código de resposta de origem
Esse recurso está disponível apenas para clientes corporativos.
// Force response to be cached for 86400 seconds for 200 status
// codes, 1 second for 404, and do not cache 500 errors.
fetch(request, {
cf: { cacheTtlByStatus: { "200-299": 86400, 404: 1, "500-599": 0 } },
})
Esta opção é uma versão do recurso cacheTtl que escolhe um TTL com base no código de status da resposta e não configura automaticamente cacheEverything: true. Se a resposta a essa solicitação tiver um código de status
que corresponda, o CIS armazenará em cache pelo tempo instruído e substituirá as diretivas de cache enviadas pela origem.
Interpretação de TTL
Os seguintes valores TTL são interpretados por CIS.
- Valores positivos: indique em segundos por quanto tempo o CIS deve armazenar em cache o ativo.
0: o ativo é armazenado em cache mas expira imediatamente (revalidar toda vez por meio da origem).-1ou qualquer valor negativo: instrui o CIS a não armazenar em cache de forma alguma.
API de cache
Cache usando a API de cache do CIS. Este exemplo também pode armazenar em cache solicitações POST.
const someOtherHostname = "my.herokuapp.com"
async function handleRequest(event) {
const request = event.request
const cacheUrl = new URL(request.url)
// Hostname for a different zone
cacheUrl.hostname = someOtherHostname
const cacheKey = new Request(cacheUrl.toString(), request)
const cache = caches.default
// Get this request from this zone's cache
let response = await cache.match(cacheKey)
if (!response) {
//If not in cache, get it from origin
response = await fetch(request)
// Must use Response constructor to inherit all of response's fields
response = new Response(response.body, response)
// Cache API respects Cache-Control headers. Setting max-age to 10
// will limit the response to be in cache for 10 seconds max
response.headers.append("Cache-Control", "max-age=10")
// Store the fetched response as cacheKey
// Use waitUntil so computational expensive tasks don"t delay the response
event.waitUntil(cache.put(cacheKey, response.clone()))
}
return response
}
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()
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(handleRequest(event))
} catch (e) {
return event.respondWith(new Response("Error thrown " + e.message))
}
})