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..718b3b3 100644 --- a/index.js +++ b/index.js @@ -133,11 +133,21 @@ 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(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 } @@ -235,6 +245,10 @@ function addPreflightHeaders (req, reply, corsOptions) { if (corsOptions.maxAge !== null) { reply.header('Access-Control-Max-Age', String(corsOptions.maxAge)) } + + if (corsOptions.cacheControl) { + reply.header('Cache-Control', corsOptions.cacheControl) + } } function resolveOriginWrapper (fastify, origin) { diff --git a/test/cors.test.js b/test/cors.test.js index b81b94d..5110126 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: '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': '456', 'content-length': '0' }) }) @@ -182,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', @@ -190,14 +195,24 @@ 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 + maxAge: 321, + cacheControl: true // Invalid value should be ignored + }, { + origin: 'sample.com', + methods: 'GET', + credentials: true, + exposedHeaders: ['zoo', 'bar'], + allowedHeaders: ['baz', 'foo'], + maxAge: 321, + cacheControl: 'public, max-age=456' }] const fastify = Fastify() @@ -238,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: '/', @@ -258,6 +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 included (custom string) '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..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, @@ -18,6 +19,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: 'authorization', maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -31,6 +33,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 'public, max-age=3500', preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -44,6 +47,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -57,6 +61,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -70,6 +75,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -83,6 +89,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -104,6 +111,7 @@ app.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, optionsSuccessStatus: 200, preflight: false, strictPreflight: false @@ -120,6 +128,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: 'authorization', maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -133,6 +142,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -146,6 +156,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -159,6 +170,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -172,6 +184,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -185,6 +198,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -204,6 +218,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -218,6 +233,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegate => (req, cb) => { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -233,6 +249,7 @@ appHttp2.register(fastifyCors, (): FastifyCorsOptionsDelegatePromise => (req) => credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -248,6 +265,7 @@ const delegate: FastifyPluginOptionsDelegate credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 13000, preflightContinue: false, optionsSuccessStatus: 200, preflight: false, @@ -276,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'], @@ -302,6 +324,7 @@ appHttp2.register(fastifyCors, { credentials: true, exposedHeaders: ['authorization'], maxAge: 13000, + cacheControl: 'public, max-age=3500', preflightContinue: false, optionsSuccessStatus: 200, preflight: false,