koa-classic-server 2.1.2 โ†’ 2.1.4

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.
@@ -0,0 +1,98 @@
1
+ # This workflow will run tests using node and then publish a package to npm when a release is published
2
+ # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3
+
4
+ name: Publish to npm
5
+
6
+ on:
7
+ release:
8
+ types: [published]
9
+
10
+ # Prevent multiple concurrent publish workflows
11
+ concurrency:
12
+ group: npm-publish-${{ github.ref }}
13
+ cancel-in-progress: false
14
+
15
+ # Set permissions for the workflow
16
+ permissions:
17
+ contents: read
18
+ id-token: write # Required for npm provenance
19
+
20
+ jobs:
21
+ build:
22
+ name: Build and Test
23
+ runs-on: ubuntu-latest
24
+ timeout-minutes: 10
25
+
26
+ steps:
27
+ - name: Checkout code
28
+ uses: actions/checkout@v4
29
+
30
+ - name: Setup Node.js
31
+ uses: actions/setup-node@v4
32
+ with:
33
+ node-version: 20
34
+ cache: 'npm'
35
+
36
+ - name: Install dependencies
37
+ run: npm ci
38
+
39
+ - name: Run tests
40
+ run: npm test --if-present
41
+
42
+ - name: Build package
43
+ run: npm run build --if-present
44
+
45
+ - name: Cache build artifacts
46
+ uses: actions/cache/save@v4
47
+ with:
48
+ path: |
49
+ node_modules
50
+ dist
51
+ build
52
+ key: build-${{ github.sha }}
53
+
54
+ publish-npm:
55
+ name: Publish to npm Registry
56
+ needs: build
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 10
59
+
60
+ steps:
61
+ - name: Checkout code
62
+ uses: actions/checkout@v4
63
+
64
+ - name: Setup Node.js
65
+ uses: actions/setup-node@v4
66
+ with:
67
+ node-version: 20
68
+ registry-url: https://registry.npmjs.org/
69
+ cache: 'npm'
70
+
71
+ - name: Restore build artifacts
72
+ uses: actions/cache/restore@v4
73
+ with:
74
+ path: |
75
+ node_modules
76
+ dist
77
+ build
78
+ key: build-${{ github.sha }}
79
+
80
+ - name: Verify package version matches release tag
81
+ run: |
82
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
83
+ RELEASE_TAG=${GITHUB_REF#refs/tags/}
84
+ # Remove 'v' prefix if present in tag
85
+ RELEASE_VERSION=${RELEASE_TAG#v}
86
+
87
+ echo "Package version: $PACKAGE_VERSION"
88
+ echo "Release version: $RELEASE_VERSION"
89
+
90
+ if [ "$PACKAGE_VERSION" != "$RELEASE_VERSION" ]; then
91
+ echo "Error: Package version ($PACKAGE_VERSION) does not match release tag ($RELEASE_VERSION)"
92
+ exit 1
93
+ fi
94
+
95
+ - name: Publish to npm with provenance
96
+ run: npm publish --provenance --access public
97
+ env:
98
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -8,9 +8,15 @@
8
8
 
9
9
  ---
10
10
 
11
- ## ๐ŸŽ‰ Version 2.1.2 - Production Release
11
+ ## ๐ŸŽ‰ Version 2.1.3 - Configuration Update
12
12
 
13
- Version 2.1.2 is a **major production release** featuring performance optimizations, enhanced directory listing, and critical bug fixes.
13
+ Version 2.1.3 updates the default caching behavior for better development experience while maintaining production-ready performance.
14
+
15
+ ### What's New in 2.1.3
16
+
17
+ โœ… **Development-Friendly Defaults** - `enableCaching` now defaults to `false` for easier development
18
+ โœ… **Production Guidance** - Clear documentation on enabling caching for production environments
19
+ โœ… **Enhanced Documentation** - Comprehensive notes on caching configuration and recommendations
14
20
 
15
21
  ### What's New in 2.1.2
16
22
 
@@ -228,10 +234,14 @@ app.use(koaClassicServer(__dirname + '/public', {
228
234
  }));
229
235
  ```
230
236
 
231
- **Benefits:**
237
+ **โš ๏ธ Important: Production Recommendation**
238
+
239
+ The default value for `enableCaching` is `false` to facilitate development (where you want changes to be immediately visible). **For production environments, it is strongly recommended to set `enableCaching: true`** to benefit from:
240
+
232
241
  - 80-95% bandwidth reduction
233
242
  - 304 Not Modified responses for unchanged files
234
243
  - Faster page loads for returning visitors
244
+ - Reduced server load
235
245
 
236
246
  **See details:** [HTTP Caching Optimization โ†’](./docs/OPTIMIZATION_HTTP_CACHING.md)
237
247
 
@@ -357,7 +367,8 @@ Creates a Koa middleware for serving static files.
357
367
  },
358
368
 
359
369
  // HTTP caching configuration
360
- enableCaching: true, // Enable ETag & Last-Modified (default: true)
370
+ // NOTE: Default is false for development. Set to true in production for better performance!
371
+ enableCaching: false, // Enable ETag & Last-Modified (default: false)
361
372
  cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 3600 = 1 hour)
362
373
  }
363
374
  ```
@@ -373,7 +384,7 @@ Creates a Koa middleware for serving static files.
373
384
  | `urlsReserved` | Array | `[]` | Reserved directory paths (first-level only) |
374
385
  | `template.render` | Function | `undefined` | Template rendering function |
375
386
  | `template.ext` | Array | `[]` | Extensions for template rendering |
376
- | `enableCaching` | Boolean | `true` | Enable HTTP caching headers |
387
+ | `enableCaching` | Boolean | `false` | Enable HTTP caching headers (recommended: `true` in production) |
377
388
  | `cacheMaxAge` | Number | `3600` | Cache duration in seconds |
378
389
 
379
390
  ---
@@ -0,0 +1,556 @@
1
+ /**
2
+ * HTTP Caching Headers Test
3
+ *
4
+ * Tests to verify correct caching behavior:
5
+ * - When enableCaching: true -> proper cache headers
6
+ * - When enableCaching: false -> anti-cache headers
7
+ */
8
+
9
+ const Koa = require('koa');
10
+ const supertest = require('supertest');
11
+ const koaClassicServer = require('../index.cjs');
12
+ const path = require('path');
13
+ const fs = require('fs');
14
+
15
+ const TEST_DIR = path.join(__dirname, 'test-caching-headers');
16
+
17
+ describe('HTTP Caching Headers', () => {
18
+ beforeAll(() => {
19
+ // Create test directory and file
20
+ if (!fs.existsSync(TEST_DIR)) {
21
+ fs.mkdirSync(TEST_DIR, { recursive: true });
22
+ }
23
+ fs.writeFileSync(path.join(TEST_DIR, 'test.txt'), 'Test content for caching');
24
+ });
25
+
26
+ afterAll(() => {
27
+ // Cleanup
28
+ if (fs.existsSync(TEST_DIR)) {
29
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
30
+ }
31
+ });
32
+
33
+ describe('When caching is DISABLED (enableCaching: false)', () => {
34
+ let app;
35
+ let server;
36
+ let request;
37
+
38
+ beforeAll(() => {
39
+ app = new Koa();
40
+ app.use(koaClassicServer(TEST_DIR, {
41
+ enableCaching: false
42
+ }));
43
+ server = app.listen();
44
+ request = supertest(server);
45
+ });
46
+
47
+ afterAll(() => {
48
+ server.close();
49
+ });
50
+
51
+ test('Should return anti-cache headers', async () => {
52
+ const res = await request.get('/test.txt');
53
+
54
+ expect(res.status).toBe(200);
55
+
56
+ // Verify anti-cache headers are present
57
+ expect(res.headers['cache-control']).toBe('no-cache, no-store, must-revalidate');
58
+ expect(res.headers['pragma']).toBe('no-cache');
59
+ expect(res.headers['expires']).toBe('0');
60
+
61
+ // Verify caching headers are NOT present
62
+ expect(res.headers['etag']).toBeUndefined();
63
+ expect(res.headers['last-modified']).toBeUndefined();
64
+ });
65
+
66
+ test('Should NOT return 304 even with If-None-Match header', async () => {
67
+ // First request to get potential ETag (should not exist)
68
+ const res1 = await request.get('/test.txt');
69
+ expect(res1.status).toBe(200);
70
+
71
+ // Second request with If-None-Match (should still return 200)
72
+ const res2 = await request
73
+ .get('/test.txt')
74
+ .set('If-None-Match', '"fake-etag"');
75
+
76
+ expect(res2.status).toBe(200);
77
+ expect(res2.text).toBe('Test content for caching');
78
+ });
79
+
80
+ test('Should NOT return 304 even with If-Modified-Since header', async () => {
81
+ const futureDate = new Date(Date.now() + 86400000).toUTCString();
82
+
83
+ const res = await request
84
+ .get('/test.txt')
85
+ .set('If-Modified-Since', futureDate);
86
+
87
+ expect(res.status).toBe(200);
88
+ expect(res.text).toBe('Test content for caching');
89
+ });
90
+ });
91
+
92
+ describe('When caching is ENABLED (enableCaching: true)', () => {
93
+ let app;
94
+ let server;
95
+ let request;
96
+
97
+ beforeAll(() => {
98
+ app = new Koa();
99
+ app.use(koaClassicServer(TEST_DIR, {
100
+ enableCaching: true,
101
+ cacheMaxAge: 3600
102
+ }));
103
+ server = app.listen();
104
+ request = supertest(server);
105
+ });
106
+
107
+ afterAll(() => {
108
+ server.close();
109
+ });
110
+
111
+ test('Should return proper cache headers', async () => {
112
+ const res = await request.get('/test.txt');
113
+
114
+ expect(res.status).toBe(200);
115
+
116
+ // Verify cache headers are present
117
+ expect(res.headers['cache-control']).toBe('public, max-age=3600, must-revalidate');
118
+ expect(res.headers['etag']).toBeDefined();
119
+ expect(res.headers['last-modified']).toBeDefined();
120
+
121
+ // Verify anti-cache headers are NOT present
122
+ expect(res.headers['pragma']).toBeUndefined();
123
+ expect(res.headers['expires']).not.toBe('0');
124
+ });
125
+
126
+ test('Should return 304 with matching ETag', async () => {
127
+ // First request to get ETag
128
+ const res1 = await request.get('/test.txt');
129
+ expect(res1.status).toBe(200);
130
+ const etag = res1.headers['etag'];
131
+ expect(etag).toBeDefined();
132
+
133
+ // Second request with If-None-Match
134
+ const res2 = await request
135
+ .get('/test.txt')
136
+ .set('If-None-Match', etag);
137
+
138
+ expect(res2.status).toBe(304);
139
+ expect(res2.text).toBe('');
140
+ });
141
+
142
+ test('Should return 304 with If-Modified-Since (not modified)', async () => {
143
+ // Get file stats and add 1 second to ensure it's after file mtime
144
+ const stats = fs.statSync(path.join(TEST_DIR, 'test.txt'));
145
+ const futureDate = new Date(stats.mtime.getTime() + 1000).toUTCString();
146
+
147
+ // Request with If-Modified-Since header (1 second in future)
148
+ const res = await request
149
+ .get('/test.txt')
150
+ .set('If-Modified-Since', futureDate);
151
+
152
+ expect(res.status).toBe(304);
153
+ expect(res.text).toBe('');
154
+ });
155
+
156
+ test('Should return 200 with If-Modified-Since (file modified)', async () => {
157
+ // Use a date in the past
158
+ const pastDate = new Date(Date.now() - 86400000).toUTCString();
159
+
160
+ const res = await request
161
+ .get('/test.txt')
162
+ .set('If-Modified-Since', pastDate);
163
+
164
+ expect(res.status).toBe(200);
165
+ expect(res.text).toBe('Test content for caching');
166
+ });
167
+ });
168
+
169
+ describe('Default behavior (caching disabled by default)', () => {
170
+ let app;
171
+ let server;
172
+ let request;
173
+
174
+ beforeAll(() => {
175
+ app = new Koa();
176
+ // No options provided - should default to enableCaching: false
177
+ app.use(koaClassicServer(TEST_DIR));
178
+ server = app.listen();
179
+ request = supertest(server);
180
+ });
181
+
182
+ afterAll(() => {
183
+ server.close();
184
+ });
185
+
186
+ test('Should have anti-cache headers by default', async () => {
187
+ const res = await request.get('/test.txt');
188
+
189
+ expect(res.status).toBe(200);
190
+ expect(res.headers['cache-control']).toBe('no-cache, no-store, must-revalidate');
191
+ expect(res.headers['pragma']).toBe('no-cache');
192
+ expect(res.headers['expires']).toBe('0');
193
+ });
194
+ });
195
+
196
+ describe('Custom cacheMaxAge values', () => {
197
+ test('Should respect custom cacheMaxAge: 7200', async () => {
198
+ const app = new Koa();
199
+ app.use(koaClassicServer(TEST_DIR, {
200
+ enableCaching: true,
201
+ cacheMaxAge: 7200
202
+ }));
203
+ const server = app.listen();
204
+ const request = supertest(server);
205
+
206
+ const res = await request.get('/test.txt');
207
+
208
+ expect(res.status).toBe(200);
209
+ expect(res.headers['cache-control']).toBe('public, max-age=7200, must-revalidate');
210
+
211
+ server.close();
212
+ });
213
+
214
+ test('Should respect custom cacheMaxAge: 0 (no browser cache)', async () => {
215
+ const app = new Koa();
216
+ app.use(koaClassicServer(TEST_DIR, {
217
+ enableCaching: true,
218
+ cacheMaxAge: 0
219
+ }));
220
+ const server = app.listen();
221
+ const request = supertest(server);
222
+
223
+ const res = await request.get('/test.txt');
224
+
225
+ expect(res.status).toBe(200);
226
+ expect(res.headers['cache-control']).toBe('public, max-age=0, must-revalidate');
227
+ // Should still have ETag for validation
228
+ expect(res.headers['etag']).toBeDefined();
229
+
230
+ server.close();
231
+ });
232
+
233
+ test('Should respect custom cacheMaxAge: 86400 (1 day)', async () => {
234
+ const app = new Koa();
235
+ app.use(koaClassicServer(TEST_DIR, {
236
+ enableCaching: true,
237
+ cacheMaxAge: 86400
238
+ }));
239
+ const server = app.listen();
240
+ const request = supertest(server);
241
+
242
+ const res = await request.get('/test.txt');
243
+
244
+ expect(res.status).toBe(200);
245
+ expect(res.headers['cache-control']).toBe('public, max-age=86400, must-revalidate');
246
+
247
+ server.close();
248
+ });
249
+ });
250
+
251
+ describe('ETag generation and validation', () => {
252
+ test('ETag should change when file is modified', async () => {
253
+ const testFile = path.join(TEST_DIR, 'dynamic-test.txt');
254
+ fs.writeFileSync(testFile, 'Original content');
255
+
256
+ const app = new Koa();
257
+ app.use(koaClassicServer(TEST_DIR, {
258
+ enableCaching: true
259
+ }));
260
+ const server = app.listen();
261
+ const request = supertest(server);
262
+
263
+ // First request
264
+ const res1 = await request.get('/dynamic-test.txt');
265
+ expect(res1.status).toBe(200);
266
+ const etag1 = res1.headers['etag'];
267
+ expect(etag1).toBeDefined();
268
+
269
+ // Wait 10ms to ensure different mtime
270
+ await new Promise(resolve => setTimeout(resolve, 10));
271
+
272
+ // Modify file
273
+ fs.writeFileSync(testFile, 'Modified content - different');
274
+
275
+ // Second request - ETag should be different
276
+ const res2 = await request.get('/dynamic-test.txt');
277
+ expect(res2.status).toBe(200);
278
+ const etag2 = res2.headers['etag'];
279
+ expect(etag2).toBeDefined();
280
+ expect(etag2).not.toBe(etag1);
281
+
282
+ // Cleanup
283
+ fs.unlinkSync(testFile);
284
+ server.close();
285
+ });
286
+
287
+ test('ETag should change when file size changes', async () => {
288
+ const testFile = path.join(TEST_DIR, 'size-test.txt');
289
+ fs.writeFileSync(testFile, 'Short');
290
+
291
+ const app = new Koa();
292
+ app.use(koaClassicServer(TEST_DIR, {
293
+ enableCaching: true
294
+ }));
295
+ const server = app.listen();
296
+ const request = supertest(server);
297
+
298
+ // First request
299
+ const res1 = await request.get('/size-test.txt');
300
+ const etag1 = res1.headers['etag'];
301
+
302
+ // Wait and change file size
303
+ await new Promise(resolve => setTimeout(resolve, 10));
304
+ fs.writeFileSync(testFile, 'Much longer content here');
305
+
306
+ // Second request
307
+ const res2 = await request.get('/size-test.txt');
308
+ const etag2 = res2.headers['etag'];
309
+
310
+ expect(etag2).not.toBe(etag1);
311
+
312
+ fs.unlinkSync(testFile);
313
+ server.close();
314
+ });
315
+ });
316
+
317
+ describe('Bandwidth savings with 304 responses', () => {
318
+ test('304 response should have no body', async () => {
319
+ const app = new Koa();
320
+ app.use(koaClassicServer(TEST_DIR, {
321
+ enableCaching: true
322
+ }));
323
+ const server = app.listen();
324
+ const request = supertest(server);
325
+
326
+ // First request
327
+ const res1 = await request.get('/test.txt');
328
+ expect(res1.status).toBe(200);
329
+ expect(res1.text).toBe('Test content for caching');
330
+ const bodySize1 = res1.text.length;
331
+
332
+ // Second request with ETag
333
+ const res2 = await request
334
+ .get('/test.txt')
335
+ .set('If-None-Match', res1.headers['etag']);
336
+
337
+ expect(res2.status).toBe(304);
338
+ expect(res2.text).toBe('');
339
+ expect(res2.text.length).toBe(0);
340
+
341
+ // Verify bandwidth saving
342
+ expect(bodySize1).toBeGreaterThan(0);
343
+ expect(res2.text.length).toBe(0);
344
+
345
+ server.close();
346
+ });
347
+
348
+ test('Should save bandwidth on multiple 304 responses', async () => {
349
+ const app = new Koa();
350
+ app.use(koaClassicServer(TEST_DIR, {
351
+ enableCaching: true
352
+ }));
353
+ const server = app.listen();
354
+ const request = supertest(server);
355
+
356
+ // First request
357
+ const res1 = await request.get('/test.txt');
358
+ const etag = res1.headers['etag'];
359
+ const originalSize = res1.text.length;
360
+
361
+ // Make 10 cached requests
362
+ let totalBytesSaved = 0;
363
+ for (let i = 0; i < 10; i++) {
364
+ const res = await request
365
+ .get('/test.txt')
366
+ .set('If-None-Match', etag);
367
+
368
+ expect(res.status).toBe(304);
369
+ totalBytesSaved += originalSize;
370
+ }
371
+
372
+ expect(totalBytesSaved).toBeGreaterThan(0);
373
+
374
+ server.close();
375
+ });
376
+ });
377
+
378
+ describe('Caching with different MIME types', () => {
379
+ beforeAll(() => {
380
+ // Create files with different types
381
+ fs.writeFileSync(path.join(TEST_DIR, 'test.html'), '<html><body>Test</body></html>');
382
+ fs.writeFileSync(path.join(TEST_DIR, 'test.json'), '{"test": "data"}');
383
+ fs.writeFileSync(path.join(TEST_DIR, 'test.css'), 'body { color: red; }');
384
+ fs.writeFileSync(path.join(TEST_DIR, 'test.js'), 'console.log("test");');
385
+ });
386
+
387
+ afterAll(() => {
388
+ fs.unlinkSync(path.join(TEST_DIR, 'test.html'));
389
+ fs.unlinkSync(path.join(TEST_DIR, 'test.json'));
390
+ fs.unlinkSync(path.join(TEST_DIR, 'test.css'));
391
+ fs.unlinkSync(path.join(TEST_DIR, 'test.js'));
392
+ });
393
+
394
+ test('HTML files should have cache headers', async () => {
395
+ const app = new Koa();
396
+ app.use(koaClassicServer(TEST_DIR, {
397
+ enableCaching: true
398
+ }));
399
+ const server = app.listen();
400
+ const request = supertest(server);
401
+
402
+ const res = await request.get('/test.html');
403
+ expect(res.status).toBe(200);
404
+ expect(res.headers['etag']).toBeDefined();
405
+ expect(res.headers['cache-control']).toContain('public');
406
+ expect(res.headers['content-type']).toContain('text/html');
407
+
408
+ server.close();
409
+ });
410
+
411
+ test('JSON files should have cache headers', async () => {
412
+ const app = new Koa();
413
+ app.use(koaClassicServer(TEST_DIR, {
414
+ enableCaching: true
415
+ }));
416
+ const server = app.listen();
417
+ const request = supertest(server);
418
+
419
+ const res = await request.get('/test.json');
420
+ expect(res.status).toBe(200);
421
+ expect(res.headers['etag']).toBeDefined();
422
+ expect(res.headers['content-type']).toContain('application/json');
423
+
424
+ server.close();
425
+ });
426
+
427
+ test('CSS files should have cache headers', async () => {
428
+ const app = new Koa();
429
+ app.use(koaClassicServer(TEST_DIR, {
430
+ enableCaching: true
431
+ }));
432
+ const server = app.listen();
433
+ const request = supertest(server);
434
+
435
+ const res = await request.get('/test.css');
436
+ expect(res.status).toBe(200);
437
+ expect(res.headers['etag']).toBeDefined();
438
+ expect(res.headers['content-type']).toContain('text/css');
439
+
440
+ server.close();
441
+ });
442
+
443
+ test('JavaScript files should have cache headers', async () => {
444
+ const app = new Koa();
445
+ app.use(koaClassicServer(TEST_DIR, {
446
+ enableCaching: true
447
+ }));
448
+ const server = app.listen();
449
+ const request = supertest(server);
450
+
451
+ const res = await request.get('/test.js');
452
+ expect(res.status).toBe(200);
453
+ expect(res.headers['etag']).toBeDefined();
454
+ expect(res.headers['content-type']).toContain('javascript');
455
+
456
+ server.close();
457
+ });
458
+ });
459
+
460
+ describe('Caching does not interfere with template rendering', () => {
461
+ test('Template files should not get cache headers during rendering', async () => {
462
+ const testFile = path.join(TEST_DIR, 'test-template.ejs');
463
+ fs.writeFileSync(testFile, '<html><body><%= name %></body></html>');
464
+
465
+ let renderCalled = false;
466
+
467
+ const app = new Koa();
468
+ app.use(koaClassicServer(TEST_DIR, {
469
+ enableCaching: true,
470
+ cacheMaxAge: 3600,
471
+ template: {
472
+ ext: ['ejs'],
473
+ render: async (ctx, next, filePath) => {
474
+ renderCalled = true;
475
+ ctx.body = '<html><body>Rendered</body></html>';
476
+ ctx.type = 'text/html';
477
+ }
478
+ }
479
+ }));
480
+ const server = app.listen();
481
+ const request = supertest(server);
482
+
483
+ const res = await request.get('/test-template.ejs');
484
+
485
+ expect(res.status).toBe(200);
486
+ expect(renderCalled).toBe(true);
487
+ expect(res.text).toBe('<html><body>Rendered</body></html>');
488
+
489
+ // Template rendering happens before caching logic,
490
+ // so cache headers should not be added by koaClassicServer
491
+ // (the template renderer controls caching)
492
+
493
+ fs.unlinkSync(testFile);
494
+ server.close();
495
+ });
496
+ });
497
+
498
+ describe('Concurrent requests with caching', () => {
499
+ test('Multiple concurrent requests should handle caching correctly', async () => {
500
+ const app = new Koa();
501
+ app.use(koaClassicServer(TEST_DIR, {
502
+ enableCaching: true
503
+ }));
504
+ const server = app.listen();
505
+ const request = supertest(server);
506
+
507
+ // Make 5 concurrent requests
508
+ const promises = Array.from({ length: 5 }, () =>
509
+ request.get('/test.txt')
510
+ );
511
+
512
+ const results = await Promise.all(promises);
513
+
514
+ // All should succeed
515
+ results.forEach(res => {
516
+ expect(res.status).toBe(200);
517
+ expect(res.headers['etag']).toBeDefined();
518
+ });
519
+
520
+ // All ETags should be identical (same file)
521
+ const etags = results.map(r => r.headers['etag']);
522
+ const uniqueEtags = new Set(etags);
523
+ expect(uniqueEtags.size).toBe(1);
524
+
525
+ server.close();
526
+ });
527
+
528
+ test('Concurrent 304 responses should work correctly', async () => {
529
+ const app = new Koa();
530
+ app.use(koaClassicServer(TEST_DIR, {
531
+ enableCaching: true
532
+ }));
533
+ const server = app.listen();
534
+ const request = supertest(server);
535
+
536
+ // First request to get ETag
537
+ const initial = await request.get('/test.txt');
538
+ const etag = initial.headers['etag'];
539
+
540
+ // Make 5 concurrent cached requests
541
+ const promises = Array.from({ length: 5 }, () =>
542
+ request.get('/test.txt').set('If-None-Match', etag)
543
+ );
544
+
545
+ const results = await Promise.all(promises);
546
+
547
+ // All should return 304
548
+ results.forEach(res => {
549
+ expect(res.status).toBe(304);
550
+ expect(res.text).toBe('');
551
+ });
552
+
553
+ server.close();
554
+ });
555
+ });
556
+ });
package/docs/CHANGELOG.md CHANGED
@@ -5,6 +5,84 @@ All notable changes to koa-classic-server will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.3] - 2025-11-25
9
+
10
+ ### ๐Ÿ”ง Configuration Changes
11
+
12
+ #### Changed Default Caching Behavior
13
+ - **Change**: `enableCaching` default value changed from `true` to `false`
14
+ - **Rationale**: Better development experience - changes are immediately visible without cache invalidation
15
+ - **Production Impact**: **Users should explicitly set `enableCaching: true` in production environments**
16
+ - **Benefits in Production**:
17
+ - 80-95% bandwidth reduction
18
+ - Faster page loads with 304 Not Modified responses
19
+ - Reduced server load
20
+ - **Code**: `index.cjs:107`
21
+
22
+ ### ๐Ÿ“ Documentation Improvements
23
+
24
+ #### Enhanced Caching Documentation
25
+ - Added comprehensive production recommendations in README.md
26
+ - Added inline code comments explaining the default behavior
27
+ - Clear guidance on when to enable caching (development vs production)
28
+ - **Files**: `README.md`, `index.cjs`
29
+
30
+ ### โš ๏ธ Migration Notice
31
+
32
+ **IMPORTANT**: If you are upgrading from 2.1.2 or earlier and rely on HTTP caching:
33
+
34
+ ```javascript
35
+ // You must now explicitly enable caching in production
36
+ app.use(koaClassicServer(__dirname + '/public', {
37
+ enableCaching: true // โ† Add this for production environments
38
+ }));
39
+ ```
40
+
41
+ **Development**: No changes needed - the new default (`false`) is better for development.
42
+
43
+ **Production**: Explicitly set `enableCaching: true` to maintain previous behavior and performance benefits.
44
+
45
+ ### ๐Ÿ“ฆ Package Changes
46
+
47
+ - **Version**: `2.1.2` โ†’ `2.1.3`
48
+
49
+ ---
50
+
51
+ ## [2.1.2] - 2025-11-24
52
+
53
+ ### ๐ŸŽจ Features
54
+
55
+ #### Sortable Directory Columns
56
+ - Apache2-like directory listing with clickable column headers
57
+ - Sort by Name, Type, or Size (ascending/descending)
58
+ - Fixed navigation bug after sorting
59
+
60
+ #### File Size Display
61
+ - Human-readable file sizes (B, KB, MB, GB, TB)
62
+ - Proper formatting and precision
63
+
64
+ #### HTTP Caching
65
+ - ETag and Last-Modified headers
66
+ - 304 Not Modified responses
67
+ - 80-95% bandwidth reduction
68
+
69
+ ### ๐Ÿงช Testing
70
+ - 153 tests passing
71
+ - Comprehensive test coverage
72
+
73
+ ---
74
+
75
+ ## [2.1.1] - 2025-11-23
76
+
77
+ ### ๐Ÿš€ Production Release
78
+
79
+ - Async/await implementation
80
+ - Non-blocking I/O
81
+ - Performance optimizations
82
+ - Flow documentation
83
+
84
+ ---
85
+
8
86
  ## [1.2.0] - 2025-11-17
9
87
 
10
88
  ### ๐ŸŽ‰ SECURITY & BUG FIX RELEASE
package/index.cjs CHANGED
@@ -45,7 +45,10 @@ module.exports = function koaClassicServer(
45
45
  ext: [], // File extensions to process with template.render
46
46
  },
47
47
  cacheMaxAge: 3600, // Cache-Control max-age in seconds (default: 1 hour)
48
- enableCaching: true, // Enable HTTP caching headers (ETag, Last-Modified)
48
+ enableCaching: false, // Enable HTTP caching headers (ETag, Last-Modified)
49
+ // NOTE: Default is false for development.
50
+ // In production, it's recommended to set enableCaching: true
51
+ // to reduce bandwidth usage and improve performance.
49
52
  }
