Skip to content

Commit 8cf5c3b

Browse files
authored
fix: set opaque on request (#560)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Enhanced timing metrics for HTTP requests, now including `dnslookup`. - Improved logging format for better clarity in debug outputs. - **Bug Fixes** - Refined error handling for various timeout errors, improving granularity in error reporting. - Enhanced error handling in fetch operations with detailed logging of request IDs and errors. - **Tests** - Updated test cases for fetch functionality to include timing assertions and improved error handling checks. - Enhanced clarity in assertions within timing tests. - **Chores** - Added a new symbol for representing internal opaque request values. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent ed6868b commit 8cf5c3b

File tree

7 files changed

+46
-25
lines changed

7 files changed

+46
-25
lines changed

src/HttpClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export class HttpClient extends EventEmitter {
300300
// socket assigned
301301
queuing: 0,
302302
// dns lookup time
303-
// dnslookup: 0,
303+
dnslookup: 0,
304304
// socket connected
305305
connected: 0,
306306
// request headers sent

src/Response.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export type Timing = {
2828
// socket assigned
2929
queuing: number;
3030
// dns lookup time
31-
// dnslookup: number;
31+
dnslookup: number;
3232
// socket connected
3333
connected: number;
3434
// request headers sent

src/diagnosticsChannel.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ export function initDiagnosticsChannel() {
8989
const opaque = getRequestOpaque(request, kHandler);
9090
// ignore non HttpClient Request
9191
if (!opaque || !opaque[symbols.kRequestId]) return;
92-
debug('[%s] Request#%d %s %s, path: %s, headers: %o',
92+
93+
Reflect.set(request, symbols.kRequestInternalOpaque, opaque);
94+
debug('[%s] Request#%d %s %s, path: %s, headers: %j',
9395
name, opaque[symbols.kRequestId], request.method, request.origin, request.path, request.headers);
9496
if (!opaque[symbols.kEnableRequestTiming]) return;
9597
opaque[symbols.kRequestTiming].queuing = performanceTime(opaque[symbols.kRequestStartTime]);
@@ -114,10 +116,10 @@ export function initDiagnosticsChannel() {
114116
sock[symbols.kSocketConnectProtocol] = connectParams.protocol;
115117
sock[symbols.kSocketConnectHost] = connectParams.host;
116118
sock[symbols.kSocketConnectPort] = connectParams.port;
117-
debug('[%s] Socket#%d connectError, connectParams: %o, error: %s, (sock: %o)',
119+
debug('[%s] Socket#%d connectError, connectParams: %j, error: %s, (sock: %j)',
118120
name, sock[symbols.kSocketId], connectParams, (error as Error).message, formatSocket(sock));
119121
} else {
120-
debug('[%s] connectError, connectParams: %o, error: %o',
122+
debug('[%s] connectError, connectParams: %j, error: %o',
121123
name, connectParams, error);
122124
}
123125
});
@@ -136,13 +138,13 @@ export function initDiagnosticsChannel() {
136138
socket[symbols.kSocketConnectProtocol] = connectParams.protocol;
137139
socket[symbols.kSocketConnectHost] = connectParams.host;
138140
socket[symbols.kSocketConnectPort] = connectParams.port;
139-
debug('[%s] Socket#%d connected (sock: %o)', name, socket[symbols.kSocketId], formatSocket(socket));
141+
debug('[%s] Socket#%d connected (sock: %j)', name, socket[symbols.kSocketId], formatSocket(socket));
140142
});
141143

142144
// This message is published right before the first byte of the request is written to the socket.
143145
subscribe('undici:client:sendHeaders', (message, name) => {
144146
const { request, socket } = message as DiagnosticsChannel.ClientSendHeadersMessage & { socket: SocketExtend };
145-
const opaque = getRequestOpaque(request, kHandler);
147+
const opaque = Reflect.get(request, symbols.kRequestInternalOpaque);
146148
if (!opaque || !opaque[symbols.kRequestId]) {
147149
debug('[%s] opaque not found', name);
148150
return;
@@ -151,7 +153,7 @@ export function initDiagnosticsChannel() {
151153
(socket[symbols.kHandledRequests] as number)++;
152154
// attach socket to opaque
153155
opaque[symbols.kRequestSocket] = socket;
154-
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
156+
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %j)',
155157
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests],
156158
formatSocket(socket));
157159

@@ -167,7 +169,7 @@ export function initDiagnosticsChannel() {
167169

168170
subscribe('undici:request:bodySent', (message, name) => {
169171
const { request } = message as DiagnosticsChannel.RequestBodySentMessage;
170-
const opaque = getRequestOpaque(request, kHandler);
172+
const opaque = Reflect.get(request, symbols.kRequestInternalOpaque);
171173
if (!opaque || !opaque[symbols.kRequestId]) {
172174
debug('[%s] opaque not found', name);
173175
return;
@@ -181,7 +183,7 @@ export function initDiagnosticsChannel() {
181183
// This message is published after the response headers have been received, i.e. the response has been completed.
182184
subscribe('undici:request:headers', (message, name) => {
183185
const { request, response } = message as DiagnosticsChannel.RequestHeadersMessage;
184-
const opaque = getRequestOpaque(request, kHandler);
186+
const opaque = Reflect.get(request, symbols.kRequestInternalOpaque);
185187
if (!opaque || !opaque[symbols.kRequestId]) {
186188
debug('[%s] opaque not found', name);
187189
return;
@@ -191,7 +193,7 @@ export function initDiagnosticsChannel() {
191193
const socket = opaque[symbols.kRequestSocket];
192194
if (socket) {
193195
socket[symbols.kHandledResponses]++;
194-
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)',
196+
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %j)',
195197
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses],
196198
formatSocket(socket));
197199
} else {
@@ -206,7 +208,7 @@ export function initDiagnosticsChannel() {
206208
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
207209
subscribe('undici:request:trailers', (message, name) => {
208210
const { request } = message as DiagnosticsChannel.RequestTrailersMessage;
209-
const opaque = getRequestOpaque(request, kHandler);
211+
const opaque = Reflect.get(request, symbols.kRequestInternalOpaque);
210212
if (!opaque || !opaque[symbols.kRequestId]) {
211213
debug('[%s] opaque not found', name);
212214
return;

src/fetch.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { AsyncLocalStorage } from 'node:async_hooks';
2+
import { debuglog } from 'node:util';
23
import {
34
fetch as UndiciFetch,
45
RequestInfo,
@@ -41,6 +42,8 @@ import { RawResponseWithMeta, SocketInfo } from './Response.js';
4142
import { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
4243
import { BaseAgent, BaseAgentOptions } from './BaseAgent.js';
4344

45+
const debug = debuglog('urllib:fetch');
46+
4447
export interface UrllibRequestInit extends RequestInit {
4548
// default is true
4649
timing?: boolean;
@@ -137,7 +140,7 @@ export class FetchFactory {
137140
// socket assigned
138141
queuing: 0,
139142
// dns lookup time
140-
// dnslookup: 0,
143+
dnslookup: 0,
141144
// socket connected
142145
connected: 0,
143146
// request headers sent
@@ -218,8 +221,9 @@ export class FetchFactory {
218221
res = await UndiciFetch(input, init);
219222
});
220223
} catch (e: any) {
221-
updateSocketInfo(socketInfo, internalOpaque /* , rawError */);
224+
updateSocketInfo(socketInfo, internalOpaque, e);
222225
urllibResponse.rt = performanceTime(requestStartTime);
226+
debug('Request#%d throw error: %s', requestId, e);
223227
channels.fetchResponse.publish({
224228
fetch: fetchMeta,
225229
error: e,
@@ -234,7 +238,7 @@ export class FetchFactory {
234238

235239
// get undici internal response
236240
const state = getResponseState(res!);
237-
updateSocketInfo(socketInfo, internalOpaque /* , rawError */);
241+
updateSocketInfo(socketInfo, internalOpaque);
238242

239243
urllibResponse.headers = convertHeader(res!.headers);
240244
urllibResponse.status = urllibResponse.statusCode = res!.status;
@@ -243,7 +247,8 @@ export class FetchFactory {
243247
urllibResponse.size = parseInt(urllibResponse.headers['content-length']);
244248
}
245249
urllibResponse.rt = performanceTime(requestStartTime);
246-
250+
debug('Request#%d got response, status: %s, headers: %j, timing: %j, socket: %j',
251+
requestId, urllibResponse.status, urllibResponse.headers, timing, urllibResponse.socket);
247252
channels.fetchResponse.publish({
248253
fetch: fetchMeta,
249254
timingInfo: state.timingInfo,

src/symbols.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@ export default {
1717
kEnableRequestTiming: Symbol('enable request timing or not'),
1818
kRequestTiming: Symbol('request timing'),
1919
kRequestOriginalOpaque: Symbol('request original opaque'),
20+
kRequestInternalOpaque: Symbol('request internal opaque'),
2021
kErrorSocket: Symbol('socket of error'),
2122
};

test/fetch.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import assert from 'node:assert/strict';
22
import diagnosticsChannel from 'node:diagnostics_channel';
3+
import { setTimeout as sleep } from 'node:timers/promises';
34
import { describe, it, beforeAll, afterAll } from 'vitest';
45
import { startServer } from './fixtures/server.js';
56
import {
@@ -20,7 +21,6 @@ describe('fetch.test.ts', () => {
2021
await close();
2122
});
2223

23-
2424
it('fetch should work', async () => {
2525
let requestDiagnosticsMessage: RequestDiagnosticsMessage;
2626
let responseDiagnosticsMessage: ResponseDiagnosticsMessage;
@@ -40,19 +40,27 @@ describe('fetch.test.ts', () => {
4040
});
4141
FetchFactory.setClientOptions({});
4242

43-
const response = await fetch(`${_url}html`);
43+
let response = await fetch(`${_url}html`);
4444

4545
assert(response);
4646
assert(requestDiagnosticsMessage!.request);
4747
assert(responseDiagnosticsMessage!.request);
4848
assert(responseDiagnosticsMessage!.response);
49+
assert(responseDiagnosticsMessage!.response.socket.localAddress);
4950
assert([ '127.0.0.1', '::1' ].includes(responseDiagnosticsMessage!.response.socket.localAddress));
5051

5152
assert(fetchDiagnosticsMessage!.fetch);
5253
assert(fetchResponseDiagnosticsMessage!.fetch);
5354
assert(fetchResponseDiagnosticsMessage!.response);
5455
assert(fetchResponseDiagnosticsMessage!.timingInfo);
5556

57+
await sleep(1);
58+
// again, keep alive
59+
response = await fetch(`${_url}html`);
60+
// console.log(responseDiagnosticsMessage!.response.socket);
61+
assert(responseDiagnosticsMessage!.response.socket.handledRequests > 1);
62+
assert(responseDiagnosticsMessage!.response.socket.handledResponses > 1);
63+
5664
const stats = FetchFactory.getDispatcherPoolStats();
5765
assert(stats);
5866
assert(Object.keys(stats).length > 0);
@@ -77,17 +85,21 @@ describe('fetch.test.ts', () => {
7785
});
7886
FetchFactory.setClientOptions({});
7987

80-
try {
88+
await assert.rejects(async () => {
8189
await fetch(`${_url}html?timeout=9999`, {
8290
signal: AbortSignal.timeout(100),
8391
});
84-
} catch (error) {
85-
console.log(error);
86-
}
92+
}, (err: any) => {
93+
assert.equal(err.name, 'TimeoutError');
94+
assert.equal(err.message, 'The operation was aborted due to timeout');
95+
return true;
96+
});
8797

8898
assert(requestDiagnosticsMessage!.request);
8999
assert(responseDiagnosticsMessage!.request);
90100
assert(responseDiagnosticsMessage!.response);
101+
// console.log(responseDiagnosticsMessage!.response.socket);
102+
assert(responseDiagnosticsMessage!.response.socket.localAddress);
91103
assert([ '127.0.0.1', '::1' ].includes(responseDiagnosticsMessage!.response.socket.localAddress));
92104

93105
assert(fetchDiagnosticsMessage!.fetch);

test/options.timing.test.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ describe('options.timing.test.ts', () => {
3232
assert(res.timing.contentDownload > 0);
3333
assert(res.timing.contentDownload > res.timing.waiting);
3434
assert(res.timing.contentDownload <= res.rt);
35-
assert(res.socket.handledResponses === 1);
35+
assert.equal(res.socket.handledResponses, 1);
3636

3737
// again connected should be zero
3838
await sleep(1);
@@ -45,13 +45,14 @@ describe('options.timing.test.ts', () => {
4545
res = response.res as RawResponseWithMeta;
4646
assert(res.timing.waiting > 0);
4747
assert(res.timing.queuing > 0);
48-
assert(res.timing.connected === 0);
48+
assert.equal(res.timing.connected, 0);
4949
assert(res.timing.requestHeadersSent > 0);
5050
assert(res.timing.requestSent > 0);
5151
assert(res.timing.contentDownload > 0);
5252
assert(res.timing.contentDownload > res.timing.waiting);
5353
assert(res.timing.contentDownload <= res.rt);
54-
assert(res.socket.handledResponses === 2);
54+
assert.equal(res.socket.handledResponses, 2);
55+
// console.log(res.timing);
5556
});
5657

5758
it('should timing = false work', async () => {

0 commit comments

Comments
 (0)