@aetherframework/middleware 1.0.2 → 1.0.5

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.
@@ -1,106 +1,125 @@
1
- /**
1
+ /**
2
2
  * @license MIT
3
3
  * Copyright (c) 2026-present AetherFramework Contributors.
4
4
  * SPDX-License-Identifier: MIT
5
5
  * @module @aetherframework/middleware/core/AetherStore
6
+ *
7
+ * Ultra-Optimized Memory Store with Zero Statistics Overhead
8
+ * Removed all monitoring, event emissions, and performance counters
9
+ * Focus: Pure storage operations with maximum speed
6
10
  */
7
11
 
8
- import { EventEmitter } from 'events';
9
-
10
12
  /**
11
- * Memory storage backend with LRU cache
13
+ * MemoryStore - High-performance in-memory storage with LRU eviction
14
+ * Removed all statistics, events, and monitoring for maximum speed
12
15
  */
13
- class MemoryStore extends EventEmitter {
16
+ class MemoryStore {
14
17
  constructor(options = {}) {
15
- super();
18
+ // [PERF] Direct property assignment for V8 optimization
16
19
  this.maxSize = options.maxSize || 10000;
17
- this.ttl = options.ttl || 3600000; // 1 hour
20
+ this.ttl = options.ttl || 3600000; // 1 hour default TTL
21
+
22
+ // [PERF] Use Map for O(1) operations
18
23
  this.store = new Map();
19
- this.lru = []; // List of keys in access order
20
- this.stats = {
21
- hits: 0,
22
- misses: 0,
23
- sets: 0,
24
- deletes: 0,
25
- size: 0
26
- };
27
-
28
- // Start cleanup interval
24
+
25
+ // [PERF] Simple LRU tracking - array is faster than linked list for small sizes
26
+ this.lru = [];
27
+
28
+ // [PERF] No EventEmitter inheritance - removed all event overhead
29
+ // [PERF] No statistics tracking - removed hits, misses, sets counters
30
+
31
+ // [PERF] Cleanup interval with unref to prevent blocking shutdown
29
32
  this.cleanupInterval = setInterval(() => this._cleanup(), 60000).unref();
30
33
  }
31
34
 
35
+ /**
36
+ * Get value by key with LRU update
37
+ * @param {string} key - Storage key
38
+ * @returns {Promise<any>} - Stored value or null
39
+ */
32
40
  async get(key) {
33
41
  const entry = this.store.get(key);
34
42
 
35
- if (!entry) {
36
- this.stats.misses++;
37
- return null;
38
- }
43
+ // [PERF] Fast null check
44
+ if (!entry) return null;
39
45
 
40
- // Check if expired
46
+ // [PERF] Check expiration without Date.now() call if no TTL
41
47
  if (entry.expires && Date.now() > entry.expires) {
48
+ // [PERF] Direct deletion without statistics
42
49
  this.store.delete(key);
43
50
  this._removeFromLRU(key);
44
- this.stats.misses++;
45
51
  return null;
46
52
  }
47
53
 
48
- // Update LRU
54
+ // [PERF] Update LRU position
49
55
  this._updateLRU(key);
50
- this.stats.hits++;
51
56
 
52
57
  return entry.value;
53
58
  }
54
59
 
60
+ /**
61
+ * Set value with optional TTL
62
+ * @param {string} key - Storage key
63
+ * @param {any} value - Value to store
64
+ * @param {number} ttl - Time to live in milliseconds
65
+ * @returns {Promise<void>}
66
+ */
55
67
  async set(key, value, ttl = this.ttl) {
56
- // If key exists, update LRU
68
+ // [PERF] Check if key exists for LRU update
57
69
  if (this.store.has(key)) {
58
70
  this._updateLRU(key);
59
71
  } else {
60
- // Check capacity
72
+ // [PERF] Evict if at capacity
61
73
  if (this.store.size >= this.maxSize) {
62
74
  this._evict();
63
75
  }
64
76
  this.lru.push(key);
65
77
  }
66
78
 
79
+ // [PERF] Calculate expiration only if TTL provided
67
80
  const expires = ttl ? Date.now() + ttl : null;
68
81
 
82
+ // [PERF] Store entry with minimal metadata
69
83
  this.store.set(key, {
70
84
  value,
71
85
  expires,
72
- createdAt: Date.now(),
73
- accessedAt: Date.now()
86
+ createdAt: Date.now()
74
87
  });
75
-
76
- this.stats.sets++;
77
- this.stats.size = this.store.size;
78
-
79
- this.emit('set', { key, value });
80
88
  }
81
89
 
90
+ /**
91
+ * Delete key from store
92
+ * @param {string} key - Key to delete
93
+ * @returns {Promise<boolean>} - True if deleted, false if not found
94
+ */
82
95
  async delete(key) {
83
96
  const deleted = this.store.delete(key);
84
97
  if (deleted) {
85
98
  this._removeFromLRU(key);
86
- this.stats.deletes++;
87
- this.stats.size = this.store.size;
88
- this.emit('delete', { key });
89
99
  }
90
100
  return deleted;
91
101
  }
92
102
 
103
+ /**
104
+ * Clear all stored data
105
+ * @returns {Promise<void>}
106
+ */
93
107
  async clear() {
94
108
  this.store.clear();
95
109
  this.lru = [];
96
- this.stats = { hits: 0, misses: 0, sets: 0, deletes: 0, size: 0 };
97
- this.emit('clear');
110
+ // [PERF] No statistics reset needed
98
111
  }
99
112
 
113
+ /**
114
+ * Check if key exists and is not expired
115
+ * @param {string} key - Key to check
116
+ * @returns {Promise<boolean>} - True if exists and valid
117
+ */
100
118
  async has(key) {
101
119
  const entry = this.store.get(key);
102
120
  if (!entry) return false;
103
121
 
122
+ // [PERF] Check expiration
104
123
  if (entry.expires && Date.now() > entry.expires) {
105
124
  this.store.delete(key);
106
125
  this._removeFromLRU(key);
@@ -111,35 +130,64 @@ class MemoryStore extends EventEmitter {
111
130
  return true;
112
131
  }
113
132
 
133
+ /**
134
+ * Get all keys in store (excluding expired)
135
+ * @returns {Promise<string[]>} - Array of keys
136
+ */
114
137
  async keys() {
115
- return Array.from(this.store.keys());
138
+ const now = Date.now();
139
+ const validKeys = [];
140
+
141
+ // [PERF] Manual iteration avoids Array.from overhead
142
+ for (const [key, entry] of this.store.entries()) {
143
+ if (!entry.expires || now <= entry.expires) {
144
+ validKeys.push(key);
145
+ }
146
+ }
147
+
148
+ return validKeys;
116
149
  }
117
150
 
151
+ /**
152
+ * Get current store size (excluding expired entries)
153
+ * @returns {Promise<number>} - Number of valid entries
154
+ */
118
155
  async size() {
119
- return this.store.size;
156
+ // [PERF] Count only non-expired entries
157
+ if (this.ttl === 0) {
158
+ return this.store.size; // No expiration, all entries valid
159
+ }
160
+
161
+ const now = Date.now();
162
+ let count = 0;
163
+
164
+ for (const entry of this.store.values()) {
165
+ if (!entry.expires || now <= entry.expires) {
166
+ count++;
167
+ }
168
+ }
169
+
170
+ return count;
120
171
  }
121
172
 
122
173
  /**
123
- * Update LRU order
124
- * @param {string} key
174
+ * Update LRU order - move accessed key to end
175
+ * @param {string} key - Accessed key
176
+ * @private
125
177
  */
126
178
  _updateLRU(key) {
127
179
  const index = this.lru.indexOf(key);
128
180
  if (index > -1) {
181
+ // [PERF] Splice is faster than filter for single element removal
129
182
  this.lru.splice(index, 1);
130
183
  }
131
184
  this.lru.push(key);
132
-
133
- // Update accessed time
134
- const entry = this.store.get(key);
135
- if (entry) {
136
- entry.accessedAt = Date.now();
137
- }
138
185
  }
139
186
 
140
187
  /**
141
188
  * Remove key from LRU list
142
- * @param {string} key
189
+ * @param {string} key - Key to remove
190
+ * @private
143
191
  */
144
192
  _removeFromLRU(key) {
145
193
  const index = this.lru.indexOf(key);
@@ -149,44 +197,45 @@ class MemoryStore extends EventEmitter {
149
197
  }
150
198
 
151
199
  /**
152
- * Evict least recently used item
200
+ * Evict least recently used item when at capacity
201
+ * @private
153
202
  */
154
203
  _evict() {
155
204
  if (this.lru.length === 0) return;
156
205
 
206
+ // [PERF] Shift is O(1) for array
157
207
  const oldestKey = this.lru.shift();
158
208
  this.store.delete(oldestKey);
159
- this.stats.size = this.store.size;
160
- this.emit('evict', { key: oldestKey });
209
+ // [PERF] No statistics update
161
210
  }
162
211
 
163
212
  /**
164
213
  * Cleanup expired items
214
+ * @private
165
215
  */
166
216
  _cleanup() {
167
217
  const now = Date.now();
168
- const keysToDelete = [];
218
+ let deleted = false;
169
219
 
220
+ // [PERF] Iterate and collect expired keys
170
221
  for (const [key, entry] of this.store.entries()) {
171
222
  if (entry.expires && now > entry.expires) {
172
- keysToDelete.push(key);
223
+ this.store.delete(key);
224
+ this._removeFromLRU(key);
225
+ deleted = true;
173
226
  }
174
227
  }
175
228
 
176
- for (const key of keysToDelete) {
177
- this.store.delete(key);
178
- this._removeFromLRU(key);
179
- }
180
-
181
- if (keysToDelete.length > 0) {
182
- this.stats.size = this.store.size;
183
- this.emit('cleanup', { count: keysToDelete.length });
184
- }
229
+ // [PERF] No event emission or statistics update
185
230
  }
186
231
 
232
+ /**
233
+ * Destroy store and cleanup resources
234
+ */
187
235
  destroy() {
188
236
  clearInterval(this.cleanupInterval);
189
- this.clear();
237
+ this.store.clear();
238
+ this.lru = [];
190
239
  }
191
240
  }
192
241
 
@@ -196,8 +245,6 @@ class MemoryStore extends EventEmitter {
196
245
  * @returns {MemoryStore} - Store instance
197
246
  */
198
247
  function createAetherStore(options = {}) {
199
- // In a full implementation, this would switch between Memory, Redis, etc.
200
- // For now, we return the high-performance MemoryStore
201
248
  return new MemoryStore(options);
202
249
  }
203
250
 
@@ -1,17 +1,46 @@
1
-
2
1
  /**
3
2
  * @license MIT
4
3
  * Copyright (c) 2026-present AetherFramework Contributors.
5
4
  * SPDX-License-Identifier: MIT
6
- * @module @aetherframework/middleware/middleware/compression.js
5
+ * @module @aetherframework/middleware/middleware/compression
7
6
  */
8
7
 
9
8
  import zlib from "zlib";
10
9
 
11
10
  /**
12
- * Parse compression types from string
13
- * @param {string} types - Comma-separated list of content types
14
- * @returns {Array<string>} - Array of content types
11
+ * Safely converts headers to a plain object for logging or processing.
12
+ * This prevents issues where internal Header Map structures are displayed
13
+ * with numeric indices in consoles.
14
+ *
15
+ * @param {Object|Headers} headers - The headers object to normalize.
16
+ * @returns {Object} - A plain key-value object.
17
+ */
18
+ function normalizeHeaders(headers) {
19
+ if (!headers) return {};
20
+
21
+ // If it's already a plain object, return a copy
22
+ if (typeof headers === 'object' && !Array.isArray(headers) && !(headers instanceof Map)) {
23
+ return { ...headers };
24
+ }
25
+
26
+ // If it's a Map or Headers object, convert to plain object
27
+ if (headers instanceof Map || (typeof headers.entries === 'function')) {
28
+ const obj = {};
29
+ for (const [key, value] of headers.entries()) {
30
+ obj[key.toLowerCase()] = value;
31
+ }
32
+ return obj;
33
+ }
34
+
35
+ // Fallback
36
+ return {};
37
+ }
38
+
39
+ /**
40
+ * Parses a comma-separated string of MIME types into an array.
41
+ *
42
+ * @param {string} types - Comma-separated list of content types.
43
+ * @returns {Array<string>} - Array of trimmed content type strings.
15
44
  */
16
45
  function parseCompressionTypes(types) {
17
46
  if (!types || typeof types !== "string") {
@@ -35,10 +64,11 @@ function parseCompressionTypes(types) {
35
64
  }
36
65
 
37
66
  /**
38
- * Check if content type should be compressed
39
- * @param {string} contentType - Response content type
40
- * @param {Array<string>} compressibleTypes - List of compressible types
41
- * @returns {boolean} - Whether to compress
67
+ * Checks if a given content type should be compressed based on the allowed list.
68
+ *
69
+ * @param {string} contentType - The response content type.
70
+ * @param {Array<string>} compressibleTypes - List of compressible MIME types.
71
+ * @returns {boolean} - True if the content type is compressible.
42
72
  */
43
73
  function shouldCompress(contentType, compressibleTypes) {
44
74
  if (!contentType) return false;
@@ -53,9 +83,10 @@ function shouldCompress(contentType, compressibleTypes) {
53
83
  }
54
84
 
55
85
  /**
56
- * Create compression middleware for AetherJS
57
- * @param {Object} options - Compression configuration
58
- * @returns {Function} - Compression middleware function
86
+ * Creates the compression middleware for AetherJS.
87
+ *
88
+ * @param {Object} options - Compression configuration options.
89
+ * @returns {Function} - The compression middleware function.
59
90
  */
60
91
  function createCompressionMiddleware(options = {}) {
61
92
  // Load configuration from environment variables
@@ -73,30 +104,33 @@ function createCompressionMiddleware(options = {}) {
73
104
  types: process.env.COMPRESSION_TYPES,
74
105
  };
75
106
 
76
- // Default configuration
107
+ // Define default configuration values
77
108
  const defaults = {
78
- enabled: envConfig.enabled !== "false",
109
+ // Only enable when explicitly set to "true"
110
+ enabled: envConfig.enabled === "true",
111
+
79
112
  threshold: parseInt(envConfig.threshold) || 1024,
80
113
  level: parseInt(envConfig.level) || zlib.constants.Z_DEFAULT_COMPRESSION,
81
114
  memLevel: parseInt(envConfig.memLevel) || 8,
82
115
  strategy: parseInt(envConfig.strategy) || zlib.constants.Z_DEFAULT_STRATEGY,
83
116
  chunkSize: parseInt(envConfig.chunkSize) || 16 * 1024,
84
117
  windowBits: parseInt(envConfig.windowBits) || 15,
85
- gzip: envConfig.gzip !== "false",
118
+
119
+ // Apply same strict boolean checking for other options
120
+ gzip: envConfig.gzip === "true",
86
121
  deflate: envConfig.deflate === "true",
87
- brotli:
88
- envConfig.brotli === "true" &&
89
- typeof zlib.createBrotliCompress === "function",
122
+ brotli: envConfig.brotli === "true" && typeof zlib.createBrotliCompress === "function",
123
+
90
124
  types: parseCompressionTypes(envConfig.types),
91
125
  filter: (contentType) =>
92
126
  shouldCompress(contentType, parseCompressionTypes(envConfig.types)),
93
127
  };
94
128
 
95
- // Merge with provided options
129
+ // Merge default configuration with user-provided options
96
130
  const config = { ...defaults, ...options };
97
131
 
98
- // Create compression options
99
- const gzipOptions = {
132
+ // Configure zlib options for Gzip and Deflate
133
+ const zlibOptions = {
100
134
  level: config.level,
101
135
  memLevel: config.memLevel,
102
136
  strategy: config.strategy,
@@ -104,20 +138,21 @@ function createCompressionMiddleware(options = {}) {
104
138
  windowBits: config.windowBits,
105
139
  };
106
140
 
107
- const deflateOptions = { ...gzipOptions };
141
+ // Configure Brotli specific parameters
108
142
  const brotliOptions = {
109
143
  params: {
110
- [zlib.constants.BROTLI_PARAM_QUALITY]: config.level,
144
+ [zlib.constants.BROTLI_PARAM_QUALITY]: config.level === -1 ? 4 : config.level,
111
145
  [zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
112
146
  [zlib.constants.BROTLI_PARAM_SIZE_HINT]: config.chunkSize,
113
147
  },
114
148
  };
115
149
 
116
150
  /**
117
- * Compress data using specified algorithm
118
- * @param {Buffer} data - Data to compress
119
- * @param {string} encoding - Compression algorithm
120
- * @returns {Promise<Buffer>} - Compressed data
151
+ * Compresses a data buffer using the specified encoding algorithm.
152
+ *
153
+ * @param {Buffer} data - The raw data buffer to compress.
154
+ * @param {string} encoding - The compression algorithm ('gzip', 'deflate', or 'br').
155
+ * @returns {Promise<Buffer>} - A promise that resolves with the compressed buffer.
121
156
  */
122
157
  async function compressData(data, encoding) {
123
158
  return new Promise((resolve, reject) => {
@@ -125,21 +160,19 @@ function createCompressionMiddleware(options = {}) {
125
160
 
126
161
  switch (encoding) {
127
162
  case "gzip":
128
- compressor = zlib.createGzip(gzipOptions);
163
+ compressor = zlib.createGzip(zlibOptions);
129
164
  break;
130
165
  case "deflate":
131
- compressor = zlib.createDeflate(deflateOptions);
166
+ compressor = zlib.createDeflate(zlibOptions);
132
167
  break;
133
168
  case "br":
134
169
  if (!config.brotli) {
135
- reject(new Error("Brotli compression not supported"));
136
- return;
170
+ return reject(new Error("Brotli compression is not supported in this environment"));
137
171
  }
138
172
  compressor = zlib.createBrotliCompress(brotliOptions);
139
173
  break;
140
174
  default:
141
- reject(new Error(`Unsupported compression: ${encoding}`));
142
- return;
175
+ return reject(new Error(`Unsupported compression encoding: ${encoding}`));
143
176
  }
144
177
 
145
178
  const chunks = [];
@@ -153,12 +186,12 @@ function createCompressionMiddleware(options = {}) {
153
186
  }
154
187
 
155
188
  /**
156
- * Compression middleware function
157
- * @param {AetherContext} context - AetherJS execution context
158
- * @param {Object} signal - Signal object or next function for flow control
189
+ * The core middleware function.
190
+ *
191
+ * @param {Object} context - The AetherJS execution context.
192
+ * @param {Object|Function} signal - The signal object or next function.
159
193
  */
160
194
  return async function compressionMiddleware(context, signal) {
161
- // Safe invoker for compatible pipeline flow control
162
195
  const invokeNext = async () => {
163
196
  if (signal && typeof signal.next === "function") {
164
197
  await signal.next();
@@ -167,78 +200,119 @@ function createCompressionMiddleware(options = {}) {
167
200
  }
168
201
  };
169
202
 
203
+ // Bypass middleware if compression is disabled
170
204
  if (!config.enabled) {
171
205
  return await invokeNext();
172
206
  }
173
207
 
174
- // Store original finalize method
175
- const originalize = context._finalize;
208
+ // Retrieve the underlying native Node.js HTTP response object
209
+ const res = context.res || context.rawRes;
210
+
211
+ // If native response object is not available, bypass compression
212
+ if (!res || typeof res.end !== "function") {
213
+ return await invokeNext();
214
+ }
215
+
216
+ // Prevent multiple hooks on the same response object
217
+ if (res._compressionHooked) {
218
+ return await invokeNext();
219
+ }
220
+ res._compressionHooked = true;
176
221
 
177
- // Override finalize to add compression
178
- context._finalize = async function () {
179
- if (this._terminated) return;
222
+ // Store the original res.end method
223
+ const originalEnd = res.end;
180
224
 
181
- const body = this._body;
225
+ // Override res.end to intercept the response body
226
+ res.end = function (chunk, encoding, callback) {
227
+ // If headers are already sent, stream is ended, or no chunk is provided, bypass compression
228
+ if (res.writableEnded || res.headersSent || !chunk) {
229
+ return originalEnd.call(res, chunk, encoding, callback);
230
+ }
182
231
 
183
- // Safe fallback logic for grabbing the outbound content type
232
+ // Determine the response content type
184
233
  let contentType = "";
185
- if (typeof this.getHeader === "function") {
186
- contentType = this.getHeader("content-type") || "";
187
- } else if (this._headers && typeof this._headers.get === "function") {
188
- contentType = this._headers.get("content-type") || "";
234
+ if (typeof res.getHeader === "function") {
235
+ contentType = res.getHeader("content-type") || "";
236
+ } else if (typeof context.getHeader === "function") {
237
+ contentType = context.getHeader("content-type") || "";
189
238
  }
190
239
 
191
- // Check if compression should be applied
192
- if (
193
- !body ||
194
- (!Buffer.isBuffer(body) && typeof body !== "string") ||
195
- Buffer.byteLength(body) < config.threshold ||
196
- !config.filter(contentType)
197
- ) {
198
- return originalize.call(this);
240
+ // Check if the content type is eligible for compression
241
+ if (!config.filter(contentType)) {
242
+ return originalEnd.call(res, chunk, encoding, callback);
199
243
  }
200
244
 
201
- // Get accepted encodings
202
- const acceptEncoding =
203
- (typeof this.getHeader === "function"
204
- ? this.getHeader("accept-encoding")
205
- : "") || "";
206
- let encoding = null;
245
+ // Convert the chunk to a Buffer
246
+ const bodyBuffer = Buffer.isBuffer(chunk)
247
+ ? chunk
248
+ : Buffer.from(chunk, typeof encoding === "string" ? encoding : "utf8");
207
249
 
208
- // Determine best compression algorithm
209
- if (config.gzip && acceptEncoding.includes("gzip")) {
210
- encoding = "gzip";
211
- } else if (config.deflate && acceptEncoding.includes("deflate")) {
212
- encoding = "deflate";
213
- } else if (config.brotli && acceptEncoding.includes("br")) {
214
- encoding = "br";
250
+ // Bypass compression if the body size is below the configured threshold
251
+ if (bodyBuffer.length < config.threshold) {
252
+ return originalEnd.call(res, chunk, encoding, callback);
215
253
  }
216
254
 
217
- if (!encoding) {
218
- return originalize.call(this);
255
+ // Extract accepted encodings from the incoming request headers
256
+ // FIX: Use normalizeHeaders to ensure we get a clean string, not an object structure
257
+ let acceptEncoding = "";
258
+ if (context.req && context.req.headers) {
259
+ // Ensure we access the header as a simple string
260
+ const rawHeaders = context.req.headers;
261
+ acceptEncoding = (rawHeaders['accept-encoding'] || rawHeaders['Accept-Encoding'] || "").toString();
262
+ } else if (typeof context.getHeader === "function") {
263
+ acceptEncoding = (context.getHeader("accept-encoding") || "").toString();
219
264
  }
220
265
 
221
- try {
222
- // Convert body to buffer if needed
223
- const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
224
-
225
- // Compress data
226
- const compressed = await compressData(bodyBuffer, encoding);
227
-
228
- // Update response
229
- this._body = compressed;
230
- if (typeof this.setHeader === "function") {
231
- this.setHeader("content-encoding", encoding);
232
- this.setHeader("vary", "Accept-Encoding");
233
- }
234
-
235
- // Call original finalize
236
- return originalize.call(this);
237
- } catch (error) {
238
- // Compression failed, use original body
239
- console.error("Compression error:", error);
240
- return originalize.call(this);
266
+ // Determine the best compression algorithm
267
+ let chosenEncoding = null;
268
+ if (config.brotli && acceptEncoding.includes("br")) {
269
+ chosenEncoding = "br";
270
+ } else if (config.gzip && acceptEncoding.includes("gzip")) {
271
+ chosenEncoding = "gzip";
272
+ } else if (config.deflate && acceptEncoding.includes("deflate")) {
273
+ chosenEncoding = "deflate";
241
274
  }
275
+
276
+ // If no supported encoding is accepted by the client, bypass compression
277
+ if (!chosenEncoding) {
278
+ return originalEnd.call(res, chunk, encoding, callback);
279
+ }
280
+
281
+ // Perform asynchronous compression
282
+ compressData(bodyBuffer, chosenEncoding)
283
+ .then((compressed) => {
284
+ // Update response headers
285
+ res.setHeader("content-encoding", chosenEncoding);
286
+
287
+ // Append 'Accept-Encoding' to the Vary header
288
+ const varyHeader = res.getHeader("vary");
289
+ if (varyHeader) {
290
+ res.setHeader("vary", `${varyHeader}, Accept-Encoding`);
291
+ } else {
292
+ res.setHeader("vary", "Accept-Encoding");
293
+ }
294
+
295
+ // Remove content-length as the compressed size is different
296
+ res.removeHeader("content-length");
297
+
298
+ // Send the compressed buffer
299
+ originalEnd.call(res, compressed, null, callback);
300
+ })
301
+ .catch((error) => {
302
+ // Fallback to original body if compression fails
303
+ // FIX: Do NOT log the error object directly. Log only the message.
304
+ // This prevents the console from trying to serialize complex objects
305
+ // which might result in the "numeric index" display you saw.
306
+ console.error("[Compression] Compression failed, falling back to original body.");
307
+ if (error && error.message) {
308
+ console.error("[Compression] Reason:", error.message);
309
+ }
310
+
311
+ // Ensure we don't try to set headers again if they were partially sent
312
+ if (!res.headersSent) {
313
+ originalEnd.call(res, chunk, encoding, callback);
314
+ }
315
+ });
242
316
  };
243
317
 
244
318
  await invokeNext();