mastercontroller 1.3.35 → 1.3.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/MasterControl.js CHANGED
@@ -902,7 +902,7 @@ class MasterControl {
902
902
  return; // Terminal - don't call next()
903
903
  }
904
904
 
905
- await next(); // Not static, continue pipeline
905
+ if (typeof next === 'function') await next(); // Not static, continue pipeline
906
906
  });
907
907
 
908
908
  // 2. Timeout Tracking (optional - disabled by default until init)
@@ -924,7 +924,7 @@ class MasterControl {
924
924
  ctx.params.formData = params.formData;
925
925
  }
926
926
 
927
- await next();
927
+ if (typeof next === 'function') await next();
928
928
  });
929
929
 
930
930
  // 4. Load Scoped Services (per request - always needed)
@@ -938,7 +938,7 @@ class MasterControl {
938
938
  const className = $that._scopedList[key];
939
939
  $that.requestList[key] = new className();
940
940
  }
941
- await next();
941
+ if (typeof next === 'function') await next();
942
942
  });
943
943
 
944
944
  // 4. HSTS Header (if enabled for HTTPS)
@@ -954,7 +954,7 @@ class MasterControl {
954
954
  }
955
955
  ctx.response.setHeader('Strict-Transport-Security', hstsValue);
956
956
  }
957
- await next();
957
+ if (typeof next === 'function') await next();
958
958
  });
959
959
 
960
960
  // 5. Routing and Error Handler are registered in start() so that user
package/MasterCors.js CHANGED
@@ -243,7 +243,7 @@ class MasterCors{
243
243
 
244
244
  // Regular request - apply CORS headers
245
245
  $that.load({ request: ctx.request, response: ctx.response });
246
- await next();
246
+ if (typeof next === 'function') await next();
247
247
  };
248
248
  }
249
249
  }
package/MasterPipeline.js CHANGED
@@ -116,12 +116,12 @@ class MasterPipeline {
116
116
  // Execute branch pipeline
117
117
  await branch.execute(ctx);
118
118
  // Stop if response already sent (e.g., auth rejection)
119
- if (!ctx.response.headersSent && !ctx.response.writableEnded) {
119
+ if (!ctx.response.headersSent && !ctx.response.writableEnded && typeof next === 'function') {
120
120
  await next();
121
121
  }
122
122
  } else {
123
123
  // Skip branch, continue main pipeline
124
- await next();
124
+ if (typeof next === 'function') await next();
125
125
  }
126
126
  };
127
127
 
