Skip to content

Commit dad4009

Browse files
authored
Feat/preflight add logLevel option to silence CORS preflight logs (#375)
* feat: add logLevel option for CORS preflight requests * test(cors): add test for logs * feat: update documentation * feat(test):logLevel, hideOptionsRoute, were tested with the delegator in the usage situation. * feat(cors): refactor logLevel handling and streamline options processing * feat(cors): remove logLevel option from default options and update type definition
1 parent d63bb08 commit dad4009

File tree

5 files changed

+124
-4
lines changed

5 files changed

+124
-4
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@ You can use it as is without passing any option or you can configure it as expla
7575
* `preflight`: Disables preflight by passing `false`. Default: `true`.
7676
* `strictPreflight`: Enforces strict requirements for the CORS preflight request headers (**Access-Control-Request-Method** and **Origin**) as defined by the [W3C CORS specification](https://www.w3.org/TR/2020/SPSD-cors-20200602/#resource-preflight-requests). Preflight requests without the required headers result in 400 errors when set to `true`. Default: `true`.
7777
* `hideOptionsRoute`: Hides the options route from documentation built using [@fastify/swagger](https://github.com/fastify/fastify-swagger). Default: `true`.
78+
* `logLevel`: Sets the Fastify log level **only** for the internal CORS pre-flight `OPTIONS *` route.
79+
Pass `'silent'` to suppress these requests in your logs, or any valid Fastify
80+
log level (`'trace'`, `'debug'`, `'info'`, `'warn'`, `'error'`, `'fatal'`).
81+
Default: inherits Fastify’s global log level.
7882

7983
#### :warning: DoS attacks
8084

index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,14 @@ function fastifyCors (fastify, opts, next) {
4646
fastify.decorateRequest('corsPreflightEnabled', false)
4747

4848
let hideOptionsRoute = true
49+
let logLevel
50+
4951
if (typeof opts === 'function') {
5052
handleCorsOptionsDelegator(opts, fastify, { hook: defaultOptions.hook }, next)
5153
} else if (opts.delegator) {
5254
const { delegator, ...options } = opts
5355
handleCorsOptionsDelegator(delegator, fastify, options, next)
5456
} else {
55-
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
5657
const corsOptions = normalizeCorsOptions(opts)
5758
validateHook(corsOptions.hook, next)
5859
if (hookWithPayload.indexOf(corsOptions.hook) !== -1) {
@@ -65,14 +66,17 @@ function fastifyCors (fastify, opts, next) {
6566
})
6667
}
6768
}
69+
if (opts.logLevel !== undefined) logLevel = opts.logLevel
70+
if (opts.hideOptionsRoute !== undefined) hideOptionsRoute = opts.hideOptionsRoute
6871

6972
// The preflight reply must occur in the hook. This allows fastify-cors to reply to
7073
// preflight requests BEFORE possible authentication plugins. If the preflight reply
7174
// occurred in this handler, other plugins may deny the request since the browser will
7275
// remove most headers (such as the Authentication header).
7376
//
7477
// This route simply enables fastify to accept preflight requests.
75-
fastify.options('*', { schema: { hide: hideOptionsRoute } }, (req, reply) => {
78+
79+
fastify.options('*', { schema: { hide: hideOptionsRoute }, logLevel }, (req, reply) => {
7680
if (!req.corsPreflightEnabled) {
7781
// Do not handle preflight requests if the origin option disabled CORS
7882
reply.callNotFound()

test/preflight.test.js

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,3 +486,105 @@ test('Should support ongoing prefix ', async t => {
486486
'content-length': '0'
487487
})
488488
})
489+
490+
test('Silences preflight logs when logLevel is "silent"', async t => {
491+
const logs = []
492+
const fastify = Fastify({
493+
logger: {
494+
level: 'info',
495+
stream: {
496+
write (line) {
497+
try {
498+
logs.push(JSON.parse(line))
499+
} catch {
500+
}
501+
}
502+
}
503+
}
504+
})
505+
506+
await fastify.register(cors, { logLevel: 'silent' })
507+
508+
fastify.get('/', async () => ({ ok: true }))
509+
510+
await fastify.ready()
511+
t.assert.ok(fastify)
512+
513+
await fastify.inject({
514+
method: 'OPTIONS',
515+
url: '/',
516+
headers: {
517+
'access-control-request-method': 'GET',
518+
origin: 'https://example.com'
519+
}
520+
})
521+
522+
await fastify.inject({ method: 'GET', url: '/' })
523+
524+
const hasOptionsLog = logs.some(l => l.req && l.req.method === 'OPTIONS')
525+
const hasGetLog = logs.some(l => l.req && l.req.method === 'GET')
526+
527+
t.assert.strictEqual(hasOptionsLog, false)
528+
t.assert.strictEqual(hasGetLog, true)
529+
530+
await fastify.close()
531+
})
532+
test('delegator + logLevel:"silent" → OPTIONS logs are suppressed', async t => {
533+
t.plan(3)
534+
535+
const logs = []
536+
const app = Fastify({
537+
logger: {
538+
level: 'info',
539+
stream: { write: l => { try { logs.push(JSON.parse(l)) } catch {} } }
540+
}
541+
})
542+
543+
await app.register(cors, {
544+
delegator: () => ({ origin: '*' }),
545+
logLevel: 'silent'
546+
})
547+
548+
app.get('/', () => ({ ok: true }))
549+
await app.ready()
550+
t.assert.ok(app)
551+
552+
await app.inject({
553+
method: 'OPTIONS',
554+
url: '/',
555+
headers: {
556+
'access-control-request-method': 'GET',
557+
origin: 'https://example.com'
558+
}
559+
})
560+
561+
await app.inject({ method: 'GET', url: '/' })
562+
563+
const hasOptionsLog = logs.some(l => l.req?.method === 'OPTIONS')
564+
const hasGetLog = logs.some(l => l.req?.method === 'GET')
565+
566+
t.assert.strictEqual(hasOptionsLog, false)
567+
t.assert.strictEqual(hasGetLog, true)
568+
569+
await app.close()
570+
})
571+
test('delegator + hideOptionsRoute:false → OPTIONS route is visible', async t => {
572+
t.plan(2)
573+
574+
const app = Fastify()
575+
576+
app.addHook('onRoute', route => {
577+
if (route.method === 'OPTIONS' && route.url === '*') {
578+
t.assert.strictEqual(route.schema.hide, false)
579+
}
580+
})
581+
582+
await app.register(cors, {
583+
delegator: () => ({ origin: '*' }),
584+
hideOptionsRoute: false
585+
})
586+
587+
await app.ready()
588+
t.assert.ok(app)
589+
await app.close()
590+
})

types/index.d.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/// <reference types="node" />
22

3-
import { FastifyInstance, FastifyPluginCallback, FastifyRequest } from 'fastify'
3+
import { FastifyInstance, FastifyPluginCallback, FastifyRequest, LogLevel } from 'fastify'
44

55
type OriginCallback = (err: Error | null, origin: ValueOrArray<OriginType>) => void
66
type OriginType = string | boolean | RegExp
@@ -99,6 +99,15 @@ declare namespace fastifyCors {
9999
* Hide options route from the documentation built using fastify-swagger (default: true).
100100
*/
101101
hideOptionsRoute?: boolean;
102+
103+
/**
104+
* Sets the Fastify log level specifically for the internal OPTIONS route
105+
* used to handle CORS preflight requests. For example, setting this to `'silent'`
106+
* will prevent these requests from being logged.
107+
* Useful for reducing noise in application logs.
108+
* Default: inherits Fastify's global log level.
109+
*/
110+
logLevel?: LogLevel;
102111
}
103112

104113
export interface FastifyCorsOptionsDelegateCallback { (req: FastifyRequest, cb: (error: Error | null, corsOptions?: FastifyCorsOptions) => void): void }

types/index.test-d.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,8 @@ appHttp2.register(fastifyCors, {
165165
preflightContinue: false,
166166
optionsSuccessStatus: 200,
167167
preflight: false,
168-
strictPreflight: false
168+
strictPreflight: false,
169+
logLevel: 'silent'
169170
})
170171

171172
appHttp2.register(fastifyCors, {

0 commit comments

Comments
 (0)