Skip to content

Commit bd19f6d

Browse files
authored
fix: should got undici:client:sendHeaders message on H2 (#553)
closes #510 <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced error handling and logging for HTTP requests, improving traceability of socket-related issues. - Augmented response diagnostics to include socket information for better monitoring. - **Bug Fixes** - Improved resilience against transient socket issues with refined error handling and retry logic. - **Tests** - Introduced a new test case for tracing socket information with HTTP/2, ensuring accurate socket reuse tracking. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 333f3b8 commit bd19f6d

File tree

4 files changed

+148
-7
lines changed

4 files changed

+148
-7
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
"mime-types": "^2.1.35",
5151
"qs": "^6.12.1",
5252
"type-fest": "^4.20.1",
53-
"undici": "^7.0.0",
53+
"undici": "^7.1.1",
5454
"ylru": "^2.0.0"
5555
},
5656
"devDependencies": {

src/HttpClient.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -681,8 +681,8 @@ export class HttpClient extends EventEmitter {
681681
res,
682682
};
683683

684-
debug('Request#%d got response, status: %s, headers: %j, timing: %j',
685-
requestId, res.status, res.headers, res.timing);
684+
debug('Request#%d got response, status: %s, headers: %j, timing: %j, socket: %j',
685+
requestId, res.status, res.headers, res.timing, res.socket);
686686

687687
if (args.retry > 0 && requestContext.retries < args.retry) {
688688
const isRetry = args.isRetry ?? defaultIsRetry;

src/utils.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -173,13 +173,13 @@ export function updateSocketInfo(socketInfo: SocketInfo, internalOpaque: any, er
173173
socketInfo.remotePort = socket.remotePort;
174174
socketInfo.remoteFamily = socket.remoteFamily;
175175
}
176+
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
177+
socketInfo.attemptedRemoteAddresses = socket.autoSelectFamilyAttemptedAddresses;
178+
}
176179
socketInfo.bytesRead = socket.bytesRead;
177180
socketInfo.bytesWritten = socket.bytesWritten;
178181
if (socket[symbols.kSocketConnectErrorTime]) {
179182
socketInfo.connectErrorTime = socket[symbols.kSocketConnectErrorTime];
180-
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
181-
socketInfo.attemptedRemoteAddresses = socket.autoSelectFamilyAttemptedAddresses;
182-
}
183183
socketInfo.connectProtocol = socket[symbols.kSocketConnectProtocol];
184184
socketInfo.connectHost = socket[symbols.kSocketConnectHost];
185185
socketInfo.connectPort = socket[symbols.kSocketConnectPort];

test/diagnostics_channel.test.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { strict as assert } from 'node:assert';
22
import diagnosticsChannel from 'node:diagnostics_channel';
33
import { setTimeout as sleep } from 'node:timers/promises';
4+
import { createSecureServer } from 'node:http2';
5+
import { once } from 'node:events';
46
import { describe, it, beforeEach, afterEach } from 'vitest';
5-
import urllib from '../src/index.js';
7+
import selfsigned from 'selfsigned';
8+
import urllib, { HttpClient } from '../src/index.js';
69
import type {
710
RequestDiagnosticsMessage,
811
ResponseDiagnosticsMessage,
@@ -138,6 +141,144 @@ describe('diagnostics_channel.test.ts', () => {
138141
diagnosticsChannel.unsubscribe('undici:request:trailers', onMessage);
139142
});
140143

144+
it('should support trace socket info with H2 by undici:client:sendHeaders and undici:request:trailers', async () => {
145+
const pem = selfsigned.generate();
146+
const server = createSecureServer({
147+
key: pem.private,
148+
cert: pem.cert,
149+
});
150+
server.on('stream', (stream, headers) => {
151+
stream.respond({
152+
'content-type': 'text/plain; charset=utf-8',
153+
'x-custom-h2': 'hello',
154+
':status': 200,
155+
});
156+
if (headers[':method'] !== 'HEAD') {
157+
stream.end('hello h2!');
158+
}
159+
});
160+
161+
server.listen(0);
162+
await once(server, 'listening');
163+
164+
const kRequests = Symbol('requests');
165+
let lastRequestOpaque: any;
166+
let kHandler: any;
167+
function onMessage(message: any, name: string | symbol) {
168+
if (name === 'undici:client:connected') {
169+
// console.log('%s %j', name, message.connectParams);
170+
message.socket[kRequests] = 0;
171+
return;
172+
}
173+
const { request, socket } = message;
174+
if (!kHandler) {
175+
const symbols = Object.getOwnPropertySymbols(request);
176+
for (const symbol of symbols) {
177+
if (symbol.description === 'handler') {
178+
kHandler = symbol;
179+
break;
180+
}
181+
}
182+
}
183+
const handler = request[kHandler];
184+
let opaque = handler.opaque || handler.opts?.opaque;
185+
assert(opaque);
186+
opaque = opaque[symbols.kRequestOriginalOpaque];
187+
if (opaque && name === 'undici:client:sendHeaders' && socket) {
188+
socket[kRequests]++;
189+
opaque.tracer.socket = {
190+
localAddress: socket.localAddress,
191+
localPort: socket.localPort,
192+
remoteAddress: socket.remoteAddress,
193+
remotePort: socket.remotePort,
194+
remoteFamily: socket.remoteFamily,
195+
timeout: socket.timeout,
196+
bytesWritten: socket.bytesWritten,
197+
bytesRead: socket.bytesRead,
198+
requests: socket[kRequests],
199+
};
200+
}
201+
// console.log('%s emit, %s %s, opaque: %j', name, request.method, request.origin, opaque);
202+
lastRequestOpaque = opaque;
203+
// console.log(request);
204+
}
205+
diagnosticsChannel.subscribe('undici:client:connected', onMessage);
206+
diagnosticsChannel.subscribe('undici:client:sendHeaders', onMessage);
207+
diagnosticsChannel.subscribe('undici:request:trailers', onMessage);
208+
209+
const httpClient = new HttpClient({
210+
allowH2: true,
211+
connect: {
212+
rejectUnauthorized: false,
213+
},
214+
});
215+
216+
let traceId = `mock-traceid-${Date.now()}`;
217+
_url = `https://localhost:${server.address().port}`;
218+
let response = await httpClient.request(`${_url}?head=true`, {
219+
method: 'HEAD',
220+
opaque: {
221+
tracer: { traceId },
222+
},
223+
});
224+
assert.equal(response.status, 200);
225+
assert(response.url.startsWith(_url));
226+
assert(!response.redirected);
227+
assert.equal(lastRequestOpaque.tracer.traceId, traceId);
228+
assert(lastRequestOpaque.tracer.socket);
229+
assert.equal(lastRequestOpaque.tracer.socket.requests, 1);
230+
231+
// HEAD, GET 请求都走同一个 http2 session socket
232+
await sleep(1);
233+
traceId = `mock-traceid-${Date.now()}`;
234+
response = await httpClient.request(_url, {
235+
method: 'GET',
236+
opaque: {
237+
tracer: { traceId },
238+
},
239+
});
240+
assert.equal(response.status, 200);
241+
assert.equal(lastRequestOpaque.tracer.traceId, traceId);
242+
assert(lastRequestOpaque.tracer.socket);
243+
assert.equal(lastRequestOpaque.tracer.socket.requests, 2);
244+
245+
await sleep(1);
246+
traceId = `mock-traceid-${Date.now()}`;
247+
response = await httpClient.request(_url, {
248+
method: 'GET',
249+
opaque: {
250+
tracer: { traceId },
251+
},
252+
});
253+
assert.equal(response.status, 200);
254+
assert.equal(lastRequestOpaque.tracer.traceId, traceId);
255+
assert(lastRequestOpaque.tracer.socket);
256+
assert.equal(lastRequestOpaque.tracer.socket.requests, 3);
257+
258+
// socket 复用 1000 次
259+
let count = 1000;
260+
while (count-- > 0) {
261+
await sleep(1);
262+
traceId = `mock-traceid-${Date.now()}`;
263+
response = await httpClient.request(`${_url}?count=${count}`, {
264+
method: 'GET',
265+
opaque: {
266+
tracer: { traceId },
267+
},
268+
});
269+
assert.equal(response.status, 200);
270+
assert.equal(lastRequestOpaque.tracer.traceId, traceId);
271+
assert(lastRequestOpaque.tracer.socket);
272+
assert.equal(lastRequestOpaque.tracer.socket.requests, 3 + 1000 - count);
273+
}
274+
assert.equal(lastRequestOpaque.tracer.socket.requests, 1003);
275+
276+
diagnosticsChannel.unsubscribe('undici:client:connected', onMessage);
277+
diagnosticsChannel.unsubscribe('undici:client:sendHeaders', onMessage);
278+
diagnosticsChannel.unsubscribe('undici:request:trailers', onMessage);
279+
server.close();
280+
});
281+
141282
it('should support trace request by urllib:request and urllib:response', async () => {
142283
let lastRequestOpaque: any;
143284
let socket: any;

0 commit comments

Comments
 (0)