package/MasterTimeout.js CHANGED
@@ -483,7 +483,7 @@ class MasterTimeout {
483
483
 
484
484
  return async (ctx, next) => {
485
485
  if (!$that.enabled) {
486
- await next();
486
+ if (typeof next === 'function') await next();
487
487
  return;
488
488
  }
489
489
 
@@ -493,7 +493,7 @@ class MasterTimeout {
493
493
  requestId = $that.startTracking(ctx);
494
494
  ctx.requestId = requestId;
495
495
 
496
- await next();
496
+ if (typeof next === 'function') await next();
497
497
  } catch (err) {
498
498
  // Stop tracking on error (with error handling)
499
499
  if (requestId) {
@@ -32,12 +32,14 @@ class MasterErrorLogger {
32
32
  file: options.file || null,
33
33
  sampleRate: options.sampleRate || 1.0, // Log 100% by default
34
34
  maxFileSize: options.maxFileSize || 10 * 1024 * 1024, // 10MB
35
+ dedupeWindowMs: options.dedupeWindowMs || 5000, // Suppress duplicate errors within 5s
35
36
  ...options
36
37
  };
37
38
 
38
39
  this.backends = [];
39
40
  this.errorCount = 0;
40
41
  this.sessionId = this._generateSessionId();
42
+ this._recentErrors = new Map(); // code -> { count, firstSeen, lastSeen }
41
43
 
42
44
  // Setup default backends
43
45
  if (this.options.console) {
@@ -76,18 +78,47 @@ class MasterErrorLogger {
76
78
  return;
77
79
  }
78
80
 
81
+ // Deduplicate repeated errors within the window
82
+ const code = data.code || 'UNKNOWN';
83
+ if (level >= LOG_LEVELS.ERROR) {
84
+ const now = Date.now();
85
+ const recent = this._recentErrors.get(code);
86
+ if (recent && (now - recent.firstSeen) < this.options.dedupeWindowMs) {
87
+ recent.count++;
88
+ recent.lastSeen = now;
89
+ return; // Suppress duplicate
90
+ }
91
+ // Flush summary of previous burst if there was one
92
+ if (recent && recent.count > 1) {
93
+ const summary = this._formatLogEntry({
94
+ code: code,
95
+ message: `Suppressed ${recent.count - 1} duplicate entries of ${code} (${recent.lastSeen - recent.firstSeen}ms window)`,
96
+ level: LOG_LEVELS.WARN
97
+ }, LOG_LEVELS.WARN);
98
+ this._dispatch(summary);
99
+ }
100
+ this._recentErrors.set(code, { count: 1, firstSeen: now, lastSeen: now });
101
+ }
102
+
79
103
  const entry = this._formatLogEntry(data, level);
104
+ this._dispatch(entry);
105
+ this.errorCount++;
106
+ }
80
107
 
81
- // Send to all backends
108
+ /**
109
+ * Dispatch entry to all backends
110
+ */
111
+ _dispatch(entry) {
82
112
  this.backends.forEach(backend => {
83
113
  try {
84
114
  backend(entry);
85
115
  } catch (error) {
86
- console.error('[MasterErrorLogger] Backend failed:', error.message);
116
+ // Avoid console methods that can trigger EPIPE recursion
117
+ if (error.code !== 'EPIPE' && error.code !== 'ERR_STREAM_DESTROYED') {
118
+ try { process.stderr.write(`[MasterErrorLogger] Backend failed: ${error.message}\n`); } catch (_) {}
119
+ }
87
120
  }
88
121
  });
89
-
90
- this.errorCount++;
91
122
  }
92
123
 
93
124
  /**
@@ -117,28 +148,39 @@ class MasterErrorLogger {
117
148
  * Format log entry with metadata
118
149
  */
119
150
  _formatLogEntry(data, level) {
120
- return {
151
+ const entry = {
121
152
  timestamp: new Date().toISOString(),
122
153
  sessionId: this.sessionId,
123
154
  level: LOG_LEVEL_NAMES[level],
124
155
  code: data.code || 'UNKNOWN',
125
- message: data.message || 'No message provided',
126
- component: data.component || null,
127
- file: data.file || null,
128
- line: data.line || null,
129
- route: data.route || null,
130
- context: data.context || {},
131
- stack: data.stack || null,
132
- originalError: data.originalError ? {
133
- message: data.originalError.message,
134
- stack: data.originalError.stack
135
- } : null,
136
- environment: process.env.NODE_ENV || 'development',
137
- nodeVersion: process.version,
138
- platform: process.platform,
139
- memory: process.memoryUsage(),
140
- uptime: process.uptime()
156
+ message: data.message || 'No message provided'
141
157
  };
158
+
159
+ // Only include optional fields when they have values
160
+ if (data.component) entry.component = data.component;
161
+ if (data.file) entry.file = data.file;
162
+ if (data.line) entry.line = data.line;
163
+ if (data.route) entry.route = data.route;
164
+ if (data.context && Object.keys(data.context).length > 0) entry.context = data.context;
165
+
166
+ // Include stack once — prefer originalError.stack to avoid duplication
167
+ if (data.originalError) {
168
+ entry.stack = data.originalError.stack || data.stack || null;
169
+ } else if (data.stack) {
170
+ entry.stack = data.stack;
171
+ }
172
+
173
+ entry.environment = process.env.NODE_ENV || 'development';
174
+
175
+ // Only include memory/system info on ERROR and FATAL
176
+ if (level >= LOG_LEVELS.ERROR) {
177
+ entry.memory = process.memoryUsage();
178
+ entry.nodeVersion = process.version;
179
+ entry.platform = process.platform;
180
+ entry.uptime = process.uptime();
181
+ }
182
+
183
+ return entry;
142
184
  }
143
185
 
144
186
  /**
@@ -156,24 +198,26 @@ class MasterErrorLogger {
156
198
  const color = levelColors[entry.level] || '';
157
199
  const reset = '\x1b[0m';
158
200
 
159
- const logFn = entry.level === 'DEBUG' || entry.level === 'INFO' ? console.log :
160
- entry.level === 'WARN' ? console.warn : console.error;
201
+ // Use process.stdout/stderr.write directly to avoid EPIPE recursion
202
+ // through console.log -> broken pipe -> uncaughtException -> logger -> console.log
203
+ const stream = (entry.level === 'DEBUG' || entry.level === 'INFO') ? process.stdout : process.stderr;
161
204
 
162
- logFn(
163
- `${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}:`,
164
- entry.message
165
- );
205
+ try {
206
+ stream.write(`${color}[${entry.timestamp}] [${entry.level}]${reset} ${entry.code}: ${entry.message}\n`);
166
207
 
167
- if (entry.component) {
168
- logFn(` Component: ${entry.component}`);
169
- }
208
+ if (entry.component) {
209
+ stream.write(` Component: ${entry.component}\n`);
210
+ }
170
211
 
171
- if (entry.file) {
172
- logFn(` File: ${entry.file}${entry.line ? `:${entry.line}` : ''}`);
173
- }
212
+ if (entry.file) {
213
+ stream.write(` File: ${entry.file}${entry.line ? `:${entry.line}` : ''}\n`);
214
+ }
174
215
 
175
- if (entry.stack && process.env.NODE_ENV !== 'production') {
176
- logFn(` Stack: ${entry.stack}`);
216
+ if (entry.stack && process.env.NODE_ENV !== 'production') {
217
+ stream.write(` Stack: ${entry.stack}\n`);
218
+ }
219
+ } catch (err) {
220
+ // If the stream is broken (EPIPE), silently drop — do not recurse
177
221
  }
178
222
  }
179
223
 
@@ -172,7 +172,14 @@ function extractUserCodeContext(stack) {
172
172
  function setupGlobalErrorHandlers() {
173
173
  // Handle uncaught exceptions
174
174
  process.on('uncaughtException', (error) => {
175
- console.error('[MasterController] Uncaught Exception:', error);
175
+ // EPIPE/stream errors from logging itself — do not recurse
176
+ if (error.code === 'EPIPE' || error.code === 'ERR_STREAM_DESTROYED') {
177
+ try { process.stderr.write(`[MasterController] Stream error suppressed: ${error.code}\n`); } catch (_) {}
178
+ return;
179
+ }
180
+
181
+ // Use stderr.write instead of console.error to avoid EPIPE recursion
182
+ try { process.stderr.write(`[MasterController] Uncaught Exception: ${error.message}\n`); } catch (_) {}
176
183
 
177
184
  // Extract context from stack trace
178
185
  const context = extractUserCodeContext(error.stack);
@@ -181,34 +188,33 @@ function setupGlobalErrorHandlers() {
181
188
  let enhancedMessage = `Uncaught exception: ${error.message}`;
182
189
 
183
190
  if (context && context.triggeringFile) {
184
- enhancedMessage += `\n\n🔍 Error Location: ${context.triggeringFile.location}`;
191
+ enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
185
192
  }
186
193
 
187
194
  if (context && context.userFiles.length > 0) {
188
- enhancedMessage += `\n\n📂 Your Code Involved:`;
195
+ enhancedMessage += `\n\nYour Code Involved:`;
189
196
  context.userFiles.forEach((file, i) => {
190
- if (i < 3) { // Show first 3 user files
197
+ if (i < 3) {
191
198
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
192
199
  }
193
200
  });
194
201
  }
195
202
 
196
203
  if (context && context.frameworkFiles.length > 0) {
197
- enhancedMessage += `\n\n🔧 Framework Files Involved:`;
204
+ enhancedMessage += `\n\nFramework Files Involved:`;
198
205
  context.frameworkFiles.forEach((file, i) => {
199
- if (i < 2) { // Show first 2 framework files
206
+ if (i < 2) {
200
207
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
201
208
  }
202
209
  });
203
210
  }
204
211
 
205
- console.error(enhancedMessage);
212
+ try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
206
213
 
207
214
  logger.fatal({
208
215
  code: 'MC_ERR_UNCAUGHT_EXCEPTION',
209
216
  message: enhancedMessage,
210
217
  originalError: error,
211
- stack: error.stack,
212
218
  context: context
213
219
  });
214
220
 
@@ -220,7 +226,7 @@ function setupGlobalErrorHandlers() {
220
226
 
221
227
  // Handle unhandled promise rejections
222
228
  process.on('unhandledRejection', (reason, promise) => {
223
- console.error('[MasterController] Unhandled Rejection:', reason);
229
+ try { process.stderr.write(`[MasterController] Unhandled Rejection: ${reason}\n`); } catch (_) {}
224
230
 
225
231
  // Extract context from stack trace if available
226
232
  const context = reason?.stack ? extractUserCodeContext(reason.stack) : null;
@@ -229,27 +235,26 @@ function setupGlobalErrorHandlers() {
229
235
  let enhancedMessage = `Unhandled promise rejection: ${reason}`;
230
236
 
231
237
  if (context && context.triggeringFile) {
232
- enhancedMessage += `\n\n🔍 Error Location: ${context.triggeringFile.location}`;
238
+ enhancedMessage += `\n\nError Location: ${context.triggeringFile.location}`;
233
239
  }
234
240
 
235
241
  if (context && context.userFiles.length > 0) {
236
- enhancedMessage += `\n\n📂 Your Code Involved:`;
242
+ enhancedMessage += `\n\nYour Code Involved:`;
237
243
  context.userFiles.forEach((file, i) => {
238
- if (i < 3) { // Show first 3 user files
244
+ if (i < 3) {
239
245
  enhancedMessage += `\n ${i + 1}. ${file.location}`;
240
246
  }
241
247
  });
242
248
  }
243
249
 
244
250
  if (enhancedMessage !== `Unhandled promise rejection: ${reason}`) {
245
- console.error(enhancedMessage);
251
+ try { process.stderr.write(enhancedMessage + '\n'); } catch (_) {}
246
252
  }
247
253
 
248
254
  logger.error({
249
255
  code: 'MC_ERR_UNHANDLED_REJECTION',
250
256
  message: enhancedMessage,
251
257
  originalError: reason,
252
- stack: reason?.stack,
253
258
  context: context
254
259
  });
255
260
  });
@@ -257,7 +262,7 @@ function setupGlobalErrorHandlers() {
257
262
  // Handle warnings
258
263
  process.on('warning', (warning) => {
259
264
  if (isDevelopment) {
260
- console.warn('[MasterController] Warning:', warning);
265
+ try { process.stderr.write(`[MasterController] Warning: ${warning.message}\n`); } catch (_) {}
261
266
  }
262
267
 
263
268
  logger.warn({
@@ -55,7 +55,8 @@ class HealthCheck {
55
55
 
56
56
  // Only handle health check endpoint
57
57
  if (requestPath !== self.options.endpoint) {
58
- return await next();
58
+ if (typeof next === 'function') return await next();
59
+ return;
59
60
  }
60
61
 
61
62
  // Log health check request
@@ -226,7 +227,8 @@ class HealthCheck {
226
227
  const requestPath = req.url.split('?')[0];
227
228
 
228
229
  if (requestPath !== self.options.endpoint) {
229
- return next();
230
+ if (typeof next === 'function') return next();
231
+ return;
230
232
  }
231
233
 
232
234
  try {
@@ -142,7 +142,7 @@ class PrometheusExporter {
142
142
 
143
143
  try {
144
144
  // Continue pipeline
145
- await next();
145
+ if (typeof next === 'function') await next();
146
146
 
147
147
  // Record metrics on success
148
148
  const duration = (Date.now() - startTime) / 1000; // Convert to seconds
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mastercontroller",
3
- "version": "1.3.35",
3
+ "version": "1.3.37",
4
4
  "description": "Fortune 500 ready Node.js MVC framework with enterprise security, monitoring, and horizontal scaling",
5
5
  "main": "MasterControl.js",
6
6
  "license": "MIT",
@@ -95,7 +95,8 @@ class CSPConfig {
95
95
  middleware() {
96
96
  return (req, res, next) => {
97
97
  if (!this.enabled) {
98
- return next();
98
+ if (typeof next === 'function') next();
99
+ return;
99
100
  }
100
101
 
101
102
  // Generate nonce for this request if needed
@@ -111,7 +112,7 @@ class CSPConfig {
111
112
  const headerName = this.reportOnly ? 'Content-Security-Policy-Report-Only' : 'Content-Security-Policy';
112
113
  res.setHeader(headerName, headerValue);
113
114
 
114
- next();
115
+ if (typeof next === 'function') next();
115
116
  };
116
117
  }
117
118
 
@@ -163,7 +163,7 @@ class SecurityEnforcement {
163
163
  SecurityEnforcement._applySecurityHeaders(ctx.response);
164
164
 
165
165
  // Continue to next middleware
166
- await next();
166
+ if (typeof next === 'function') await next();
167
167
  };
168
168
  }
169
169
 
@@ -74,7 +74,8 @@ class SecurityMiddleware {
74
74
  */
75
75
  securityHeadersMiddleware(req, res, next) {
76
76
  if (!this.headersEnabled) {
77
- return next();
77
+ if (typeof next === 'function') next();
78
+ return;
78
79
  }
79
80
 
80
81
  // Apply standard security headers
@@ -96,7 +97,7 @@ class SecurityMiddleware {
96
97
  }
97
98
  }
98
99
 
99
- next();
100
+ if (typeof next === 'function') next();
100
101
  }
101
102
 
102
103
  /**
@@ -104,7 +105,8 @@ class SecurityMiddleware {
104
105
  */
105
106
  corsMiddleware(req, res, next) {
106
107
  if (!this.corsEnabled) {
107
- return next();
108
+ if (typeof next === 'function') next();
109
+ return;
108
110
  }
109
111
 
110
112
  const origin = req.headers.origin;
@@ -125,7 +127,7 @@ class SecurityMiddleware {
125
127
  return;
126
128
  }
127
129
 
128
- next();
130
+ if (typeof next === 'function') next();
129
131
  }
130
132
 
131
133
  /**
@@ -133,7 +135,8 @@ class SecurityMiddleware {
133
135
  */
134
136
  rateLimitMiddleware(req, res, next) {
135
137
  if (!this.rateLimitEnabled) {
136
- return next();
138
+ if (typeof next === 'function') next();
139
+ return;
137
140
  }
138
141
 
139
142
  const identifier = this._getClientIdentifier(req);
@@ -209,7 +212,7 @@ class SecurityMiddleware {
209
212
  res.setHeader('X-RateLimit-Remaining', remaining);
210
213
  res.setHeader('X-RateLimit-Reset', new Date(resetTime).toISOString());
211
214
 
212
- next();
215
+ if (typeof next === 'function') next();
213
216
  }
214
217
 
215
218
  /**
@@ -217,13 +220,15 @@ class SecurityMiddleware {
217
220
  */
218
221
  csrfMiddleware(req, res, next) {
219
222
  if (!this.csrfEnabled) {
220
- return next();
223
+ if (typeof next === 'function') next();
224
+ return;
221
225
  }
222
226
 
223
227
  // Skip CSRF for safe methods
224
228
  const safeMethods = ['GET', 'HEAD', 'OPTIONS'];
225
229
  if (safeMethods.includes(req.method)) {
226
- return next();
230
+ if (typeof next === 'function') next();
231
+ return;
227
232
  }
228
233
 
229
234
  // Get CSRF token from request
@@ -291,7 +296,7 @@ class SecurityMiddleware {
291
296
  }
292
297
 
293
298
  // Token valid, continue
294
- next();
299
+ if (typeof next === 'function') next();
295
300
  }
296
301
 
297
302
  /**
@@ -489,7 +494,7 @@ function pipelineSecurityHeaders(options = {}) {
489
494
  instance.securityHeadersMiddleware(ctx.request, ctx.response, oldNext);
490
495
 
491
496
  // Continue pipeline if next was called
492
- if (nextCalled) {
497
+ if (nextCalled && typeof next === 'function') {
493
498
  await next();
494
499
  }
495
500
  };
@@ -504,7 +509,7 @@ function pipelineCors(options = {}) {
504
509
  instance.corsMiddleware(ctx.request, ctx.response, oldNext);
505
510
 
506
511
  // CORS might terminate for OPTIONS - check if response ended
507
- if (!ctx.response.writableEnded && nextCalled) {
512
+ if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
508
513
  await next();
509
514
  }
510
515
  };
@@ -519,7 +524,7 @@ function pipelineRateLimit(options = {}) {
519
524
  instance.rateLimitMiddleware(ctx.request, ctx.response, oldNext);
520
525
 
521
526
  // Rate limit might terminate - check if response ended
522
- if (!ctx.response.writableEnded && nextCalled) {
527
+ if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
523
528
  await next();
524
529
  }
525
530
  };
@@ -534,7 +539,7 @@ function pipelineCsrf(options = {}) {
534
539
  instance.csrfMiddleware(ctx.request, ctx.response, oldNext);
535
540
 
536
541
  // CSRF might terminate - check if response ended
537
- if (!ctx.response.writableEnded && nextCalled) {
542
+ if (!ctx.response.writableEnded && nextCalled && typeof next === 'function') {
538
543
  await next();
539
544
  }
540
545
  };
@@ -36,18 +36,25 @@ class SessionSecurity {
36
36
  * Session middleware
37
37
  */
38
38
  middleware() {
39
- return async (req, res, next) => {
39
+ const self = this;
40
+
41
+ // Return pipeline-compatible (ctx, next) middleware.
42
+ // MasterPipeline.execute() calls handler(ctx, next), not handler(req, res, next).
43
+ return async (ctx, next) => {
44
+ const req = ctx.request;
45
+ const res = ctx.response;
46
+
40
47
  // Parse session from cookie
41
- const sessionId = this._parseCookie(req);
48
+ const sessionId = self._parseCookie(req);
42
49
 
43
50
  if (sessionId) {
44
51
  // Load existing session
45
52
  const session = sessionStore.get(sessionId);
46
53
 
47
- if (session && this._isSessionValid(session, req)) {
54
+ if (session && self._isSessionValid(session, req)) {
48
55
  // Check if session needs regeneration
49
- if (this._shouldRegenerate(session)) {
50
- req.session = await this._regenerateSession(sessionId, session, res);
56
+ if (self._shouldRegenerate(session)) {
57
+ req.session = await self._regenerateSession(sessionId, session, res);
51
58
  } else {
52
59
  // Use existing session
53
60
  req.session = session.data;
@@ -57,9 +64,9 @@ class SessionSecurity {
57
64
  session.lastAccess = Date.now();
58
65
 
59
66
  // Extend expiry if rolling
60
- if (this.rolling) {
61
- session.expiry = Date.now() + this.maxAge;
62
- this._setCookie(res, sessionId);
67
+ if (self.rolling) {
68
+ session.expiry = Date.now() + self.maxAge;
69
+ self._setCookie(res, sessionId);
63
70
  }
64
71
  }
65
72
  } else {
@@ -74,23 +81,25 @@ class SessionSecurity {
74
81
  }
75
82
 
76
83
  // Create new session
77
- req.session = await this._createSession(req, res);
84
+ req.session = await self._createSession(req, res);
78
85
  }
79
86
  } else {
80
87
  // No session cookie, create new session
81
- req.session = await this._createSession(req, res);
88
+ req.session = await self._createSession(req, res);
82
89
  }
83
90
 
84
91
  // Save session on response
85
92
  if (typeof res?.end === 'function') {
86
93
  const originalEnd = res.end;
87
94
  res.end = (...args) => {
88
- this._saveSession(req);
95
+ self._saveSession(req);
89
96
  originalEnd.apply(res, args);
90
97
  };
91
98
  }
92
99
 
93
- next();
100
+ if (typeof next === 'function') {
101
+ await next();
102
+ }
94
103
  };
95
104
  }
96
105
 
@@ -318,7 +318,8 @@ class RedisCSRFStore {
318
318
  message: 'CSRF check skipped - no session ID',
319
319
  path: ctx.request.url
320
320
  });
321
- return await next();
321
+ if (typeof next === 'function') return await next();
322
+ return;
322
323
  }
323
324
 
324
325
  // Skip CSRF check for safe methods
@@ -326,7 +327,8 @@ class RedisCSRFStore {
326
327
  if (ignoreMethods.includes(method)) {
327
328
  // Ensure token exists for this session
328
329
  await self.get(sessionId);
329
- return await next();
330
+ if (typeof next === 'function') return await next();
331
+ return;
330
332
  }
331
333
 
332
334
  // Get token from request (header or body)
@@ -373,7 +375,7 @@ class RedisCSRFStore {
373
375
  }
374
376
 
375
377
  // Token valid, continue pipeline
376
- await next();
378
+ if (typeof next === 'function') await next();
377
379
 
378
380
  } catch (error) {
379
381
  logger.error({
@@ -441,7 +441,7 @@ class RedisRateLimiter {
441
441
  }
442
442
 
443
443
  // Request allowed, continue pipeline
444
- await next();
444
+ if (typeof next === 'function') await next();
445
445
 
446
446
  } catch (error) {
447
447
  logger.error({
@@ -451,7 +451,7 @@ class RedisRateLimiter {
451
451
  });
452
452
 
453
453
  // On error, allow request (fail open)
454
- await next();
454
+ if (typeof next === 'function') await next();
455
455
  }
456
456
  };
457
457
  }