Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
at v0.2.0 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 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}