diff --git a/index.js b/index.js index 28dfc9a..ef6b584 100644 --- a/index.js +++ b/index.js @@ -152,9 +152,11 @@ function normalizeCorsOptions (opts) { } function addCorsHeadersHandler (fastify, options, req, reply, next) { - // Always set Vary header - // https://github.com/rs/cors/issues/10 - addOriginToVaryHeader(reply) + if (typeof options.origin !== 'string' && options.origin !== false) { + // Always set Vary header for non-static origin option + // https://fetch.spec.whatwg.org/#cors-protocol-and-http-caches + addOriginToVaryHeader(reply) + } const resolveOriginOption = typeof options.origin === 'function' ? resolveOriginWrapper(fastify, options.origin) : (_, cb) => cb(null, options.origin) @@ -263,13 +265,8 @@ function resolveOriginWrapper (fastify, origin) { } function getAccessControlAllowOriginHeader (reqOrigin, originOption) { - if (originOption === '*') { - // allow any origin - return '*' - } - if (typeof originOption === 'string') { - // fixed origin + // fixed or any origin ('*') return originOption } diff --git a/test/cors.test.js b/test/cors.test.js index 8819277..39b575e 100644 --- a/test/cors.test.js +++ b/test/cors.test.js @@ -63,7 +63,7 @@ test('Should add cors headers when payload is a stream', t => { }) test('Should add cors headers (custom values)', t => { - t.plan(8) + t.plan(10) const fastify = Fastify() fastify.register(cors, { @@ -94,7 +94,6 @@ test('Should add cors headers (custom values)', t => { t.equal(res.payload, '') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'access-control-allow-methods': 'GET', @@ -103,6 +102,7 @@ test('Should add cors headers (custom values)', t => { 'cache-control': 'max-age=321', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -115,16 +115,16 @@ test('Should add cors headers (custom values)', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) }) test('Should support dynamic config (callback)', t => { - t.plan(16) + t.plan(18) const configs = [{ origin: 'example.com', @@ -175,11 +175,11 @@ test('Should support dynamic config (callback)', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -196,7 +196,6 @@ test('Should support dynamic config (callback)', t => { 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', @@ -205,6 +204,7 @@ test('Should support dynamic config (callback)', t => { 'cache-control': '456', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -221,7 +221,7 @@ test('Should support dynamic config (callback)', t => { }) test('Should support dynamic config (Promise)', t => { - t.plan(23) + t.plan(26) const configs = [{ origin: 'example.com', @@ -280,11 +280,11 @@ test('Should support dynamic config (Promise)', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -301,7 +301,6 @@ test('Should support dynamic config (Promise)', t => { 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', @@ -309,6 +308,7 @@ test('Should support dynamic config (Promise)', t => { 'access-control-max-age': '321', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) t.equal(res.headers['cache-control'], undefined, 'cache-control omitted (invalid value)') }) @@ -326,7 +326,6 @@ test('Should support dynamic config (Promise)', t => { 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', @@ -335,6 +334,7 @@ test('Should support dynamic config (Promise)', t => { 'cache-control': 'public, max-age=456', // cache-control included (custom string) 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -564,7 +564,7 @@ test('Dynamic origin resolution (errored - promises)', t => { }) }) -test('Should reply 404 without cors headers other than `vary` when origin is false', t => { +test('Should reply 404 without cors headers when origin is false', t => { t.plan(8) const fastify = Fastify() @@ -592,8 +592,7 @@ test('Should reply 404 without cors headers other than `vary` when origin is fal t.same(res.headers, { 'content-length': '76', 'content-type': 'application/json; charset=utf-8', - connection: 'keep-alive', - vary: 'Origin' + connection: 'keep-alive' }) }) @@ -608,8 +607,7 @@ test('Should reply 404 without cors headers other than `vary` when origin is fal t.same(res.headers, { 'content-length': '2', 'content-type': 'text/plain; charset=utf-8', - connection: 'keep-alive', - vary: 'Origin' + connection: 'keep-alive' }) }) }) @@ -632,14 +630,13 @@ test('Server error if origin option is falsy but not false', t => { t.same(res.headers, { 'content-length': '89', 'content-type': 'application/json; charset=utf-8', - connection: 'keep-alive', - vary: 'Origin' + connection: 'keep-alive' }) }) }) test('Allow only request from a specific origin', t => { - t.plan(4) + t.plan(5) const fastify = Fastify() fastify.register(cors, { origin: 'other.io' }) @@ -658,9 +655,9 @@ test('Allow only request from a specific origin', t => { t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': 'other.io', - vary: 'Origin' + 'access-control-allow-origin': 'other.io' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) }) @@ -772,11 +769,11 @@ test('Disable preflight', t => { }) }) -test('Should always add vary header to `Origin` by default', t => { +test('Should always add vary header to `Origin` for reflected origin', t => { t.plan(12) const fastify = Fastify() - fastify.register(cors) + fastify.register(cors, { origin: true }) fastify.get('/', (req, reply) => { reply.send('ok') @@ -829,7 +826,7 @@ test('Should always add vary header to `Origin` by default', t => { }) }) -test('Should always add vary header to `Origin` by default (vary is array)', t => { +test('Should always add vary header to `Origin` for reflected origin (vary is array)', t => { t.plan(4) const fastify = Fastify() @@ -837,7 +834,7 @@ test('Should always add vary header to `Origin` by default (vary is array)', t = // Mock getHeader function fastify.decorateReply('getHeader', (name) => ['foo', 'bar']) - fastify.register(cors) + fastify.register(cors, { origin: true }) fastify.get('/', (req, reply) => { reply.send('ok') @@ -858,7 +855,7 @@ test('Should always add vary header to `Origin` by default (vary is array)', t = }) test('Allow only request from with specific headers', t => { - t.plan(7) + t.plan(8) const fastify = Fastify() fastify.register(cors, { @@ -882,9 +879,9 @@ test('Allow only request from with specific headers', t => { delete res.headers.date t.equal(res.statusCode, 204) t.match(res.headers, { - 'access-control-allow-headers': 'foo', - vary: 'Origin' + 'access-control-allow-headers': 'foo' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ diff --git a/test/hooks.test.js b/test/hooks.test.js index 6495d73..37345c3 100644 --- a/test/hooks.test.js +++ b/test/hooks.test.js @@ -87,7 +87,7 @@ test('Should set hook onRequest if hook option is set to onRequest', async (t) = }) test('Should set hook preParsing if hook option is set to preParsing', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -120,13 +120,13 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t) t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should set hook preValidation if hook option is set to preValidation', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -159,13 +159,13 @@ test('Should set hook preValidation if hook option is set to preValidation', asy t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should set hook preParsing if hook option is set to preParsing', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -198,13 +198,13 @@ test('Should set hook preParsing if hook option is set to preParsing', async (t) t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should set hook preHandler if hook option is set to preHandler', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -237,13 +237,13 @@ test('Should set hook preHandler if hook option is set to preHandler', async (t) t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should set hook onSend if hook option is set to onSend', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -276,13 +276,13 @@ test('Should set hook onSend if hook option is set to onSend', async (t) => { t.equal(res.statusCode, 200) t.equal(res.payload, 'ok') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should set hook preSerialization if hook option is set to preSerialization', async (t) => { - t.plan(10) + t.plan(11) const fastify = Fastify() @@ -315,13 +315,13 @@ test('Should set hook preSerialization if hook option is set to preSerialization t.equal(res.statusCode, 200) t.equal(res.payload, '{"nonString":true}') t.match(res.headers, { - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) test('Should support custom hook with dynamic config', t => { - t.plan(16) + t.plan(18) const configs = [{ origin: 'example.com', @@ -373,11 +373,11 @@ test('Should support custom hook with dynamic config', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -394,7 +394,6 @@ test('Should support custom hook with dynamic config', t => { 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', @@ -402,6 +401,7 @@ test('Should support custom hook with dynamic config', t => { 'access-control-max-age': '321', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -418,7 +418,7 @@ test('Should support custom hook with dynamic config', t => { }) test('Should support custom hook with dynamic config (callback)', t => { - t.plan(16) + t.plan(18) const configs = [{ origin: 'example.com', @@ -470,11 +470,11 @@ test('Should support custom hook with dynamic config (callback)', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -491,7 +491,6 @@ test('Should support custom hook with dynamic config (callback)', t => { 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', @@ -499,6 +498,7 @@ test('Should support custom hook with dynamic config (callback)', t => { 'access-control-max-age': '321', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -515,7 +515,7 @@ test('Should support custom hook with dynamic config (callback)', t => { }) test('Should support custom hook with dynamic config (Promise)', t => { - t.plan(16) + t.plan(18) const configs = [{ origin: 'example.com', @@ -568,11 +568,11 @@ test('Should support custom hook with dynamic config (Promise)', t => { t.equal(res.payload, 'ok') t.match(res.headers, { 'access-control-allow-origin': 'example.com', - vary: 'Origin', 'access-control-allow-credentials': 'true', 'access-control-expose-headers': 'foo, bar', 'content-length': '2' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ @@ -589,7 +589,6 @@ test('Should support custom hook with dynamic config (Promise)', t => { 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', @@ -597,6 +596,7 @@ test('Should support custom hook with dynamic config (Promise)', t => { 'access-control-max-age': '321', 'content-length': '0' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) fastify.inject({ diff --git a/test/preflight.test.js b/test/preflight.test.js index 5c57ffa..b8049ce 100644 --- a/test/preflight.test.js +++ b/test/preflight.test.js @@ -25,7 +25,7 @@ test('Should reply to preflight requests', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -55,7 +55,7 @@ test('Should add access-control-allow-headers to response if preflight req has a 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', 'access-control-allow-headers': 'x-requested-with', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -82,14 +82,14 @@ test('Should reply to preflight requests with custom status code', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) }) test('Should be able to override preflight response with a route', t => { - t.plan(4) + t.plan(5) const fastify = Fastify() fastify.register(cors, { preflightContinue: true }) @@ -112,9 +112,9 @@ test('Should be able to override preflight response with a route', t => { t.equal(res.payload, 'ok') t.match(res.headers, { // Only the base cors headers and no preflight headers - 'access-control-allow-origin': '*', - vary: 'Origin' + 'access-control-allow-origin': '*' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) }) @@ -139,7 +139,7 @@ test('Should reply to all options requests', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -177,7 +177,7 @@ test('Should support a prefix for preflight requests', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -218,7 +218,7 @@ test('show options route', t => { }) test('Allow only request from with specific methods', t => { - t.plan(3) + t.plan(4) const fastify = Fastify() fastify.register(cors, { methods: ['GET', 'POST'] }) @@ -235,9 +235,9 @@ test('Allow only request from with specific methods', t => { delete res.headers.date t.equal(res.statusCode, 204) t.match(res.headers, { - 'access-control-allow-methods': 'GET, POST', - vary: 'Origin' + 'access-control-allow-methods': 'GET, POST' }) + t.notMatch(res.headers, { vary: 'Origin' }) }) }) @@ -299,7 +299,7 @@ test('Should reply to all preflight requests when strictPreflight is disabled', t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -326,7 +326,7 @@ test('Default empty 200 response with preflightContinue on OPTIONS routes', t => t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers' + vary: 'Access-Control-Request-Headers' }) }) }) @@ -356,7 +356,7 @@ test('Can override preflight response with preflightContinue', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers' + vary: 'Access-Control-Request-Headers' }) }) }) @@ -386,7 +386,7 @@ test('Should support ongoing prefix ', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -407,7 +407,7 @@ test('Should support ongoing prefix ', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) }) @@ -428,7 +428,7 @@ test('Should support ongoing prefix ', t => { t.match(res.headers, { 'access-control-allow-origin': '*', 'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', - vary: 'Origin, Access-Control-Request-Headers', + vary: 'Access-Control-Request-Headers', 'content-length': '0' }) })