Mirror: A Node.js fetch shim using built-in Request, Response, and Headers (but without native fetch)
at v0.4.5 107 lines 3.3 kB view raw
1import { describe, it, expect, beforeEach, afterEach } from 'vitest'; 2import { Readable } from 'node:stream'; 3 4import TestServer from './utils/server.js'; 5import { fetch } from '../fetch'; 6 7describe('fetch error handling', () => { 8 const server = new TestServer(); 9 10 beforeEach(() => server.start()); 11 afterEach(() => server.stop()); 12 13 describe('incoming stream errors', () => { 14 it('should propagate error when connection resets mid-body', async () => { 15 const url = server.mock((_req, res) => { 16 res.writeHead(200, { 'Content-Length': '1000' }); 17 res.write('partial'); 18 setTimeout(() => res.destroy(), 10); 19 }); 20 21 const response = await fetch(url); 22 expect(response.ok).toBe(true); 23 await expect(response.text()).rejects.toThrow(); 24 }); 25 26 it('should not cause unhandled errors on incoming stream', async () => { 27 const errors: Error[] = []; 28 const handler = (e: Error) => errors.push(e); 29 process.on('uncaughtException', handler); 30 31 const url = server.mock((_req, res) => { 32 res.writeHead(200, { 'Transfer-Encoding': 'chunked' }); 33 res.write('data'); 34 setTimeout(() => res.destroy(), 10); 35 }); 36 37 const response = await fetch(url); 38 try { 39 await response.text(); 40 } catch {} 41 await new Promise(r => setTimeout(r, 50)); 42 43 process.off('uncaughtException', handler); 44 expect(errors).toHaveLength(0); 45 }); 46 }); 47 48 describe('request body pipeline errors', () => { 49 it('should reject when request body stream errors', async () => { 50 const body = new Readable({ 51 read() { 52 this.push('data'); 53 setTimeout(() => this.destroy(new Error('stream error')), 10); 54 }, 55 }); 56 57 const url = server.mock((req, res) => { 58 req.on('data', () => {}); 59 req.on('end', () => res.end('ok')); 60 req.on('error', () => {}); 61 }); 62 63 await expect(fetch(url, { method: 'POST', body })).rejects.toThrow( 64 'stream error' 65 ); 66 }); 67 }); 68 69 describe('decompression errors', () => { 70 it('should propagate gzip decompression errors', async () => { 71 const url = server.mock((_req, res) => { 72 res.writeHead(200, { 'Content-Encoding': 'gzip' }); 73 res.end('not gzip'); 74 }); 75 76 const response = await fetch(url); 77 expect(response.ok).toBe(true); 78 await expect(response.text()).rejects.toThrow(); 79 }); 80 81 it('should propagate brotli decompression errors', async () => { 82 const url = server.mock((_req, res) => { 83 res.writeHead(200, { 'Content-Encoding': 'br' }); 84 res.end('not brotli'); 85 }); 86 87 const response = await fetch(url); 88 await expect(response.text()).rejects.toThrow(); 89 }); 90 }); 91 92 describe('abort handling', () => { 93 it('should abort during response streaming', async () => { 94 const controller = new AbortController(); 95 96 const url = server.mock((_req, res) => { 97 res.writeHead(200, { 'Transfer-Encoding': 'chunked' }); 98 const id = setInterval(() => res.write('x'), 10); 99 res.on('close', () => clearInterval(id)); 100 }); 101 102 const response = await fetch(url, { signal: controller.signal }); 103 setTimeout(() => controller.abort(), 30); 104 await expect(response.text()).rejects.toThrow(); 105 }); 106 }); 107});