Socket
Socket
Sign inDemoInstall

@cloudflare/kv-asset-handler

Package Overview
Dependencies
1
Maintainers
5
Versions
19
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.2.0 to 0.3.0

2

dist/index.d.ts

@@ -30,3 +30,3 @@ import { Options, CacheControl, MethodNotAllowedError, NotFoundError, InternalError } from './types';

* */
declare type Evt = {
type Evt = {
request: Request;

@@ -33,0 +33,0 @@ waitUntil: (promise: Promise<any>) => void;

@@ -24,2 +24,3 @@ "use strict";

pathIsEncoded: false,
defaultETag: 'strong',
};

@@ -157,3 +158,3 @@ function assignOptions(options) {

// header is "null". Could be modified in future to base64 encode etc
const formatETag = (entityId = pathKey, validatorType = 'strong') => {
const formatETag = (entityId = pathKey, validatorType = options.defaultETag) => {
if (!entityId) {

@@ -165,3 +166,6 @@ return '';

if (!entityId.startsWith('W/')) {
return `W/${entityId}`;
if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) {
return `W/${entityId}`;
}
return `W/"${entityId}"`;
}

@@ -236,6 +240,6 @@ return entityId;

response.headers.set('Accept-Ranges', 'bytes');
response.headers.set('Content-Length', body.length);
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));
}

@@ -250,3 +254,3 @@ // determine Cloudflare cache behavior

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');

@@ -253,0 +257,0 @@ let proxyCacheStatus = response.headers.get('CF-Cache-Status');

@@ -415,2 +415,39 @@ "use strict";

});
(0, ava_1.default)('getAssetFromKV should support weak etag override of resource', async (t) => {
(0, mocks_1.mockRequestScope)();
const resourceKey = 'key1.png';
const resourceVersion = JSON.parse((0, mocks_1.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 = (0, mocks_1.getEvent)(req1);
const event2 = (0, mocks_1.getEvent)(req2);
const event3 = (0, mocks_1.getEvent)(req3);
const res1 = await (0, index_1.getAssetFromKV)(event1, { defaultETag: 'weak' });
const res2 = await (0, index_1.getAssetFromKV)(event2, { defaultETag: 'weak' });
const res3 = await (0, index_1.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');
}
});
(0, ava_1.default)('getAssetFromKV if-none-match not sent but resource in cache, should return cache hit 200 OK', async (t) => {

@@ -417,0 +454,0 @@ const resourceKey = 'cache.html';

@@ -1,2 +0,2 @@

export declare type CacheControl = {
export type CacheControl = {
browserTTL: number;

@@ -6,4 +6,4 @@ edgeTTL: number;

};
export declare type AssetManifestType = Record<string, string>;
export declare type Options = {
export type AssetManifestType = Record<string, string>;
export type Options = {
cacheControl: ((req: Request) => Partial<CacheControl>) | Partial<CacheControl>;

@@ -16,2 +16,3 @@ ASSET_NAMESPACE: any;

pathIsEncoded: boolean;
defaultETag: 'strong' | 'weak';
};

@@ -18,0 +19,0 @@ export declare class KVError extends Error {

{
"name": "@cloudflare/kv-asset-handler",
"version": "0.2.0",
"description": "Routes requests to KV assets",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepack": "npm run build",
"build": "tsc -d",
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
"pretest": "npm run build",
"lint:code": "prettier --check \"**/*.{js,ts,json,md}\"",
"lint:markdown": "markdownlint \"**/*.md\" --ignore node_modules",
"test": "ava dist/test/*.js --verbose"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cloudflare/kv-asset-handler.git"
},
"keywords": [
"kv",
"cloudflare",
"workers",
"wrangler",
"assets"
],
"files": [
"src",
"dist",
"LICENSE_APACHE",
"LICENSE_MIT"
],
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
"bugs": {
"url": "https://github.com/cloudflare/kv-asset-handler/issues"
},
"homepage": "https://github.com/cloudflare/kv-asset-handler#readme",
"dependencies": {
"mime": "^3.0.0"
},
"devDependencies": {
"@ava/typescript": "^3.0.0",
"@cloudflare/workers-types": "^3.0.0",
"@types/mime": "^2.0.3",
"@types/node": "^15.14.9",
"ava": "^3.15.0",
"prettier": "^2.4.1",
"service-worker-mock": "^2.0.5",
"typescript": "^4.4.4"
}
}
"name": "@cloudflare/kv-asset-handler",
"version": "0.3.0",
"description": "Routes requests to KV assets",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"prepack": "npm run build",
"build": "tsc -d",
"format": "prettier --write \"**/*.{js,ts,json,md}\"",
"pretest": "npm run build",
"lint:code": "prettier --check \"**/*.{js,ts,json,md}\"",
"lint:markdown": "markdownlint \"**/*.md\" --ignore node_modules",
"test": "ava dist/test/*.js --verbose"
},
"repository": {
"type": "git",
"url": "git+https://github.com/cloudflare/kv-asset-handler.git"
},
"keywords": [
"kv",
"cloudflare",
"workers",
"wrangler",
"assets"
],
"files": [
"src",
"dist",
"LICENSE_APACHE",
"LICENSE_MIT"
],
"author": "wrangler@cloudflare.com",
"license": "MIT OR Apache-2.0",
"bugs": {
"url": "https://github.com/cloudflare/kv-asset-handler/issues"
},
"homepage": "https://github.com/cloudflare/kv-asset-handler#readme",
"dependencies": {
"mime": "^3.0.0"
},
"devDependencies": {
"@ava/typescript": "^3.0.1",
"@cloudflare/workers-types": "^4.20221111.1",
"@types/mime": "^3.0.1",
"@types/node": "^18.11.12",
"ava": "^5.1.0",
"prettier": "^2.8.1",
"service-worker-mock": "^2.0.5",
"typescript": "^4.9.4"
}
}

@@ -15,3 +15,3 @@ # @cloudflare/kv-asset-handler

The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers-sites` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.gg/cloudflaredev)!
The Cloudflare Workers Discord server is an active place where Workers users get help, share feedback, and collaborate on making our platform better. The `#workers` channel in particular is a great place to chat about `kv-asset-handler`, and building cool experiences for your users using these tools! If you have questions, want to share what you're working on, or give feedback, [join us in Discord and say hello](https://discord.gg/cloudflaredev)!

@@ -32,2 +32,3 @@ - [Installation](#installation)

- [`ASSET_MANIFEST` (required for ES Modules)](#asset_manifest-required-for-es-modules)
- [`defaultETag`](#defaultetag-optional)

@@ -80,28 +81,28 @@ * [Helper functions](#helper-functions)

export default {
async fetch(request, env, ctx) {
if (request.url.includes('/docs')) {
try {
return await getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: assetManifest,
},
)
} catch (e) {
if (e instanceof NotFoundError) {
// ...
} else if (e instanceof MethodNotAllowedError) {
// ...
} else {
return new Response('An unexpected error occurred', { status: 500 })
}
}
} else return fetch(request)
},
async fetch(request, env, ctx) {
if (request.url.includes('/docs')) {
try {
return await getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
ASSET_MANIFEST: assetManifest,
},
)
} catch (e) {
if (e instanceof NotFoundError) {
// ...
} else if (e instanceof MethodNotAllowedError) {
// ...
} else {
return new Response('An unexpected error occurred', { status: 500 })
}
}
} else return fetch(request)
},
}

@@ -116,19 +117,19 @@ ```

addEventListener('fetch', (event) => {
event.respondWith(handleEvent(event))
event.respondWith(handleEvent(event))
})
async function handleEvent(event) {
if (event.request.url.includes('/docs')) {
try {
return await getAssetFromKV(event)
} catch (e) {
if (e instanceof NotFoundError) {
// ...
} else if (e instanceof MethodNotAllowedError) {
// ...
} else {
return new Response('An unexpected error occurred', { status: 500 })
}
}
} else return fetch(event.request)
if (event.request.url.includes('/docs')) {
try {
return await getAssetFromKV(event)
} catch (e) {
if (e instanceof NotFoundError) {
// ...
} else if (e instanceof MethodNotAllowedError) {
// ...
} else {
return new Response('An unexpected error occurred', { status: 500 })
}
}
} else return fetch(event.request)
}

@@ -179,5 +180,5 @@ ```

let cacheControl = {
browserTTL: null, // do not set cache control ttl on responses
edgeTTL: 2 * 60 * 60 * 24, // 2 days
bypassCache: false, // do not bypass Cloudflare's cache
browserTTL: null, // do not set cache control ttl on responses
edgeTTL: 2 * 60 * 60 * 24, // 2 days
bypassCache: false, // do not bypass Cloudflare's cache
}

@@ -220,11 +221,11 @@ ```

return getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
},
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_NAMESPACE: env.__STATIC_CONTENT,
},
)

@@ -257,12 +258,12 @@ ```

return getAssetFromKV(
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_MANIFEST: manifest,
// ...
},
{
request,
waitUntil(promise) {
return ctx.waitUntil(promise)
},
},
{
ASSET_MANIFEST: manifest,
// ...
},
)

@@ -292,2 +293,9 @@ ```

#### `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

@@ -331,3 +339,3 @@

let cacheControl = {
bypassCache: true,
bypassCache: true,
}

@@ -334,0 +342,0 @@ ```

import * as mime from 'mime'
import {
Options,
CacheControl,
MethodNotAllowedError,
NotFoundError,
InternalError,
AssetManifestType,
Options,
CacheControl,
MethodNotAllowedError,
NotFoundError,
InternalError,
AssetManifestType,
} from './types'
declare global {
var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string
var __STATIC_CONTENT: any, __STATIC_CONTENT_MANIFEST: string
}
const defaultCacheControl: CacheControl = {
browserTTL: null,
edgeTTL: 2 * 60 * 60 * 24, // 2 days
bypassCache: false, // do not bypass Cloudflare's cache
browserTTL: null,
edgeTTL: 2 * 60 * 60 * 24, // 2 days
bypassCache: false, // do not bypass Cloudflare's cache
}
const parseStringAsObject = <T>(maybeString: string | T): T =>
typeof maybeString === 'string' ? (JSON.parse(maybeString) as T) : maybeString
typeof maybeString === 'string' ? (JSON.parse(maybeString) as T) : maybeString
const getAssetFromKVDefaultOptions: Partial<Options> = {
ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined,
ASSET_MANIFEST:
typeof __STATIC_CONTENT_MANIFEST !== 'undefined'
? parseStringAsObject<AssetManifestType>(__STATIC_CONTENT_MANIFEST)
: {},
cacheControl: defaultCacheControl,
defaultMimeType: 'text/plain',
defaultDocument: 'index.html',
pathIsEncoded: false,
ASSET_NAMESPACE: typeof __STATIC_CONTENT !== 'undefined' ? __STATIC_CONTENT : undefined,
ASSET_MANIFEST:
typeof __STATIC_CONTENT_MANIFEST !== 'undefined'
? parseStringAsObject<AssetManifestType>(__STATIC_CONTENT_MANIFEST)
: {},
cacheControl: defaultCacheControl,
defaultMimeType: 'text/plain',
defaultDocument: 'index.html',
pathIsEncoded: false,
defaultETag: 'strong',
}
function assignOptions(options?: Partial<Options>): Options {
// Assign any missing options passed in to the default
// options.mapRequestToAsset is handled manually later
return <Options>Object.assign({}, getAssetFromKVDefaultOptions, options)
// Assign any missing options passed in to the default
// options.mapRequestToAsset is handled manually later
return <Options>Object.assign({}, getAssetFromKVDefaultOptions, options)
}

@@ -50,19 +51,19 @@

const mapRequestToAsset = (request: Request, options?: Partial<Options>) => {
options = assignOptions(options)
options = assignOptions(options)
const parsedUrl = new URL(request.url)
let pathname = parsedUrl.pathname
const parsedUrl = new URL(request.url)
let pathname = parsedUrl.pathname
if (pathname.endsWith('/')) {
// If path looks like a directory append options.defaultDocument
// e.g. If path is /about/ -> /about/index.html
pathname = pathname.concat(options.defaultDocument)
} else if (!mime.getType(pathname)) {
// If path doesn't look like valid content
// e.g. /about.me -> /about.me/index.html
pathname = pathname.concat('/' + options.defaultDocument)
}
if (pathname.endsWith('/')) {
// If path looks like a directory append options.defaultDocument
// e.g. If path is /about/ -> /about/index.html
pathname = pathname.concat(options.defaultDocument)
} else if (!mime.getType(pathname)) {
// If path doesn't look like valid content
// e.g. /about.me -> /about.me/index.html
pathname = pathname.concat('/' + options.defaultDocument)
}
parsedUrl.pathname = pathname
return new Request(parsedUrl.toString(), request)
parsedUrl.pathname = pathname
return new Request(parsedUrl.toString(), request)
}

@@ -76,20 +77,20 @@

function serveSinglePageApp(request: Request, options?: Partial<Options>): Request {
options = assignOptions(options)
options = assignOptions(options)
// First apply the default handler, which already has logic to detect
// paths that should map to HTML files.
request = mapRequestToAsset(request, options)
// First apply the default handler, which already has logic to detect
// paths that should map to HTML files.
request = mapRequestToAsset(request, options)
const parsedUrl = new URL(request.url)
const parsedUrl = new URL(request.url)
// Detect if the default handler decided to map to
// a HTML file in some specific directory.
if (parsedUrl.pathname.endsWith('.html')) {
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument)
return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request)
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return request
}
// Detect if the default handler decided to map to
// a HTML file in some specific directory.
if (parsedUrl.pathname.endsWith('.html')) {
// If expected HTML file was missing, just return the root index.html (or options.defaultDocument)
return new Request(`${parsedUrl.origin}/${options.defaultDocument}`, request)
} else {
// The default handler decided this is not an HTML page. It's probably
// an image, CSS, or JS file. Leave it as-is.
return request
}
}

@@ -110,203 +111,206 @@

type Evt = {
request: Request
waitUntil: (promise: Promise<any>) => void
request: Request
waitUntil: (promise: Promise<any>) => void
}
const getAssetFromKV = async (event: Evt, options?: Partial<Options>): Promise<Response> => {
options = assignOptions(options)
options = assignOptions(options)
const request = event.request
const ASSET_NAMESPACE = options.ASSET_NAMESPACE
const ASSET_MANIFEST = parseStringAsObject<AssetManifestType>(options.ASSET_MANIFEST)
const request = event.request
const ASSET_NAMESPACE = options.ASSET_NAMESPACE
const ASSET_MANIFEST = parseStringAsObject<AssetManifestType>(options.ASSET_MANIFEST)
if (typeof ASSET_NAMESPACE === 'undefined') {
throw new InternalError(`there is no KV namespace bound to the script`)
}
if (typeof ASSET_NAMESPACE === 'undefined') {
throw new InternalError(`there is no KV namespace bound to the script`)
}
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s
let pathIsEncoded = options.pathIsEncoded
let requestKey
// if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions
// otherwise handle request as normal, with default mapRequestToAsset below
if (options.mapRequestToAsset) {
requestKey = options.mapRequestToAsset(request)
} else if (ASSET_MANIFEST[rawPathKey]) {
requestKey = request
} else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) {
pathIsEncoded = true
requestKey = request
} else {
const mappedRequest = mapRequestToAsset(request)
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '')
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) {
pathIsEncoded = true
requestKey = mappedRequest
} else {
// use default mapRequestToAsset
requestKey = mapRequestToAsset(request, options)
}
}
const rawPathKey = new URL(request.url).pathname.replace(/^\/+/, '') // strip any preceding /'s
let pathIsEncoded = options.pathIsEncoded
let requestKey
// if options.mapRequestToAsset is explicitly passed in, always use it and assume user has own intentions
// otherwise handle request as normal, with default mapRequestToAsset below
if (options.mapRequestToAsset) {
requestKey = options.mapRequestToAsset(request)
} else if (ASSET_MANIFEST[rawPathKey]) {
requestKey = request
} else if (ASSET_MANIFEST[decodeURIComponent(rawPathKey)]) {
pathIsEncoded = true
requestKey = request
} else {
const mappedRequest = mapRequestToAsset(request)
const mappedRawPathKey = new URL(mappedRequest.url).pathname.replace(/^\/+/, '')
if (ASSET_MANIFEST[decodeURIComponent(mappedRawPathKey)]) {
pathIsEncoded = true
requestKey = mappedRequest
} else {
// use default mapRequestToAsset
requestKey = mapRequestToAsset(request, options)
}
}
const SUPPORTED_METHODS = ['GET', 'HEAD']
if (!SUPPORTED_METHODS.includes(requestKey.method)) {
throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`)
}
const SUPPORTED_METHODS = ['GET', 'HEAD']
if (!SUPPORTED_METHODS.includes(requestKey.method)) {
throw new MethodNotAllowedError(`${requestKey.method} is not a valid request method`)
}
const parsedUrl = new URL(requestKey.url)
const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary
const parsedUrl = new URL(requestKey.url)
const pathname = pathIsEncoded ? decodeURIComponent(parsedUrl.pathname) : parsedUrl.pathname // decode percentage encoded path only when necessary
// pathKey is the file path to look up in the manifest
let pathKey = pathname.replace(/^\/+/, '') // remove prepended /
// pathKey is the file path to look up in the manifest
let pathKey = pathname.replace(/^\/+/, '') // remove prepended /
// @ts-ignore
const cache = caches.default
let mimeType = mime.getType(pathKey) || options.defaultMimeType
if (mimeType.startsWith('text') || mimeType === 'application/javascript') {
mimeType += '; charset=utf-8'
}
// @ts-ignore
const cache = caches.default
let mimeType = mime.getType(pathKey) || options.defaultMimeType
if (mimeType.startsWith('text') || mimeType === 'application/javascript') {
mimeType += '; charset=utf-8'
}
let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash
// check manifest for map from file path to hash
if (typeof ASSET_MANIFEST !== 'undefined') {
if (ASSET_MANIFEST[pathKey]) {
pathKey = ASSET_MANIFEST[pathKey]
// if path key is in asset manifest, we can assume it contains a content hash and can be cached
shouldEdgeCache = true
}
}
let shouldEdgeCache = false // false if storing in KV by raw file path i.e. no hash
// check manifest for map from file path to hash
if (typeof ASSET_MANIFEST !== 'undefined') {
if (ASSET_MANIFEST[pathKey]) {
pathKey = ASSET_MANIFEST[pathKey]
// if path key is in asset manifest, we can assume it contains a content hash and can be cached
shouldEdgeCache = true
}
}
// TODO this excludes search params from cache, investigate ideal behavior
let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request)
// TODO this excludes search params from cache, investigate ideal behavior
let cacheKey = new Request(`${parsedUrl.origin}/${pathKey}`, request)
// if argument passed in for cacheControl is a function then
// evaluate that function. otherwise return the Object passed in
// or default Object
const evalCacheOpts = (() => {
switch (typeof options.cacheControl) {
case 'function':
return options.cacheControl(request)
case 'object':
return options.cacheControl
default:
return defaultCacheControl
}
})()
// if argument passed in for cacheControl is a function then
// evaluate that function. otherwise return the Object passed in
// or default Object
const evalCacheOpts = (() => {
switch (typeof options.cacheControl) {
case 'function':
return options.cacheControl(request)
case 'object':
return options.cacheControl
default:
return defaultCacheControl
}
})()
// formats the etag depending on the response context. if the entityId
// 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') => {
if (!entityId) {
return ''
}
switch (validatorType) {
case 'weak':
if (!entityId.startsWith('W/')) {
return `W/${entityId}`
}
return entityId
case 'strong':
if (entityId.startsWith(`W/"`)) {
entityId = entityId.replace('W/', '')
}
if (!entityId.endsWith(`"`)) {
entityId = `"${entityId}"`
}
return entityId
default:
return ''
}
}
// formats the etag depending on the response context. if the entityId
// 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 = options.defaultETag) => {
if (!entityId) {
return ''
}
switch (validatorType) {
case 'weak':
if (!entityId.startsWith('W/')) {
if (entityId.startsWith(`"`) && entityId.endsWith(`"`)) {
return `W/${entityId}`
}
return `W/"${entityId}"`
}
return entityId
case 'strong':
if (entityId.startsWith(`W/"`)) {
entityId = entityId.replace('W/', '')
}
if (!entityId.endsWith(`"`)) {
entityId = `"${entityId}"`
}
return entityId
default:
return ''
}
}
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts)
options.cacheControl = Object.assign({}, defaultCacheControl, evalCacheOpts)
// override shouldEdgeCache if options say to bypassCache
if (
options.cacheControl.bypassCache ||
options.cacheControl.edgeTTL === null ||
request.method == 'HEAD'
) {
shouldEdgeCache = false
}
// only set max-age if explicitly passed in a number as an arg
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number'
// override shouldEdgeCache if options say to bypassCache
if (
options.cacheControl.bypassCache ||
options.cacheControl.edgeTTL === null ||
request.method == 'HEAD'
) {
shouldEdgeCache = false
}
// only set max-age if explicitly passed in a number as an arg
const shouldSetBrowserCache = typeof options.cacheControl.browserTTL === 'number'
let response = null
if (shouldEdgeCache) {
response = await cache.match(cacheKey)
}
let response = null
if (shouldEdgeCache) {
response = await cache.match(cacheKey)
}
if (response) {
if (response.status > 300 && response.status < 400) {
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) {
// Body exists and environment supports readable streams
response.body.cancel()
} else {
// Environment doesnt support readable streams, or null repsonse body. Nothing to do
}
response = new Response(null, response)
} else {
// fixes #165
let opts = {
headers: new Headers(response.headers),
status: 0,
statusText: '',
}
if (response) {
if (response.status > 300 && response.status < 400) {
if (response.body && 'cancel' in Object.getPrototypeOf(response.body)) {
// Body exists and environment supports readable streams
response.body.cancel()
} else {
// Environment doesnt support readable streams, or null repsonse body. Nothing to do
}
response = new Response(null, response)
} else {
// fixes #165
let opts = {
headers: new Headers(response.headers),
status: 0,
statusText: '',
}
opts.headers.set('cf-cache-status', 'HIT')
opts.headers.set('cf-cache-status', 'HIT')
if (response.status) {
opts.status = response.status
opts.statusText = response.statusText
} else if (opts.headers.has('Content-Range')) {
opts.status = 206
opts.statusText = 'Partial Content'
} else {
opts.status = 200
opts.statusText = 'OK'
}
response = new Response(response.body, opts)
}
} else {
const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer')
if (body === null) {
throw new NotFoundError(`could not find ${pathKey} in your content namespace`)
}
response = new Response(body)
if (response.status) {
opts.status = response.status
opts.statusText = response.statusText
} else if (opts.headers.has('Content-Range')) {
opts.status = 206
opts.statusText = 'Partial Content'
} else {
opts.status = 200
opts.statusText = 'OK'
}
response = new Response(response.body, opts)
}
} else {
const body = await ASSET_NAMESPACE.get(pathKey, 'arrayBuffer')
if (body === null) {
throw new NotFoundError(`could not find ${pathKey} in your content namespace`)
}
response = new Response(body)
if (shouldEdgeCache) {
response.headers.set('Accept-Ranges', 'bytes')
response.headers.set('Content-Length', body.length)
// set etag before cache insertion
if (!response.headers.has('etag')) {
response.headers.set('etag', formatETag(pathKey, 'strong'))
}
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`)
event.waitUntil(cache.put(cacheKey, response.clone()))
response.headers.set('CF-Cache-Status', 'MISS')
}
}
response.headers.set('Content-Type', mimeType)
if (shouldEdgeCache) {
response.headers.set('Accept-Ranges', 'bytes')
response.headers.set('Content-Length', String(body.byteLength))
// set etag before cache insertion
if (!response.headers.has('etag')) {
response.headers.set('etag', formatETag(pathKey))
}
// determine Cloudflare cache behavior
response.headers.set('Cache-Control', `max-age=${options.cacheControl.edgeTTL}`)
event.waitUntil(cache.put(cacheKey, response.clone()))
response.headers.set('CF-Cache-Status', 'MISS')
}
}
response.headers.set('Content-Type', mimeType)
if (response.status === 304) {
let etag = formatETag(response.headers.get('etag'), 'strong')
let ifNoneMatch = cacheKey.headers.get('if-none-match')
let proxyCacheStatus = response.headers.get('CF-Cache-Status')
if (etag) {
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') {
response.headers.set('CF-Cache-Status', 'EXPIRED')
} else {
response.headers.set('CF-Cache-Status', 'REVALIDATED')
}
response.headers.set('etag', formatETag(etag, 'weak'))
}
}
if (shouldSetBrowserCache) {
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`)
} else {
response.headers.delete('Cache-Control')
}
return response
if (response.status === 304) {
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) {
if (ifNoneMatch && ifNoneMatch === etag && proxyCacheStatus === 'MISS') {
response.headers.set('CF-Cache-Status', 'EXPIRED')
} else {
response.headers.set('CF-Cache-Status', 'REVALIDATED')
}
response.headers.set('etag', formatETag(etag, 'weak'))
}
}
if (shouldSetBrowserCache) {
response.headers.set('Cache-Control', `max-age=${options.cacheControl.browserTTL}`)
} else {
response.headers.delete('Cache-Control')
}
return response
}

