koa-classic-server 2.6.1 → 3.0.0
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/CLAUDE.md +101 -0
- package/README.md +564 -591
- package/__tests__/benchmark-results-v3.0.0.txt +372 -0
- package/__tests__/benchmark.js +1 -1
- package/__tests__/caching-headers.test.js +30 -30
- package/__tests__/compression-fixtures/data.json +1 -0
- package/__tests__/compression-fixtures/large.txt +1 -0
- package/__tests__/compression-fixtures/small.txt +1 -0
- package/__tests__/compression.test.js +284 -0
- package/__tests__/customTest/serversToLoad.util.js +5 -5
- package/__tests__/demo-regex-index.js +4 -4
- package/__tests__/deprecation-warnings.test.js +71 -183
- package/__tests__/directory-sorting-links.test.js +1 -1
- package/__tests__/dt-unknown.test.js +39 -28
- package/__tests__/ejs.test.js +1 -1
- package/__tests__/hidden-fixtures/.dot-dir/inside.txt +1 -0
- package/__tests__/hidden-fixtures/.env +2 -0
- package/__tests__/hidden-fixtures/.well-known/acme-challenge.txt +1 -0
- package/__tests__/hidden-fixtures/config/secrets/password.txt +1 -0
- package/__tests__/hidden-fixtures/data.key +1 -0
- package/__tests__/hidden-fixtures/file.secret +1 -0
- package/__tests__/hidden-fixtures/index.html +1 -0
- package/__tests__/hidden-fixtures/normal.txt +1 -0
- package/__tests__/hidden-fixtures/subdir/.env +1 -0
- package/__tests__/hidden-fixtures/subdir/regular.txt +1 -0
- package/__tests__/hidden-option.test.js +407 -0
- package/__tests__/hideExtension.test.js +70 -13
- package/__tests__/index-option.test.js +18 -16
- package/__tests__/index.test.js +14 -10
- package/__tests__/listing.test.js +437 -0
- package/__tests__/logger.test.js +232 -0
- package/__tests__/range-fixtures/sample.txt +1 -0
- package/__tests__/range.test.js +223 -0
- package/__tests__/security-headers.test.js +165 -0
- package/__tests__/security.test.js +148 -162
- package/__tests__/server-cache-fixtures/large.txt +1 -0
- package/__tests__/server-cache-fixtures/small.txt +1 -0
- package/__tests__/server-cache.test.js +594 -0
- package/__tests__/symlink.test.js +18 -15
- package/__tests__/template-timeout.test.js +321 -0
- package/docs/ACTION_PLAN.md +293 -0
- package/docs/CHANGELOG.md +289 -0
- package/docs/CODE_REVIEW.md +2 -0
- package/docs/DOCUMENTATION.md +259 -32
- package/docs/EXAMPLES_INDEX_OPTION.md +3 -3
- package/docs/FLOW_DIAGRAM.md +15 -13
- package/docs/INDEX_OPTION_PRIORITY.md +2 -2
- package/docs/OPTIMIZATION_HTTP_CACHING.md +2 -0
- package/docs/OPTIMIZATION_ROADMAP_for_V3.md +864 -0
- package/docs/PERFORMANCE_COMPARISON.md +7 -7
- package/docs/security_improvement_for_V3.md +421 -0
- package/docs/template-engine/TEMPLATE_ENGINE_GUIDE.md +5 -5
- package/docs/template-engine/esempi-incrementali.js +1 -1
- package/eslint.config.mjs +17 -0
- package/index.cjs +1507 -429
- package/index.mjs +1 -5
- package/package.json +9 -1
|
@@ -124,7 +124,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
124
124
|
const app = new Koa();
|
|
125
125
|
app.use(koaClassicServer(tmpDir, {
|
|
126
126
|
index: ['index.html'],
|
|
127
|
-
|
|
127
|
+
dirListing: { enabled: true }
|
|
128
128
|
}));
|
|
129
129
|
server = app.listen();
|
|
130
130
|
request = supertest(server);
|
|
@@ -150,7 +150,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
150
150
|
const app = new Koa();
|
|
151
151
|
app.use(koaClassicServer(tmpDir, {
|
|
152
152
|
index: ['index.ejs'],
|
|
153
|
-
|
|
153
|
+
dirListing: { enabled: true }
|
|
154
154
|
}));
|
|
155
155
|
server = app.listen();
|
|
156
156
|
request = supertest(server);
|
|
@@ -161,8 +161,9 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
161
161
|
test('GET / serves symlinked index.ejs, not directory listing', async () => {
|
|
162
162
|
const res = await request.get('/');
|
|
163
163
|
expect(res.status).toBe(200);
|
|
164
|
-
|
|
165
|
-
expect(
|
|
164
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
165
|
+
expect(body).toContain('EJS via Symlink');
|
|
166
|
+
expect(body).not.toContain('Index of');
|
|
166
167
|
});
|
|
167
168
|
});
|
|
168
169
|
|
|
@@ -176,7 +177,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
176
177
|
const app = new Koa();
|
|
177
178
|
app.use(koaClassicServer(tmpDir, {
|
|
178
179
|
index: [],
|
|
179
|
-
|
|
180
|
+
dirListing: { enabled: true }
|
|
180
181
|
}));
|
|
181
182
|
server = app.listen();
|
|
182
183
|
request = supertest(server);
|
|
@@ -187,7 +188,8 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
187
188
|
test('GET /index.ejs via symlink returns 200', async () => {
|
|
188
189
|
const res = await request.get('/index.ejs');
|
|
189
190
|
expect(res.status).toBe(200);
|
|
190
|
-
|
|
191
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
192
|
+
expect(body).toContain('EJS via Symlink');
|
|
191
193
|
});
|
|
192
194
|
|
|
193
195
|
test('GET /linked-style.css via symlink returns 200 with correct mime', async () => {
|
|
@@ -208,7 +210,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
208
210
|
const app = new Koa();
|
|
209
211
|
app.use(koaClassicServer(tmpDir, {
|
|
210
212
|
index: ['index.ejs'],
|
|
211
|
-
|
|
213
|
+
dirListing: { enabled: true },
|
|
212
214
|
template: {
|
|
213
215
|
ext: ['ejs'],
|
|
214
216
|
render: async (ctx, next, filePath) => {
|
|
@@ -242,7 +244,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
242
244
|
const app = new Koa();
|
|
243
245
|
app.use(koaClassicServer(tmpDir, {
|
|
244
246
|
index: [],
|
|
245
|
-
|
|
247
|
+
dirListing: { enabled: true }
|
|
246
248
|
}));
|
|
247
249
|
server = app.listen();
|
|
248
250
|
request = supertest(server);
|
|
@@ -273,7 +275,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
273
275
|
const app = new Koa();
|
|
274
276
|
app.use(koaClassicServer(tmpDir, {
|
|
275
277
|
index: [],
|
|
276
|
-
|
|
278
|
+
dirListing: { enabled: true }
|
|
277
279
|
}));
|
|
278
280
|
server = app.listen();
|
|
279
281
|
request = supertest(server);
|
|
@@ -299,7 +301,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
299
301
|
const app = new Koa();
|
|
300
302
|
app.use(koaClassicServer(tmpDir, {
|
|
301
303
|
index: [],
|
|
302
|
-
|
|
304
|
+
dirListing: { enabled: true }
|
|
303
305
|
}));
|
|
304
306
|
server = app.listen();
|
|
305
307
|
request = supertest(server);
|
|
@@ -328,7 +330,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
328
330
|
const app = new Koa();
|
|
329
331
|
app.use(koaClassicServer(tmpDir, {
|
|
330
332
|
index: ['index.html'],
|
|
331
|
-
|
|
333
|
+
dirListing: { enabled: true }
|
|
332
334
|
}));
|
|
333
335
|
server = app.listen();
|
|
334
336
|
request = supertest(server);
|
|
@@ -354,7 +356,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
354
356
|
const app = new Koa();
|
|
355
357
|
app.use(koaClassicServer(tmpDir, {
|
|
356
358
|
index: [],
|
|
357
|
-
|
|
359
|
+
dirListing: { enabled: true }
|
|
358
360
|
}));
|
|
359
361
|
server = app.listen();
|
|
360
362
|
request = supertest(server);
|
|
@@ -420,7 +422,7 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
420
422
|
const app = new Koa();
|
|
421
423
|
app.use(koaClassicServer(tmpDir, {
|
|
422
424
|
index: [/index\.[eE][jJ][sS]/],
|
|
423
|
-
|
|
425
|
+
dirListing: { enabled: true }
|
|
424
426
|
}));
|
|
425
427
|
server = app.listen();
|
|
426
428
|
request = supertest(server);
|
|
@@ -431,8 +433,9 @@ describeIfSymlinks('koa-classic-server - symlink support', () => {
|
|
|
431
433
|
test('GET / finds symlinked index.ejs via RegExp pattern', async () => {
|
|
432
434
|
const res = await request.get('/');
|
|
433
435
|
expect(res.status).toBe(200);
|
|
434
|
-
|
|
435
|
-
expect(
|
|
436
|
+
const body = res.text !== undefined ? res.text : res.body.toString('utf8');
|
|
437
|
+
expect(body).toContain('EJS via Symlink');
|
|
438
|
+
expect(body).not.toContain('Index of');
|
|
436
439
|
});
|
|
437
440
|
});
|
|
438
441
|
});
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const Koa = require('koa');
|
|
2
|
+
const koaClassicServer = require('../index.cjs');
|
|
3
|
+
const supertest = require('supertest');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const ROOT = path.join(__dirname, 'publicWwwTest');
|
|
7
|
+
const TEMPLATE_URL = '/ejs-templates/simple.ejs';
|
|
8
|
+
|
|
9
|
+
function buildServer(renderFn, opts = {}) {
|
|
10
|
+
const app = new Koa();
|
|
11
|
+
app.silent = true;
|
|
12
|
+
app.use(koaClassicServer(ROOT, {
|
|
13
|
+
method: ['GET'],
|
|
14
|
+
hidden: { dotFiles: { default: 'hidden' }, dotDirs: { default: 'visible' } },
|
|
15
|
+
template: {
|
|
16
|
+
ext: ['ejs'],
|
|
17
|
+
render: renderFn,
|
|
18
|
+
...(opts.renderTimeout !== undefined ? { renderTimeout: opts.renderTimeout } : {})
|
|
19
|
+
}
|
|
20
|
+
}));
|
|
21
|
+
const server = app.listen();
|
|
22
|
+
return { server, request: supertest(server) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Sleep that does not keep the event loop alive — used to simulate
|
|
26
|
+
// non-cooperative long-running work without leaving orphan timers behind.
|
|
27
|
+
function unrefSleep(ms) {
|
|
28
|
+
return new Promise(resolve => {
|
|
29
|
+
const t = setTimeout(resolve, ms);
|
|
30
|
+
if (typeof t.unref === 'function') t.unref();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe('template.renderTimeout', () => {
|
|
35
|
+
let consoleErrorSpy;
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
consoleErrorSpy.mockRestore();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('Factory validation', () => {
|
|
46
|
+
test('rejects negative renderTimeout', () => {
|
|
47
|
+
expect(() => koaClassicServer(ROOT, {
|
|
48
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: -1 }
|
|
49
|
+
})).toThrow(/renderTimeout must be a finite number/);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('rejects non-number renderTimeout', () => {
|
|
53
|
+
expect(() => koaClassicServer(ROOT, {
|
|
54
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: '5000' }
|
|
55
|
+
})).toThrow(/renderTimeout must be a finite number/);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('rejects NaN renderTimeout', () => {
|
|
59
|
+
expect(() => koaClassicServer(ROOT, {
|
|
60
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: NaN }
|
|
61
|
+
})).toThrow(/renderTimeout must be a finite number/);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('rejects Infinity renderTimeout', () => {
|
|
65
|
+
expect(() => koaClassicServer(ROOT, {
|
|
66
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: Infinity }
|
|
67
|
+
})).toThrow(/renderTimeout must be a finite number/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('accepts 0 (disabled)', () => {
|
|
71
|
+
expect(() => koaClassicServer(ROOT, {
|
|
72
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: 0 }
|
|
73
|
+
})).not.toThrow();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('accepts positive integer', () => {
|
|
77
|
+
expect(() => koaClassicServer(ROOT, {
|
|
78
|
+
template: { ext: ['ejs'], render: () => {}, renderTimeout: 1000 }
|
|
79
|
+
})).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('defaults to 30000 when undefined', () => {
|
|
83
|
+
const opts = { template: { ext: ['ejs'], render: () => {} } };
|
|
84
|
+
koaClassicServer(ROOT, opts);
|
|
85
|
+
expect(opts.template.renderTimeout).toBe(30000);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('Successful render within timeout', () => {
|
|
90
|
+
let env;
|
|
91
|
+
afterEach(() => env && env.server.close());
|
|
92
|
+
|
|
93
|
+
test('returns 200 when render completes before timeout', async () => {
|
|
94
|
+
env = buildServer(async (ctx) => {
|
|
95
|
+
await new Promise(r => setTimeout(r, 20));
|
|
96
|
+
ctx.type = 'text/html';
|
|
97
|
+
ctx.body = '<p>ok</p>';
|
|
98
|
+
}, { renderTimeout: 500 });
|
|
99
|
+
|
|
100
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
101
|
+
expect(res.status).toBe(200);
|
|
102
|
+
expect(res.text).toContain('ok');
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('Timeout behaviour', () => {
|
|
107
|
+
let env;
|
|
108
|
+
afterEach(() => env && env.server.close());
|
|
109
|
+
|
|
110
|
+
test('returns 504 when render exceeds timeout', async () => {
|
|
111
|
+
env = buildServer(async () => {
|
|
112
|
+
await unrefSleep(5000);
|
|
113
|
+
}, { renderTimeout: 100 });
|
|
114
|
+
|
|
115
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
116
|
+
expect(res.status).toBe(504);
|
|
117
|
+
expect(res.text).toContain('Gateway Timeout');
|
|
118
|
+
expect(res.text).toContain('took too long to render');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('504 response carries security headers', async () => {
|
|
122
|
+
env = buildServer(async () => {
|
|
123
|
+
await unrefSleep(5000);
|
|
124
|
+
}, { renderTimeout: 50 });
|
|
125
|
+
|
|
126
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
127
|
+
expect(res.status).toBe(504);
|
|
128
|
+
expect(res.headers['content-security-policy']).toMatch(/default-src 'none'/);
|
|
129
|
+
expect(res.headers['x-content-type-options']).toBe('nosniff');
|
|
130
|
+
expect(res.headers['x-frame-options']).toBe('DENY');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('renderTimeout: 0 disables the timer (render is allowed to run long)', async () => {
|
|
134
|
+
env = buildServer(async (ctx) => {
|
|
135
|
+
await new Promise(r => setTimeout(r, 200));
|
|
136
|
+
ctx.type = 'text/html';
|
|
137
|
+
ctx.body = '<p>slow but ok</p>';
|
|
138
|
+
}, { renderTimeout: 0 });
|
|
139
|
+
|
|
140
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
141
|
+
expect(res.status).toBe(200);
|
|
142
|
+
expect(res.text).toContain('slow but ok');
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
describe('Render argument contract', () => {
|
|
147
|
+
let env;
|
|
148
|
+
afterEach(() => env && env.server.close());
|
|
149
|
+
|
|
150
|
+
test('render is called with (ctx, next, filePath, rawBuffer, signal) in that order', async () => {
|
|
151
|
+
let received;
|
|
152
|
+
env = buildServer(async (...args) => {
|
|
153
|
+
received = args;
|
|
154
|
+
args[0].body = 'ok';
|
|
155
|
+
}, { renderTimeout: 1000 });
|
|
156
|
+
|
|
157
|
+
await env.request.get(TEMPLATE_URL);
|
|
158
|
+
|
|
159
|
+
expect(received).toHaveLength(5);
|
|
160
|
+
// ctx: Koa context — must expose req/res/state
|
|
161
|
+
expect(received[0]).toBeDefined();
|
|
162
|
+
expect(received[0].req).toBeDefined();
|
|
163
|
+
expect(received[0].res).toBeDefined();
|
|
164
|
+
expect(received[0].state).toBeDefined();
|
|
165
|
+
// next: function (downstream middleware)
|
|
166
|
+
expect(typeof received[1]).toBe('function');
|
|
167
|
+
// filePath: absolute path to the requested file
|
|
168
|
+
expect(typeof received[2]).toBe('string');
|
|
169
|
+
expect(path.isAbsolute(received[2])).toBe(true);
|
|
170
|
+
expect(received[2]).toMatch(/simple\.ejs$/);
|
|
171
|
+
// rawBuffer: Buffer or null depending on serverCache.rawFile state
|
|
172
|
+
expect(received[3] === null || Buffer.isBuffer(received[3])).toBe(true);
|
|
173
|
+
// signal: AbortSignal
|
|
174
|
+
expect(received[4]).toBeInstanceOf(AbortSignal);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('AbortSignal contract', () => {
|
|
179
|
+
let env;
|
|
180
|
+
afterEach(() => env && env.server.close());
|
|
181
|
+
|
|
182
|
+
test('passes an AbortSignal as 5th argument', async () => {
|
|
183
|
+
let receivedSignal;
|
|
184
|
+
env = buildServer(async (ctx, _next, _path, _buf, signal) => {
|
|
185
|
+
receivedSignal = signal;
|
|
186
|
+
ctx.body = 'ok';
|
|
187
|
+
}, { renderTimeout: 1000 });
|
|
188
|
+
|
|
189
|
+
await env.request.get(TEMPLATE_URL);
|
|
190
|
+
expect(receivedSignal).toBeInstanceOf(AbortSignal);
|
|
191
|
+
expect(receivedSignal.aborted).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('signal is NOT aborted after a successful render completes', async () => {
|
|
195
|
+
let signalAtEnd;
|
|
196
|
+
let signalRef;
|
|
197
|
+
env = buildServer(async (ctx, _next, _path, _buf, signal) => {
|
|
198
|
+
signalRef = signal;
|
|
199
|
+
await new Promise(r => setTimeout(r, 20));
|
|
200
|
+
signalAtEnd = signal.aborted;
|
|
201
|
+
ctx.type = 'text/html';
|
|
202
|
+
ctx.body = '<p>done</p>';
|
|
203
|
+
}, { renderTimeout: 1000 });
|
|
204
|
+
|
|
205
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
206
|
+
expect(res.status).toBe(200);
|
|
207
|
+
// Signal must not be aborted at the end of a successful render
|
|
208
|
+
expect(signalAtEnd).toBe(false);
|
|
209
|
+
// ...and must remain non-aborted after the handler finishes
|
|
210
|
+
// (i.e. our cleanup must not abort it as a side-effect)
|
|
211
|
+
await new Promise(r => setTimeout(r, 50));
|
|
212
|
+
expect(signalRef.aborted).toBe(false);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test('signal aborts when render times out', async () => {
|
|
216
|
+
let signalAtTimeout = null;
|
|
217
|
+
env = buildServer(async (ctx, _next, _path, _buf, signal) => {
|
|
218
|
+
await unrefSleep(300);
|
|
219
|
+
signalAtTimeout = signal.aborted;
|
|
220
|
+
}, { renderTimeout: 50 });
|
|
221
|
+
|
|
222
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
223
|
+
expect(res.status).toBe(504);
|
|
224
|
+
await new Promise(r => setTimeout(r, 400));
|
|
225
|
+
expect(signalAtTimeout).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('cooperative render that honours signal terminates early', async () => {
|
|
229
|
+
const renderDuration = jest.fn();
|
|
230
|
+
env = buildServer(async (ctx, _next, _path, _buf, signal) => {
|
|
231
|
+
const start = Date.now();
|
|
232
|
+
try {
|
|
233
|
+
await new Promise((resolve, reject) => {
|
|
234
|
+
const t = setTimeout(resolve, 5000);
|
|
235
|
+
signal.addEventListener('abort', () => {
|
|
236
|
+
clearTimeout(t);
|
|
237
|
+
reject(new Error('aborted'));
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
} finally {
|
|
241
|
+
renderDuration(Date.now() - start);
|
|
242
|
+
}
|
|
243
|
+
}, { renderTimeout: 50 });
|
|
244
|
+
|
|
245
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
246
|
+
expect(res.status).toBe(504);
|
|
247
|
+
await new Promise(r => setTimeout(r, 200));
|
|
248
|
+
const elapsed = renderDuration.mock.calls[0]?.[0] ?? 9999;
|
|
249
|
+
expect(elapsed).toBeLessThan(1000);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('Error handling integrity', () => {
|
|
254
|
+
let env;
|
|
255
|
+
afterEach(() => env && env.server.close());
|
|
256
|
+
|
|
257
|
+
test('late rejection after timeout does not crash the process', async () => {
|
|
258
|
+
const unhandledHandler = jest.fn();
|
|
259
|
+
process.on('unhandledRejection', unhandledHandler);
|
|
260
|
+
|
|
261
|
+
env = buildServer(async () => {
|
|
262
|
+
await new Promise(r => setTimeout(r, 100));
|
|
263
|
+
throw new Error('late failure');
|
|
264
|
+
}, { renderTimeout: 30 });
|
|
265
|
+
|
|
266
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
267
|
+
expect(res.status).toBe(504);
|
|
268
|
+
|
|
269
|
+
await new Promise(r => setTimeout(r, 250));
|
|
270
|
+
process.off('unhandledRejection', unhandledHandler);
|
|
271
|
+
expect(unhandledHandler).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test('synchronous render returning rejected promise still produces 500', async () => {
|
|
275
|
+
env = buildServer(() => Promise.reject(new Error('boom')), { renderTimeout: 1000 });
|
|
276
|
+
|
|
277
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
278
|
+
expect(res.status).toBe(500);
|
|
279
|
+
expect(res.text).toContain('Internal Server Error');
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test('synchronous throw in render produces 500', async () => {
|
|
283
|
+
env = buildServer(() => { throw new Error('sync boom'); }, { renderTimeout: 1000 });
|
|
284
|
+
|
|
285
|
+
const res = await env.request.get(TEMPLATE_URL);
|
|
286
|
+
expect(res.status).toBe(500);
|
|
287
|
+
expect(res.text).toContain('Internal Server Error');
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
describe('Client disconnect', () => {
|
|
292
|
+
let env;
|
|
293
|
+
afterEach(() => env && env.server.close());
|
|
294
|
+
|
|
295
|
+
test('signal aborts when client closes the connection before render finishes', async () => {
|
|
296
|
+
let abortObserved = false;
|
|
297
|
+
env = buildServer(async (ctx, _next, _path, _buf, signal) => {
|
|
298
|
+
signal.addEventListener('abort', () => { abortObserved = true; });
|
|
299
|
+
await unrefSleep(1000);
|
|
300
|
+
ctx.body = 'late';
|
|
301
|
+
}, { renderTimeout: 0 });
|
|
302
|
+
|
|
303
|
+
const addr = env.server.address();
|
|
304
|
+
const http = require('http');
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
const req = http.request({
|
|
307
|
+
host: addr.address === '::' ? '127.0.0.1' : addr.address,
|
|
308
|
+
port: addr.port,
|
|
309
|
+
path: TEMPLATE_URL,
|
|
310
|
+
method: 'GET'
|
|
311
|
+
}, () => {});
|
|
312
|
+
req.on('error', () => {});
|
|
313
|
+
req.end();
|
|
314
|
+
setTimeout(() => { req.destroy(); resolve(); }, 50);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
await new Promise(r => setTimeout(r, 100));
|
|
318
|
+
expect(abortObserved).toBe(true);
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
});
|