mastercontroller 1.3.13 → 1.3.15

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/MasterRequest.js CHANGED
@@ -1,18 +1,56 @@
1
1
 
2
2
  // version 0.0.2
3
3
 
4
- var url = require('url');
4
+ const url = require('url');
5
5
  const StringDecoder = require('string_decoder').StringDecoder;
6
- var qs = require('qs');
6
+ const qs = require('qs');
7
7
  const formidable = require('formidable');
8
- var contentTypeManager = require("content-type");
9
- var path = require('path');
8
+ const contentTypeManager = require("content-type");
9
+ const path = require('path');
10
10
  const fs = require('fs');
11
-
11
+ const fsPromises = require('fs').promises;
12
+ const { logger } = require('./error/MasterErrorLogger');
13
+
14
+ // Content Type Constants
15
+ const CONTENT_TYPES = {
16
+ FORM_URLENCODED: 'application/x-www-form-urlencoded',
17
+ MULTIPART_FORM: 'multipart/form-data',
18
+ JSON: 'application/json',
19
+ HTML: 'text/html',
20
+ PLAIN: 'text/plain'
21
+ };
22
+
23
+ // Size Limit Constants (DoS Protection)
24
+ const SIZE_LIMITS = {
25
+ MAX_FILES: 10,
26
+ MAX_FILE_SIZE: 50 * 1024 * 1024, // 50MB per file
27
+ MAX_TOTAL_FILE_SIZE: 100 * 1024 * 1024, // 100MB total
28
+ MAX_FIELDS: 1000,
29
+ MAX_FIELDS_SIZE: 20 * 1024 * 1024, // 20MB for all fields
30
+ MAX_BODY_SIZE: 10 * 1024 * 1024, // 10MB default
31
+ MAX_JSON_SIZE: 1 * 1024 * 1024, // 1MB for JSON
32
+ MAX_TEXT_SIZE: 1 * 1024 * 1024 // 1MB for text
33
+ };
34
+
35
+ /**
36
+ * MasterRequest - Request parsing and stream handling
37
+ *
38
+ * Handles all incoming request parsing including:
39
+ * - URL-encoded form data
40
+ * - Multipart form data (file uploads)
41
+ * - JSON payloads
42
+ * - Plain text/HTML
43
+ * - Raw body preservation for webhook signature verification
44
+ *
45
+ * Includes DoS protection via configurable size limits
46
+ *
47
+ * @class MasterRequest
48
+ */
12
49
  class MasterRequest{
13
50
  parsedURL = {};
14
51
  request = {};
15
52
  response = {};
53
+ __requestId = null;
16
54
 
17
55
  // Lazy-load master to avoid circular dependency (Google-style lazy initialization)
18
56
  get _master() {
@@ -22,7 +60,33 @@ class MasterRequest{
22
60
  return this.__masterCache;
23
61
  }
24
62
 
63
+ /**
64
+ * Initialize request handler with configuration options
65
+ *
66
+ * @param {Object} options - Configuration options
67
+ * @param {boolean} [options.disableFormidableMultipartFormData=false] - Disable multipart parsing
68
+ * @param {Object} [options.formidable] - Formidable configuration
69
+ * @param {number} [options.formidable.maxFiles=10] - Maximum number of files
70
+ * @param {number} [options.formidable.maxFileSize=50MB] - Maximum file size in bytes
71
+ * @param {number} [options.formidable.maxTotalFileSize=100MB] - Maximum total upload size
72
+ * @param {number} [options.formidable.maxFields=1000] - Maximum number of form fields
73
+ * @param {number} [options.formidable.maxFieldsSize=20MB] - Maximum size for all fields
74
+ * @param {number} [options.maxBodySize=10MB] - Maximum URL-encoded body size
75
+ * @param {number} [options.maxJsonSize=1MB] - Maximum JSON payload size
76
+ * @param {number} [options.maxTextSize=1MB] - Maximum text payload size
77
+ * @returns {void}
78
+ * @example
79
+ * masterRequest.init({
80
+ * formidable: { maxFileSize: 10 * 1024 * 1024 },
81
+ * maxJsonSize: 2 * 1024 * 1024
82
+ * });
83
+ */
25
84
  init(options){
85
+ // Input validation
86
+ if (options !== undefined && (typeof options !== 'object' || options === null || Array.isArray(options))) {
87
+ throw new TypeError('init() options must be an object');
88
+ }
89
+
26
90
  if(options){
27
91
  this.options = {};
28
92
  this.options.disableFormidableMultipartFormData = options.disableFormidableMultipartFormData === null? false : options.disableFormidableMultipartFormData;
@@ -30,32 +94,88 @@ class MasterRequest{
30
94
  // CRITICAL FIX: Add file upload limits to prevent DoS attacks
31
95
  // Default formidable configuration with security limits
32
96
  this.options.formidable = {
33
- maxFiles: 10, // CRITICAL: Limit number of files per request
34
- maxFileSize: 50 * 1024 * 1024, // CRITICAL: 50MB per file (was unlimited)
35
- maxTotalFileSize: 100 * 1024 * 1024, // CRITICAL: 100MB total for all files
36
- maxFields: 1000, // Limit number of fields
37
- maxFieldsSize: 20 * 1024 * 1024, // 20MB for all fields combined
38
- allowEmptyFiles: false, // Reject empty file uploads
39
- minFileSize: 1, // Minimum 1 byte
40
- ...(options.formidable || {}) // Allow user overrides
97
+ maxFiles: SIZE_LIMITS.MAX_FILES,
98
+ maxFileSize: SIZE_LIMITS.MAX_FILE_SIZE,
99
+ maxTotalFileSize: SIZE_LIMITS.MAX_TOTAL_FILE_SIZE,
100
+ maxFields: SIZE_LIMITS.MAX_FIELDS,
101
+ maxFieldsSize: SIZE_LIMITS.MAX_FIELDS_SIZE,
102
+ allowEmptyFiles: false,
103
+ minFileSize: 1,
104
+ ...(options.formidable || {})
41
105
  };
42
106
 
43
107
  // Body size limits (DoS protection)
44
- this.options.maxBodySize = options.maxBodySize || 10 * 1024 * 1024; // 10MB default
45
- this.options.maxJsonSize = options.maxJsonSize || 1 * 1024 * 1024; // 1MB default for JSON
46
- this.options.maxTextSize = options.maxTextSize || 1 * 1024 * 1024; // 1MB default for text
108
+ this.options.maxBodySize = options.maxBodySize || SIZE_LIMITS.MAX_BODY_SIZE;
109
+ this.options.maxJsonSize = options.maxJsonSize || SIZE_LIMITS.MAX_JSON_SIZE;
110
+ this.options.maxTextSize = options.maxTextSize || SIZE_LIMITS.MAX_TEXT_SIZE;
47
111
  }
48
112
  }
49
113
 
114
+ /**
115
+ * Parse incoming request and extract parameters
116
+ *
117
+ * Supports both old pattern (request object) and new pattern (context object)
118
+ * for backward compatibility. Automatically detects content-type and routes to
119
+ * appropriate parser.
120
+ *
121
+ * Supported content types:
122
+ * - application/x-www-form-urlencoded
123
+ * - multipart/form-data (file uploads)
124
+ * - application/json
125
+ * - text/html
126
+ * - text/plain
127
+ *
128
+ * @param {Object|Request} requestOrContext - Request or context object
129
+ * @param {Object} requestOrContext.request - HTTP request (new pattern)
130
+ * @param {string} requestOrContext.requrl - Pre-parsed URL (optional)
131
+ * @param {Response} res - HTTP response object
132
+ * @returns {Promise<Object>} Parsed request data
133
+ * @returns {Object} parsedURL.query - Query string parameters
134
+ * @returns {Object} parsedURL.formData - Parsed body data
135
+ * @returns {Object} parsedURL.formData.fields - Form fields (multipart)
136
+ * @returns {Object} parsedURL.formData.files - Uploaded files (multipart)
137
+ * @returns {string} parsedURL.formData._rawBody - Raw body string (for webhook verification)
138
+ * @throws {Error} If upload size exceeds limits
139
+ * @throws {Error} If file upload fails
140
+ * @throws {Error} If content type parsing fails
141
+ * @example
142
+ * const parsedData = await masterRequest.getRequestParam(req, res);
143
+ * console.log(parsedData.query.id);
144
+ * console.log(parsedData.formData.fields.username);
145
+ */
50
146
  getRequestParam(requestOrContext, res){
51
- var $that = this;
147
+ // Input validation
148
+ if (!requestOrContext || typeof requestOrContext !== 'object') {
149
+ return Promise.reject(new TypeError('getRequestParam() requires a request or context object'));
150
+ }
151
+
152
+ if (!res || typeof res !== 'object' || typeof res.writeHead !== 'function') {
153
+ return Promise.reject(new TypeError('getRequestParam() requires a valid response object'));
154
+ }
155
+
156
+ const $that = this;
52
157
  $that.response = res;
53
- try {
54
- return new Promise(function (resolve, reject) {
158
+
159
+ // Return Promise with proper error handling
160
+ return new Promise(function (resolve, reject) {
161
+ try {
55
162
  // BACKWARD COMPATIBILITY: Support both old and new patterns
56
163
  // New pattern (v1.3.x pipeline): Pass context with requrl property
57
164
  // Old pattern (pre-v1.3.x): Pass request with requrl property
58
165
  const request = requestOrContext.request || requestOrContext;
166
+
167
+ // SECURITY: Validate headers before processing
168
+ const securityCheck = $that._validateSecurityHeaders(request);
169
+ if (!securityCheck.valid) {
170
+ logger.warn({
171
+ code: 'MC_REQ_SECURITY_VALIDATION_FAILED',
172
+ message: 'Request failed security validation',
173
+ requestId: $that.getRequestId(),
174
+ reason: securityCheck.reason
175
+ });
176
+ reject(new Error(securityCheck.reason));
177
+ return;
178
+ }
59
179
  let requrl = requestOrContext.requrl || request.requrl;
60
180
 
61
181
  // Fallback: If requrl not set, parse from request.url
@@ -63,19 +183,19 @@ class MasterRequest{
63
183
  requrl = url.parse(request.url, true);
64
184
  }
65
185
 
66
- var querydata = url.parse(requrl, true);
186
+ const querydata = url.parse(requrl, true);
67
187
  $that.parsedURL.query = querydata.query;
68
188
  $that.form = new formidable.IncomingForm($that.options.formidable);
69
189
  if(request.headers['content-type'] || request.headers['transfer-encoding'] ){
70
190
  var contentType = contentTypeManager.parse(request);
71
191
  switch(contentType.type){
72
- case "application/x-www-form-urlencoded":
192
+ case CONTENT_TYPES.FORM_URLENCODED:
73
193
  $that.urlEncodeStream(request, function(data){
74
194
  $that.parsedURL.formData = data;
75
195
  resolve($that.parsedURL);
76
196
  });
77
197
  break
78
- case "multipart/form-data" :
198
+ case CONTENT_TYPES.MULTIPART_FORM:
79
199
  // Offer operturnity to add options. find a way to add dependecy injection. to request
80
200
  if(!$that.options.disableFormidableMultipartFormData){
81
201
 
@@ -108,17 +228,21 @@ class MasterRequest{
108
228
  const maxTotalSize = $that.options.formidable.maxTotalFileSize || 100 * 1024 * 1024;
109
229
  if (totalUploadedSize > maxTotalSize) {
110
230
  uploadAborted = true;
111
- console.error(`[MasterRequest] Total upload size (${totalUploadedSize} bytes) exceeds limit (${maxTotalSize} bytes)`);
112
-
113
- // Cleanup all uploaded files
114
- uploadedFiles.forEach(f => {
115
- if (f.filepath) {
116
- try {
117
- $that.deleteFileBuffer(f.filepath);
118
- } catch (err) {
119
- console.error('[MasterRequest] Cleanup failed:', err.message);
120
- }
121
- }
231
+ logger.error({
232
+ code: 'MC_REQ_UPLOAD_SIZE_EXCEEDED',
233
+ message: 'Total upload size exceeds limit',
234
+ requestId: $that.getRequestId(),
235
+ totalSize: totalUploadedSize,
236
+ maxSize: maxTotalSize
237
+ });
238
+
239
+ // Cleanup all uploaded files (async)
240
+ Promise.all(
241
+ uploadedFiles
242
+ .filter(f => f.filepath)
243
+ .map(f => $that.deleteFileBuffer(f.filepath))
244
+ ).catch(() => {
245
+ // Cleanup errors already logged by deleteFileBuffer
122
246
  });
123
247
 
124
248
  reject(new Error(`Total upload size exceeds limit (${maxTotalSize} bytes)`));
@@ -134,23 +258,32 @@ class MasterRequest{
134
258
  }
135
259
 
136
260
  // CRITICAL: Log file upload for security audit trail
137
- console.log(`[MasterRequest] File uploaded: ${file.originalFilename || file.name} (${file.size} bytes)`);
261
+ logger.info({
262
+ code: 'MC_REQ_FILE_UPLOADED',
263
+ message: 'File uploaded',
264
+ requestId: $that.getRequestId(),
265
+ filename: file.originalFilename || file.name,
266
+ size: file.size
267
+ });
138
268
  });
139
269
 
140
270
  $that.form.on('error', function(err) {
141
271
  // CRITICAL: Handle upload errors
142
272
  uploadAborted = true;
143
- console.error('[MasterRequest] File upload error:', err.message);
144
-
145
- // Cleanup temporary files
146
- uploadedFiles.forEach(file => {
147
- if (file.filepath) {
148
- try {
149
- $that.deleteFileBuffer(file.filepath);
150
- } catch (cleanupErr) {
151
- console.error('[MasterRequest] Failed to cleanup temp file:', cleanupErr.message);
152
- }
153
- }
273
+ logger.error({
274
+ code: 'MC_REQ_UPLOAD_ERROR',
275
+ message: 'File upload error',
276
+ requestId: $that.getRequestId(),
277
+ error: err.message
278
+ });
279
+
280
+ // Cleanup temporary files (async)
281
+ Promise.all(
282
+ uploadedFiles
283
+ .filter(file => file.filepath)
284
+ .map(file => $that.deleteFileBuffer(file.filepath))
285
+ ).catch(() => {
286
+ // Cleanup errors already logged by deleteFileBuffer
154
287
  });
155
288
 
156
289
  reject(new Error(`File upload failed: ${err.message}`));
@@ -159,17 +292,19 @@ class MasterRequest{
159
292
  $that.form.on('aborted', function() {
160
293
  // CRITICAL: Handle client abort (connection closed)
161
294
  uploadAborted = true;
162
- console.warn('[MasterRequest] File upload aborted by client');
163
-
164
- // Cleanup temporary files
165
- uploadedFiles.forEach(file => {
166
- if (file.filepath) {
167
- try {
168
- $that.deleteFileBuffer(file.filepath);
169
- } catch (cleanupErr) {
170
- console.error('[MasterRequest] Failed to cleanup temp file:', cleanupErr.message);
171
- }
172
- }
295
+ logger.warn({
296
+ code: 'MC_REQ_UPLOAD_ABORTED',
297
+ message: 'File upload aborted by client',
298
+ requestId: $that.getRequestId()
299
+ });
300
+
301
+ // Cleanup temporary files (async)
302
+ Promise.all(
303
+ uploadedFiles
304
+ .filter(file => file.filepath)
305
+ .map(file => $that.deleteFileBuffer(file.filepath))
306
+ ).catch(() => {
307
+ // Cleanup errors already logged by deleteFileBuffer
173
308
  });
174
309
 
175
310
  reject(new Error('File upload aborted by client'));
@@ -187,17 +322,21 @@ class MasterRequest{
187
322
  }else{
188
323
 
189
324
  resolve($that.parsedURL);
190
- console.log("skipped multipart/form-data")
325
+ logger.debug({
326
+ code: 'MC_REQ_MULTIPART_SKIPPED',
327
+ message: 'Multipart form-data parsing disabled',
328
+ requestId: $that.getRequestId()
329
+ });
191
330
  }
192
331
  break
193
- case "application/json" :
332
+ case CONTENT_TYPES.JSON:
194
333
  $that.jsonStream(request, function(data){
195
334
  $that.parsedURL.formData = data;
196
335
  resolve($that.parsedURL);
197
336
  });
198
337
 
199
338
  break
200
- case "text/html" :
339
+ case CONTENT_TYPES.HTML:
201
340
  $that.textStream(request, function(data){
202
341
  $that.parsedURL.formData = {};
203
342
  $that.parsedURL.formData.textField = data;
@@ -205,7 +344,7 @@ class MasterRequest{
205
344
  });
206
345
 
207
346
  break
208
- case "text/plain" :
347
+ case CONTENT_TYPES.PLAIN:
209
348
  $that.fetchData(request, function(data){
210
349
  $that.parsedURL.formData = data;
211
350
  resolve($that.parsedURL);
@@ -216,22 +355,164 @@ class MasterRequest{
216
355
  default:
217
356
  var errorMessage = `Cannot parse - We currently support text/plain, text/html, application/json, multipart/form-data, and application/x-www-form-urlencoded - your sending us = ${contentType.type}`;
218
357
  resolve(errorMessage);
219
- console.log(errorMessage);
358
+ logger.warn({
359
+ code: 'MC_REQ_UNSUPPORTED_CONTENT_TYPE',
360
+ message: 'Unsupported content type',
361
+ requestId: $that.getRequestId(),
362
+ contentType: contentType.type
363
+ });
220
364
  }
221
365
 
222
366
  }
223
367
  else{
224
368
  resolve($that.parsedURL);
225
369
  }
370
+ } catch (error) {
371
+ // Catch any synchronous errors in Promise executor
372
+ logger.error({
373
+ code: 'MC_REQ_PARSE_ERROR',
374
+ message: 'Failed to parse request',
375
+ requestId: $that.getRequestId(),
376
+ error: error.message,
377
+ stack: error.stack
378
+ });
379
+ reject(new Error(`Request parsing failed: ${error.message}`));
380
+ }
381
+ }).catch((error) => {
382
+ // Catch any unhandled promise rejections
383
+ logger.error({
384
+ code: 'MC_REQ_UNHANDLED_REJECTION',
385
+ message: 'Unhandled promise rejection in getRequestParam',
386
+ requestId: this.getRequestId(),
387
+ error: error.message,
388
+ stack: error.stack
226
389
  });
390
+ // Re-throw to propagate to caller
391
+ throw error;
392
+ });
393
+ };
227
394
 
395
+ /**
396
+ * Validate security-related headers and request properties
397
+ *
398
+ * Checks for common security issues:
399
+ * - Content-Length bomb attacks
400
+ * - Suspicious content-type values
401
+ * - Oversized headers
402
+ *
403
+ * @private
404
+ * @param {Request} request - HTTP request object
405
+ * @returns {Object} Validation result
406
+ * @returns {boolean} result.valid - Whether request passes validation
407
+ * @returns {string} result.reason - Reason for validation failure
408
+ */
409
+ _validateSecurityHeaders(request) {
410
+ // Check content-length header
411
+ const contentLength = parseInt(request.headers['content-length'], 10);
412
+ if (!isNaN(contentLength) && contentLength > 200 * 1024 * 1024) { // 200MB hard limit
413
+ return {
414
+ valid: false,
415
+ reason: 'Content-Length exceeds maximum allowed size (200MB)'
416
+ };
228
417
  }
229
- catch (ex) {
230
- throw ex;
418
+
419
+ // Check for suspiciously long header values (potential header injection)
420
+ for (const [key, value] of Object.entries(request.headers)) {
421
+ if (typeof value === 'string' && value.length > 8192) { // 8KB per header
422
+ return {
423
+ valid: false,
424
+ reason: `Header ${key} exceeds maximum length`
425
+ };
426
+ }
231
427
  }
232
- };
233
428
 
429
+ // Check for null bytes in headers (potential injection)
430
+ for (const [key, value] of Object.entries(request.headers)) {
431
+ if (typeof value === 'string' && value.includes('\0')) {
432
+ return {
433
+ valid: false,
434
+ reason: `Header ${key} contains null bytes`
435
+ };
436
+ }
437
+ }
438
+
439
+ return { valid: true };
440
+ }
441
+
442
+ /**
443
+ * Get or generate request ID for tracing
444
+ *
445
+ * Checks for x-request-id header first, then generates a unique ID.
446
+ * Used for correlating logs and tracking requests through the system.
447
+ *
448
+ * @returns {string} Unique request identifier
449
+ * @example
450
+ * const reqId = this.getRequestId();
451
+ * logger.info({ requestId: reqId, message: 'Processing request' });
452
+ */
453
+ getRequestId() {
454
+ if (!this.__requestId) {
455
+ // Check for existing request ID header
456
+ const headerReqId = this.request?.headers?.['x-request-id'];
457
+ if (headerReqId) {
458
+ this.__requestId = headerReqId;
459
+ } else {
460
+ // Generate new request ID: req_timestamp_random
461
+ this.__requestId = `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
462
+ }
463
+ }
464
+ return this.__requestId;
465
+ }
234
466
 
467
+ /**
468
+ * Handle stream errors with structured logging
469
+ *
470
+ * Centralized error handler for all stream parsing methods. Logs error
471
+ * and calls callback with error object.
472
+ *
473
+ * @private
474
+ * @param {string} code - Error code for logging
475
+ * @param {string} message - Human-readable error message
476
+ * @param {Function} callback - Callback function to invoke with error
477
+ * @param {Object} details - Additional error details
478
+ * @returns {void}
479
+ */
480
+ _handleStreamError(code, message, callback, details = {}) {
481
+ logger.error({
482
+ code,
483
+ message,
484
+ requestId: this.getRequestId(),
485
+ ...details
486
+ });
487
+
488
+ callback({
489
+ error: message,
490
+ ...details
491
+ });
492
+ }
493
+
494
+ /**
495
+ * Parse text/plain request body with DoS protection
496
+ *
497
+ * Implements size limits and stream error handling. Destroys request
498
+ * stream if payload exceeds maxBytes limit.
499
+ *
500
+ * @private
501
+ * @param {Request} request - HTTP request stream
502
+ * @param {Function} func - Callback function (data) => void
503
+ * @param {string|Object} func.data - Parsed text or error object
504
+ * @param {string} func.data.error - Error message if failed
505
+ * @param {number} func.data.maxSize - Maximum allowed size if exceeded
506
+ * @returns {void}
507
+ * @example
508
+ * this.fetchData(req, (data) => {
509
+ * if (data.error) {
510
+ * console.error('Parse failed:', data.error);
511
+ * } else {
512
+ * console.log('Text:', data);
513
+ * }
514
+ * });
515
+ */
235
516
  fetchData(request, func) {
236
517
 
237
518
  let body = '';
@@ -249,9 +530,13 @@ class MasterRequest{
249
530
  // Prevent memory overload
250
531
  if (receivedBytes > maxBytes) {
251
532
  errorOccurred = true;
252
- request.destroy(); // ✅ Fixed: was 'req', now 'request'
253
- console.error(`Plain text payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
254
- func({ error: 'Payload too large', maxSize: maxBytes });
533
+ request.destroy();
534
+ this._handleStreamError(
535
+ 'MC_REQ_TEXT_PAYLOAD_TOO_LARGE',
536
+ 'Payload too large',
537
+ func,
538
+ { receivedBytes, maxBytes, maxSize: maxBytes }
539
+ );
255
540
  return;
256
541
  }
257
542
 
@@ -267,8 +552,12 @@ class MasterRequest{
267
552
  const responseData = body;
268
553
  func(responseData);
269
554
  } catch (err) {
270
- console.error('Processing error handling text/plain:', err);
271
- func({ error: err.message });
555
+ this._handleStreamError(
556
+ 'MC_REQ_TEXT_PROCESSING_ERROR',
557
+ 'Error processing text/plain',
558
+ func,
559
+ { error: err.message }
560
+ );
272
561
  }
273
562
 
274
563
  });
@@ -276,32 +565,107 @@ class MasterRequest{
276
565
  request.on('error', (err) => {
277
566
  if (errorOccurred) return;
278
567
  errorOccurred = true;
279
- console.error('[MasterRequest] Stream error in fetchData:', err.message);
280
- func({ error: err.message });
568
+ this._handleStreamError(
569
+ 'MC_REQ_STREAM_ERROR',
570
+ 'Stream error in fetchData',
571
+ func,
572
+ { error: err.message }
573
+ );
281
574
  });
282
575
 
283
576
  request.on('aborted', () => {
284
577
  if (errorOccurred) return;
285
578
  errorOccurred = true;
286
- console.warn('[MasterRequest] Request aborted in fetchData');
579
+ logger.warn({
580
+ code: 'MC_REQ_ABORTED',
581
+ message: 'Request aborted in fetchData',
582
+ requestId: this.getRequestId()
583
+ });
287
584
  func({ error: 'Request aborted' });
288
585
  });
289
586
 
290
587
  } catch (error) {
291
- console.error("Failed to fetch data:", error);
292
- func({ error: error.message });
588
+ this._handleStreamError(
589
+ 'MC_REQ_FETCH_FAILED',
590
+ 'Failed to fetch data',
591
+ func,
592
+ { error: error.message }
593
+ );
293
594
  }
294
595
  }
295
596
 
296
- deleteFileBuffer(filePath){
297
- fs.unlink(filePath, function (err) {
298
- if (err) {
299
- console.error(err);
300
- }
301
- console.log('Temp File Delete');
302
- });
597
+ /**
598
+ * Delete temporary file buffer (async operation)
599
+ *
600
+ * Used to cleanup uploaded files after processing or on error.
601
+ * Logs success/failure via structured logger. Uses fs.promises for
602
+ * proper error propagation.
603
+ *
604
+ * @param {string} filePath - Absolute path to temporary file
605
+ * @returns {Promise<void>}
606
+ * @throws {Error} If file deletion fails (error is logged but not thrown)
607
+ * @example
608
+ * await this.deleteFileBuffer('/tmp/upload-12345.tmp');
609
+ * // Or with error handling:
610
+ * try {
611
+ * await this.deleteFileBuffer(filePath);
612
+ * } catch (err) {
613
+ * console.error('Cleanup failed:', err);
614
+ * }
615
+ */
616
+ async deleteFileBuffer(filePath){
617
+ // Input validation
618
+ if (!filePath || typeof filePath !== 'string') {
619
+ logger.error({
620
+ code: 'MC_REQ_INVALID_FILE_PATH',
621
+ message: 'deleteFileBuffer() requires a valid file path string',
622
+ requestId: this.getRequestId(),
623
+ filePath
624
+ });
625
+ return;
626
+ }
627
+
628
+ try {
629
+ await fsPromises.unlink(filePath);
630
+ logger.debug({
631
+ code: 'MC_REQ_FILE_DELETED',
632
+ message: 'Temporary file deleted',
633
+ requestId: this.getRequestId(),
634
+ filePath
635
+ });
636
+ } catch (err) {
637
+ logger.error({
638
+ code: 'MC_REQ_FILE_DELETE_FAILED',
639
+ message: 'Failed to delete temporary file',
640
+ requestId: this.getRequestId(),
641
+ filePath,
642
+ error: err.message
643
+ });
644
+ // Don't throw - cleanup failures shouldn't break the request
645
+ }
303
646
  }
304
647
 
648
+ /**
649
+ * Parse application/x-www-form-urlencoded request body
650
+ *
651
+ * Uses StringDecoder for proper UTF-8 handling. Includes DoS protection
652
+ * via configurable size limits. Preserves raw body string for webhook
653
+ * signature verification (accessible via data._rawBody).
654
+ *
655
+ * @private
656
+ * @param {Request} request - HTTP request stream
657
+ * @param {Function} func - Callback function (data) => void
658
+ * @param {Object} func.data - Parsed form data object or error object
659
+ * @param {string} func.data._rawBody - Original raw body string
660
+ * @param {string} func.data.error - Error message if failed
661
+ * @param {number} func.data.maxSize - Maximum allowed size if exceeded
662
+ * @returns {void}
663
+ * @example
664
+ * this.urlEncodeStream(req, (data) => {
665
+ * console.log(data.username, data.password);
666
+ * // Verify webhook signature using data._rawBody
667
+ * });
668
+ */
305
669
  urlEncodeStream(request, func){
306
670
  const decoder = new StringDecoder('utf-8');
307
671
  let buffer = '';
@@ -318,8 +682,12 @@ class MasterRequest{
318
682
  if (receivedBytes > maxBytes) {
319
683
  errorOccurred = true;
320
684
  request.destroy();
321
- console.error(`Form data too large: ${receivedBytes} bytes (max: ${maxBytes})`);
322
- func({ error: 'Payload too large', maxSize: maxBytes });
685
+ this._handleStreamError(
686
+ 'MC_REQ_FORM_DATA_TOO_LARGE',
687
+ 'Payload too large',
688
+ func,
689
+ { receivedBytes, maxBytes, maxSize: maxBytes }
690
+ );
323
691
  return;
324
692
  }
325
693
 
@@ -330,7 +698,7 @@ class MasterRequest{
330
698
  if (errorOccurred) return;
331
699
 
332
700
  buffer += decoder.end();
333
- var buff = qs.parse(buffer);
701
+ const buff = qs.parse(buffer);
334
702
  // Preserve raw body for signature verification
335
703
  buff._rawBody = buffer;
336
704
  func(buff);
@@ -339,19 +707,56 @@ class MasterRequest{
339
707
  request.on('error', (err) => {
340
708
  if (errorOccurred) return;
341
709
  errorOccurred = true;
342
- console.error('[MasterRequest] Stream error in urlEncodeStream:', err.message);
343
- func({ error: err.message });
710
+ this._handleStreamError(
711
+ 'MC_REQ_STREAM_ERROR',
712
+ 'Stream error in urlEncodeStream',
713
+ func,
714
+ { error: err.message }
715
+ );
344
716
  });
345
717
 
346
718
  request.on('aborted', () => {
347
719
  if (errorOccurred) return;
348
720
  errorOccurred = true;
349
- console.warn('[MasterRequest] Request aborted in urlEncodeStream');
721
+ logger.warn({
722
+ code: 'MC_REQ_ABORTED',
723
+ message: 'Request aborted in urlEncodeStream',
724
+ requestId: this.getRequestId()
725
+ });
350
726
  func({ error: 'Request aborted' });
351
727
  });
352
728
 
353
729
  }
354
730
 
731
+ /**
732
+ * Parse application/json request body
733
+ *
734
+ * Includes DoS protection via size limits. Handles empty bodies gracefully.
735
+ * Preserves raw body string for webhook signature verification (required
736
+ * by Stripe, GitHub, Shopify, etc. for HMAC validation).
737
+ *
738
+ * SECURITY: Does NOT fallback to qs.parse to prevent prototype pollution.
739
+ *
740
+ * @private
741
+ * @param {Request} request - HTTP request stream
742
+ * @param {Function} func - Callback function (data) => void
743
+ * @param {Object} func.data - Parsed JSON object or error object
744
+ * @param {string} func.data._rawBody - Original raw body string
745
+ * @param {string} func.data.error - Error message if failed
746
+ * @param {string} func.data.details - Error details if JSON parsing failed
747
+ * @param {number} func.data.maxSize - Maximum allowed size if exceeded
748
+ * @returns {void}
749
+ * @example
750
+ * this.jsonStream(req, (data) => {
751
+ * if (data.error) {
752
+ * res.statusCode = 400;
753
+ * res.end(JSON.stringify({ error: data.details }));
754
+ * } else {
755
+ * console.log('Received:', data);
756
+ * // Verify signature: hmac(data._rawBody, secret)
757
+ * }
758
+ * });
759
+ */
355
760
  jsonStream(request, func){
356
761
  let buffer = '';
357
762
  let receivedBytes = 0;
@@ -367,8 +772,12 @@ class MasterRequest{
367
772
  if (receivedBytes > maxBytes) {
368
773
  errorOccurred = true;
369
774
  request.destroy();
370
- console.error(`JSON payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
371
- func({ error: 'JSON payload too large', maxSize: maxBytes });
775
+ this._handleStreamError(
776
+ 'MC_REQ_JSON_PAYLOAD_TOO_LARGE',
777
+ 'JSON payload too large',
778
+ func,
779
+ { receivedBytes, maxBytes, maxSize: maxBytes }
780
+ );
372
781
  return;
373
782
  }
374
783
 
@@ -385,7 +794,7 @@ class MasterRequest{
385
794
  }
386
795
 
387
796
  try {
388
- var buff = JSON.parse(buffer);
797
+ const buff = JSON.parse(buffer);
389
798
  // IMPORTANT: Preserve raw body for webhook signature verification
390
799
  // Many webhook providers (Stripe, GitHub, Shopify, etc.) require the
391
800
  // exact raw body string to verify HMAC signatures
@@ -393,27 +802,62 @@ class MasterRequest{
393
802
  func(buff);
394
803
  } catch (e) {
395
804
  // Security: Don't fallback to qs.parse to avoid prototype pollution
396
- console.error('Invalid JSON payload:', e.message);
397
- func({ error: 'Invalid JSON', details: e.message });
805
+ this._handleStreamError(
806
+ 'MC_REQ_INVALID_JSON',
807
+ 'Invalid JSON',
808
+ func,
809
+ { details: e.message, error: e.message }
810
+ );
398
811
  }
399
812
  });
400
813
 
401
814
  request.on('error', (err) => {
402
815
  if (errorOccurred) return;
403
816
  errorOccurred = true;
404
- console.error('[MasterRequest] Stream error in jsonStream:', err.message);
405
- func({ error: err.message });
817
+ this._handleStreamError(
818
+ 'MC_REQ_STREAM_ERROR',
819
+ 'Stream error in jsonStream',
820
+ func,
821
+ { error: err.message }
822
+ );
406
823
  });
407
824
 
408
825
  request.on('aborted', () => {
409
826
  if (errorOccurred) return;
410
827
  errorOccurred = true;
411
- console.warn('[MasterRequest] Request aborted in jsonStream');
828
+ logger.warn({
829
+ code: 'MC_REQ_ABORTED',
830
+ message: 'Request aborted in jsonStream',
831
+ requestId: this.getRequestId()
832
+ });
412
833
  func({ error: 'Request aborted' });
413
834
  });
414
835
 
415
836
  }
416
837
 
838
+ /**
839
+ * Parse text/html request body
840
+ *
841
+ * Uses StringDecoder for proper UTF-8 handling. Includes DoS protection
842
+ * via configurable size limits. Returns raw string (no parsing).
843
+ *
844
+ * @private
845
+ * @param {Request} request - HTTP request stream
846
+ * @param {Function} func - Callback function (data) => void
847
+ * @param {string|Object} func.data - Raw text string or error object
848
+ * @param {string} func.data.error - Error message if failed
849
+ * @param {number} func.data.maxSize - Maximum allowed size if exceeded
850
+ * @returns {void}
851
+ * @example
852
+ * this.textStream(req, (html) => {
853
+ * if (html.error) {
854
+ * res.statusCode = 413;
855
+ * res.end('Payload too large');
856
+ * } else {
857
+ * console.log('HTML:', html);
858
+ * }
859
+ * });
860
+ */
417
861
  textStream(request, func){
418
862
  const decoder = new StringDecoder('utf-8');
419
863
  let buffer = '';
@@ -430,8 +874,12 @@ class MasterRequest{
430
874
  if (receivedBytes > maxBytes) {
431
875
  errorOccurred = true;
432
876
  request.destroy();
433
- console.error(`Text payload too large: ${receivedBytes} bytes (max: ${maxBytes})`);
434
- func({ error: 'Text payload too large', maxSize: maxBytes });
877
+ this._handleStreamError(
878
+ 'MC_REQ_TEXT_PAYLOAD_TOO_LARGE',
879
+ 'Text payload too large',
880
+ func,
881
+ { receivedBytes, maxBytes, maxSize: maxBytes }
882
+ );
435
883
  return;
436
884
  }
437
885
 
@@ -447,22 +895,51 @@ class MasterRequest{
447
895
  request.on('error', (err) => {
448
896
  if (errorOccurred) return;
449
897
  errorOccurred = true;
450
- console.error('[MasterRequest] Stream error in textStream:', err.message);
451
- func({ error: err.message });
898
+ this._handleStreamError(
899
+ 'MC_REQ_STREAM_ERROR',
900
+ 'Stream error in textStream',
901
+ func,
902
+ { error: err.message }
903
+ );
452
904
  });
453
905
 
454
906
  request.on('aborted', () => {
455
907
  if (errorOccurred) return;
456
908
  errorOccurred = true;
457
- console.warn('[MasterRequest] Request aborted in textStream');
909
+ logger.warn({
910
+ code: 'MC_REQ_ABORTED',
911
+ message: 'Request aborted in textStream',
912
+ requestId: this.getRequestId()
913
+ });
458
914
  func({ error: 'Request aborted' });
459
915
  });
460
916
 
461
917
  }
462
918
 
463
- // have a clear all object that you can run that will delete all rununing objects
919
+ /**
920
+ * Clear parsed request data and close response
921
+ *
922
+ * Resets parsedURL object and delegates to MasterAction.close() to send
923
+ * final response with appropriate content-type and status code.
924
+ *
925
+ * @param {number} code - HTTP status code
926
+ * @param {string} end - Response body content
927
+ * @returns {void}
928
+ * @example
929
+ * this.clear(200, 'OK');
930
+ * this.clear(404, JSON.stringify({ error: 'Not Found' }));
931
+ */
464
932
  clear(code, end){
465
- this.parsedURL = {};
933
+ // Input validation
934
+ if (typeof code !== 'number' || code < 100 || code > 599) {
935
+ throw new TypeError('clear() code must be a valid HTTP status code (100-599)');
936
+ }
937
+
938
+ if (end === undefined || end === null) {
939
+ throw new TypeError('clear() end parameter is required');
940
+ }
941
+
942
+ this.parsedURL = {};
466
943
  this._master.action.close(this.response, code, contentTypeManager.parse(this.request), end);
467
944
  }
468
945
  }