Skip to content

Commit 9803c4e

Browse files
authored
feat: upgrade to undici v7 (#547)
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 -->
1 parent 6d8311e commit 9803c4e

19 files changed

+499
-99
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { fetch, setGlobalDispatcher, Agent } = require('..');
2+
3+
setGlobalDispatcher(new Agent({
4+
allowH2: true,
5+
}));
6+
7+
async function main() {
8+
for (let i = 0; i < 100; i++) {
9+
try {
10+
const r = await fetch('https://edgeupdates.microsoft.com/api/products');
11+
console.log(r.status, r.headers, (await r.text()).length);
12+
} catch (err) {
13+
// console.error(err);
14+
// throw err;
15+
if (err.code === 'UND_ERR_SOCKET') {
16+
continue;
17+
} else {
18+
throw err;
19+
}
20+
}
21+
}
22+
}
23+
24+
main().then(() => {
25+
console.log('main end');
26+
}).catch(err => {
27+
console.error('main error throw: %s', err);
28+
// console.error(err);
29+
process.exit(1);
30+
});
31+
32+
process.on('beforeExit', (...args) => {
33+
console.error('beforeExit', args);
34+
});
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { request, Agent, setGlobalDispatcher } = require('undici');
2+
3+
setGlobalDispatcher(new Agent({
4+
allowH2: true,
5+
}));
6+
7+
async function main() {
8+
for (let i = 0; i < 100; i++) {
9+
try {
10+
const r = await request('https://edgeupdates.microsoft.com/api/products');
11+
console.log(r.statusCode, r.headers, (await r.body.blob()).size);
12+
} catch (err) {
13+
// console.error(err);
14+
// throw err;
15+
if (err.code === 'UND_ERR_SOCKET') {
16+
continue;
17+
} else {
18+
throw err;
19+
}
20+
}
21+
}
22+
}
23+
24+
main().then(() => {
25+
console.log('main end');
26+
}).catch(err => {
27+
console.error('main error throw: %s', err);
28+
// console.error(err);
29+
process.exit(1);
30+
});
31+
32+
process.on('beforeExit', (...args) => {
33+
console.error('beforeExit', args);
34+
});

examples/longruning.cjs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
const { HttpClient } = require('..');
2+
3+
const httpClient = new HttpClient({
4+
allowH2: true,
5+
});
6+
7+
async function main() {
8+
for (let i = 0; i < 1000000; i++) {
9+
// await httpClient.request('https://registry.npmmirror.com/');
10+
// console.log(r.status, r.headers, r.res.timing);
11+
try {
12+
const r = await httpClient.request('https://edgeupdates.microsoft.com/api/products');
13+
// console.log(r.status, r.headers, r.data.length, r.res.timing);
14+
if (i % 10 === 0) {
15+
// console.log(r.status, r.headers, r.data.length, r.res.timing);
16+
console.log(i, r.status, process.memoryUsage());
17+
}
18+
} catch (err) {
19+
console.error('%s error: %s', i, err.message);
20+
}
21+
}
22+
}
23+
24+
main().then(() => {
25+
console.log('main end');
26+
}).catch(err => {
27+
console.error('main error throw: %s', err);
28+
console.error(err);
29+
process.exit(1);
30+
});
31+
32+
// process.on('uncaughtException', (...args) => {
33+
// console.error('uncaughtException', args);
34+
// process.exit(1);
35+
// });
36+
37+
// process.on('unhandledRejection', (...args) => {
38+
// console.error('unhandledRejection', args);
39+
// process.exit(2);
40+
// });
41+
42+
// process.on('uncaughtExceptionMonitor', (...args) => {
43+
// console.error('uncaughtExceptionMonitor', args);
44+
// process.exit(2);
45+
// });
46+
47+
process.on('beforeExit', (...args) => {
48+
console.error('beforeExit', args);
49+
});

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "urllib",
3-
"version": "4.4.0",
3+
"version": "4.5.0-beta.3",
44
"publishConfig": {
55
"tag": "latest"
66
},
@@ -44,11 +44,12 @@
4444
"prepublishOnly": "npm run build"
4545
},
4646
"dependencies": {
47+
"form-data": "^4.0.1",
4748
"formstream": "^1.5.1",
4849
"mime-types": "^2.1.35",
4950
"qs": "^6.12.1",
5051
"type-fest": "^4.20.1",
51-
"undici": "^6.19.2",
52+
"undici": "^7.0.0",
5253
"ylru": "^2.0.0"
5354
},
5455
"devDependencies": {
@@ -68,6 +69,7 @@
6869
"cross-env": "^7.0.3",
6970
"eslint": "8",
7071
"eslint-config-egg": "14",
72+
"https-pem": "^3.0.0",
7173
"iconv-lite": "^0.6.3",
7274
"proxy": "^1.0.2",
7375
"selfsigned": "^2.0.1",

scripts/replace_urllib_version.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ async function main() {
1313
for (const file of files) {
1414
const content = await fs.readFile(file, 'utf-8');
1515
// replace "const VERSION = 'VERSION';" to "const VERSION = '4.0.0';"
16-
const newContent = content.replace(/const VERSION = 'VERSION';/, match => {
17-
const after = `const VERSION = '${pkg.version}';`;
16+
// "exports.VERSION = 'VERSION';" => "exports.VERSION = '4.0.0';"
17+
const newContent = content.replace(/ = 'VERSION';/, match => {
18+
const after = ` = '${pkg.version}';`;
1819
console.log('[%s] %s => %s', file, match, after);
1920
return after;
2021
});

src/FetchOpaqueInterceptor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export interface OpaqueInterceptorOptions {
3232
export function fetchOpaqueInterceptor(opts: OpaqueInterceptorOptions) {
3333
const opaqueLocalStorage = opts?.opaqueLocalStorage;
3434
return (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] => {
35-
return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) {
35+
return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler) {
3636
const opaque = opaqueLocalStorage?.getStore();
3737
(handler as any).opaque = opaque;
3838
return dispatch(opts, handler);

src/FormData.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import path from 'node:path';
2+
import _FormData from 'form-data';
3+
4+
export class FormData extends _FormData {
5+
_getContentDisposition(value: any, options: any) {
6+
// support non-ascii filename
7+
// https://github.com/form-data/form-data/pull/571
8+
let filename;
9+
let contentDisposition;
10+
11+
if (typeof options.filepath === 'string') {
12+
// custom filepath for relative paths
13+
filename = path.normalize(options.filepath).replace(/\\/g, '/');
14+
} else if (options.filename || value.name || value.path) {
15+
// custom filename take precedence
16+
// formidable and the browser add a name property
17+
// fs- and request- streams have path property
18+
filename = path.basename(options.filename || value.name || value.path);
19+
} else if (value.readable && value.hasOwnProperty('httpVersion')) {
20+
// or try http response
21+
filename = path.basename(value.client._httpMessage.path || '');
22+
}
23+
24+
if (filename) {
25+
// https://datatracker.ietf.org/doc/html/rfc6266#section-4.1
26+
// support non-ascii filename
27+
contentDisposition = 'filename="' + filename + '"; filename*=UTF-8\'\'' + encodeURIComponent(filename);
28+
}
29+
30+
return contentDisposition;
31+
}
32+
}

src/HttpAgent.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export class HttpAgent extends Agent {
7070
this.#checkAddress = options.checkAddress;
7171
}
7272

73-
dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandlers): boolean {
73+
dispatch(options: Agent.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
7474
if (this.#checkAddress && options.origin) {
7575
const originUrl = typeof options.origin === 'string' ? new URL(options.origin) : options.origin;
7676
let hostname = originUrl.hostname;

0 commit comments

Comments
 (0)