Skip to content

Commit

Permalink
feat: upgrade to undici v7 (#547)
Browse files Browse the repository at this point in the history
fix server side close unexpected exit

closes #541

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

## Release Notes

- **New Features**
- Introduced new example scripts demonstrating HTTP/2 requests with
enhanced error handling.
- Added support for logging response status and memory usage in
long-running request scenarios.
- Added a new `FormData` class to handle file uploads with non-ASCII
filenames.
- Enhanced the `HttpClient` with improved handling of connection
closures and multiple concurrent requests.
- Updated test suite to ensure robust handling of edge cases, including
file uploads with special characters.

- **Bug Fixes**
- Improved error handling in HTTP request processes to better manage
socket errors and timeouts.
- Enhanced diagnostics logging to provide better context for missing
data during request processing.

- **Documentation**
- Updated package configuration to reflect new dependencies and module
structure.

- **Refactor**
- Enhanced type specificity in various components to improve clarity and
maintainability.
- Improved clarity in test cases with better type handling and naming
consistency.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
  • Loading branch information
fengmk2 authored Nov 30, 2024
1 parent 6d8311e commit 9803c4e
Show file tree
Hide file tree
Showing 19 changed files with 499 additions and 99 deletions.
34 changes: 34 additions & 0 deletions examples/h2-other-side-closed-exit-0-fetch.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { fetch, setGlobalDispatcher, Agent } = require('..');

setGlobalDispatcher(new Agent({
allowH2: true,
}));

async function main() {
for (let i = 0; i < 100; i++) {
try {
const r = await fetch('https://edgeupdates.microsoft.com/api/products');
console.log(r.status, r.headers, (await r.text()).length);
} catch (err) {
// console.error(err);
// throw err;
if (err.code === 'UND_ERR_SOCKET') {
continue;
} else {
throw err;
}
}
}
}

main().then(() => {
console.log('main end');
}).catch(err => {
console.error('main error throw: %s', err);
// console.error(err);
process.exit(1);
});

process.on('beforeExit', (...args) => {
console.error('beforeExit', args);
});
34 changes: 34 additions & 0 deletions examples/h2-other-side-closed-exit-0.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { request, Agent, setGlobalDispatcher } = require('undici');

setGlobalDispatcher(new Agent({
allowH2: true,
}));

async function main() {
for (let i = 0; i < 100; i++) {
try {
const r = await request('https://edgeupdates.microsoft.com/api/products');
console.log(r.statusCode, r.headers, (await r.body.blob()).size);
} catch (err) {
// console.error(err);
// throw err;
if (err.code === 'UND_ERR_SOCKET') {
continue;
} else {
throw err;
}
}
}
}

main().then(() => {
console.log('main end');
}).catch(err => {
console.error('main error throw: %s', err);
// console.error(err);
process.exit(1);
});

process.on('beforeExit', (...args) => {
console.error('beforeExit', args);
});
49 changes: 49 additions & 0 deletions examples/longruning.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
const { HttpClient } = require('..');

const httpClient = new HttpClient({
allowH2: true,
});

async function main() {
for (let i = 0; i < 1000000; i++) {
// await httpClient.request('https://registry.npmmirror.com/');
// console.log(r.status, r.headers, r.res.timing);
try {
const r = await httpClient.request('https://edgeupdates.microsoft.com/api/products');
// console.log(r.status, r.headers, r.data.length, r.res.timing);
if (i % 10 === 0) {
// console.log(r.status, r.headers, r.data.length, r.res.timing);
console.log(i, r.status, process.memoryUsage());
}
} catch (err) {
console.error('%s error: %s', i, err.message);
}
}
}

main().then(() => {
console.log('main end');
}).catch(err => {
console.error('main error throw: %s', err);
console.error(err);
process.exit(1);
});

// process.on('uncaughtException', (...args) => {
// console.error('uncaughtException', args);
// process.exit(1);
// });

// process.on('unhandledRejection', (...args) => {
// console.error('unhandledRejection', args);
// process.exit(2);
// });

// process.on('uncaughtExceptionMonitor', (...args) => {
// console.error('uncaughtExceptionMonitor', args);
// process.exit(2);
// });

process.on('beforeExit', (...args) => {
console.error('beforeExit', args);
});
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "urllib",
"version": "4.4.0",
"version": "4.5.0-beta.3",
"publishConfig": {
"tag": "latest"
},
Expand Down Expand Up @@ -44,11 +44,12 @@
"prepublishOnly": "npm run build"
},
"dependencies": {
"form-data": "^4.0.1",
"formstream": "^1.5.1",
"mime-types": "^2.1.35",
"qs": "^6.12.1",
"type-fest": "^4.20.1",
"undici": "^6.19.2",
"undici": "^7.0.0",
"ylru": "^2.0.0"
},
"devDependencies": {
Expand All @@ -68,6 +69,7 @@
"cross-env": "^7.0.3",
"eslint": "8",
"eslint-config-egg": "14",
"https-pem": "^3.0.0",
"iconv-lite": "^0.6.3",
"proxy": "^1.0.2",
"selfsigned": "^2.0.1",
Expand Down
5 changes: 3 additions & 2 deletions scripts/replace_urllib_version.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ async function main() {
for (const file of files) {
const content = await fs.readFile(file, 'utf-8');
// replace "const VERSION = 'VERSION';" to "const VERSION = '4.0.0';"
const newContent = content.replace(/const VERSION = 'VERSION';/, match => {
const after = `const VERSION = '${pkg.version}';`;
// "exports.VERSION = 'VERSION';" => "exports.VERSION = '4.0.0';"
const newContent = content.replace(/ = 'VERSION';/, match => {
const after = ` = '${pkg.version}';`;
console.log('[%s] %s => %s', file, match, after);
return after;
});
Expand Down
2 changes: 1 addition & 1 deletion src/FetchOpaqueInterceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export interface OpaqueInterceptorOptions {
export function fetchOpaqueInterceptor(opts: OpaqueInterceptorOptions) {
const opaqueLocalStorage = opts?.opaqueLocalStorage;
return (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] => {
return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) {
return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler) {
const opaque = opaqueLocalStorage?.getStore();
(handler as any).opaque = opaque;
return dispatch(opts, handler);
Expand Down
32 changes: 32 additions & 0 deletions src/FormData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import path from 'node:path';
import _FormData from 'form-data';

export class FormData extends _FormData {
_getContentDisposition(value: any, options: any) {
// support non-ascii filename
// https://github.com/form-data/form-data/pull/571
let filename;
let contentDisposition;

if (typeof options.filepath === 'string') {
// custom filepath for relative paths
filename = path.normalize(options.filepath).replace(/\\/g, '/');
} else if (options.filename || value.name || value.path) {
// custom filename take precedence
// formidable and the browser add a name property
// fs- and request- streams have path property
filename = path.basename(options.filename || value.name || value.path);
} else if (value.readable && value.hasOwnProperty('httpVersion')) {
// or try http response
filename = path.basename(value.client._httpMessage.path || '');
}

if (filename) {
// https://datatracker.ietf.org/doc/html/rfc6266#section-4.1
// support non-ascii filename
contentDisposition = 'filename="' + filename + '"; filename*=UTF-8\'\'' + encodeURIComponent(filename);
}

return contentDisposition;
}
}
2 changes: 1 addition & 1 deletion src/HttpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export class HttpAgent extends Agent {
this.#checkAddress = options.checkAddress;
}

dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean {
dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
if (this.#checkAddress && options.origin) {
const originUrl = typeof options.origin === 'string' ? new URL(options.origin) : options.origin;
let hostname = originUrl.hostname;
Expand Down
Loading

0 comments on commit 9803c4e

Please sign in to comment.