@@ -313,0 +317,0 @@

@@ -6,54 +6,54 @@ const makeServiceWorkerEnv = require('service-worker-mock')

export const getEvent = (request: Request): any => {
const waitUntil = async (callback: any) => {
await callback
}
return {
request,
waitUntil,
}
const waitUntil = async (callback: any) => {
await callback
}
return {
request,
waitUntil,
}
}
const store: any = {
'key1.123HASHBROWN.txt': 'val1',
'key1.123HASHBROWN.png': 'val1',
'index.123HASHBROWN.html': 'index.html',
'cache.123HASHBROWN.html': 'cache me if you can',
'测试.123HASHBROWN.html': 'My filename is non-ascii',
'%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded',
'%2F.123HASHBROWN.html': 'user percent encoded',
'你好.123HASHBROWN.html': 'I shouldnt be served',
'%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important',
'nohash.txt': 'no hash but still got some result',
'sub/blah.123HASHBROWN.png': 'picturedis',
'sub/index.123HASHBROWN.html': 'picturedis',
'client.123HASHBROWN': 'important file',
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above',
'image.123HASHBROWN.png': 'imagepng',
'image.123HASHBROWN.webp': 'imagewebp',
'你好/index.123HASHBROWN.html': 'My path is non-ascii',
'key1.123HASHBROWN.txt': 'val1',
'key1.123HASHBROWN.png': 'val1',
'index.123HASHBROWN.html': 'index.html',
'cache.123HASHBROWN.html': 'cache me if you can',
'测试.123HASHBROWN.html': 'My filename is non-ascii',
'%not-really-percent-encoded.123HASHBROWN.html': 'browser percent encoded',
'%2F.123HASHBROWN.html': 'user percent encoded',
'你好.123HASHBROWN.html': 'I shouldnt be served',
'%E4%BD%A0%E5%A5%BD.123HASHBROWN.html': 'Im important',
'nohash.txt': 'no hash but still got some result',
'sub/blah.123HASHBROWN.png': 'picturedis',
'sub/index.123HASHBROWN.html': 'picturedis',
'client.123HASHBROWN': 'important file',
'client.123HASHBROWN/index.html': 'Im here but serve my big bro above',
'image.123HASHBROWN.png': 'imagepng',
'image.123HASHBROWN.webp': 'imagewebp',
'你好/index.123HASHBROWN.html': 'My path is non-ascii',
}
export const mockKV = (store: any) => {
return {
get: (path: string) => store[path] || null,
}
return {
get: (path: string) => store[path] || null,
}
}
export const mockManifest = () => {
return JSON.stringify({
'key1.txt': `key1.${HASH}.txt`,
'key1.png': `key1.${HASH}.png`,
'cache.html': `cache.${HASH}.html`,
'测试.html': `测试.${HASH}.html`,
'你好.html': `你好.${HASH}.html`,
'%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`,
'%2F.html': `%2F.${HASH}.html`,
'%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
'index.html': `index.${HASH}.html`,
'sub/blah.png': `sub/blah.${HASH}.png`,
'sub/index.html': `sub/index.${HASH}.html`,
client: `client.${HASH}`,
'client/index.html': `client.${HASH}`,
'image.png': `image.${HASH}.png`,
'image.webp': `image.${HASH}.webp`,
'你好/index.html': `你好/index.${HASH}.html`,
})
return JSON.stringify({
'key1.txt': `key1.${HASH}.txt`,
'key1.png': `key1.${HASH}.png`,
'cache.html': `cache.${HASH}.html`,
'测试.html': `测试.${HASH}.html`,
'你好.html': `你好.${HASH}.html`,
'%not-really-percent-encoded.html': `%not-really-percent-encoded.${HASH}.html`,
'%2F.html': `%2F.${HASH}.html`,
'%E4%BD%A0%E5%A5%BD.html': `%E4%BD%A0%E5%A5%BD.${HASH}.html`,
'index.html': `index.${HASH}.html`,
'sub/blah.png': `sub/blah.${HASH}.png`,
'sub/index.html': `sub/index.${HASH}.html`,
client: `client.${HASH}`,
'client/index.html': `client.${HASH}`,
'image.png': `image.${HASH}.png`,
'image.webp': `image.${HASH}.webp`,
'你好/index.html': `你好/index.${HASH}.html`,
})
}

