mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 11:29:46 +00:00
Merge branch 'cache-control' into 'main'
Add Cache-Control headers See merge request soapbox-pub/ditto!624
This commit is contained in:
commit
cd2619dbf3
6 changed files with 256 additions and 34 deletions
103
src/app.ts
103
src/app.ts
|
|
@ -131,6 +131,7 @@ import { nodeInfoController, nodeInfoSchemaController } from '@/controllers/well
|
|||
import { nostrController } from '@/controllers/well-known/nostr.ts';
|
||||
import { DittoTranslator } from '@/interfaces/DittoTranslator.ts';
|
||||
import { auth98Middleware, requireProof, requireRole } from '@/middleware/auth98Middleware.ts';
|
||||
import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts';
|
||||
import { cspMiddleware } from '@/middleware/cspMiddleware.ts';
|
||||
import { metricsMiddleware } from '@/middleware/metricsMiddleware.ts';
|
||||
import { notActivitypubMiddleware } from '@/middleware/notActivitypubMiddleware.ts';
|
||||
|
|
@ -198,15 +199,39 @@ app.use(
|
|||
|
||||
app.get('/metrics', metricsController);
|
||||
|
||||
app.get('/.well-known/nodeinfo', nodeInfoController);
|
||||
app.get(
|
||||
'/.well-known/nodeinfo',
|
||||
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||
nodeInfoController,
|
||||
);
|
||||
app.get('/.well-known/nostr.json', nostrController);
|
||||
|
||||
app.get('/nodeinfo/:version', nodeInfoSchemaController);
|
||||
app.get('/manifest.webmanifest', manifestController);
|
||||
app.get(
|
||||
'/nodeinfo/:version',
|
||||
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||
nodeInfoSchemaController,
|
||||
);
|
||||
app.get(
|
||||
'/manifest.webmanifest',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
manifestController,
|
||||
);
|
||||
|
||||
app.get('/api/v1/instance', instanceV1Controller);
|
||||
app.get('/api/v2/instance', instanceV2Controller);
|
||||
app.get('/api/v1/instance/extended_description', instanceDescriptionController);
|
||||
app.get(
|
||||
'/api/v1/instance',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
instanceV1Controller,
|
||||
);
|
||||
app.get(
|
||||
'/api/v2/instance',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
instanceV2Controller,
|
||||
);
|
||||
app.get(
|
||||
'/api/v1/instance/extended_description',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
instanceDescriptionController,
|
||||
);
|
||||
|
||||
app.get('/api/v1/apps/verify_credentials', appCredentialsController);
|
||||
app.post('/api/v1/apps', createAppController);
|
||||
|
|
@ -295,12 +320,28 @@ app.get('/api/v1/preferences', preferencesController);
|
|||
app.get('/api/v1/search', searchController);
|
||||
app.get('/api/v2/search', searchController);
|
||||
|
||||
app.get('/api/pleroma/frontend_configurations', frontendConfigController);
|
||||
app.get(
|
||||
'/api/pleroma/frontend_configurations',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
frontendConfigController,
|
||||
);
|
||||
|
||||
app.get('/api/v1/trends/statuses', rateLimitMiddleware(8, Time.seconds(30)), trendingStatusesController);
|
||||
app.get('/api/v1/trends/links', trendingLinksController);
|
||||
app.get('/api/v1/trends/tags', trendingTagsController);
|
||||
app.get('/api/v1/trends', trendingTagsController);
|
||||
app.get(
|
||||
'/api/v1/trends/links',
|
||||
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||
trendingLinksController,
|
||||
);
|
||||
app.get(
|
||||
'/api/v1/trends/tags',
|
||||
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||
trendingTagsController,
|
||||
);
|
||||
app.get(
|
||||
'/api/v1/trends',
|
||||
cacheControlMiddleware({ maxAge: 300, staleWhileRevalidate: 300, staleIfError: 21600, public: true }),
|
||||
trendingTagsController,
|
||||
);
|
||||
|
||||
app.get('/api/v1/suggestions', suggestionsV1Controller);
|
||||
app.get('/api/v2/suggestions', suggestionsV2Controller);
|
||||
|
|
@ -344,7 +385,11 @@ app.post(
|
|||
captchaVerifyController,
|
||||
);
|
||||
|
||||
app.get('/api/v1/ditto/zap_splits', getZapSplitsController);
|
||||
app.get(
|
||||
'/api/v1/ditto/zap_splits',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, public: true }),
|
||||
getZapSplitsController,
|
||||
);
|
||||
app.get('/api/v1/ditto/:id{[0-9a-f]{64}}/zap_splits', statusZapSplitsController);
|
||||
|
||||
app.put('/api/v1/admin/ditto/zap_splits', requireRole('admin'), updateZapSplitsController);
|
||||
|
|
@ -408,16 +453,36 @@ app.get('/timeline/*', frontendController);
|
|||
|
||||
// Known static file routes
|
||||
app.get('/sw.js', publicFiles);
|
||||
app.get('/favicon.ico', publicFiles, staticFiles);
|
||||
app.get('/images/*', publicFiles, staticFiles);
|
||||
app.get('/instance/*', publicFiles);
|
||||
app.get(
|
||||
'/favicon.ico',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
publicFiles,
|
||||
staticFiles,
|
||||
);
|
||||
app.get(
|
||||
'/images/*',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
publicFiles,
|
||||
staticFiles,
|
||||
);
|
||||
app.get(
|
||||
'/instance/*',
|
||||
cacheControlMiddleware({ maxAge: 5, staleWhileRevalidate: 5, staleIfError: 21600, public: true }),
|
||||
publicFiles,
|
||||
);
|
||||
|
||||
// Packs contains immutable static files
|
||||
app.get('/packs/*', async (c, next) => {
|
||||
c.header('Cache-Control', 'public, max-age=31536000, immutable');
|
||||
c.header('Strict-Transport-Security', '"max-age=31536000" always');
|
||||
await next();
|
||||
}, publicFiles);
|
||||
app.get(
|
||||
'/packs/*',
|
||||
cacheControlMiddleware({
|
||||
maxAge: 31536000,
|
||||
staleWhileRevalidate: 86400,
|
||||
staleIfError: 21600,
|
||||
public: true,
|
||||
immutable: true,
|
||||
}),
|
||||
publicFiles,
|
||||
);
|
||||
|
||||
// Site index
|
||||
app.get('/', frontendController, indexController);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,13 @@
|
|||
import { type Context } from '@hono/hono';
|
||||
import { Handler } from '@hono/hono';
|
||||
|
||||
const emptyArrayController = (c: Context) => c.json([]);
|
||||
const notImplementedController = (c: Context) => Promise.resolve(c.json({ error: 'Not implemented' }, 404));
|
||||
const emptyArrayController: Handler = (c) => {
|
||||
c.header('Cache-Control', 'max-age=300, public, stale-while-revalidate=60');
|
||||
return c.json([]);
|
||||
};
|
||||
|
||||
const notImplementedController: Handler = (c) => {
|
||||
c.header('Cache-Control', 'max-age=300, public, stale-while-revalidate=60');
|
||||
return c.json({ error: 'Not implemented' }, 404);
|
||||
};
|
||||
|
||||
export { emptyArrayController, notImplementedController };
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export const frontendController: AppMiddleware = async (c, next) => {
|
|||
try {
|
||||
const entities = await getEntities(params ?? {});
|
||||
const meta = renderMetadata(c.req.url, entities);
|
||||
c.header('Cache-Control', 'max-age=30, public, stale-while-revalidate=30');
|
||||
return c.html(content.replace(META_PLACEHOLDER, meta));
|
||||
} catch (e) {
|
||||
console.log(`Error building meta tags: ${e}`);
|
||||
|
|
|
|||
|
|
@ -1,36 +1,50 @@
|
|||
import { NostrJson } from '@nostrify/nostrify';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { AppController } from '@/app.ts';
|
||||
import { localNip05Lookup } from '@/utils/nip05.ts';
|
||||
|
||||
const nameSchema = z.string().min(1).regex(/^\w+$/);
|
||||
const nameSchema = z.string().min(1).regex(/^[\w.-]+$/);
|
||||
const emptyResult: NostrJson = { names: {}, relays: {} };
|
||||
|
||||
/**
|
||||
* Serves NIP-05's nostr.json.
|
||||
* https://github.com/nostr-protocol/nips/blob/master/05.md
|
||||
*/
|
||||
const nostrController: AppController = async (c) => {
|
||||
// If there are no query parameters, this will always return an empty result.
|
||||
if (!Object.entries(c.req.queries()).length) {
|
||||
c.header('Cache-Control', 'max-age=31536000, public, immutable, stale-while-revalidate=86400');
|
||||
return c.json(emptyResult);
|
||||
}
|
||||
|
||||
const store = c.get('store');
|
||||
|
||||
const result = nameSchema.safeParse(c.req.query('name'));
|
||||
const name = result.success ? result.data : undefined;
|
||||
|
||||
const pointer = name ? await localNip05Lookup(store, name) : undefined;
|
||||
|
||||
if (!name || !pointer) {
|
||||
return c.json({ names: {}, relays: {} });
|
||||
// Not found, cache for 5 minutes.
|
||||
c.header('Cache-Control', 'max-age=300, public, stale-while-revalidate=30');
|
||||
return c.json(emptyResult);
|
||||
}
|
||||
|
||||
const { pubkey, relays } = pointer;
|
||||
const { pubkey, relays = [] } = pointer;
|
||||
|
||||
return c.json({
|
||||
names: {
|
||||
[name]: pubkey,
|
||||
},
|
||||
relays: {
|
||||
[pubkey]: relays,
|
||||
},
|
||||
});
|
||||
// It's found, so cache for 6 hours.
|
||||
c.header('Cache-Control', 'max-age=21600, public, stale-while-revalidate=3600');
|
||||
|
||||
return c.json(
|
||||
{
|
||||
names: {
|
||||
[name]: pubkey,
|
||||
},
|
||||
relays: {
|
||||
[pubkey]: relays,
|
||||
},
|
||||
} satisfies NostrJson,
|
||||
);
|
||||
};
|
||||
|
||||
export { nostrController };
|
||||
|
|
|
|||
33
src/middleware/cacheControlMiddleware.test.ts
Normal file
33
src/middleware/cacheControlMiddleware.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Hono } from '@hono/hono';
|
||||
import { assertEquals } from '@std/assert';
|
||||
|
||||
import { cacheControlMiddleware } from '@/middleware/cacheControlMiddleware.ts';
|
||||
|
||||
Deno.test('cacheControlMiddleware with multiple options', async () => {
|
||||
const app = new Hono();
|
||||
|
||||
app.use(cacheControlMiddleware({
|
||||
maxAge: 31536000,
|
||||
public: true,
|
||||
immutable: true,
|
||||
}));
|
||||
|
||||
app.get('/', (c) => c.text('OK'));
|
||||
|
||||
const response = await app.request('/');
|
||||
const cacheControl = response.headers.get('Cache-Control');
|
||||
|
||||
assertEquals(cacheControl, 'max-age=31536000, public, immutable');
|
||||
});
|
||||
|
||||
Deno.test('cacheControlMiddleware with no options does not add header', async () => {
|
||||
const app = new Hono();
|
||||
|
||||
app.use(cacheControlMiddleware({}));
|
||||
app.get('/', (c) => c.text('OK'));
|
||||
|
||||
const response = await app.request('/');
|
||||
const cacheControl = response.headers.get('Cache-Control');
|
||||
|
||||
assertEquals(cacheControl, null);
|
||||
});
|
||||
102
src/middleware/cacheControlMiddleware.ts
Normal file
102
src/middleware/cacheControlMiddleware.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
import { MiddlewareHandler } from '@hono/hono';
|
||||
|
||||
/**
|
||||
* Options for the `cacheControlMiddleware` middleware.
|
||||
*
|
||||
* NOTE: All numerical values are in **seconds**.
|
||||
*
|
||||
* See the definitions of [fresh](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age) and [stale](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching#fresh_and_stale_based_on_age).
|
||||
*/
|
||||
export interface CacheControlMiddlewareOpts {
|
||||
/** Indicates that the response remains fresh until _N_ seconds after the response is generated. */
|
||||
maxAge?: number;
|
||||
/** Indicates how long the response remains fresh in a shared cache. */
|
||||
sMaxAge?: number;
|
||||
/** Indicates that the response can be stored in caches, but the response must be validated with the origin server before each reuse, even when the cache is disconnected from the origin server. */
|
||||
noCache?: boolean;
|
||||
/** Indicates that the response can be stored in caches and can be reused while fresh. */
|
||||
mustRevalidate?: boolean;
|
||||
/** Equivalent of `must-revalidate`, but specifically for shared caches only. */
|
||||
proxyRevalidate?: boolean;
|
||||
/** Indicates that any caches of any kind (private or shared) should not store this response. */
|
||||
noStore?: boolean;
|
||||
/** Indicates that the response can be stored only in a private cache (e.g. local caches in browsers). */
|
||||
private?: boolean;
|
||||
/** Indicates that the response can be stored in a shared cache. */
|
||||
public?: boolean;
|
||||
/** Indicates that a cache should store the response only if it understands the requirements for caching based on status code. */
|
||||
mustUnderstand?: boolean;
|
||||
/** Indicates that any intermediary (regardless of whether it implements a cache) shouldn't transform the response contents. */
|
||||
noTransform?: boolean;
|
||||
/** Indicates that the response will not be updated while it's fresh. */
|
||||
immutable?: boolean;
|
||||
/** Indicates that the cache could reuse a stale response while it revalidates it to a cache. */
|
||||
staleWhileRevalidate?: number;
|
||||
/** indicates that the cache can reuse a stale response when an upstream server generates an error, or when the error is generated locally. */
|
||||
staleIfError?: number;
|
||||
}
|
||||
|
||||
/** Adds a [`Cache-Control`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) header to the response. */
|
||||
export function cacheControlMiddleware(opts: CacheControlMiddlewareOpts): MiddlewareHandler {
|
||||
return async (c, next) => {
|
||||
const directives: string[] = [];
|
||||
|
||||
if (typeof opts.maxAge === 'number') {
|
||||
directives.push(`max-age=${opts.maxAge}`);
|
||||
}
|
||||
|
||||
if (typeof opts.sMaxAge === 'number') {
|
||||
directives.push(`s-maxage=${opts.sMaxAge}`);
|
||||
}
|
||||
|
||||
if (opts.noCache) {
|
||||
directives.push('no-cache');
|
||||
}
|
||||
|
||||
if (opts.mustRevalidate) {
|
||||
directives.push('must-revalidate');
|
||||
}
|
||||
|
||||
if (opts.proxyRevalidate) {
|
||||
directives.push('proxy-revalidate');
|
||||
}
|
||||
|
||||
if (opts.noStore) {
|
||||
directives.push('no-store');
|
||||
}
|
||||
|
||||
if (opts.private) {
|
||||
directives.push('private');
|
||||
}
|
||||
|
||||
if (opts.public) {
|
||||
directives.push('public');
|
||||
}
|
||||
|
||||
if (opts.mustUnderstand) {
|
||||
directives.push('must-understand');
|
||||
}
|
||||
|
||||
if (opts.noTransform) {
|
||||
directives.push('no-transform');
|
||||
}
|
||||
|
||||
if (opts.immutable) {
|
||||
directives.push('immutable');
|
||||
}
|
||||
|
||||
if (typeof opts.staleWhileRevalidate === 'number') {
|
||||
directives.push(`stale-while-revalidate=${opts.staleWhileRevalidate}`);
|
||||
}
|
||||
|
||||
if (typeof opts.staleIfError === 'number') {
|
||||
directives.push(`stale-if-error=${opts.staleIfError}`);
|
||||
}
|
||||
|
||||
if (directives.length) {
|
||||
c.header('Cache-Control', directives.join(', '));
|
||||
}
|
||||
|
||||
await next();
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue