Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
1// See: https://github.com/remix-run/web-std-io/blob/7a8596e/packages/fetch/test/utils/server.js
2
3import http from 'http';
4import zlib from 'zlib';
5import Busboy from 'busboy';
6import {once} from 'events';
7
8export default class TestServer {
9 constructor() {
10 this.server = http.createServer(this.router);
11 // Node 8 default keepalive timeout is 5000ms
12 // make it shorter here as we want to close server quickly at the end of tests
13 this.server.keepAliveTimeout = 1000;
14 this.server.on('error', err => {
15 console.log(err.stack);
16 });
17 this.server.on('connection', socket => {
18 socket.setTimeout(1500);
19 });
20 }
21
22 async start() {
23 this.server.listen(0, 'localhost');
24 return once(this.server, 'listening');
25 }
26
27 async stop() {
28 this.server.close();
29 return once(this.server, 'close');
30 }
31
32 get port() {
33 return this.server.address().port;
34 }
35
36 get hostname() {
37 return 'localhost';
38 }
39
40 mockResponse(responseHandler) {
41 this.server.nextResponseHandler = responseHandler;
42 return `http://${this.hostname}:${this.port}/mocked`;
43 }
44
45 router(request, res) {
46 const p = request.url;
47
48 if (p === '/mocked') {
49 if (this.nextResponseHandler) {
50 this.nextResponseHandler(res);
51 this.nextResponseHandler = undefined;
52 } else {
53 throw new Error('No mocked response. Use ’TestServer.mockResponse()’.');
54 }
55 }
56
57 if (p === '/hello') {
58 res.statusCode = 200;
59 res.setHeader('Content-Type', 'text/plain');
60 res.end('world');
61 }
62
63 if (p.includes('question')) {
64 res.statusCode = 200;
65 res.setHeader('Content-Type', 'text/plain');
66 res.end('ok');
67 }
68
69 if (p === '/plain') {
70 res.statusCode = 200;
71 res.setHeader('Content-Type', 'text/plain');
72 res.end('text');
73 }
74
75 if (p === '/no-status-text') {
76 res.writeHead(200, '', {}).end();
77 }
78
79 if (p === '/options') {
80 res.statusCode = 200;
81 res.setHeader('Allow', 'GET, HEAD, OPTIONS');
82 res.end('hello world');
83 }
84
85 if (p === '/html') {
86 res.statusCode = 200;
87 res.setHeader('Content-Type', 'text/html');
88 res.end('<html></html>');
89 }
90
91 if (p === '/json') {
92 res.statusCode = 200;
93 res.setHeader('Content-Type', 'application/json');
94 res.end(JSON.stringify({
95 name: 'value'
96 }));
97 }
98
99 if (p === '/gzip') {
100 res.statusCode = 200;
101 res.setHeader('Content-Type', 'text/plain');
102 res.setHeader('Content-Encoding', 'gzip');
103 zlib.gzip('hello world', (err, buffer) => {
104 if (err) {
105 throw err;
106 }
107
108 res.end(buffer);
109 });
110 }
111
112 if (p === '/gzip-truncated') {
113 res.statusCode = 200;
114 res.setHeader('Content-Type', 'text/plain');
115 res.setHeader('Content-Encoding', 'gzip');
116 zlib.gzip('hello world', (err, buffer) => {
117 if (err) {
118 throw err;
119 }
120
121 // Truncate the CRC checksum and size check at the end of the stream
122 res.end(buffer.slice(0, -8));
123 });
124 }
125
126 if (p === '/gzip-capital') {
127 res.statusCode = 200;
128 res.setHeader('Content-Type', 'text/plain');
129 res.setHeader('Content-Encoding', 'GZip');
130 zlib.gzip('hello world', (err, buffer) => {
131 if (err) {
132 throw err;
133 }
134
135 res.end(buffer);
136 });
137 }
138
139 if (p === '/deflate') {
140 res.statusCode = 200;
141 res.setHeader('Content-Type', 'text/plain');
142 res.setHeader('Content-Encoding', 'deflate');
143 zlib.deflate('hello world', (err, buffer) => {
144 if (err) {
145 throw err;
146 }
147
148 res.end(buffer);
149 });
150 }
151
152 if (p === '/brotli') {
153 res.statusCode = 200;
154 res.setHeader('Content-Type', 'text/plain');
155 if (typeof zlib.createBrotliDecompress === 'function') {
156 res.setHeader('Content-Encoding', 'br');
157 zlib.brotliCompress('hello world', (err, buffer) => {
158 if (err) {
159 throw err;
160 }
161
162 res.end(buffer);
163 });
164 }
165 }
166
167 if (p === '/deflate-raw') {
168 res.statusCode = 200;
169 res.setHeader('Content-Type', 'text/plain');
170 res.setHeader('Content-Encoding', 'deflate');
171 zlib.deflateRaw('hello world', (err, buffer) => {
172 if (err) {
173 throw err;
174 }
175
176 res.end(buffer);
177 });
178 }
179
180 if (p === '/sdch') {
181 res.statusCode = 200;
182 res.setHeader('Content-Type', 'text/plain');
183 res.setHeader('Content-Encoding', 'sdch');
184 res.end('fake sdch string');
185 }
186
187 if (p === '/invalid-content-encoding') {
188 res.statusCode = 200;
189 res.setHeader('Content-Type', 'text/plain');
190 res.setHeader('Content-Encoding', 'gzip');
191 res.end('fake gzip string');
192 }
193
194 if (p === '/timeout') {
195 setTimeout(() => {
196 res.statusCode = 200;
197 res.setHeader('Content-Type', 'text/plain');
198 res.end('text');
199 }, 1000);
200 }
201
202 if (p === '/slow') {
203 res.statusCode = 200;
204 res.setHeader('Content-Type', 'text/plain');
205 res.write('test');
206 setTimeout(() => {
207 res.end('test');
208 }, 1000);
209 }
210
211 if (p === '/cookie') {
212 res.statusCode = 200;
213 res.setHeader('Set-Cookie', ['a=1', 'b=1']);
214 res.end('cookie');
215 }
216
217 if (p === '/size/chunk') {
218 res.statusCode = 200;
219 res.setHeader('Content-Type', 'text/plain');
220 setTimeout(() => {
221 res.write('test');
222 }, 10);
223 setTimeout(() => {
224 res.end('test');
225 }, 20);
226 }
227
228 if (p === '/size/long') {
229 res.statusCode = 200;
230 res.setHeader('Content-Type', 'text/plain');
231 res.end('testtest');
232 }
233
234 if (p === '/redirect/301') {
235 res.statusCode = 301;
236 res.setHeader('Location', '/inspect');
237 res.end();
238 }
239
240 if (p === '/redirect/301/file') {
241 res.statusCode = 301;
242 res.setHeader('Location', 'file://inspect');
243 res.end();
244 }
245
246 if (p === '/redirect/301/rn') {
247 res.statusCode = 301
248 res.setHeader('Location', '/403')
249 res.write('301 Permanently moved.\r\n');
250 res.end();
251 }
252
253 if (p === '/403') {
254 res.statusCode = 403;
255 res.write('403 Forbidden');
256 res.end();
257 }
258
259 if (p === '/redirect/302') {
260 res.statusCode = 302;
261 res.setHeader('Location', '/inspect');
262 res.end();
263 }
264
265 if (p === '/redirect/303') {
266 res.statusCode = 303;
267 res.setHeader('Location', '/inspect');
268 res.end();
269 }
270
271 if (p === '/redirect/307') {
272 res.statusCode = 307;
273 res.setHeader('Location', '/inspect');
274 res.end();
275 }
276
277 if (p === '/redirect/308') {
278 res.statusCode = 308;
279 res.setHeader('Location', '/inspect');
280 res.end();
281 }
282
283 if (p === '/redirect/chain') {
284 res.statusCode = 301;
285 res.setHeader('Location', '/redirect/301');
286 res.end();
287 }
288
289 if (p === '/redirect/no-location') {
290 res.statusCode = 301;
291 res.end();
292 }
293
294 if (p === '/redirect/slow') {
295 res.statusCode = 301;
296 res.setHeader('Location', '/redirect/301');
297 setTimeout(() => {
298 res.end();
299 }, 1000);
300 }
301
302 if (p === '/redirect/slow-chain') {
303 res.statusCode = 301;
304 res.setHeader('Location', '/redirect/slow');
305 setTimeout(() => {
306 res.end();
307 }, 10);
308 }
309
310 if (p === '/redirect/slow-stream') {
311 res.statusCode = 301;
312 res.setHeader('Location', '/slow');
313 res.end();
314 }
315
316 if (p === '/redirect/bad-location') {
317 res.socket.write('HTTP/1.1 301\r\nLocation: ☃\r\nContent-Length: 0\r\n');
318 res.socket.end('\r\n');
319 }
320
321 if (p === '/redirect/chunked') {
322 res.writeHead(301, {
323 Location: '/inspect',
324 'Transfer-Encoding': 'chunked'
325 });
326 setTimeout(() => res.end(), 10);
327 }
328
329 if (p === '/error/400') {
330 res.statusCode = 400;
331 res.setHeader('Content-Type', 'text/plain');
332 res.end('client error');
333 }
334
335 if (p === '/error/404') {
336 res.statusCode = 404;
337 res.setHeader('Content-Encoding', 'gzip');
338 res.end();
339 }
340
341 if (p === '/error/500') {
342 res.statusCode = 500;
343 res.setHeader('Content-Type', 'text/plain');
344 res.end('server error');
345 }
346
347 if (p === '/error/reset') {
348 res.destroy();
349 }
350
351 if (p === '/error/premature') {
352 res.writeHead(200, {'content-length': 50});
353 res.write('foo');
354 setTimeout(() => {
355 res.destroy();
356 }, 100);
357 }
358
359 if (p === '/error/premature/chunked') {
360 res.writeHead(200, {
361 'Content-Type': 'application/json',
362 'Transfer-Encoding': 'chunked'
363 });
364
365 res.write(`${JSON.stringify({data: 'hi'})}\n`);
366
367 setTimeout(() => {
368 res.write(`${JSON.stringify({data: 'bye'})}\n`);
369 }, 50);
370
371 setTimeout(() => {
372 res.destroy();
373 }, 100);
374 }
375
376 if (p === '/chunked/split-ending') {
377 res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n');
378 res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n');
379
380 setTimeout(() => {
381 res.socket.write('0\r\n');
382 }, 10);
383
384 setTimeout(() => {
385 res.socket.end('\r\n');
386 }, 20);
387 }
388
389 if (p === '/chunked/multiple-ending') {
390 res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n');
391 res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n');
392 res.end();
393 }
394
395 if (p === '/error/json') {
396 res.statusCode = 200;
397 res.setHeader('Content-Type', 'application/json');
398 res.end('invalid json');
399 }
400
401 if (p === '/no-content') {
402 res.statusCode = 204;
403 res.end();
404 }
405
406 if (p === '/no-content/gzip') {
407 res.statusCode = 204;
408 res.setHeader('Content-Encoding', 'gzip');
409 res.end();
410 }
411
412 if (p === '/no-content/brotli') {
413 res.statusCode = 204;
414 res.setHeader('Content-Encoding', 'br');
415 res.end();
416 }
417
418 if (p === '/not-modified') {
419 res.statusCode = 304;
420 res.end();
421 }
422
423 if (p === '/not-modified/gzip') {
424 res.statusCode = 304;
425 res.setHeader('Content-Encoding', 'gzip');
426 res.end();
427 }
428
429 if (p === '/inspect') {
430 res.statusCode = 200;
431 res.setHeader('Content-Type', 'application/json');
432 res.setHeader('X-Inspect', 'inspect');
433 let body = '';
434 request.on('data', c => {
435 body += c;
436 });
437 request.on('end', () => {
438 res.end(JSON.stringify({
439 inspect: true,
440 method: request.method,
441 url: request.url,
442 headers: request.headers,
443 body
444 }));
445 });
446 }
447
448 if (p === '/multipart') {
449 res.statusCode = 200;
450 res.setHeader('Content-Type', 'application/json');
451 const busboy = new Busboy({headers: request.headers});
452 let body = '';
453 busboy.on('file', async (fieldName, file, fileName) => {
454 body += `${fieldName}=${fileName}`;
455 // consume file data
456 // eslint-disable-next-line no-empty, no-unused-vars
457 for await (const c of file) { }
458 });
459
460 busboy.on('field', (fieldName, value) => {
461 body += `${fieldName}=${value}`;
462 });
463 busboy.on('finish', () => {
464 res.end(JSON.stringify({
465 method: request.method,
466 url: request.url,
467 headers: request.headers,
468 body
469 }));
470 });
471 request.pipe(busboy);
472 }
473
474 if (p === '/m%C3%B6bius') {
475 res.statusCode = 200;
476 res.setHeader('Content-Type', 'text/plain');
477 res.end('ok');
478 }
479 }
480}