Skip to content

Commit

Permalink
Test to check if fetchNodeHttp leaks (#1162)
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan authored Mar 1, 2024
1 parent 2190955 commit 0c6e9ca
Show file tree
Hide file tree
Showing 8 changed files with 84 additions and 13 deletions.
6 changes: 6 additions & 0 deletions .changeset/tough-rats-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@whatwg-node/node-fetch": patch
"@whatwg-node/fetch": patch
---

Consume the body with PassThrough
2 changes: 1 addition & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"website",
"scripts",
"e2e",
"benchmark"
"benchmarks"
],
"globals": {
"BigInt": true
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"ci:lint": "eslint --ext .ts . --output-file eslint_report.json --format json",
"clean-dist": "rimraf \"dist\" && rimraf \".bob\"",
"esm:check": "bob check",
"jest-with-gc": "node --expose-gc ./node_modules/.bin/jest",
"lint": "eslint --ext .ts .",
"postinstall": "patch-package && husky install",
"prebuild": "yarn clean-dist",
Expand Down
29 changes: 19 additions & 10 deletions packages/node-fetch/src/fetchCurl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Readable } from 'node:stream';
import { PassThrough, Readable } from 'node:stream';
import { PonyfillRequest } from './Request.js';
import { PonyfillResponse } from './Response.js';
import { defaultHeadersSerializer, isNodeReadable } from './utils.js';
Expand Down Expand Up @@ -93,6 +93,7 @@ export function fetchCurl<TResponseJSON = any, TRequestJSON = any>(
curlHandle.once(
'stream',
function streamListener(stream: Readable, status: number, headersBuf: Buffer) {
const pipedStream = stream.pipe(new PassThrough());
const headersFlat = headersBuf
.toString('utf8')
.split(/\r?\n|\r/g)
Expand All @@ -102,7 +103,7 @@ export function fetchCurl<TResponseJSON = any, TRequestJSON = any>(
fetchRequest.redirect === 'error' &&
(headerFilter.includes('location') || headerFilter.includes('Location'))
) {
stream.destroy();
pipedStream.destroy();
reject(new Error('redirect is not allowed'));
}
return true;
Expand All @@ -112,14 +113,22 @@ export function fetchCurl<TResponseJSON = any, TRequestJSON = any>(
const headersInit = headersFlat.map(
headerFlat => headerFlat.split(/:\s(.+)/).slice(0, 2) as [string, string],
);
resolve(
new PonyfillResponse(stream, {
status,
headers: headersInit,
url: fetchRequest.url,
}),
);
streamResolved = stream;
pipedStream.on('pause', () => {
stream.pause();
});
pipedStream.on('resume', () => {
stream.resume();
});
pipedStream.on('close', () => {
stream.destroy();
});
const ponyfillResponse = new PonyfillResponse(pipedStream, {
status,
headers: headersInit,
url: fetchRequest.url,
});
resolve(ponyfillResponse);
streamResolved = pipedStream;
},
);
curlHandle.perform();
Expand Down
20 changes: 19 additions & 1 deletion packages/node-fetch/src/fetchNodeHttp.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { request as httpRequest } from 'http';
import { request as httpsRequest } from 'https';
import { Readable } from 'stream';
import { PassThrough, Readable } from 'stream';
import { createBrotliDecompress, createGunzip, createInflate } from 'zlib';
import { PonyfillAbortError } from './AbortError.js';
import { PonyfillRequest } from './Request.js';
import { PonyfillResponse } from './Response.js';
import { PonyfillURL } from './URL.js';
Expand Down Expand Up @@ -78,6 +79,23 @@ export function fetchNodeHttp<TResponseJSON = any, TRequestJSON = any>(
return;
}
}
if (responseBody === nodeResponse) {
responseBody = nodeResponse.pipe(new PassThrough());
responseBody.on('pause', () => {
nodeResponse.pause();
});
responseBody.on('resume', () => {
nodeResponse.resume();
});
responseBody.on('close', () => {
nodeResponse.destroy();
});
fetchRequest['_signal']?.addEventListener('abort', () => {
if (!nodeResponse.destroyed) {
responseBody.emit('error', new PonyfillAbortError());
}
});
}
const ponyfillResponse = new PonyfillResponse(responseBody, {
status: nodeResponse.statusCode,
statusText: nodeResponse.statusMessage,
Expand Down
31 changes: 31 additions & 0 deletions packages/node-fetch/tests/cleanup-resources.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Response } from '@whatwg-node/node-fetch';
import { createServerAdapter } from '@whatwg-node/server';
import { runTestsForEachFetchImpl } from '../../server/test/test-fetch';
import { runTestsForEachServerImpl } from '../../server/test/test-server';
import { fetchPonyfill } from '../src/fetch';

describe('Cleanup Resources', () => {
runTestsForEachFetchImpl(() => {
describe('internal calls', () => {
runTestsForEachServerImpl(testServer => {
beforeEach(() => {
testServer.addOnceHandler(createServerAdapter(() => Response.json({ test: 'test' })));
});
it('should free resources when body is not consumed', async () => {
const response = await fetchPonyfill(testServer.url);
expect(response.status).toBe(200);
});
});
});
describe('external calls', () => {
it('http - should free resources when body is not consumed', async () => {
const response = await fetchPonyfill('http://google.com');
expect(response.status).toBe(200);
});
it('https - should free resources when body is not consumed', async () => {
const response = await fetchPonyfill('https://google.com');
expect(response.status).toBe(200);
});
});
});
});
2 changes: 1 addition & 1 deletion packages/server/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ export function sendNodeResponse(
serverResponse: NodeResponse,
nodeRequest: NodeRequest,
) {
if (serverResponse.closed || serverResponse.destroyed) {
if (serverResponse.closed || serverResponse.destroyed || serverResponse.writableEnded) {
return;
}
if (!fetchResponse) {
Expand Down
6 changes: 6 additions & 0 deletions packages/server/test/test-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export function runTestsForEachFetchImpl(callback: (implementationName: string)
if (libcurl) {
describe('libcurl', () => {
callback('libcurl');
afterAll(() => {
libcurl.Curl.globalCleanup();
});
});
}
describe('node-http', () => {
Expand All @@ -20,4 +23,7 @@ export function runTestsForEachFetchImpl(callback: (implementationName: string)
});
callback('node-http');
});
afterEach(() => {
globalThis?.gc?.();
});
}

0 comments on commit 0c6e9ca

Please sign in to comment.