50
53
  */
51
54
  ) {
@@ -97,8 +100,11 @@ module.exports = function koaClassicServer(
97
100
  options.template.ext = Array.isArray(options.template.ext) ? options.template.ext : [];
98
101
 
99
102
  // OPTIMIZATION: HTTP Caching options
103
+ // NOTE: Default enableCaching is false for development environments.
104
+ // For production deployments, it's strongly recommended to enable caching
105
+ // by setting enableCaching: true to benefit from reduced bandwidth and improved performance.
100
106
  options.cacheMaxAge = typeof options.cacheMaxAge === 'number' && options.cacheMaxAge >= 0 ? options.cacheMaxAge : 3600;
101
- options.enableCaching = typeof options.enableCaching === 'boolean' ? options.enableCaching : true;
107
+ options.enableCaching = typeof options.enableCaching === 'boolean' ? options.enableCaching : false;
102
108
 
103
109
  return async (ctx, next) => {
104
110
  // Check if method is allowed
@@ -347,6 +353,12 @@ module.exports = function koaClassicServer(
347
353
  return;
348
354
  }
349
355
  }
356
+ } else {
357
+ // BUGFIX: When caching is disabled, explicitly prevent browser caching
358
+ // Without these headers, browsers may use heuristic caching and serve stale content
359
+ ctx.set('Cache-Control', 'no-cache, no-store, must-revalidate');
360
+ ctx.set('Pragma', 'no-cache'); // HTTP 1.0 compatibility
361
+ ctx.set('Expires', '0'); // Proxies
350
362
  }
351
363
 
352
364
  // Verify file is still readable (race condition protection)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koa-classic-server",
3
- "version": "2.1.2",
3
+ "version": "2.1.4",
4
4
  "description": "High-performance Koa middleware for serving static files with Apache-like directory listing, HTTP caching, template engine support, and comprehensive security fixes",
5
5
  "main": "index.cjs",
6
6
  "exports": {
@@ -29,6 +29,10 @@
29
29
  ],
30
30
  "author": "Italo Paesano",
31
31
  "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "https://github.com/italopaesano/koa-classic-server"
35
+ },
32
36
  "dependencies": {
33
37
  "mime-types": "^2.1.35"
34
38
  },