Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: print more socket info on UND_ERR_CONNECT_TIMEOUT error #477

Merged
merged 5 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ src/**/*.d.ts
!src/formstream.d.ts
.DS_Store
test/fixtures/ts/*.js
test/fixtures/ts-cjs-es2021/*.js
test/fixtures/ts-esm/*.js
.eslintcache
.tshy*
4 changes: 2 additions & 2 deletions examples/httpclient.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ function tryHttpclient(HttpClient, name) {
urllib.on('response', function(info) {
// console.log(name, httpAgent, httpAgent.getCurrentStatus());
// console.log(name, httpsAgent, httpsAgent.getCurrentStatus());
console.log(name, info.res);
console.log('response', name, info.res);
});
urllib.request('https://nodejs.org', options)
.then(function() {
Expand All @@ -22,6 +22,6 @@ function tryHttpclient(HttpClient, name) {
return urllib.request('https://nodejs.org/en/', options);
})
.catch(function(err) {
console.error(err);
console.error('catch', err);
});
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@
"build:esm:test": "cd test/esm && rm -rf node_modules && npm link ../.. && node index.js",
"build:mts:test": "cd test/mts && rm -rf node_modules && npm link ../.. && tsc",
"build:test": "npm run build && npm run build:cjs:test && npm run build:esm:test && npm run build:mts:test && npm run test-tsc",
"test-tsc": "npm run test-tsc:cjs && npm run test-tsc:esm",
"test-tsc": "npm run test-tsc:cjs:es2021 && npm run test-tsc:cjs && npm run test-tsc:esm",
"test-tsc:cjs": "cd test/fixtures/ts && rm -rf node_modules && npm link ../../.. && npm run build",
"test-tsc:cjs:es2021": "cd test/fixtures/ts-cjs-es2021 && rm -rf node_modules && npm link ../../.. && npm run build",
"test-tsc:esm": "cd test/fixtures/ts-esm && rm -rf node_modules && npm link ../../.. && npm run build",
"test": "npm run lint && vitest run",
"test-keepalive": "cross-env TEST_KEEPALIVE_COUNT=50 vitest run --test-timeout 180000 keep-alive-header.test.ts",
Expand Down
2 changes: 1 addition & 1 deletion src/HttpAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ export class HttpAgent extends Agent {
}
const family = isIP(hostname);
if (family === 4 || family === 6) {
// if request hostname is ip, custom lookup won't excute
// if request hostname is ip, custom lookup won't execute
if (!this.#checkAddress(hostname, family)) {
throw new IllegalAddressError(hostname, hostname, family);
}
Expand Down
59 changes: 35 additions & 24 deletions src/HttpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import { parseJSON, sleep, digestAuthHeader, globalId, performanceTime, isReadable } from './utils.js';
import symbols from './symbols.js';
import { initDiagnosticsChannel } from './diagnosticsChannel.js';
import { HttpClientConnectTimeoutError, HttpClientRequestTimeoutError } from './HttpClientError.js';

type Exists<T> = T extends undefined ? never : T;
type UndiciRequestOption = Exists<Parameters<typeof undiciRequest>[1]>;
Expand Down Expand Up @@ -121,15 +122,6 @@
}
}

class HttpClientRequestTimeoutError extends Error {
constructor(timeout: number, options: ErrorOptions) {
const message = `Request timeout for ${timeout} ms`;
super(message, options);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

export const HEADER_USER_AGENT = createUserAgent('node-urllib', 'VERSION');

function getFileName(stream: Readable) {
Expand Down Expand Up @@ -653,13 +645,15 @@
}

return clientResponse;
} catch (e: any) {
debug('Request#%d throw error: %s', requestId, e);
let err = e;
} catch (rawError: any) {
debug('Request#%d throw error: %s', requestId, rawError);
let err = rawError;
if (err.name === 'HeadersTimeoutError') {
err = new HttpClientRequestTimeoutError(headersTimeout, { cause: e });
err = new HttpClientRequestTimeoutError(headersTimeout, { cause: err });
} else if (err.name === 'BodyTimeoutError') {
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: e });
err = new HttpClientRequestTimeoutError(bodyTimeout, { cause: err });
} else if (err.code === 'UND_ERR_CONNECT_TIMEOUT') {
err = new HttpClientConnectTimeoutError(err.message, err.code, { cause: err });

Check warning on line 656 in src/HttpClient.ts

View check run for this annotation

Codecov / codecov/patch

src/HttpClient.ts#L656

Added line #L656 was not covered by tests
} else if (err.code === 'UND_ERR_SOCKET' || err.code === 'ECONNRESET') {
// auto retry on socket error, https://github.com/node-modules/urllib/issues/454
if (args.socketErrorRetry > 0 && requestContext.socketErrorRetries < args.socketErrorRetry) {
Expand All @@ -681,7 +675,7 @@
res.requestUrls.push(requestUrl.href);
}
res.rt = performanceTime(requestStartTime);
this.#updateSocketInfo(socketInfo, internalOpaque);
this.#updateSocketInfo(socketInfo, internalOpaque, rawError);

channels.response.publish({
request: reqMeta,
Expand All @@ -704,21 +698,38 @@
}
}

#updateSocketInfo(socketInfo: SocketInfo, internalOpaque: any) {
const socket = internalOpaque[symbols.kRequestSocket];
#updateSocketInfo(socketInfo: SocketInfo, internalOpaque: any, err?: any) {
const socket = internalOpaque[symbols.kRequestSocket] ?? err?.[symbols.kErrorSocket];
if (socket) {
socketInfo.id = socket[symbols.kSocketId];
socketInfo.handledRequests = socket[symbols.kHandledRequests];
socketInfo.handledResponses = socket[symbols.kHandledResponses];
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
socketInfo.localPort = socket[symbols.kSocketLocalPort];
socketInfo.remoteAddress = socket.remoteAddress;
socketInfo.remotePort = socket.remotePort;
socketInfo.remoteFamily = socket.remoteFamily;
if (socket[symbols.kSocketLocalAddress]) {
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
socketInfo.localPort = socket[symbols.kSocketLocalPort];
}
if (socket.remoteAddress) {
socketInfo.remoteAddress = socket.remoteAddress;
socketInfo.remotePort = socket.remotePort;
socketInfo.remoteFamily = socket.remoteFamily;
}
socketInfo.bytesRead = socket.bytesRead;
socketInfo.bytesWritten = socket.bytesWritten;
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
if (socket[symbols.kSocketConnectErrorTime]) {
socketInfo.connectErrorTime = socket[symbols.kSocketConnectErrorTime];
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
socketInfo.attemptedRemoteAddresses = socket.autoSelectFamilyAttemptedAddresses;
}

Check warning on line 722 in src/HttpClient.ts

View check run for this annotation

Codecov / codecov/patch

src/HttpClient.ts#L721-L722

Added lines #L721 - L722 were not covered by tests
socketInfo.connectProtocol = socket[symbols.kSocketConnectProtocol];
socketInfo.connectHost = socket[symbols.kSocketConnectHost];
socketInfo.connectPort = socket[symbols.kSocketConnectPort];
}
if (socket[symbols.kSocketConnectedTime]) {
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
}
if (socket[symbols.kSocketRequestEndTime]) {
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
}
socket[symbols.kSocketRequestEndTime] = new Date();
}
}
Expand Down
34 changes: 34 additions & 0 deletions src/HttpClientError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { RawResponseWithMeta, SocketInfo } from './Response.js';
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';

// need to support ES2021
interface ErrorOptions {
cause?: Error;
}

export class HttpClientRequestError extends Error {
status?: number;
headers?: IncomingHttpHeaders;
socket?: SocketInfo;
res?: RawResponseWithMeta;
}

export class HttpClientRequestTimeoutError extends HttpClientRequestError {
constructor(timeout: number, options: ErrorOptions) {
const message = `Request timeout for ${timeout} ms`;
super(message, options);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}

export class HttpClientConnectTimeoutError extends HttpClientRequestError {
code: string;

constructor(message: string, code: string, options: ErrorOptions) {
super(message, options);
this.name = this.constructor.name;
this.code = code;
Error.captureStackTrace(this, this.constructor);
}

Check warning on line 33 in src/HttpClientError.ts

View check run for this annotation

Codecov / codecov/patch

src/HttpClientError.ts#L29-L33

Added lines #L29 - L33 were not covered by tests
}
5 changes: 5 additions & 0 deletions src/Response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@ export type SocketInfo = {
handledRequests: number;
handledResponses: number;
connectedTime?: Date;
connectErrorTime?: Date;
lastRequestEndTime?: Date;
attemptedRemoteAddresses?: string[];
connectProtocol?: string;
connectHost?: string;
connectPort?: string;
};

/**
Expand Down
50 changes: 48 additions & 2 deletions src/diagnosticsChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,25 @@
localPort: socket[symbols.kSocketLocalPort],
remoteAddress: socket.remoteAddress,
remotePort: socket.remotePort,
attemptedAddresses: socket.autoSelectFamilyAttemptedAddresses,
connecting: socket.connecting,
};
}

// make sure error contains socket info
const kDestroy = Symbol('kDestroy');
Socket.prototype[kDestroy] = Socket.prototype.destroy;
Socket.prototype.destroy = function(err?: any) {
if (err) {
Object.defineProperty(err, symbols.kErrorSocket, {
// don't show on console log
enumerable: false,
value: this,
});
}
return this[kDestroy](err);
};

export function initDiagnosticsChannel() {
// makre sure init global DiagnosticsChannel once
if (initedDiagnosticsChannel) return;
Expand Down Expand Up @@ -67,10 +83,37 @@
});

// diagnosticsChannel.channel('undici:client:beforeConnect')
// diagnosticsChannel.channel('undici:client:connectError')

subscribe('undici:client:connectError', (message, name) => {
const { error, connectParams } = message as DiagnosticsChannel.ClientConnectErrorMessage & { error: any };
let { socket } = message as DiagnosticsChannel.ClientConnectErrorMessage;
if (!socket && error[symbols.kErrorSocket]) {
socket = error[symbols.kErrorSocket];
}
if (socket) {
socket[symbols.kSocketId] = globalId('UndiciSocket');
socket[symbols.kSocketConnectErrorTime] = new Date();
socket[symbols.kHandledRequests] = 0;
socket[symbols.kHandledResponses] = 0;
// copy local address to symbol, avoid them be reset after request error throw
if (socket.localAddress) {
socket[symbols.kSocketLocalAddress] = socket.localAddress;
socket[symbols.kSocketLocalPort] = socket.localPort;
}

Check warning on line 102 in src/diagnosticsChannel.ts

View check run for this annotation

Codecov / codecov/patch

src/diagnosticsChannel.ts#L100-L102

Added lines #L100 - L102 were not covered by tests
socket[symbols.kSocketConnectProtocol] = connectParams.protocol;
socket[symbols.kSocketConnectHost] = connectParams.host;
socket[symbols.kSocketConnectPort] = connectParams.port;
debug('[%s] Socket#%d connectError, connectParams: %o, error: %s, (sock: %o)',
name, socket[symbols.kSocketId], connectParams, (error as Error).message, formatSocket(socket));
} else {
debug('[%s] connectError, connectParams: %o, error: %o',
name, connectParams, error);
}

Check warning on line 111 in src/diagnosticsChannel.ts

View check run for this annotation

Codecov / codecov/patch

src/diagnosticsChannel.ts#L109-L111

Added lines #L109 - L111 were not covered by tests
});

// This message is published after a connection is established.
subscribe('undici:client:connected', (message, name) => {
const { socket } = message as DiagnosticsChannel.ClientConnectedMessage;
const { socket, connectParams } = message as DiagnosticsChannel.ClientConnectedMessage;
socket[symbols.kSocketId] = globalId('UndiciSocket');
socket[symbols.kSocketStartTime] = performance.now();
socket[symbols.kSocketConnectedTime] = new Date();
Expand All @@ -79,6 +122,9 @@
// copy local address to symbol, avoid them be reset after request error throw
socket[symbols.kSocketLocalAddress] = socket.localAddress;
socket[symbols.kSocketLocalPort] = socket.localPort;
socket[symbols.kSocketConnectProtocol] = connectParams.protocol;
socket[symbols.kSocketConnectHost] = connectParams.host;
socket[symbols.kSocketConnectPort] = connectParams.port;
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols.kSocketId], formatSocket(socket));
});

Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export {
export {
IncomingHttpHeaders,
} from './IncomingHttpHeaders.js';
export * from './HttpClientError.js';

export default {
request,
Expand Down
5 changes: 5 additions & 0 deletions src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ export default {
kSocketId: Symbol('socket id'),
kSocketStartTime: Symbol('socket start time'),
kSocketConnectedTime: Symbol('socket connected time'),
kSocketConnectErrorTime: Symbol('socket connectError time'),
kSocketRequestEndTime: Symbol('socket request end time'),
kSocketLocalAddress: Symbol('socket local address'),
kSocketLocalPort: Symbol('socket local port'),
kSocketConnectHost: Symbol('socket connect params: host'),
kSocketConnectPort: Symbol('socket connect params: port'),
kSocketConnectProtocol: Symbol('socket connect params: protocol'),
kHandledRequests: Symbol('handled requests per socket'),
kHandledResponses: Symbol('handled responses per socket'),
kRequestSocket: Symbol('request on the socket'),
Expand All @@ -13,4 +17,5 @@ export default {
kEnableRequestTiming: Symbol('enable request timing or not'),
kRequestTiming: Symbol('request timing'),
kRequestOriginalOpaque: Symbol('request original opaque'),
kErrorSocket: Symbol('socket of error'),
};
14 changes: 14 additions & 0 deletions test/fixtures/ts-cjs-es2021/hello.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { RequestURL, RequestOptions2, RequestOptions } from 'urllib';
import { HttpClientResponse } from 'urllib';
import urllib from 'urllib';
import * as urllib2 from 'urllib';

async function request(url: RequestURL, options: RequestOptions): Promise<HttpClientResponse> {
return await urllib.request(url, options);
}

async function request2(url: RequestURL, options: RequestOptions2): Promise<HttpClientResponse> {
return await urllib2.curl(url, options);
}

console.log(request, request2);
6 changes: 6 additions & 0 deletions test/fixtures/ts-cjs-es2021/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "ts-cjs-demo",
"scripts": {
"build": "tsc && node hello.js"
}
}
12 changes: 12 additions & 0 deletions test/fixtures/ts-cjs-es2021/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2021",
"module": "CommonJS",
"moduleResolution": "Node",
"baseUrl": "./",
"paths": {
"urllib": ["../../.."]
}
}
}
2 changes: 1 addition & 1 deletion test/fixtures/ts-esm/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2021",
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"baseUrl": "./",
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures/ts/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2021",
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "Node",
"baseUrl": "./",
Expand Down
2 changes: 1 addition & 1 deletion test/keep-alive-header.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ describe('keep-alive-header.test.ts', () => {

it('should handle Keep-Alive header and not throw reset error on 1s keepalive agent', async () => {
let count = 0;
const max = process.env.TEST_KEEPALIVE_COUNT ? parseInt(process.env.TEST_KEEPALIVE_COUNT) : 10;
const max = process.env.TEST_KEEPALIVE_COUNT ? parseInt(process.env.TEST_KEEPALIVE_COUNT) : 3;
let otherSideClosed = 0;
let readECONNRESET = 0;
while (count < max) {
Expand Down
6 changes: 4 additions & 2 deletions test/options.streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,8 @@ describe('options.streaming.test.ts', () => {
const bytes1 = await readableToBytes(response1.res);
assert.equal(bytes1.length, size);
const use1 = Date.now() - start;
console.log('highWaterMark 64KB use %dms', use1);
// console.log('highWaterMark 64KB use %dms', use1);
assert(use1 > 0);

start = Date.now();
const response2 = await urllib.request(`${_url}mock-bytes?size=${size}`, {
Expand All @@ -165,7 +166,8 @@ describe('options.streaming.test.ts', () => {
const bytes2 = await readableToBytes(response2.res);
assert.equal(bytes2.length, size);
const use2 = Date.now() - start;
console.log('highWaterMark 128KB use %dms', use2);
// console.log('highWaterMark 128KB use %dms', use2);
// assert(use2 < use1);
assert(use2 > 0);
});
});
Loading
Loading