koa-classic-server 2.1.3 → 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.
- package/__tests__/caching-headers.test.js +556 -0
- package/index.cjs +6 -0
- package/package.json +1 -1
|
@@ -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/index.cjs
CHANGED
|
@@ -353,6 +353,12 @@ module.exports = function koaClassicServer(
|
|
|
353
353
|
return;
|
|
354
354
|
}
|
|
355
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
|
|
356
362
|
}
|
|
357
363
|
|
|
358
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.
|
|
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": {
|