From 3f676012103b9d8313c8126ee4db1cdbe1638c8b Mon Sep 17 00:00:00 2001 From: Brett Willis Date: Thu, 18 May 2023 10:22:08 +1200 Subject: [PATCH 1/3] Add cacheControl option with tests and docs --- README.md | 1 + index.js | 6 ++++++ test/cors.test.js | 15 +++++++++++---- types/index.d.ts | 7 +++++++ types/index.test-d.ts | 18 ++++++++++++++++++ 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 26e46e6..220fba3 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ You can use it as is without passing any option or you can configure it as expla * `exposedHeaders`: Configures the **Access-Control-Expose-Headers** CORS header. Expects a comma-delimited string (ex: `'Content-Range,X-Content-Range'`) or an array (ex: `['Content-Range', 'X-Content-Range']`). If not specified, no custom headers are exposed. * `credentials`: Configures the **Access-Control-Allow-Credentials** CORS header. Set to `true` to pass the header, otherwise it is omitted. * `maxAge`: Configures the **Access-Control-Max-Age** CORS header. In seconds. Set to an integer to pass the header, otherwise it is omitted. +* `cacheControl`: Configures the **Cache-Control** header for CORS preflight responses. Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define the header value), otherwise the header is omitted. * `preflightContinue`: Pass the CORS preflight response to the route handler (default: `false`). * `optionsSuccessStatus`: Provides a status code to use for successful `OPTIONS` requests, since some legacy browsers (IE11, various SmartTVs) choke on `204`. * `preflight`: if needed you can entirely disable preflight by passing `false` here (default: `true`). diff --git a/index.js b/index.js index 0837807..e51a666 100644 --- a/index.js +++ b/index.js @@ -235,6 +235,12 @@ function addPreflightHeaders (req, reply, corsOptions) { if (corsOptions.maxAge !== null) { reply.header('Access-Control-Max-Age', String(corsOptions.maxAge)) } + + if (typeof corsOptions.cacheControl === 'number') { + reply.header('Cache-Control', `max-age=${corsOptions.cacheControl}`) + } else if (typeof corsOptions.cacheControl === 'string') { + reply.header('Cache-Control', corsOptions.cacheControl) + } } function resolveOriginWrapper (fastify, origin) { diff --git a/test/cors.test.js b/test/cors.test.js index b81b94d..c79b0cc 100644 --- a/test/cors.test.js +++ b/test/cors.test.js @@ -38,7 +38,8 @@ test('Should add cors headers (custom values)', t => { credentials: true, exposedHeaders: ['foo', 'bar'], allowedHeaders: ['baz', 'woo'], - maxAge: 123 + maxAge: 123, + cacheControl: 321 }) fastify.get('/', (req, reply) => { @@ -65,6 +66,7 @@ test('Should add cors headers (custom values)', t => { 'access-control-allow-methods': 'GET', 'access-control-allow-headers': 'baz, woo', 'access-control-max-age': '123', + 'cache-control': 'max-age=321', 'content-length': '0' }) }) @@ -96,14 +98,16 @@ test('Should support dynamic config (callback)', t => { credentials: true, exposedHeaders: ['foo', 'bar'], allowedHeaders: ['baz', 'woo'], - maxAge: 123 + maxAge: 123, + cacheControl: 456 }, { origin: 'sample.com', methods: 'GET', credentials: true, exposedHeaders: ['zoo', 'bar'], allowedHeaders: ['baz', 'foo'], - maxAge: 321 + maxAge: 321, + cacheControl: 'public, max-age=456' }] const fastify = Fastify() @@ -164,6 +168,7 @@ test('Should support dynamic config (callback)', t => { 'access-control-allow-methods': 'GET', 'access-control-allow-headers': 'baz, foo', 'access-control-max-age': '321', + 'cache-control': 'public, max-age=456', 'content-length': '0' }) }) @@ -197,7 +202,8 @@ test('Should support dynamic config (Promise)', t => { credentials: true, exposedHeaders: ['zoo', 'bar'], allowedHeaders: ['baz', 'foo'], - maxAge: 321 + maxAge: 321, + cacheControl: 'public, max-age=456' }] const fastify = Fastify() @@ -258,6 +264,7 @@ test('Should support dynamic config (Promise)', t => { 'access-control-allow-methods': 'GET', 'access-control-allow-headers': 'baz, foo', 'access-control-max-age': '321', + 'cache-control': 'public, max-age=456', 'content-length': '0' }) }) diff --git a/types/index.d.ts b/types/index.d.ts index ab4b577..0abf15f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -69,6 +69,13 @@ declare namespace fastifyCors { * Set to an integer to pass the header, otherwise it is omitted. */ maxAge?: number; + /** + * Configures the Cache-Control header for CORS preflight responses. + * Set to an integer to pass the header as `Cache-Control: max-age=${cacheControl}`, + * or set to a string to pass the header as `Cache-Control: ${cacheControl}` (fully define + * the header value), otherwise the header is omitted. + */ + cacheControl?: number | string; /** * Pass the CORS preflight response to the route handler (default: false). */ diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 97ff24d..6a5cd51 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -18,6 +18,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: 'authorization', maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -31,6 +32,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 'public, max-age=3500', preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -44,6 +46,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -57,6 +60,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -70,6 +74,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -83,6 +88,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -104,6 +110,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, optionsSuccessStatus: 200, preflight: false, strictPreflight: false @@ -120,6 +127,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: 'authorization', maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -133,6 +141,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -146,6 +155,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -159,6 +169,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -172,6 +183,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -185,6 +197,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -204,6 +217,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -218,6 +232,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (req, cb) => { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -233,6 +248,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => (req) => credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -248,6 +264,7 @@ const delegate: FastifyPluginOptionsDelegate credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -302,6 +319,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, From 8f08a447d4170c76ce7f74b70632e0e7e8317c6f Mon Sep 17 00:00:00 2001 From: Brett Willis Date: Sat, 20 May 2023 09:02:09 +1200 Subject: [PATCH 2/3] Normalise cacheControl option --- index.js | 10 +++++++--- types/index.test-d.ts | 17 +++++++++++------ 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/index.js b/index.js index e51a666..d2c9268 100644 --- a/index.js +++ b/index.js @@ -133,11 +133,17 @@ function handleCorsOptionsCallbackDelegator (optionsResolver, fastify, req, repl }) } +/** + * @param {import('./types').FastifyCorsOptions} opts + */ function normalizeCorsOptions (opts) { const corsOptions = Object.assign({}, defaultOptions, opts) if (Array.isArray(opts.origin) && opts.origin.indexOf('*') !== -1) { corsOptions.origin = '*' } + if (Number.isInteger(Number(corsOptions.cacheControl)) === true) { + corsOptions.cacheControl = `max-age=${corsOptions.cacheControl}` + } return corsOptions } @@ -236,9 +242,7 @@ function addPreflightHeaders (req, reply, corsOptions) { reply.header('Access-Control-Max-Age', String(corsOptions.maxAge)) } - if (typeof corsOptions.cacheControl === 'number') { - reply.header('Cache-Control', `max-age=${corsOptions.cacheControl}`) - } else if (typeof corsOptions.cacheControl === 'string') { + if (corsOptions.cacheControl && (typeof corsOptions.cacheControl === 'string')) { reply.header('Cache-Control', corsOptions.cacheControl) } } diff --git a/types/index.test-d.ts b/types/index.test-d.ts index 6a5cd51..9e83223 100644 --- a/types/index.test-d.ts +++ b/types/index.test-d.ts @@ -1,6 +1,7 @@ -import fastify from 'fastify' +import fastify, { FastifyRequest } from 'fastify' import { expectType } from 'tsd' import fastifyCors, { + FastifyCorsOptions, FastifyCorsOptionsDelegate, FastifyCorsOptionsDelegatePromise, FastifyPluginOptionsDelegate, @@ -293,25 +294,29 @@ appHttp2.register(fastifyCors, { appHttp2.register(fastifyCors, { hook: 'preParsing', - delegator: () => { - return { + delegator: (req, cb) => { + if (req.url.startsWith('/some-value')) { + cb(new Error()) + } + cb(null, { origin: [/\*/, /something/], allowedHeaders: ['authorization', 'content-type'], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'], credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 12000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, strictPreflight: false - } + }) } }) appHttp2.register(fastifyCors, { hook: 'preParsing', - delegator: () => { + delegator: async (req: FastifyRequest): Promise => { return { origin: [/\*/, /something/], allowedHeaders: ['authorization', 'content-type'], @@ -319,7 +324,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, - cacheControl: 13000, + cacheControl: 'public, max-age=3500', preflightContinue: false, optionsSuccessStatus: 200, preflight: false, From 59104e5d4e4850d34e8fc5d1e656f1caf888fba7 Mon Sep 17 00:00:00 2001 From: Brett Willis Date: Wed, 24 May 2023 07:47:01 +1200 Subject: [PATCH 3/3] Don't coerce cacheControl to number --- index.js | 8 ++++++-- test/cors.test.js | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index d2c9268..718b3b3 100644 --- a/index.js +++ b/index.js @@ -141,8 +141,12 @@ function normalizeCorsOptions (opts) { if (Array.isArray(opts.origin) && opts.origin.indexOf('*') !== -1) { corsOptions.origin = '*' } - if (Number.isInteger(Number(corsOptions.cacheControl)) === true) { + if (Number.isInteger(corsOptions.cacheControl)) { + // integer numbers are formatted this way corsOptions.cacheControl = `max-age=${corsOptions.cacheControl}` + } else if (typeof corsOptions.cacheControl !== 'string') { + // strings are applied directly and any other value is ignored + corsOptions.cacheControl = null } return corsOptions } @@ -242,7 +246,7 @@ function addPreflightHeaders (req, reply, corsOptions) { reply.header('Access-Control-Max-Age', String(corsOptions.maxAge)) } - if (corsOptions.cacheControl && (typeof corsOptions.cacheControl === 'string')) { + if (corsOptions.cacheControl) { reply.header('Cache-Control', corsOptions.cacheControl) } } diff --git a/test/cors.test.js b/test/cors.test.js index c79b0cc..5110126 100644 --- a/test/cors.test.js +++ b/test/cors.test.js @@ -107,7 +107,7 @@ test('Should support dynamic config (callback)', t => { exposedHeaders: ['zoo', 'bar'], allowedHeaders: ['baz', 'foo'], maxAge: 321, - cacheControl: 'public, max-age=456' + cacheControl: '456' }] const fastify = Fastify() @@ -168,7 +168,7 @@ test('Should support dynamic config (callback)', t => { 'access-control-allow-methods': 'GET', 'access-control-allow-headers': 'baz, foo', 'access-control-max-age': '321', - 'cache-control': 'public, max-age=456', + 'cache-control': '456', 'content-length': '0' }) }) @@ -187,7 +187,7 @@ test('Should support dynamic config (callback)', t => { }) test('Should support dynamic config (Promise)', t => { - t.plan(16) + t.plan(23) const configs = [{ origin: 'example.com', @@ -195,7 +195,16 @@ test('Should support dynamic config (Promise)', t => { credentials: true, exposedHeaders: ['foo', 'bar'], allowedHeaders: ['baz', 'woo'], - maxAge: 123 + maxAge: 123, + cacheControl: 456 + }, { + origin: 'sample.com', + methods: 'GET', + credentials: true, + exposedHeaders: ['zoo', 'bar'], + allowedHeaders: ['baz', 'foo'], + maxAge: 321, + cacheControl: true // Invalid value should be ignored }, { origin: 'sample.com', methods: 'GET', @@ -244,6 +253,31 @@ test('Should support dynamic config (Promise)', t => { }) }) + fastify.inject({ + method: 'OPTIONS', + url: '/', + headers: { + 'access-control-request-method': 'GET', + origin: 'sample.com' + } + }, (err, res) => { + t.error(err) + delete res.headers.date + t.equal(res.statusCode, 204) + t.equal(res.payload, '') + t.match(res.headers, { + 'access-control-allow-origin': 'sample.com', + vary: 'Origin', + 'access-control-allow-credentials': 'true', + 'access-control-expose-headers': 'zoo, bar', + 'access-control-allow-methods': 'GET', + 'access-control-allow-headers': 'baz, foo', + 'access-control-max-age': '321', + 'content-length': '0' + }) + t.equal(res.headers['cache-control'], undefined, 'cache-control omitted (invalid value)') + }) + fastify.inject({ method: 'OPTIONS', url: '/', @@ -264,7 +298,7 @@ test('Should support dynamic config (Promise)', t => { 'access-control-allow-methods': 'GET', 'access-control-allow-headers': 'baz, foo', 'access-control-max-age': '321', - 'cache-control': 'public, max-age=456', + 'cache-control': 'public, max-age=456', // cache-control included (custom string) 'content-length': '0' }) })