Skip to content
This repository has been archived by the owner on Feb 9, 2024. It is now read-only.

Commit

Permalink
Merge pull request #315 from awwong1/master
Browse files Browse the repository at this point in the history
  • Loading branch information
Cherry committed Dec 9, 2022
2 parents a921117 + 2bd1efe commit 1b7fccd
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 4 deletions.
8 changes: 8 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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`
Expand Down
12 changes: 8 additions & 4 deletions src/index.ts
Expand Up @@ -31,6 +31,7 @@ const getAssetFromKVDefaultOptions: Partial<Options> = {
defaultMimeType: 'text/plain',
defaultDocument: 'index.html',
pathIsEncoded: false,
defaultETag: 'strong',
}

function assignOptions(options?: Partial<Options>): Options {
Expand Down Expand Up @@ -193,14 +194,17 @@ const getAssetFromKV = async (event: Evt, options?: Partial<Options>): Promise<R
// is invalid, returns an empty string (instead of null) to prevent the
// the potentially disastrous scenario where the value of the Etag resp
// header is "null". Could be modified in future to base64 encode etc
const formatETag = (entityId: any = pathKey, validatorType: string = 'strong') => {
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':
Expand Down Expand Up @@ -277,7 +281,7 @@ const getAssetFromKV = async (event: Evt, options?: Partial<Options>): Promise<R
response.headers.set('Content-Length', String(body.byteLength))
// set etag before cache insertion
if (!response.headers.has('etag')) {
response.headers.set('etag', formatETag(pathKey, 'strong'))
response.headers.set('etag', formatETag(pathKey))
}
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`)
Expand All @@ -288,7 +292,7 @@ const getAssetFromKV = async (event: Evt, options?: Partial<Options>): Promise<R
response.headers.set('Content-Type', mimeType)

if (response.status === 304) {
let etag = formatETag(response.headers.get('etag'), 'strong')
let etag = formatETag(response.headers.get('etag'))
let ifNoneMatch = cacheKey.headers.get('if-none-match')
let proxyCacheStatus = response.headers.get('CF-Cache-Status')
if (etag) {
Expand Down
36 changes: 36 additions & 0 deletions src/test/getAssetFromKV.ts
Expand Up @@ -451,6 +451,42 @@ test('getAssetFromKV when resource in cache, etag should be weakened before retu
t.fail('Response was undefined')
}
})
test('getAssetFromKV should support weak etag override of resource', async (t) => {
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'
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Expand Up @@ -14,6 +14,7 @@ export type Options = {
defaultMimeType: string
defaultDocument: string
pathIsEncoded: boolean
defaultETag: 'strong' | 'weak'
}

export class KVError extends Error {
Expand Down

0 comments on commit 1b7fccd

Please sign in to comment.