@@ -63,70 +63,70 @@

interface CacheKey {
url: object
headers: object
url: object
headers: object
}
export const mockCaches = () => {
return {
default: {
async match(key: any) {
let cacheKey: CacheKey = {
url: key.url,
headers: {},
}
let response
if (key.headers.has('if-none-match')) {
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '')
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag)
response = cacheStore.get(JSON.stringify(cacheKey))
} else {
// if client doesn't send if-none-match, we need to iterate through these keys
// and just test the URL
const activeCacheKeys: Array<string> = Array.from(cacheStore.keys())
for (const cacheStoreKey of activeCacheKeys) {
if (JSON.parse(cacheStoreKey).url === key.url) {
response = cacheStore.get(cacheStoreKey)
}
}
}
// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags
if (response && !key.headers.has('if-none-match')) {
// this appears overly verbose, but is necessary to document edge cache behavior
// The Range request header triggers the response header Content-Range ...
const range = key.headers.get('range')
if (range) {
response.headers.set(
'content-range',
`bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`,
)
}
// ... which we are using in this repository to set status 206
if (response.headers.has('content-range')) {
response.status = 206
} else {
response.status = 200
}
let etag = response.headers.get('etag')
if (etag && !etag.includes('W/')) {
response.headers.set('etag', `W/${etag}`)
}
}
return response
},
async put(key: any, val: Response) {
let headers = new Headers(val.headers)
let url = new URL(key.url)
let resWithBody = new Response(val.body, { headers, status: 200 })
let resNoBody = new Response(null, { headers, status: 304 })
let cacheKey: CacheKey = {
url: key.url,
headers: {
etag: `"${url.pathname.replace('/', '')}"`,
},
}
cacheStore.set(JSON.stringify(cacheKey), resNoBody)
cacheKey.headers = {}
cacheStore.set(JSON.stringify(cacheKey), resWithBody)
return
},
},
}
return {
default: {
async match(key: any) {
let cacheKey: CacheKey = {
url: key.url,
headers: {},
}
let response
if (key.headers.has('if-none-match')) {
let makeStrongEtag = key.headers.get('if-none-match').replace('W/', '')
Reflect.set(cacheKey.headers, 'etag', makeStrongEtag)
response = cacheStore.get(JSON.stringify(cacheKey))
} else {
// if client doesn't send if-none-match, we need to iterate through these keys
// and just test the URL
const activeCacheKeys: Array<string> = Array.from(cacheStore.keys())
for (const cacheStoreKey of activeCacheKeys) {
if (JSON.parse(cacheStoreKey).url === key.url) {
response = cacheStore.get(cacheStoreKey)
}
}
}
// TODO: write test to accomodate for rare scenarios with where range requests accomodate etags
if (response && !key.headers.has('if-none-match')) {
// this appears overly verbose, but is necessary to document edge cache behavior
// The Range request header triggers the response header Content-Range ...
const range = key.headers.get('range')
if (range) {
response.headers.set(
'content-range',
`bytes ${range.split('=').pop()}/${response.headers.get('content-length')}`,
)
}
// ... which we are using in this repository to set status 206
if (response.headers.has('content-range')) {
response.status = 206
} else {
response.status = 200
}
let etag = response.headers.get('etag')
if (etag && !etag.includes('W/')) {
response.headers.set('etag', `W/${etag}`)
}
}
return response
},
async put(key: any, val: Response) {
let headers = new Headers(val.headers)
let url = new URL(key.url)
let resWithBody = new Response(val.body, { headers, status: 200 })
let resNoBody = new Response(null, { headers, status: 304 })
let cacheKey: CacheKey = {
url: key.url,
headers: {
etag: `"${url.pathname.replace('/', '')}"`,
},
}
cacheStore.set(JSON.stringify(cacheKey), resNoBody)
cacheKey.headers = {}
cacheStore.set(JSON.stringify(cacheKey), resWithBody)
return
},
},
}
}

