エッジ機能のユース・ケース
以下のユース・ケースは例としてのみ提供されており、ご使用の環境に正確に同じであることを意図したものではありません。
以下のコードをテストする際には、サービスの中断を引き起こす可能性があるため、注意してください。
A/B テスト
CIS エッジ機能を作成して、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
}
}
応答ヘッダーの追加
応答ヘッダーを変更するには、まず応答のコピーを作成して、変更可能にします。 次に、Headersインターフェースを使用してヘッダーを追加、変更、または削除することができます。
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;
}
複数の要求の集約
この例では、異なるAPIエンドポイントに複数のリクエストを行い、それらの応答をまとめて単一の応答として返します。
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)
}
条件付きルーティング
使用されているデバイスに応じて異なるコンテンツを配信する最も簡単な方法は、関心のある条件に基づいてリクエストの URL を書き換えることです。 以下の例を参照してください。
装置タイプ
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)
}
カスタム・ヘッダー
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)
}
ホット・リンクの保護
CIS エッジ機能を使用して、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
}
起点のない応答
直接エッジから応答を戻すことができます。 発信元に要求を送信する必要はありません。
POST と PUT の HTTP 要求の無視
POST と PUT の HTTP 要求を無視します。 以下のスニペットを使用すると、その他の要求をすべて起点にパススルーできます。
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)
}
スパイダーまたはクローラーの拒否
望ましくないスパイダーまたはクローラーから起点を保護します。 以下のケースでは、ユーザー・エージェントが「うるさいロボット」の場合、エッジ機能は要求を起点に送信する代わりに応答を戻します。
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)
}
特定の IP の接続の防止
IP アドレスを拒否リストに登録します。 このコードの断片は、特定のIP(この場合は 225.0.0.1
)がオリジンに接続するのを防ぎます。
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)
}
Post 要求
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')
}
}
エッジ機能から HTTP POST 要求を作成します。
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
}
署名済み要求
Web 暗号 API の助けを借りて、要求の署名という一般的な URL 認証方式をエッジ機能に実装できます。
ここで紹介する例では、 CIS は、 SHA-256 のダイジェストアルゴリズムを使用したHMAC(Hash-based Message Authentication Code)により、 URL のパスと有効期限を示すタイムスタンプを認証します。 認証済みリソースを正常にフェッチするには、ユーザー・エージェントは、照会パラメーターを使用して正しいパス、有効期限タイム・スタンプ、および HMAC を指定する必要があります。 これらの 3 つのパラメーターのいずれかが改ざんされている場合、要求は失敗します。
有効期限のタイムスタンプの信頼性は HMAC によって保証されており、つまり、 URL が期限切れとなった際に、HMAC が正しければ、ユーザーが提供したタイムスタンプが正しいと信頼できるということです。 また、お客様が所有する URL が有効期限切れかどうかを確認することもできます。
署名済み要求の検証
この例では、パス名が /verify/
で始まるすべてのリクエストに対するHMACを検証します。 URL。
デバッグの利便性を考慮して、このEdge機能は、 URL またはHMACが無効である場合、または URL が期限切れである場合に、403メッセージを返します。 実際の実装では 404 を戻すこともできます。
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
}
署名済み要求の生成
通常、署名されたリクエストは、電子メールなど、通常とは異なる方法でお客様に送信されます。または、対称鍵をお持ちの場合は、お客様ご自身で生成することも可能です。 署名付きリクエストは、Edge 機能でも生成できます。
URL へのリクエストで、 /generate/
で始まるものは、 CIS が /generate/
を /verify/
に置き換え、その結果のパスをタイムスタンプで署名し、署名済みの完全な URL をレスポンスボディに返します。
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)
}
応答のストリーミング
Edge 関数スクリプトは、 event.respondWith()
にレスポンスを送信する前に、レスポンス本文全体を用意しておく必要はありません。 TransformStream, を使用すると、レスポンスのフロントマター(例えば、 HTTP ステータス行やヘッダー)を送信した後にレスポンスボディをストリームすることができます。 この合理化により、 CIS は、Edge機能スクリプトで実行される最初のバイトまでの時間とバッファリングの量を最小限に抑えることができます。
バッファリングを最小にすることは、エッジ機能のメモリー制限より大きい応答本体を処理したり変換したりしなければならない場合に特に重要です。 このようなケースでは、ストリーミングが唯一の実現可能な実装戦略になります。
デフォルトでは、CIS エッジ機能サービスは可能な場合には必ずストリーミングしています。 これらの API は、ストリーミング動作を維持しながら、何らかの方法で応答本体を変更する場合にのみ必要です。 エッジ機能スクリプトが、本体を読み取らずにサブリクエスト応答をクライアントに逐語的に戻す場合、本体の処理は既に最適です。
パススルーのストリーミング
以下の最小限のパススルーの例から始めます。
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()
}
注意すべき重要な点を以下に示します。
streamBody()
は非同期関数ですが、await
を呼び出すと、fetchAndStream()
関数の呼び出しの進行がブロックされてしまうため、呼び出したくありません。 関数は、未解決のreader.read()
またはwriter.write()
操作がある期間、非同期で実行され続けます。- バックプレッシャー:
await
書き込み操作を呼び出す前の読み取り操作。 同様に、次の読み取り操作を呼び出す前に、await
書き込み操作を実行してください。 このパターンに従うことで、バック・プレッシャーは起点に伝搬されます。 - 完了:最後に
writer.close()
にコールすることで、Edge関数実行にこのレスポンスボディの書き込みが完了したことを通知します。 呼び出された後、streamBody()
は終了します。この動作が望ましくない場合は、返されたプロミスをFetchEvent.waitUntil()
に渡します。 ご使用のスクリプトでwriter.close()
が呼び出されることがない場合は、ランタイムには本体が切り捨てられたように見えますが、引き続き想定どおりに機能している可能性があります。
複数の要求の集約とストリーム
このユースケースは、複数のリクエストをまとめるレシピと似ていますが、今回はすべてのサブリクエストが成功したことを確認すると同時にレスポンスの作成を開始します。レスポンスボディを待つ必要はありません。
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()
}
ランタイムは TransformStream の読み取り可能な側で TypedArray を受け取ることを予期しています。 そのため、 writer.write()
にストリングを渡すことはなく、Uint 8Arraysのみを渡します。 ストリングを書き込む必要がある場合は、TextEncoder を使用します。
エッジ機能によるカスタム・ロード・バランサー
ロードバランシングは、ホストするウェブサイトの拡張性と信頼性を維持するのに役立ちます。 エッジ機能を使用して、特定のニーズに対応するカスタムロードバランサーを作成することができます。
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);
}
フェッチを使用したキャッシング
フェッチ要求に TTL、カスタム・キャッシュ・キー、およびキャッシュ・ヘッダーを設定することで、リソースのキャッシュ方法を決定します。
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))
})
HTML リソースのキャッシング
// Force CIS to cache an asset
fetch(event.request, { cf: { cacheEverything: true } })
キャッシュ・レベルを「すべてキャッシュ」に設定すると、アセットのデフォルトの "cacheability" がオーバーライドされます。 TTL については、CIS は起点によって設定されたヘッダーを引き続き使用します。
カスタム・キャッシュ・キー
この機能はエンタープライズのお客様にのみ提供されています。
キャッシュの目的で2つのリクエストが「同じ」かどうかを決定するのは、リクエストのキャッシュキーです。 要求のキャッシュ・キーが過去の要求と同じであれば、どちらにも、キャッシュされた同じ応答を提供できます。
// Set cache key for this request to "some-string".
fetch(event.request, { cf: { cacheKey: "some-string" } })
CIS リクエストのキャッシュキーをリクエストの に基づいて計算しますが、キャッシュ目的で異なるURLを同じように扱いたい場合もあるでしょう。 URL 例えば、お客様のウェブサイトコンテンツが Amazon S3 と Google Cloud Storage の両方からホストされている場合(両方に同じコンテンツがある)、エッジ機能を使用して2つの間でランダムにバランスを取る。 しかし、コンテンツのコピーを2つキャッシュするのは避けたいものです。 カスタムキャッシュキーを使用すると、サブリクエスト URL ではなく、オリジナルリクエスト 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 },
})
)
})
異なるゾーンを代表して動作するエッジ機能は、互いのキャッシュに影響を与えることはないことを覚えておいてください。 キャッシュキーを上書きできるのは、自身のゾーン内(先の例では event.request.url
が保存されたキー)へのリクエスト、または CIS にないホストへのリクエストの場合のみです。 他の CIS ゾーン(例えば、異なる CIS 顧客に属するゾーン)にリクエストを送信する場合、そのゾーンは、 CIS 内で自身のコンテンツがどのようにキャッシュされるかを完全に制御します。これを上書きすることはできません。
起点応答コードに基づくオーバーライド
この機能はエンタープライズのお客様にのみ提供されています。
// 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 } },
})
このオプションは、cacheTtl
機能のバージョンの 1 つです。これは応答の状況コードに基づいて TTL を選択し、自動的には cacheEverything: true
を設定しません。 この要求に対する応答の状況コードが一致した場合、CIS は、指定された時間にわたってキャッシュし、起点から送信されたキャッシュに関するディレクティブをオーバーライドします。
TTL の解釈
CIS では、以下の TTL 値が解釈されます。
- 正の値: CIS がアセットをキャッシュする時間 (秒単位) を示します。
0
: アセットはキャッシュされますが、すぐに期限切れになります (毎回起点から再検証します)。-1
または任意の負の値: CIS に対して何もキャッシュしないように指示します。
キャッシュ API
CIS キャッシュ API を使用したキャッシュ。 この例では 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))
}
})