Forward client HTPP headers to improve caching

Signed-off-by: Joe Ma <rikkaneko23@gmail.com>
This commit is contained in:
Joe Ma 2023-11-10 19:50:13 +08:00
parent 601bd0b7dc
commit e53deac322
No known key found for this signature in database
GPG key ID: 7A0ECF5F5EDC587F
3 changed files with 80 additions and 17 deletions

View file

@ -20,7 +20,8 @@ import { AwsClient } from 'aws4fetch';
import { customAlphabet } from 'nanoid';
import { sha256 } from 'js-sha256';
import dedent from 'dedent-js';
import { PasteIndexEntry } from './types';
import { Env, PasteIndexEntry } from './types';
import { serve_static } from './proxy';
// Constants
const SERVICE_URL = 'pb.nekoid.cc';
@ -28,14 +29,6 @@ const PASTE_WEB_URL_v1 = 'https://raw.githubusercontent.com/rikkaneko/paste/main
const PASTE_WEB_URL = 'https://raw.githubusercontent.com/rikkaneko/paste/main/static/v2';
const UUID_LENGTH = 4;
export interface Env {
PASTE_INDEX: KVNamespace;
QRCODE: ServiceWorkerGlobalScope;
AWS_ACCESS_KEY_ID: string;
AWS_SECRET_ACCESS_KEY: string;
ENDPOINT: string;
}
const gen_id = customAlphabet('1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz', UUID_LENGTH);
export default {
@ -43,6 +36,7 @@ export default {
const { url, method, headers } = request;
const { pathname, searchParams } = new URL(url);
const path = pathname.replace(/\/+$/, '') || '/';
const match_etag = headers.get('If-None-Match') || undefined;
let cache = caches.default;
const agent = headers.get('user-agent') ?? '';
@ -67,18 +61,18 @@ export default {
}
if (path === '/v1' && method == 'GET') {
return await proxy_uri(PASTE_WEB_URL_v1 + '/paste.html');
return await serve_static(PASTE_WEB_URL_v1 + '/paste.html', headers);
}
if (/\/(js|css)\/.*$/.test(path) && method == 'GET') {
return await proxy_uri(PASTE_WEB_URL + path);
return await serve_static(PASTE_WEB_URL + path, headers);
}
if (path === '/') {
switch (method) {
case 'GET': {
// Fetch the HTML for uploading text/file
return await proxy_uri(PASTE_WEB_URL + '/paste.html');
return await serve_static(PASTE_WEB_URL + '/paste.html', headers);
}
// Create new paste
@ -343,14 +337,32 @@ export default {
// Enable CF cache for authorized request
// Match in existing cache
let res = await cache.match(request.url);
let res = await cache.match(
new Request(`https://${SERVICE_URL}/${uuid}`, {
method: 'GET',
headers: match_etag
? {
// ETag to cache file
'if-none-match': match_etag,
}
: undefined,
})
);
if (res === undefined) {
// Fetch form origin if not hit cache
let origin = await s3.fetch(`${env.ENDPOINT}/${uuid}`, {
method: 'GET',
headers: match_etag
? {
'if-none-match': match_etag,
}
: undefined,
});
res = new Response(origin.body);
// Reserve ETag header
res = new Response(origin.body, { status: origin.status });
const etag = origin.headers.get('etag');
if (etag) res.headers.append('etag', etag);
if (res.status == 404) {
// UUID exists in index but not found in remote object storage service, probably expired
@ -361,7 +373,7 @@ export default {
return new Response('Paste expired.\n', {
status: 410,
});
} else if (!res.ok) {
} else if (!res.ok && res.status !== 304) {
// Other error
return new Response('Internal server error.\n', {
status: 500,
@ -410,13 +422,15 @@ export default {
// res.body cannot be read twice
// Do not block when writing to cache
ctx.waitUntil(cache.put(url, res.clone()));
if (res.ok) ctx.waitUntil(cache.put(url, res.clone()));
return res;
}
// Cache hit
// Matched Etag, no body
if (res.status == 304) return res;
let { readable, writable } = new TransformStream();
res.body!.pipeTo(writable);
res.body?.pipeTo(writable);
return new Response(readable, res);
}

41
src/proxy.ts Normal file
View file

@ -0,0 +1,41 @@
// Proxy URI (Accept *.js, *.css, *.html, *.ico only)
// Use ETag and If-None-Match to cache file
export async function serve_static(path: string, req_headers?: Headers): Promise<Response> {
// Filter static file extension
let mime = 'text/plain; charset=UTF-8;';
if (path.endsWith('.js')) mime = 'application/javascript; charset=UTF-8;';
else if (path.endsWith('.css')) mime = 'text/css; charset=UTF-8;';
else if (path.endsWith('.html')) mime = 'text/html; charset=UTF-8;';
else if (path.endsWith('.ico')) mime = 'image/x-icon';
else
return new Response(null, {
headers: {
'cache-control': 'public, max-age=14400',
},
status: 404,
});
try {
const res = await fetch(path, {
headers: req_headers,
cf: {
cacheEverything: true,
},
});
// Append ETag and Cache
const etag = res.headers.get('etag');
const nres = new Response(res.body, {
headers: {
'content-type': mime,
'cache-control': 'public, max-age=14400',
},
status: res.status,
});
if (etag) nres.headers.append('etag', etag);
return nres;
} catch (err) {
return new Response('Internal server error.\n', {
status: 500,
});
}
}

8
src/types.d.ts vendored
View file

@ -8,3 +8,11 @@ export interface PasteIndexEntry {
read_count_remain?: number;
type?: string;
}
export interface Env {
PASTE_INDEX: KVNamespace;
QRCODE: ServiceWorkerGlobalScope;
AWS_ACCESS_KEY_ID: string;
AWS_SECRET_ACCESS_KEY: string;
ENDPOINT: string;
}