@@ -136,6 +136,6 @@

export function mockRequestScope() {
Object.assign(global, makeServiceWorkerEnv())
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
Object.assign(global, { caches: mockCaches() })
Object.assign(global, makeServiceWorkerEnv())
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
Object.assign(global, { caches: mockCaches() })
}

@@ -145,8 +145,8 @@

export function mockGlobalScope() {
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
Object.assign(global, { __STATIC_CONTENT_MANIFEST: mockManifest() })
Object.assign(global, { __STATIC_CONTENT: mockKV(store) })
}
export const sleep = (milliseconds: number) => {
return new Promise((resolve) => setTimeout(resolve, milliseconds))
return new Promise((resolve) => setTimeout(resolve, milliseconds))
}

@@ -11,15 +11,15 @@ import test from 'ava'

test('getAssetFromKV return correct val from KV without manifest', async (t) => {
mockRequestScope()
// manually reset manifest global, to test optional behaviour
Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined })
mockRequestScope()
// manually reset manifest global, to test optional behaviour
Object.assign(global, { __STATIC_CONTENT_MANIFEST: undefined })
const event = getEvent(new Request('https://blah.com/key1.123HASHBROWN.txt'))
const res = await getAssetFromKV(event)
const event = getEvent(new Request('https://blah.com/key1.123HASHBROWN.txt'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'val1')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'val1')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
})

