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