Skip to content

Commit

Permalink
fix: caching
Browse files Browse the repository at this point in the history
Closes #73
  • Loading branch information
flakey5 committed Nov 21, 2023
1 parent a8936d6 commit c53e585
Show file tree
Hide file tree
Showing 16 changed files with 146 additions and 100 deletions.
19 changes: 0 additions & 19 deletions src/commonResponses.ts

This file was deleted.

6 changes: 6 additions & 0 deletions src/constants/cache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const CACHE = caches.default;

export const CACHE_HEADERS = {
success: 'public, max-age=3600, s-maxage=14400',
failure: 'private, no-cache, no-store, max-age=0, must-revalidate',
};
40 changes: 40 additions & 0 deletions src/constants/commonResponses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { CACHE_HEADERS } from './cache';

export const METHOD_NOT_ALLOWED = new Response(undefined, {
status: 405,
headers: {
allow: 'GET, HEAD, OPTIONS',
'cache-control': CACHE_HEADERS.failure,
},
});

export const BAD_REQUEST = new Response(undefined, {
status: 400,
headers: {
'cache-control': CACHE_HEADERS.failure,
},
});

export const FILE_NOT_FOUND = (request: Request): Response => {
return new Response(
request.method !== 'HEAD' ? 'File not found' : undefined,
{
status: 404,
headers: {
'cache-control': CACHE_HEADERS.failure,
},
}
);
};

export const DIRECTORY_NOT_FOUND = (request: Request): Response => {
return new Response(
request.method !== 'HEAD' ? 'Directory not found' : undefined,
{
status: 404,
headers: {
'cache-control': CACHE_HEADERS.failure,
},
}
);
};
12 changes: 12 additions & 0 deletions src/constants/r2Prefixes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,15 @@ export const VIRTUAL_DIRS: Record<string, Set<string>> = {
.map(([key]) => key.substring(12) + '/')
),
};

