From 2bd1efe7f42ed7b61e2ba11d3316181f21041bfb Mon Sep 17 00:00:00 2001 From: Alexander Wong Date: Sat, 30 Apr 2022 00:33:37 -0600 Subject: [PATCH] Allow configurable downgrade of ETag validator strength --- README.md | 8 ++++++++ src/index.ts | 12 ++++++++---- src/test/getAssetFromKV.ts | 36 ++++++++++++++++++++++++++++++++++++ src/types.ts | 1 + 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index d5baa0a..99cee88 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ The Cloudflare Workers Discord server is an active place where Workers users get - [`bypassCache`](#bypasscache) - [`ASSET_NAMESPACE` (required for ES Modules)](#asset_namespace-required-for-es-modules) - [`ASSET_MANIFEST` (required for ES Modules)](#asset_manifest-required-for-es-modules) + - [`defaultETag`](#defaultetag-optional) * [Helper functions](#helper-functions) - [`mapRequestToAsset`](#maprequesttoasset-1) @@ -282,6 +283,13 @@ type: string This is the default document that will be concatenated for requests ends in `'/'` or without a valid mime type like `'/about'` or `'/about.me'`. The default value is `'index.html'`. +#### `defaultETag` (optional) + +type: `'strong' | 'weak'` + +This determines the format of the response [ETag header](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag). If the resource is in the cache, the ETag will always be weakened before being returned. +The default value is `'strong'`. + # Helper functions ## `mapRequestToAsset` diff --git a/src/index.ts b/src/index.ts index a39f143..17066dd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,7 @@ const getAssetFromKVDefaultOptions: Partial = { defaultMimeType: 'text/plain', defaultDocument: 'index.html', pathIsEncoded: false, + defaultETag: 'strong', } function assignOptions(options?: Partial): Options { @@ -193,14 +194,17 @@ const getAssetFromKV = async (event: Evt, options?: Partial): Promise { + const formatETag = (entityId: any = pathKey, validatorType: string = options.defaultETag) => { if (!entityId) { return '' } switch (validatorType) { case 'weak': if (!entityId.startsWith('W/')) { - return `W/${entityId}` + if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) { + return `W/${entityId}` + } + return `W/"${entityId}"` } return entityId case 'strong': @@ -277,7 +281,7 @@ const getAssetFromKV = async (event: Evt, options?: Partial): Promise): Promise { + mockRequestScope() + const resourceKey = 'key1.png' + const resourceVersion = JSON.parse(mockManifest())[resourceKey] + const req1 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `W/"${resourceVersion}"`, + }, + }) + const req2 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}"`, + }, + }) + const req3 = new Request(`https://blah-weak.com/${resourceKey}`, { + headers: { + 'if-none-match': `"${resourceVersion}-another-version"`, + }, + }) + const event1 = getEvent(req1) + const event2 = getEvent(req2) + const event3 = getEvent(req3) + const res1 = await getAssetFromKV(event1, { defaultETag: 'weak' }) + const res2 = await getAssetFromKV(event2, { defaultETag: 'weak' }) + const res3 = await getAssetFromKV(event3, { defaultETag: 'weak' }) + if (res1 && res2 && res3) { + t.is(res1.headers.get('cf-cache-status'), 'MISS') + t.is(res1.headers.get('etag'), req1.headers.get('if-none-match')) + t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED') + t.is(res2.headers.get('etag'), `W/${req2.headers.get('if-none-match')}`) + t.is(res3.headers.get('cf-cache-status'), 'MISS') + t.not(res3.headers.get('etag'), req2.headers.get('if-none-match')) + } else { + t.fail('Response was undefined') + } +}) test('getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK', async (t) => { const resourceKey = 'cache.html' diff --git a/src/types.ts b/src/types.ts index c806076..db75991 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,6 +14,7 @@ export type Options = { defaultMimeType: string defaultDocument: string pathIsEncoded: boolean + defaultETag: 'strong' | 'weak' } export class KVError extends Error {