@@ -9,481 +9,517 @@ import test from 'ava'

test('getAssetFromKV return correct val from KV and default caching', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/key1.txt'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/key1.txt'))
const res = await getAssetFromKV(event)
if (res) {
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), 'MISS')
t.is(await res.text(), 'val1')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), 'MISS')
t.is(await res.text(), 'val1')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV evaluated the file matching the extensionless path first /client/ -> client', async (t) => {
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client/`))
const res = await getAssetFromKV(event)
t.is(await res.text(), 'important file')
t.true(res.headers.get('content-type').includes('text'))
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client/`))
const res = await getAssetFromKV(event)
t.is(await res.text(), 'important file')
t.true(res.headers.get('content-type').includes('text'))
})
test('getAssetFromKV evaluated the file matching the extensionless path first /client -> client', async (t) => {
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client`))
const res = await getAssetFromKV(event)
t.is(await res.text(), 'important file')
t.true(res.headers.get('content-type').includes('text'))
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client`))
const res = await getAssetFromKV(event)
t.is(await res.text(), 'important file')
t.true(res.headers.get('content-type').includes('text'))
})
test('getAssetFromKV if not in asset manifest still returns nohash.txt', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/nohash.txt'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/nohash.txt'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'no hash but still got some result')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'no hash but still got some result')
t.true(res.headers.get('content-type').includes('text'))
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV if no asset manifest /client -> client fails', async (t) => {
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client`))
const error: KVError = await t.throwsAsync(getAssetFromKV(event, { ASSET_MANIFEST: {} }))
t.is(error.status, 404)
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/client`))
const error: KVError = await t.throwsAsync(getAssetFromKV(event, { ASSET_MANIFEST: {} }))
t.is(error.status, 404)
})
test('getAssetFromKV if sub/ -> sub/index.html served', async (t) => {
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/sub`))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
mockRequestScope()
const event = getEvent(new Request(`https://foo.com/sub`))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV gets index.html by default for / requests', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'index.html')
t.true(res.headers.get('content-type').includes('html'))
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'index.html')
t.true(res.headers.get('content-type').includes('html'))
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV non ASCII path support', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/测试.html'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/测试.html'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'My filename is non-ascii')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'My filename is non-ascii')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV supports browser percent encoded URLs', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://example.com/%not-really-percent-encoded.html'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://example.com/%not-really-percent-encoded.html'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'browser percent encoded')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'browser percent encoded')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV supports user percent encoded URLs', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/%2F.html'))
const res = await getAssetFromKV(event)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/%2F.html'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'user percent encoded')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'user percent encoded')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV only decode URL when necessary', async (t) => {
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html'))
const event2 = getEvent(new Request('https://blah.com/你好.html'))
const res1 = await getAssetFromKV(event1)
const res2 = await getAssetFromKV(event2)
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD.html'))
const event2 = getEvent(new Request('https://blah.com/你好.html'))
const res1 = await getAssetFromKV(event1)
const res2 = await getAssetFromKV(event2)
if (res1 && res2) {
t.is(await res1.text(), 'Im important')
t.is(await res2.text(), 'Im important')
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(await res1.text(), 'Im important')
t.is(await res2.text(), 'Im important')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV Support for user decode url path', async (t) => {
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/'))
const event2 = getEvent(new Request('https://blah.com/你好/'))
const res1 = await getAssetFromKV(event1)
const res2 = await getAssetFromKV(event2)
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/%E4%BD%A0%E5%A5%BD/'))
const event2 = getEvent(new Request('https://blah.com/你好/'))
const res1 = await getAssetFromKV(event1)
const res2 = await getAssetFromKV(event2)
if (res1 && res2) {
t.is(await res1.text(), 'My path is non-ascii')
t.is(await res2.text(), 'My path is non-ascii')
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(await res1.text(), 'My path is non-ascii')
t.is(await res2.text(), 'My path is non-ascii')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV custom key modifier', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/docs/sub/blah.png'))
mockRequestScope()
const event = getEvent(new Request('https://blah.com/docs/sub/blah.png'))
const customRequestMapper = (request: Request) => {
let defaultModifiedRequest = mapRequestToAsset(request)
const customRequestMapper = (request: Request) => {
let defaultModifiedRequest = mapRequestToAsset(request)
let url = new URL(defaultModifiedRequest.url)
url.pathname = url.pathname.replace('/docs', '')
return new Request(url.toString(), request)
}
let url = new URL(defaultModifiedRequest.url)
url.pathname = url.pathname.replace('/docs', '')
return new Request(url.toString(), request)
}
const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })
const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV request override with existing manifest file', async (t) => {
// see https://github.com/cloudflare/kv-asset-handler/pull/159 for more info
mockRequestScope()
const event = getEvent(new Request('https://blah.com/image.png')) // real file in manifest
// see https://github.com/cloudflare/kv-asset-handler/pull/159 for more info
mockRequestScope()
const event = getEvent(new Request('https://blah.com/image.png')) // real file in manifest
const customRequestMapper = (request: Request) => {
let defaultModifiedRequest = mapRequestToAsset(request)
const customRequestMapper = (request: Request) => {
let defaultModifiedRequest = mapRequestToAsset(request)
let url = new URL(defaultModifiedRequest.url)
url.pathname = '/image.webp' // other different file in manifest
return new Request(url.toString(), request)
}
let url = new URL(defaultModifiedRequest.url)
url.pathname = '/image.webp' // other different file in manifest
return new Request(url.toString(), request)
}
const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })
const res = await getAssetFromKV(event, { mapRequestToAsset: customRequestMapper })
if (res) {
t.is(await res.text(), 'imagewebp')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(await res.text(), 'imagewebp')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV when setting browser caching', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event, { cacheControl: { browserTTL: 22 } })
const res = await getAssetFromKV(event, { cacheControl: { browserTTL: 22 } })
if (res) {
t.is(res.headers.get('cache-control'), 'max-age=22')
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(res.headers.get('cache-control'), 'max-age=22')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV when setting custom cache setting', async (t) => {
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/'))
const event2 = getEvent(new Request('https://blah.com/key1.png?blah=34'))
const cacheOnlyPngs = (req: Request) => {
if (new URL(req.url).pathname.endsWith('.png'))
return {
browserTTL: 720,
edgeTTL: 720,
}
else
return {
bypassCache: true,
}
}
mockRequestScope()
const event1 = getEvent(new Request('https://blah.com/'))
const event2 = getEvent(new Request('https://blah.com/key1.png?blah=34'))
const cacheOnlyPngs = (req: Request) => {
if (new URL(req.url).pathname.endsWith('.png'))
return {
browserTTL: 720,
edgeTTL: 720,
}
else
return {
bypassCache: true,
}
}
const res1 = await getAssetFromKV(event1, { cacheControl: cacheOnlyPngs })
const res2 = await getAssetFromKV(event2, { cacheControl: cacheOnlyPngs })
const res1 = await getAssetFromKV(event1, { cacheControl: cacheOnlyPngs })
const res2 = await getAssetFromKV(event2, { cacheControl: cacheOnlyPngs })
if (res1 && res2) {
t.is(res1.headers.get('cache-control'), null)
t.true(res2.headers.get('content-type').includes('png'))
t.is(res2.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('cf-cache-status'), 'MISS')
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(res1.headers.get('cache-control'), null)
t.true(res2.headers.get('content-type').includes('png'))
t.is(res2.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('cf-cache-status'), 'MISS')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV caches on two sequential requests', async (t) => {
mockRequestScope()
const resourceKey = 'cache.html'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
}),
)
mockRequestScope()
const resourceKey = 'cache.html'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
}),
)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } })
await sleep(1)
const res2 = await getAssetFromKV(event2)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720, browserTTL: 720 } })
await sleep(1)
const res2 = await getAssetFromKV(event2)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), 'max-age=720')
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV does not store max-age on two sequential requests', async (t) => {
mockRequestScope()
const resourceKey = 'cache.html'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
}),
)
mockRequestScope()
const resourceKey = 'cache.html'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
}),
)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await sleep(100)
const res2 = await getAssetFromKV(event2)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await sleep(100)
const res2 = await getAssetFromKV(event2)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), null)
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
t.is(res2.headers.get('cache-control'), null)
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), null)
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
t.is(res2.headers.get('cache-control'), null)
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV does not cache on Cloudflare when bypass cache set', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event, { cacheControl: { bypassCache: true } })
const res = await getAssetFromKV(event, { cacheControl: { bypassCache: true } })
if (res) {
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), null)
} else {
t.fail('Response was undefined')
}
if (res) {
t.is(res.headers.get('cache-control'), null)
t.is(res.headers.get('cf-cache-status'), null)
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV with no trailing slash on root', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'index.html')
} else {
t.fail('Response was undefined')
}
mockRequestScope()
const event = getEvent(new Request('https://blah.com'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'index.html')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV with no trailing slash on a subdirectory', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/sub/blah.png'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
mockRequestScope()
const event = getEvent(new Request('https://blah.com/sub/blah.png'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'picturedis')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV no result throws an error', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/random'))
const error: KVError = await t.throwsAsync(getAssetFromKV(event))
t.is(error.status, 404)
mockRequestScope()
const event = getEvent(new Request('https://blah.com/random'))
const error: KVError = await t.throwsAsync(getAssetFromKV(event))
t.is(error.status, 404)
})
test('getAssetFromKV TTls set to null should not cache on browser or edge', async (t) => {
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
mockRequestScope()
const event = getEvent(new Request('https://blah.com/'))
const res1 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })
await sleep(100)
const res2 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })
const res1 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })
await sleep(100)
const res2 = await getAssetFromKV(event, { cacheControl: { browserTTL: null, edgeTTL: null } })
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), null)
t.is(res1.headers.get('cache-control'), null)
t.is(res2.headers.get('cf-cache-status'), null)
t.is(res2.headers.get('cache-control'), null)
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), null)
t.is(res1.headers.get('cache-control'), null)
t.is(res2.headers.get('cf-cache-status'), null)
t.is(res2.headers.get('cache-control'), null)
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV passing in a custom NAMESPACE serves correct asset', async (t) => {
mockRequestScope()
let CUSTOM_NAMESPACE = mockKV({
'key1.123HASHBROWN.txt': 'val1',
})
Object.assign(global, { CUSTOM_NAMESPACE })
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'index.html')
t.true(res.headers.get('content-type').includes('html'))
} else {
t.fail('Response was undefined')
}
mockRequestScope()
let CUSTOM_NAMESPACE = mockKV({
'key1.123HASHBROWN.txt': 'val1',
})
Object.assign(global, { CUSTOM_NAMESPACE })
const event = getEvent(new Request('https://blah.com/'))
const res = await getAssetFromKV(event)
if (res) {
t.is(await res.text(), 'index.html')
t.true(res.headers.get('content-type').includes('html'))
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV when custom namespace without the asset should fail', async (t) => {
mockRequestScope()
let CUSTOM_NAMESPACE = mockKV({
'key5.123HASHBROWN.txt': 'customvalu',
})
mockRequestScope()
let CUSTOM_NAMESPACE = mockKV({
'key5.123HASHBROWN.txt': 'customvalu',
})
const event = getEvent(new Request('https://blah.com'))
const error: KVError = await t.throwsAsync(
getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }),
)
t.is(error.status, 404)
const event = getEvent(new Request('https://blah.com'))
const error: KVError = await t.throwsAsync(
getAssetFromKV(event, { ASSET_NAMESPACE: CUSTOM_NAMESPACE }),
)
t.is(error.status, 404)
})
test('getAssetFromKV when namespace not bound fails', async (t) => {
mockRequestScope()
var MY_CUSTOM_NAMESPACE = undefined
Object.assign(global, { MY_CUSTOM_NAMESPACE })
mockRequestScope()
var MY_CUSTOM_NAMESPACE = undefined
Object.assign(global, { MY_CUSTOM_NAMESPACE })
const event = getEvent(new Request('https://blah.com/'))
const error: KVError = await t.throwsAsync(
getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }),
)
t.is(error.status, 500)
const event = getEvent(new Request('https://blah.com/'))
const error: KVError = await t.throwsAsync(
getAssetFromKV(event, { ASSET_NAMESPACE: MY_CUSTOM_NAMESPACE }),
)
t.is(error.status, 500)
})
test('getAssetFromKV when if-none-match === active resource version, should revalidate', async (t) => {
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `W/"${resourceVersion}"`,
},
}),
)
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `W/"${resourceVersion}"`,
},
}),
)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await sleep(100)
const res2 = await getAssetFromKV(event2)
const res1 = await getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await sleep(100)
const res2 = await getAssetFromKV(event2)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
} else {
t.fail('Response was undefined')
}
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV when if-none-match equals etag of stale resource then should bypass cache', async (t) => {
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const req1 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
})
const req2 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}-another-version"`,
},
})
const event = getEvent(req1)
const event2 = getEvent(req2)
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
const res2 = await getAssetFromKV(event)
const res3 = await getAssetFromKV(event2)
if (res1 && res2 && res3) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`)
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
t.not(res3.headers.get('etag'), req2.headers.get('if-none-match'))
t.is(res3.headers.get('cf-cache-status'), 'MISS')
} else {
t.fail('Response was undefined')
}
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const req1 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
})
const req2 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}-another-version"`,
},
})
const event = getEvent(req1)
const event2 = getEvent(req2)
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
const res2 = await getAssetFromKV(event)
const res3 = await getAssetFromKV(event2)
if (res1 && res2 && res3) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`)
t.is(res2.headers.get('cf-cache-status'), 'REVALIDATED')
t.not(res3.headers.get('etag'), req2.headers.get('if-none-match'))
t.is(res3.headers.get('cf-cache-status'), 'MISS')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV when resource in cache, etag should be weakened before returned to eyeball', async (t) => {
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const req1 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
})
const event = getEvent(req1)
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
const res2 = await getAssetFromKV(event)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`)
} else {
t.fail('Response was undefined')
}
mockRequestScope()
const resourceKey = 'key1.png'
const resourceVersion = JSON.parse(mockManifest())[resourceKey]
const req1 = new Request(`https://blah.com/${resourceKey}`, {
headers: {
'if-none-match': `"${resourceVersion}"`,
},
})
const event = getEvent(req1)
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
const res2 = await getAssetFromKV(event)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res2.headers.get('etag'), `W/${req1.headers.get('if-none-match')}`)
} else {
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'
const event = getEvent(new Request(`https://blah.com/${resourceKey}`))
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
await sleep(1)
const res2 = await getAssetFromKV(event)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), null)
t.is(res2.status, 200)
t.is(res2.headers.get('cf-cache-status'), 'HIT')
} else {
t.fail('Response was undefined')
}
const resourceKey = 'cache.html'
const event = getEvent(new Request(`https://blah.com/${resourceKey}`))
const res1 = await getAssetFromKV(event, { cacheControl: { edgeTTL: 720 } })
await sleep(1)
const res2 = await getAssetFromKV(event)
if (res1 && res2) {
t.is(res1.headers.get('cf-cache-status'), 'MISS')
t.is(res1.headers.get('cache-control'), null)
t.is(res2.status, 200)
t.is(res2.headers.get('cf-cache-status'), 'HIT')
} else {
t.fail('Response was undefined')
}
})
test('getAssetFromKV if range request submitted and resource in cache, request fulfilled', async (t) => {
const resourceKey = 'cache.html'
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } }),
)
const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await res1
await sleep(2)
const res2 = await getAssetFromKV(event2)
if (res2.headers.has('content-range')) {
t.is(res2.status, 206)
} else {
t.fail('Response was undefined')
}
const resourceKey = 'cache.html'
const event1 = getEvent(new Request(`https://blah.com/${resourceKey}`))
const event2 = getEvent(
new Request(`https://blah.com/${resourceKey}`, { headers: { range: 'bytes=0-10' } }),
)
const res1 = getAssetFromKV(event1, { cacheControl: { edgeTTL: 720 } })
await res1
await sleep(2)
const res2 = await getAssetFromKV(event2)
if (res2.headers.has('content-range')) {
t.is(res2.status, 206)
} else {
t.fail('Response was undefined')
}
})
test.todo('getAssetFromKV when body not empty, should invoke .cancel()')

