From 50082b33921d6606b08625bed94d0517be6d5ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emanuel=20Tesa=C5=99?= Date: Sat, 25 Nov 2023 11:23:57 +0100 Subject: [PATCH] Implement explicit cache control configuration --- packages/api/config/signed-api.example.json | 5 +-- packages/api/src/constants.ts | 10 ----- packages/api/src/handlers.test.ts | 6 --- packages/api/src/handlers.ts | 8 ++-- packages/api/src/headers.test.ts | 43 +++++++++++++++++++++ packages/api/src/headers.ts | 28 ++++++++++++++ packages/api/src/schema.ts | 11 ++++-- packages/api/src/utils.ts | 4 +- packages/e2e/src/signed-api/signed-api.json | 3 -- 9 files changed, 86 insertions(+), 32 deletions(-) delete mode 100644 packages/api/src/constants.ts create mode 100644 packages/api/src/headers.test.ts create mode 100644 packages/api/src/headers.ts diff --git a/packages/api/config/signed-api.example.json b/packages/api/config/signed-api.example.json index 7b8075b4..f7aa83c7 100644 --- a/packages/api/config/signed-api.example.json +++ b/packages/api/config/signed-api.example.json @@ -10,8 +10,5 @@ } ], "allowedAirnodes": ["0x8A791620dd6260079BF849Dc5567aDC3F2FdC318"], - "maxBatchSize": 10, - "cache": { - "maxAgeSeconds": 300 - } + "maxBatchSize": 10 } diff --git a/packages/api/src/constants.ts b/packages/api/src/constants.ts deleted file mode 100644 index 5be95b96..00000000 --- a/packages/api/src/constants.ts +++ /dev/null @@ -1,10 +0,0 @@ -export const COMMON_HEADERS = { - 'content-type': 'application/json', - 'access-control-allow-origin': '*', - 'access-control-allow-methods': '*', -}; - -export const CACHE_HEADERS = { - 'cache-control': 'no-store', // Disable browser-caching - 'cdn-cache-control': 'max-age=10', // Enable CDN caching and set to 10 -}; diff --git a/packages/api/src/handlers.test.ts b/packages/api/src/handlers.test.ts index ce7b64bd..0b8a210f 100644 --- a/packages/api/src/handlers.test.ts +++ b/packages/api/src/handlers.test.ts @@ -174,8 +174,6 @@ describe(getData.name, () => { headers: { 'access-control-allow-methods': '*', 'access-control-allow-origin': '*', - 'cache-control': 'no-store', - 'cdn-cache-control': 'max-age=10', 'content-type': 'application/json', }, statusCode: 200, @@ -203,8 +201,6 @@ describe(getData.name, () => { headers: { 'access-control-allow-methods': '*', 'access-control-allow-origin': '*', - 'cache-control': 'no-store', - 'cdn-cache-control': 'max-age=10', 'content-type': 'application/json', }, statusCode: 200, @@ -228,8 +224,6 @@ describe(listAirnodeAddresses.name, () => { headers: { 'access-control-allow-methods': '*', 'access-control-allow-origin': '*', - 'cache-control': 'no-store', - 'cdn-cache-control': 'max-age=300', 'content-type': 'application/json', }, statusCode: 200, diff --git a/packages/api/src/handlers.ts b/packages/api/src/handlers.ts index 66d7e6e9..a0e02d4d 100644 --- a/packages/api/src/handlers.ts +++ b/packages/api/src/handlers.ts @@ -2,8 +2,8 @@ import { go, goSync } from '@api3/promise-utils'; import { isEmpty, isNil, omit, size } from 'lodash'; import { getConfig } from './config'; -import { CACHE_HEADERS, COMMON_HEADERS } from './constants'; import { deriveBeaconId, recoverSignerAddress } from './evm'; +import { createResponseHeaders } from './headers'; import { get, getAll, getAllAirnodeAddresses, prune, putAll } from './in-memory-cache'; import { logger } from './logger'; import { type SignedData, batchSignedDataSchema, evmAddressSchema } from './schema'; @@ -112,7 +112,7 @@ export const batchInsertData = async (requestBody: unknown): Promise => { return { statusCode: 200, - headers: { ...COMMON_HEADERS, ...CACHE_HEADERS, 'cdn-cache-control': `max-age=${getConfig().cache.maxAgeSeconds}` }, + headers: createResponseHeaders(getConfig().cache), body: JSON.stringify({ count: airnodeAddresses.length, 'available-airnodes': airnodeAddresses }), }; }; diff --git a/packages/api/src/headers.test.ts b/packages/api/src/headers.test.ts new file mode 100644 index 00000000..61debd96 --- /dev/null +++ b/packages/api/src/headers.test.ts @@ -0,0 +1,43 @@ +import { createResponseHeaders } from './headers'; + +describe(createResponseHeaders.name, () => { + it('returns common headers when cache is not set', () => { + const expectedHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + }; + expect(createResponseHeaders()).toStrictEqual(expectedHeaders); + }); + + it('returns browser cache headers when cache type is browser', () => { + const expectedHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + 'cache-control': 'max-age=3600', + }; + expect( + createResponseHeaders({ + type: 'browser', + maxAgeSeconds: 3600, + }) + ).toStrictEqual(expectedHeaders); + }); + + it('returns CDN cache headers when cache type is cdn', () => { + const expectedHeaders = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', + 'cache-control': 'no-store', + 'cdn-cache-control': 'max-age=3600', + }; + expect( + createResponseHeaders({ + type: 'cdn', + maxAgeSeconds: 3600, + }) + ).toStrictEqual(expectedHeaders); + }); +}); diff --git a/packages/api/src/headers.ts b/packages/api/src/headers.ts new file mode 100644 index 00000000..3d65bc7f --- /dev/null +++ b/packages/api/src/headers.ts @@ -0,0 +1,28 @@ +import type { Cache } from './schema'; + +const COMMON_HEADERS = { + 'content-type': 'application/json', + 'access-control-allow-origin': '*', + 'access-control-allow-methods': '*', +}; + +export const createResponseHeaders = (cache?: Cache | undefined) => { + if (!cache) return COMMON_HEADERS; + + const { type, maxAgeSeconds } = cache; + switch (type) { + case 'browser': { + return { + ...COMMON_HEADERS, + 'cache-control': `max-age=${maxAgeSeconds}`, + }; + } + case 'cdn': { + return { + ...COMMON_HEADERS, + 'cache-control': 'no-store', // Disable browser-caching + 'cdn-cache-control': `max-age=${maxAgeSeconds}`, + }; + } + } +}; diff --git a/packages/api/src/schema.ts b/packages/api/src/schema.ts index eb092e22..b59dd52b 100644 --- a/packages/api/src/schema.ts +++ b/packages/api/src/schema.ts @@ -24,12 +24,17 @@ export const endpointsSchema = z export const allowedAirnodesSchema = z.union([z.literal('*'), z.array(evmAddressSchema).nonempty()]); +export const cacheSchema = z.strictObject({ + type: z.union([z.literal('browser'), z.literal('cdn')]), + maxAgeSeconds: z.number().nonnegative().int(), +}); + +export type Cache = z.infer; + export const configSchema = z.strictObject({ endpoints: endpointsSchema, maxBatchSize: z.number().nonnegative().int(), - cache: z.strictObject({ - maxAgeSeconds: z.number().nonnegative().int(), - }), + cache: cacheSchema.optional(), allowedAirnodes: allowedAirnodesSchema, }); diff --git a/packages/api/src/utils.ts b/packages/api/src/utils.ts index bf816bbb..6d21a6d1 100644 --- a/packages/api/src/utils.ts +++ b/packages/api/src/utils.ts @@ -1,4 +1,4 @@ -import { COMMON_HEADERS } from './constants'; +import { createResponseHeaders } from './headers'; import type { BatchSignedData, SignedData } from './schema'; import type { ApiResponse } from './types'; @@ -18,5 +18,5 @@ export const generateErrorResponse = ( detail?: string, extra?: unknown ): ApiResponse => { - return { statusCode, headers: COMMON_HEADERS, body: JSON.stringify({ message, detail, extra }) }; + return { statusCode, headers: createResponseHeaders(), body: JSON.stringify({ message, detail, extra }) }; }; diff --git a/packages/e2e/src/signed-api/signed-api.json b/packages/e2e/src/signed-api/signed-api.json index ff6fc5e9..573aac62 100644 --- a/packages/e2e/src/signed-api/signed-api.json +++ b/packages/e2e/src/signed-api/signed-api.json @@ -10,8 +10,5 @@ } ], "maxBatchSize": 10, - "cache": { - "maxAgeSeconds": 0 - }, "allowedAirnodes": ["0xbF3137b0a7574563a23a8fC8badC6537F98197CC"] }