export const URL_TO_BUCKET_PATH_MAP: Record<string, (url: URL) => string> = {
dist: (url): string =>
DIST_PATH_PREFIX + (url.pathname.substring('/dist'.length) || '/'),
download: (url): string =>
DOWNLOAD_PATH_PREFIX + (url.pathname.substring('/download'.length) || '/'),
docs: (url): string =>
DOCS_PATH_PREFIX + (url.pathname.substring('/docs'.length) || '/'),
api: (url): string =>
API_PATH_PREFIX + (url.pathname.substring('/api'.length) || '/'),
metrics: (url): string => url.pathname.substring(1), // substring to cut off the /
};
10 changes: 1 addition & 9 deletions src/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export interface Env {
/**
* Environment the worker is running in
*/
ENVIRONMENT: 'dev' | 'staging' | 'prod';
ENVIRONMENT: 'dev' | 'staging' | 'prod' | 'e2e-tests';
/**
* R2 bucket we read from
*/
Expand Down Expand Up @@ -36,14 +36,6 @@ export interface Env {
* Api key for /_cf/cache-purge. If undefined, the endpoint is disabled.
*/
CACHE_PURGE_API_KEY?: string;
/**
* Cache control header for files
*/
FILE_CACHE_CONTROL: string;
/**
* Cache control header for directory listing
*/
DIRECTORY_CACHE_CONTROL: string;
/**
* Sentry DSN, used for error monitoring
* If missing, Sentry isn't used
Expand Down
16 changes: 8 additions & 8 deletions src/handlers/get.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import responses from '../commonResponses';
import { CACHE } from '../constants/cache';
import { BAD_REQUEST, FILE_NOT_FOUND } from '../constants/commonResponses';
import { VIRTUAL_DIRS } from '../constants/r2Prefixes';
import {
isCacheEnabled,
Expand All @@ -14,12 +15,12 @@ import {
} from './strategies/directoryListing';
import { getFile } from './strategies/serveFile';

const getHandler: Handler = async (request, env, ctx, cache) => {
const getHandler: Handler = async (request, env, ctx) => {
const shouldServeCache = isCacheEnabled(env);

if (shouldServeCache) {
// Caching is enabled, let's see if the request is cached
const response = await cache.match(request);
const response = await CACHE.match(request);

if (typeof response !== 'undefined') {
return response;
Expand All @@ -29,7 +30,7 @@ const getHandler: Handler = async (request, env, ctx, cache) => {
const requestUrl = parseUrl(request);

if (requestUrl === undefined) {
return responses.BAD_REQUEST;
return BAD_REQUEST;
}

const bucketPath = mapUrlPathToBucketPath(requestUrl, env);
Expand All @@ -46,7 +47,7 @@ const getHandler: Handler = async (request, env, ctx, cache) => {
if (env.DIRECTORY_LISTING === 'off') {
// File not found since we should only be allowing
// file paths if directory listing is off
return responses.FILE_NOT_FOUND(request);
return FILE_NOT_FOUND(request);
}

if (bucketPath && !hasTrailingSlash(requestUrl.pathname)) {
Expand All @@ -65,8 +66,7 @@ const getHandler: Handler = async (request, env, ctx, cache) => {
requestUrl,
request,
VIRTUAL_DIRS[bucketPath],
[],
env
[]
);
} else if (isPathADirectory) {
// List the directory
Expand All @@ -86,7 +86,7 @@ const getHandler: Handler = async (request, env, ctx, cache) => {

cachedResponse.headers.append('x-cache-status', 'hit');

ctx.waitUntil(cache.put(request, cachedResponse));
ctx.waitUntil(CACHE.put(request, cachedResponse));
}

response.headers.append('x-cache-status', 'miss');
Expand Down
3 changes: 1 addition & 2 deletions src/handlers/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,5 @@ import { Env } from '../env';
export type Handler = (
request: Request,
env: Env,
ctx: ExecutionContext,
cache: Cache
ctx: ExecutionContext
) => Promise<Response>;
12 changes: 6 additions & 6 deletions src/handlers/post.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import responses from '../commonResponses';
import { isCacheEnabled, parseUrl } from '../util';
import { BAD_REQUEST } from '../constants/commonResponses';
import { parseUrl } from '../util';
import { Handler } from './handler';
import { cachePurge } from './strategies/cachePurge';

const postHandler: Handler = async (request, env, _, cache) => {
const postHandler: Handler = async (request, env) => {
const url = parseUrl(request);

if (url === undefined) {
return responses.BAD_REQUEST;
return BAD_REQUEST;
}

// This endpoint is called from the sync script to purge
// directories that are commonly updated so we don't need to
// wait for the cache to expire
if (isCacheEnabled(env) && url.pathname === '/_cf/cache-purge') {
return cachePurge(url, request, cache, env);
if (url.pathname === '/_cf/cache-purge') {
return cachePurge(url, request, env);
}

return new Response(url.pathname, { status: 404 });
Expand Down
10 changes: 5 additions & 5 deletions src/handlers/strategies/cachePurge.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { z } from 'zod';
import { Env } from '../../env';
import responses from '../../commonResponses';
import { mapBucketPathToUrlPath } from '../../util';
import { CACHE } from '../../constants/cache';
import { BAD_REQUEST } from '../../constants/commonResponses';

const CachePurgeBodySchema = z.object({
paths: z.array(z.string()),
Expand All @@ -28,14 +29,14 @@ async function parseBody(request: Request): Promise<CachePurgeBody | Response> {
bodyObject = await request.json<object>();
} catch (e) {
// content-type header lied to us
return responses.BAD_REQUEST;
return BAD_REQUEST;
}

// Validate the body's contents
const parseResult = CachePurgeBodySchema.safeParse(bodyObject);

if (!parseResult.success) {
return responses.BAD_REQUEST;
return BAD_REQUEST;
}

return parseResult.data;
Expand All @@ -52,7 +53,6 @@ async function parseBody(request: Request): Promise<CachePurgeBody | Response> {
export async function cachePurge(
url: URL,
request: Request,
cache: Cache,
env: Env
): Promise<Response> {
const providedApiKey = request.headers.get('x-api-key');
Expand Down Expand Up @@ -82,7 +82,7 @@ export async function cachePurge(
}

for (const urlPath of urlPaths) {
promises.push(cache.delete(new Request(`${baseUrl}/${urlPath}`)));
promises.push(CACHE.delete(new Request(`${baseUrl}/${urlPath}`)));
}
}

Expand Down
12 changes: 6 additions & 6 deletions src/handlers/strategies/directoryListing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {
} from '@aws-sdk/client-s3';
import Handlebars from 'handlebars';
import { Env } from '../../env';
import responses from '../../commonResponses';
import { niceBytes } from '../../util';
import { getFile } from './serveFile';

// Imports the Precompiled Handlebars Template
import htmlTemplate from '../../templates/directoryListing.out.js';
import { S3_MAX_KEYS, S3_RETRY_LIMIT } from '../../constants/limits';
import { CACHE_HEADERS } from '../../constants/cache';
import { DIRECTORY_NOT_FOUND } from '../../constants/commonResponses';

// Applies the Template into a Handlebars Template Function
const handleBarsTemplate = Handlebars.template(htmlTemplate);
Expand All @@ -31,8 +32,7 @@ export function renderDirectoryListing(
url: URL,
request: Request,
delimitedPrefixes: Set<string>,
objects: _Object[],
env: Env
objects: _Object[]
): Response {
// Holds the contents of the listing (directories and files)
const tableElements = [];
Expand Down Expand Up @@ -101,7 +101,7 @@ export function renderDirectoryListing(
headers: {
'last-modified': lastModifiedUTC,
'content-type': 'text/html',
'cache-control': env.DIRECTORY_CACHE_CONTROL || 'no-store',
'cache-control': CACHE_HEADERS.success,
},
});
}
Expand Down Expand Up @@ -222,12 +222,12 @@ export async function listDirectory(

// Directory needs either subdirectories or files in it cannot be empty
if (delimitedPrefixes.size === 0 && objects.length === 0) {
return responses.DIRECTORY_NOT_FOUND(request);
return DIRECTORY_NOT_FOUND(request);
}

if (request.method === 'HEAD') {
return new Response(undefined, { status: 200 });
}

return renderDirectoryListing(url, request, delimitedPrefixes, objects, env);
return renderDirectoryListing(url, request, delimitedPrefixes, objects);
}
23 changes: 15 additions & 8 deletions src/handlers/strategies/serveFile.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { Env } from '../../env';
import { objectHasBody } from '../../util';
import responses from '../../commonResponses';
import { CACHE_HEADERS } from '../../constants/cache';
import {
BAD_REQUEST,
FILE_NOT_FOUND,
METHOD_NOT_ALLOWED,
} from '../../constants/commonResponses';

/**
* Decides on what status code to return to
Expand Down Expand Up @@ -68,36 +73,38 @@ export async function getFile(
break;
} catch (e) {
// Unquoted etags make R2 api throw an error
return responses.BAD_REQUEST;
return BAD_REQUEST;
}
case 'HEAD':
file = await env.R2_BUCKET.head(bucketPath);
break;
default:
return responses.METHOD_NOT_ALLOWED;
return METHOD_NOT_ALLOWED;
}

if (file === null) {
return responses.FILE_NOT_FOUND(request);
return FILE_NOT_FOUND(request);
}

const hasBody = objectHasBody(file);

const cacheControl =
file.httpMetadata?.cacheControl ?? (env.FILE_CACHE_CONTROL || 'no-store');
const statusCode = getStatusCode(request, hasBody);

return new Response(
hasBody && file.size != 0 ? (file as R2ObjectBody).body : null,
{
status: getStatusCode(request, hasBody),
status: statusCode,
headers: {
etag: file.httpEtag,
'accept-range': 'bytes',
// https://github.com/nodejs/build/blob/e3df25d6a23f033db317a53ab1e904c953ba1f00/ansible/www-standalone/resources/config/nodejs.org?plain=1#L194-L196
'access-control-allow-origin': url.pathname.endsWith('.json')
? '*'
: '',
'cache-control': cacheControl,
'cache-control':
statusCode === 200
? file.httpMetadata?.cacheControl ?? CACHE_HEADERS.success
: CACHE_HEADERS.failure,
expires: file.httpMetadata?.cacheExpiry?.toUTCString() ?? '',
'last-modified': file.uploaded.toUTCString(),
'content-encoding': file.httpMetadata?.contentEncoding ?? '',
Expand Down
Loading

0 comments on commit c53e585

Please sign in to comment.