@@ -8,31 +8,31 @@ import test from 'ava'

test('mapRequestToAsset() correctly changes /about -> /about/index.html', async (t) => {
mockRequestScope()
let path = '/about'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + '/index.html')
mockRequestScope()
let path = '/about'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + '/index.html')
})
test('mapRequestToAsset() correctly changes /about/ -> /about/index.html', async (t) => {
mockRequestScope()
let path = '/about/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
mockRequestScope()
let path = '/about/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
})
test('mapRequestToAsset() correctly changes /about.me/ -> /about.me/index.html', async (t) => {
mockRequestScope()
let path = '/about.me/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
mockRequestScope()
let path = '/about.me/'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request)
t.is(newRequest.url, request.url + 'index.html')
})
test('mapRequestToAsset() correctly changes /about -> /about/default.html', async (t) => {
mockRequestScope()
let path = '/about'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request, { defaultDocument: 'default.html' })
t.is(newRequest.url, request.url + '/default.html')
mockRequestScope()
let path = '/about'
let request = new Request(`https://foo.com${path}`)
let newRequest = mapRequestToAsset(request, { defaultDocument: 'default.html' })
t.is(newRequest.url, request.url + '/default.html')
})

@@ -8,38 +8,38 @@ import test from 'ava'

function testRequest(path: string) {
mockRequestScope()
let url = new URL('https://example.com')
url.pathname = path
let request = new Request(url.toString())
mockRequestScope()
let url = new URL('https://example.com')
url.pathname = path
let request = new Request(url.toString())
return request
return request
}
test('serveSinglePageApp returns root asset path when request path ends in .html', async (t) => {
let path = '/foo/thing.html'
let request = testRequest(path)
let path = '/foo/thing.html'
let request = testRequest(path)
let expected_request = testRequest('/index.html')
let actual_request = serveSinglePageApp(request)
let expected_request = testRequest('/index.html')
let actual_request = serveSinglePageApp(request)
t.deepEqual(expected_request, actual_request)
t.deepEqual(expected_request, actual_request)
})
test('serveSinglePageApp returns root asset path when request path does not have extension', async (t) => {
let path = '/foo/thing'
let request = testRequest(path)
let path = '/foo/thing'
let request = testRequest(path)
let expected_request = testRequest('/index.html')
let actual_request = serveSinglePageApp(request)
let expected_request = testRequest('/index.html')
let actual_request = serveSinglePageApp(request)
t.deepEqual(expected_request, actual_request)
t.deepEqual(expected_request, actual_request)
})
test('serveSinglePageApp returns requested asset when request path has non-html extension', async (t) => {
let path = '/foo/thing.js'
let request = testRequest(path)
let path = '/foo/thing.js'
let request = testRequest(path)
let expected_request = request
let actual_request = serveSinglePageApp(request)
let expected_request = request
let actual_request = serveSinglePageApp(request)
t.deepEqual(expected_request, actual_request)
t.deepEqual(expected_request, actual_request)
})
export type CacheControl = {
browserTTL: number
edgeTTL: number
bypassCache: boolean
browserTTL: number
edgeTTL: number
bypassCache: boolean
}

