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