fastify-cors
Advanced tools
Comparing version 5.0.0 to 5.1.0
@@ -47,2 +47,6 @@ /// <reference types="node" /> | ||
/** | ||
* Pass the CORS preflight response to the route handler (default: false). | ||
*/ | ||
preflightContinue?: boolean; | ||
/** | ||
* Provides a status code to use for successful OPTIONS requests, | ||
@@ -49,0 +53,0 @@ * since some legacy browsers (IE11, various SmartTVs) choke on 204. |
91
index.js
@@ -31,3 +31,4 @@ 'use strict' | ||
hideOptionsRoute, | ||
strictPreflight | ||
strictPreflight, | ||
preflightContinue | ||
} = Object.assign({}, defaultOptions, opts) | ||
@@ -37,7 +38,21 @@ | ||
fastify.decorateRequest('corsPreflightEnabled', undefined) | ||
fastify.decorateRequest('corsPreflightEnabled', false) | ||
fastify.addHook('onRequest', onRequest) | ||
if (preflight === true) { | ||
fastify.options('*', { schema: { hide: hideOptionsRoute } }, preflightHandler) | ||
// The preflight reply must occur in the hook. This allows fastify-cors to reply to | ||
// preflight requests BEFORE possible authentication plugins. If the preflight reply | ||
// occurred in this handler, other plugins may deny the request since the browser will | ||
// remove most headers (such as the Authentication header). | ||
// | ||
// This route simply enables fastify to accept preflight requests. | ||
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => { | ||
if (!req.corsPreflightEnabled) { | ||
// Do not handle preflight requests if the origin option disabled CORS | ||
reply.callNotFound() | ||
return | ||
} | ||
reply.send() | ||
}) | ||
} | ||
@@ -57,4 +72,2 @@ | ||
req.corsPreflightEnabled = resolvedOriginOption | ||
// Disable CORS and preflight if false | ||
@@ -65,14 +78,30 @@ if (resolvedOriginOption === false) { | ||
reply.header('Access-Control-Allow-Origin', | ||
getAccessControlAllowOriginHeader(req.headers.origin, resolvedOriginOption)) | ||
if (credentials) { | ||
reply.header('Access-Control-Allow-Credentials', 'true') | ||
// Falsy values are invalid | ||
if (!resolvedOriginOption) { | ||
return next(new Error('Invalid CORS origin option')) | ||
} | ||
if (exposedHeaders !== null) { | ||
reply.header( | ||
'Access-Control-Expose-Headers', | ||
Array.isArray(exposedHeaders) ? exposedHeaders.join(', ') : exposedHeaders | ||
) | ||
addCorsHeaders(req, reply, resolvedOriginOption) | ||
if (req.raw.method === 'OPTIONS' && preflight === true) { | ||
// Strict mode enforces the required headers for preflight | ||
if (strictPreflight === true && (!req.headers.origin || !req.headers['access-control-request-method'])) { | ||
reply.status(400).type('text/plain').send('Invalid Preflight Request') | ||
return | ||
} | ||
req.corsPreflightEnabled = true | ||
addPreflightHeaders(req, reply) | ||
if (!preflightContinue) { | ||
// Do not call the hook callback and terminate the request | ||
// Safari (and potentially other browsers) need content-length 0, | ||
// for 204 or they just hang waiting for a body | ||
reply | ||
.code(optionsSuccessStatus) | ||
.header('Content-Length', '0') | ||
.send() | ||
return | ||
} | ||
} | ||
@@ -84,16 +113,19 @@ | ||
function preflightHandler (req, reply) { | ||
// Do not handle preflight requests if the origin was not allowed | ||
if (!req.corsPreflightEnabled) { | ||
reply.code(404).type('text/plain').send('Not Found') | ||
return | ||
function addCorsHeaders (req, reply, originOption) { | ||
reply.header('Access-Control-Allow-Origin', | ||
getAccessControlAllowOriginHeader(req.headers.origin, originOption)) | ||
if (credentials) { | ||
reply.header('Access-Control-Allow-Credentials', 'true') | ||
} | ||
// Strict mode enforces the required headers for preflight | ||
if (strictPreflight === true && (!req.headers.origin || !req.headers['access-control-request-method'])) { | ||
reply.status(400).type('text/plain').send('Invalid Preflight Request') | ||
return | ||
if (exposedHeaders !== null) { | ||
reply.header( | ||
'Access-Control-Expose-Headers', | ||
Array.isArray(exposedHeaders) ? exposedHeaders.join(', ') : exposedHeaders | ||
) | ||
} | ||
} | ||
// Handle preflight headers | ||
function addPreflightHeaders (req, reply) { | ||
reply.header( | ||
@@ -120,9 +152,2 @@ 'Access-Control-Allow-Methods', | ||
} | ||
// Safari (and potentially other browsers) need content-length 0, | ||
// for 204 or they just hang waiting for a body | ||
reply | ||
.code(optionsSuccessStatus) | ||
.header('Content-Length', '0') | ||
.send() | ||
} | ||
@@ -141,3 +166,3 @@ | ||
function getAccessControlAllowOriginHeader (reqOrigin, originOption) { | ||
if (!originOption || originOption === '*') { | ||
if (originOption === '*') { | ||
// allow any origin | ||
@@ -144,0 +169,0 @@ return '*' |
{ | ||
"name": "fastify-cors", | ||
"version": "5.0.0", | ||
"version": "5.1.0", | ||
"description": "Fastify CORS", | ||
@@ -42,3 +42,3 @@ "main": "index.js", | ||
"tap": "^14.11.0", | ||
"tsd": "^0.13.1", | ||
"tsd": "^0.14.0", | ||
"typescript": "^4.0.2" | ||
@@ -45,0 +45,0 @@ }, |
@@ -35,3 +35,3 @@ # fastify-cors | ||
- `Boolean` - set `origin` to `true` to reflect the [request origin](http://tools.ietf.org/html/draft-abarth-origin-09), or set it to `false` to disable CORS. | ||
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. | ||
- `String` - set `origin` to a specific origin. For example if you set it to `"http://example.com"` only requests from "http://example.com" will be allowed. The special `*` value (default) allows any origin. | ||
- `RegExp` - set `origin` to a regular expression pattern which will be used to test the request origin. If it's a match, the request origin will be reflected. For example the pattern `/example\.com$/` will reflect any request that is coming from an origin ending with "example.com". | ||
@@ -56,2 +56,3 @@ - `Array` - set `origin` to an array of valid origins. Each origin can be a `String` or a `RegExp`. For example `["http://example1.com", /\.example2\.com$/]` will accept any request from "http://example1.com" or from a subdomain of "example2.com". | ||
* `maxAge`: Configures the **Access-Control-Max-Age** CORS header. In seconds. Set to an integer to pass the header, otherwise it 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`. | ||
@@ -62,20 +63,2 @@ * `preflight`: if needed you can entirely disable preflight by passing `false` here (default: `true`). | ||
### Preflight Requests | ||
When preflight is enabled (`preflight` option is `true`), a `*` wildcard `OPTIONS` route is added to the fastify instance. The response behavior can be overridden for individual routes by adding `OPTIONS` routes to the fastify instance (`*` wildcard routes are always lowest priority). | ||
This is an important difference between fastify-cors and the [express cors](https://github.com/expressjs/cors#configuration-options) middleware `preflightContinue` option. | ||
```js | ||
const fastify = require('fastify')() | ||
// Fastify-cors handles CORS preflight OPTIONS requests | ||
fastify.register(require('fastify-cors')) | ||
// Except for OPTIONS /not-preflight | ||
fastify.options('/not-preflight', (req, reply) => { | ||
reply.send({hello: 'world'}) | ||
}) | ||
``` | ||
## Acknowledgements | ||
@@ -82,0 +65,0 @@ |
@@ -173,2 +173,22 @@ 'use strict' | ||
test('Dynamic origin resolution (invalid result)', t => { | ||
t.plan(3) | ||
const fastify = Fastify() | ||
const origin = (header, cb) => { | ||
t.strictEqual(header, 'example.com') | ||
cb(null, undefined) | ||
} | ||
fastify.register(cors, { origin }) | ||
fastify.inject({ | ||
method: 'GET', | ||
url: '/', | ||
headers: { origin: 'example.com' } | ||
}, (err, res) => { | ||
t.error(err) | ||
t.strictEqual(res.statusCode, 500) | ||
}) | ||
}) | ||
test('Dynamic origin resolution (valid origin - promises)', t => { | ||
@@ -286,6 +306,6 @@ t.plan(5) | ||
t.strictEqual(res.statusCode, 404) | ||
t.strictEqual(res.payload, 'Not Found') | ||
t.strictEqual(res.payload, '{"message":"Route OPTIONS:/ not found","error":"Not Found","statusCode":404}') | ||
t.deepEqual(res.headers, { | ||
'content-length': '9', | ||
'content-type': 'text/plain', | ||
'content-length': '76', | ||
'content-type': 'application/json; charset=utf-8', | ||
connection: 'keep-alive', | ||
@@ -313,2 +333,26 @@ vary: 'Origin' | ||
test('Server error if origin option is falsy but not false', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(cors, { origin: '' }) | ||
fastify.inject({ | ||
method: 'GET', | ||
url: '/', | ||
headers: { origin: 'example.com' } | ||
}, (err, res) => { | ||
t.error(err) | ||
delete res.headers.date | ||
t.strictEqual(res.statusCode, 500) | ||
t.deepEqual(res.json(), { statusCode: 500, error: 'Internal Server Error', message: 'Invalid CORS origin option' }) | ||
t.deepEqual(res.headers, { | ||
'content-length': '89', | ||
'content-type': 'application/json; charset=utf-8', | ||
connection: 'keep-alive', | ||
vary: 'Origin' | ||
}) | ||
}) | ||
}) | ||
test('Allow only request from a specific origin', t => { | ||
@@ -315,0 +359,0 @@ t.plan(4) |
@@ -15,2 +15,3 @@ import fastify from 'fastify' | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -28,2 +29,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -41,2 +43,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -54,2 +57,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -67,2 +71,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -80,2 +85,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -117,2 +123,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -130,2 +137,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -143,2 +151,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -156,2 +165,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -169,2 +179,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -182,2 +193,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -201,2 +213,3 @@ preflight: false, | ||
maxAge: 13000, | ||
preflightContinue: false, | ||
optionsSuccessStatus: 200, | ||
@@ -203,0 +216,0 @@ preflight: false, |
@@ -103,3 +103,7 @@ 'use strict' | ||
method: 'OPTIONS', | ||
url: '/' | ||
url: '/', | ||
headers: { | ||
'access-control-request-method': 'GET', | ||
origin: 'example.com' | ||
} | ||
}, (err, res) => { | ||
@@ -287,1 +291,57 @@ t.error(err) | ||
}) | ||
test('Default empty 200 response with preflightContinue on OPTIONS routes', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(cors, { preflightContinue: true }) | ||
fastify.inject({ | ||
method: 'OPTIONS', | ||
url: '/doesnotexist', | ||
headers: { | ||
'access-control-request-method': 'GET', | ||
origin: 'example.com' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
delete res.headers.date | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.payload, '') | ||
t.match(res.headers, { | ||
'access-control-allow-origin': '*', | ||
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', | ||
vary: 'Origin, Access-Control-Request-Headers' | ||
}) | ||
}) | ||
}) | ||
test('Can override preflight response with preflightContinue', t => { | ||
t.plan(4) | ||
const fastify = Fastify() | ||
fastify.register(cors, { preflightContinue: true }) | ||
fastify.options('/', (req, reply) => { | ||
reply.send('ok') | ||
}) | ||
fastify.inject({ | ||
method: 'OPTIONS', | ||
url: '/', | ||
headers: { | ||
'access-control-request-method': 'GET', | ||
origin: 'example.com' | ||
} | ||
}, (err, res) => { | ||
t.error(err) | ||
delete res.headers.date | ||
t.strictEqual(res.statusCode, 200) | ||
t.strictEqual(res.payload, 'ok') | ||
t.match(res.headers, { | ||
'access-control-allow-origin': '*', | ||
'access-control-allow-methods': 'GET,HEAD,PUT,PATCH,POST,DELETE', | ||
vary: 'Origin, Access-Control-Request-Headers' | ||
}) | ||
}) | ||
}) |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
46391
1291
0
69