@@ -10,35 +10,36 @@

export type Options = {
cacheControl: ((req: Request) => Partial<CacheControl>) | Partial<CacheControl>
ASSET_NAMESPACE: any
ASSET_MANIFEST: AssetManifestType | string
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request
defaultMimeType: string
defaultDocument: string
pathIsEncoded: boolean
cacheControl: ((req: Request) => Partial<CacheControl>) | Partial<CacheControl>
ASSET_NAMESPACE: any
ASSET_MANIFEST: AssetManifestType | string
mapRequestToAsset?: (req: Request, options?: Partial<Options>) => Request
defaultMimeType: string
defaultDocument: string
pathIsEncoded: boolean
defaultETag: 'strong' | 'weak'
}
export class KVError extends Error {
constructor(message?: string, status: number = 500) {
super(message)
// see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
this.name = KVError.name // stack traces display correctly now
this.status = status
}
status: number
constructor(message?: string, status: number = 500) {
super(message)
// see: typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html
Object.setPrototypeOf(this, new.target.prototype) // restore prototype chain
this.name = KVError.name // stack traces display correctly now
this.status = status
}
status: number
}
export class MethodNotAllowedError extends KVError {
constructor(message: string = `Not a valid request method`, status: number = 405) {
super(message, status)
}
constructor(message: string = `Not a valid request method`, status: number = 405) {
super(message, status)
}
}
export class NotFoundError extends KVError {
constructor(message: string = `Not Found`, status: number = 404) {
super(message, status)
}
constructor(message: string = `Not Found`, status: number = 404) {
super(message, status)
}
}
export class InternalError extends KVError {
constructor(message: string = `Internal Error in KV Asset Handler`, status: number = 500) {
super(message, status)
}
constructor(message: string = `Internal Error in KV Asset Handler`, status: number = 500) {
super(message, status)
}
}
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc