Skip to content

Commit 9a98389

Browse files
committed
f
1 parent 7f4fd62 commit 9a98389

File tree

10 files changed

+109
-94
lines changed

10 files changed

+109
-94
lines changed

src/HttpClient.ts

Lines changed: 30 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ import { FormData } from './FormData.js';
3636
import { HttpAgent, CheckAddressFunction } from './HttpAgent.js';
3737
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
3838
import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request.js';
39-
import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response.js';
39+
import { RawResponseWithMeta, HttpClientResponse, SocketInfo, InternalStore } from './Response.js';
4040
import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable, updateSocketInfo } from './utils.js';
41-
import symbols from './symbols.js';
4241
import { initDiagnosticsChannel } from './diagnosticsChannel.js';
4342
import { HttpClientConnectTimeoutError, HttpClientRequestTimeoutError } from './HttpClientError.js';
44-
import { asyncLocalStorage } from './asyncLocalStorage2.js';
43+
import { asyncLocalStorage } from './asyncLocalStorage.js';
4544

4645
type Exists<T> = T extends undefined ? never : T;
4746
type UndiciRequestOption = Exists<Parameters<typeof undiciRequest>[1]>;
@@ -137,7 +136,6 @@ function defaultIsRetry(response: HttpClientResponse) {
137136
export type RequestContext = {
138137
retries: number;
139138
socketErrorRetries: number;
140-
requestStartTime?: number;
141139
redirects: number;
142140
history: string[];
143141
};
@@ -247,7 +245,16 @@ export class HttpClient extends EventEmitter {
247245
}
248246

249247
async request<T = any>(url: RequestURL, options?: RequestOptions) {
250-
return await this.#requestInternal<T>(url, options);
248+
// using opaque to diagnostics channel, binding request and socket
249+
const requestId = globalId('HttpClientRequest');
250+
const internalStore = {
251+
requestId,
252+
requestStartTime: performance.now(),
253+
enableRequestTiming: !!options?.timing,
254+
} as InternalStore;
255+
return await asyncLocalStorage.run(internalStore, async () => {
256+
return await this.#requestInternal<T>(url, options);
257+
});
251258
}
252259

253260
// alias to request, keep compatible with urllib@2 HttpClient.curl
@@ -259,12 +266,12 @@ export class HttpClient extends EventEmitter {
259266
return [
260267
(dispatch: any) => {
261268
return function dnsAfterInterceptor(options: any, handler: any) {
262-
const opaque = options.opaque;
263-
if (opaque?.[symbols.kEnableRequestTiming]) {
264-
const dnslookup = opaque[symbols.kRequestTiming].dnslookup =
265-
performanceTime(opaque[symbols.kRequestStartTime]);
269+
const store = asyncLocalStorage.getStore();
270+
if (store?.enableRequestTiming) {
271+
const dnslookup = store.requestTiming.dnslookup =
272+
performanceTime(store.requestStartTime);
266273
debug('Request#%d dns lookup %sms, servername: %s, origin: %s',
267-
opaque[symbols.kRequestId], dnslookup, options.servername, options.origin);
274+
store.requestId, dnslookup, options.servername, options.origin);
268275
}
269276
return dispatch(options, handler);
270277
};
@@ -274,7 +281,6 @@ export class HttpClient extends EventEmitter {
274281
}
275282

276283
async #requestInternal<T>(url: RequestURL, options?: RequestOptions, requestContext?: RequestContext): Promise<HttpClientResponse<T>> {
277-
const requestId = globalId('HttpClientRequest');
278284
let requestUrl: URL;
279285
if (typeof url === 'string') {
280286
if (!PROTO_RE.test(url)) {
@@ -298,7 +304,6 @@ export class HttpClient extends EventEmitter {
298304
const args = {
299305
retry: 0,
300306
socketErrorRetry: 1,
301-
timing: true,
302307
...this.#defaultArgs,
303308
...options,
304309
// keep method and headers exists on args for request event handler to easy use
@@ -312,12 +317,11 @@ export class HttpClient extends EventEmitter {
312317
history: [],
313318
...requestContext,
314319
};
315-
if (!requestContext.requestStartTime) {
316-
requestContext.requestStartTime = performance.now();
317-
}
318320
requestContext.history.push(requestUrl.href);
319-
const requestStartTime = requestContext.requestStartTime;
320321

322+
const internalStore = asyncLocalStorage.getStore()!;
323+
const requestStartTime = internalStore.requestStartTime;
324+
const requestId = internalStore.requestId;
321325
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
322326
const timing = {
323327
// socket assigned
@@ -335,15 +339,9 @@ export class HttpClient extends EventEmitter {
335339
// the response body and trailers have been received
336340
contentDownload: 0,
337341
};
342+
internalStore.requestTiming = timing;
343+
internalStore.enableRequestTiming = !!args.timing;
338344
const originalOpaque = args.opaque;
339-
// using opaque to diagnostics channel, binding request and socket
340-
const internalOpaque = {
341-
[symbols.kRequestId]: requestId,
342-
[symbols.kRequestStartTime]: requestStartTime,
343-
[symbols.kEnableRequestTiming]: !!args.timing,
344-
[symbols.kRequestTiming]: timing,
345-
[symbols.kRequestOriginalOpaque]: originalOpaque,
346-
};
347345
const reqMeta = {
348346
requestId,
349347
url: requestUrl.href,
@@ -447,7 +445,7 @@ export class HttpClient extends EventEmitter {
447445
headersTimeout,
448446
headers,
449447
bodyTimeout,
450-
opaque: internalOpaque,
448+
opaque: originalOpaque,
451449
dispatcher: args.dispatcher ?? this.#dispatcher,
452450
signal: args.signal,
453451
};
@@ -592,7 +590,9 @@ export class HttpClient extends EventEmitter {
592590
}
593591

594592
debug('Request#%d %s %s, headers: %j, headersTimeout: %s, bodyTimeout: %s, isStreamingRequest: %s, isStreamingResponse: %s, maxRedirections: %s, redirects: %s',
595-
requestId, requestOptions.method, requestUrl.href, headers, headersTimeout, bodyTimeout, isStreamingRequest, isStreamingResponse, maxRedirects, requestContext.redirects);
593+
requestId, requestOptions.method, requestUrl.href, headers,
594+
headersTimeout, bodyTimeout, isStreamingRequest, isStreamingResponse,
595+
maxRedirects, requestContext.redirects);
596596
requestOptions.headers = headers;
597597
channels.request.publish({
598598
request: reqMeta,
@@ -601,7 +601,7 @@ export class HttpClient extends EventEmitter {
601601
this.emit('request', reqMeta);
602602
}
603603

604-
let response = await this.#undiciRequest(internalOpaque, requestUrl, requestOptions as UndiciRequestOption);
604+
let response = await undiciRequest(requestUrl, requestOptions as UndiciRequestOption);
605605
if (response.statusCode === 401
606606
&& (response.headers['www-authenticate'] || response.headers['x-www-authenticate'])
607607
&& !requestOptions.headers.authorization
@@ -622,7 +622,7 @@ export class HttpClient extends EventEmitter {
622622
}
623623
// Ensure the previous response is consumed as we re-use the same variable
624624
await response.body.arrayBuffer();
625-
response = await this.#undiciRequest(internalOpaque, requestUrl, requestOptions as UndiciRequestOption);
625+
response = await undiciRequest(requestUrl, requestOptions as UndiciRequestOption);
626626
}
627627
}
628628
const contentEncoding = response.headers['content-encoding'];
@@ -690,7 +690,7 @@ export class HttpClient extends EventEmitter {
690690
}
691691
res.rt = performanceTime(requestStartTime);
692692
// get real socket info from internalOpaque
693-
updateSocketInfo(socketInfo, internalOpaque);
693+
updateSocketInfo(socketInfo, internalStore);
694694

695695
const clientResponse: HttpClientResponse = {
696696
opaque: originalOpaque,
@@ -738,7 +738,7 @@ export class HttpClient extends EventEmitter {
738738

739739
return clientResponse;
740740
} catch (rawError: any) {
741-
updateSocketInfo(socketInfo, internalOpaque, rawError);
741+
updateSocketInfo(socketInfo, internalStore, rawError);
742742
debug('Request#%d throw error: %s, socketErrorRetry: %s, socketErrorRetries: %s, socket: %j',
743743
requestId, rawError, args.socketErrorRetry, requestContext.socketErrorRetries, socketInfo);
744744
let err = rawError;
@@ -790,10 +790,4 @@ export class HttpClient extends EventEmitter {
790790
throw err;
791791
}
792792
}
793-
794-
async #undiciRequest(internalOpaque: unknown, requestUrl: URL, requestOptions: UndiciRequestOption) {
795-
return await asyncLocalStorage.run(internalOpaque, async () => {
796-
return await undiciRequest(requestUrl, requestOptions);
797-
});
798-
}
799793
}

src/Response.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Readable } from 'node:stream';
2+
import type { Socket } from 'node:net';
23
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
34

45
export type SocketInfo = {
@@ -41,6 +42,14 @@ export type Timing = {
4142
contentDownload: number;
4243
};
4344

45+
export interface InternalStore {
46+
requestId: number;
47+
requestStartTime: number;
48+
enableRequestTiming: boolean;
49+
requestTiming: Timing;
50+
requestSocket?: Socket;
51+
}
52+
4453
export type RawResponseWithMeta = Readable & {
4554
status: number;
4655
statusCode: number;

src/asyncLocalStorage.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import { AsyncLocalStorage } from 'node:async_hooks';
2+
import type { InternalStore } from './Response.js';
3+
4+
export const asyncLocalStorage = new AsyncLocalStorage<InternalStore>();

src/asyncLocalStorage2.ts

Lines changed: 0 additions & 3 deletions
This file was deleted.

src/diagnosticsChannel.ts

Lines changed: 41 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Socket } from 'node:net';
55
import { DiagnosticsChannel } from 'undici';
66
import symbols from './symbols.js';
77
import { globalId, performanceTime } from './utils.js';
8-
import { asyncLocalStorage } from './asyncLocalStorage2.js';
8+
import { asyncLocalStorage } from './asyncLocalStorage.js';
99

1010
const debug = debuglog('urllib:DiagnosticsChannel');
1111
let initedDiagnosticsChannel = false;
@@ -62,17 +62,17 @@ export function initDiagnosticsChannel() {
6262
// Note: a request is only loosely completed to a given socket.
6363
subscribe('undici:request:create', (message, name) => {
6464
const { request } = message as DiagnosticsChannel.RequestCreateMessage;
65-
const opaque = asyncLocalStorage.getStore();
66-
if (!opaque?.[symbols.kRequestId]) {
67-
debug('[%s] opaque not found', name);
65+
const store = asyncLocalStorage.getStore();
66+
if (!store?.requestId) {
67+
debug('[%s] store not found', name);
6868
return;
6969
}
7070
let queuing = 0;
71-
if (opaque[symbols.kEnableRequestTiming]) {
72-
queuing = opaque[symbols.kRequestTiming].queuing = performanceTime(opaque[symbols.kRequestStartTime]);
71+
if (store.enableRequestTiming) {
72+
queuing = store.requestTiming.queuing = performanceTime(store.requestStartTime);
7373
}
7474
debug('[%s] Request#%d %s %s, path: %s, headers: %o, queuing: %d',
75-
name, opaque[symbols.kRequestId], request.method, request.origin, request.path,
75+
name, store.requestId, request.method, request.origin, request.path,
7676
request.headers, queuing);
7777
});
7878

@@ -123,80 +123,84 @@ export function initDiagnosticsChannel() {
123123
// This message is published right before the first byte of the request is written to the socket.
124124
subscribe('undici:client:sendHeaders', (message, name) => {
125125
const { socket } = message as DiagnosticsChannel.ClientSendHeadersMessage & { socket: SocketExtend };
126-
const opaque = asyncLocalStorage.getStore();
127-
if (!opaque?.[symbols.kRequestId]) {
128-
debug('[%s] opaque not found', name);
126+
const store = asyncLocalStorage.getStore();
127+
if (!store?.requestId) {
128+
debug('[%s] store not found', name);
129129
return;
130130
}
131131

132132
(socket[symbols.kHandledRequests] as number)++;
133-
// attach socket to opaque
134-
opaque[symbols.kRequestSocket] = socket;
133+
// attach socket to store
134+
store.requestSocket = socket;
135135
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)',
136-
name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests],
136+
name, store.requestId, socket[symbols.kSocketId], socket[symbols.kHandledRequests],
137137
formatSocket(socket));
138138

139-
if (!opaque[symbols.kEnableRequestTiming]) return;
140-
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
139+
if (!store.enableRequestTiming) return;
140+
store.requestTiming.requestHeadersSent = performanceTime(store.requestStartTime);
141141
// first socket need to calculate the connected time
142142
if (socket[symbols.kHandledRequests] === 1) {
143143
// kSocketStartTime - kRequestStartTime = connected time
144-
opaque[symbols.kRequestTiming].connected =
145-
performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime] as number);
144+
store.requestTiming.connected =
145+
performanceTime(store.requestStartTime, socket[symbols.kSocketStartTime] as number);
146146
}
147147
});
148148

149149
subscribe('undici:request:bodySent', (_message, name) => {
150150
// const { request } = message as DiagnosticsChannel.RequestBodySentMessage;
151-
const opaque = asyncLocalStorage.getStore();
152-
if (!opaque?.[symbols.kRequestId]) {
153-
debug('[%s] opaque not found', name);
151+
const store = asyncLocalStorage.getStore();
152+
if (!store?.requestId) {
153+
debug('[%s] store not found', name);
154154
return;
155155
}
156156

157-
debug('[%s] Request#%d send body', name, opaque[symbols.kRequestId]);
158-
if (!opaque[symbols.kEnableRequestTiming]) return;
159-
opaque[symbols.kRequestTiming].requestSent = performanceTime(opaque[symbols.kRequestStartTime]);
157+
debug('[%s] Request#%d send body', name, store.requestId);
158+
if (!store.enableRequestTiming) return;
159+
store.requestTiming.requestSent = performanceTime(store.requestStartTime);
160160
});
161161

162162
// This message is published after the response headers have been received, i.e. the response has been completed.
163163
subscribe('undici:request:headers', (message, name) => {
164164
const { response } = message as DiagnosticsChannel.RequestHeadersMessage;
165-
const opaque = asyncLocalStorage.getStore();
166-
if (!opaque?.[symbols.kRequestId]) {
167-
debug('[%s] opaque not found', name);
165+
const store = asyncLocalStorage.getStore();
166+
if (!store?.requestId) {
167+
debug('[%s] store not found', name);
168168
return;
169169
}
170170

171171
// get socket from opaque
172-
const socket = opaque[symbols.kRequestSocket];
172+
const socket = store.requestSocket as any;
173+
// console.log(name, opaque[symbols.kRequestId], formatSocket(socket), performanceTime(opaque[symbols.kRequestStartTime]), opaque[symbols.kEnableRequestTiming]);
173174
if (socket) {
174175
socket[symbols.kHandledResponses]++;
175176
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)',
176-
name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses],
177+
name, store.requestId, response.statusCode,
178+
socket[symbols.kSocketId], socket[symbols.kHandledResponses],
177179
formatSocket(socket));
178180
} else {
179181
debug('[%s] Request#%d get %s response headers on Unknown Socket',
180-
name, opaque[symbols.kRequestId], response.statusCode);
182+
name, store.requestId, response.statusCode);
181183
}
182184

183-
if (!opaque[symbols.kEnableRequestTiming]) return;
184-
opaque[symbols.kRequestTiming].waiting = performanceTime(opaque[symbols.kRequestStartTime]);
185+
if (!store.enableRequestTiming) return;
186+
// console.log(name, opaque[symbols.kRequestId], 'waiting', opaque[symbols.kRequestTiming]);
187+
store.requestTiming.waiting = performanceTime(store.requestStartTime);
188+
// console.log(name, opaque[symbols.kRequestId], 'waiting', opaque[symbols.kRequestTiming]);
185189
});
186190

187191
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
188192
subscribe('undici:request:trailers', (_message, name) => {
189193
// const { request } = message as DiagnosticsChannel.RequestTrailersMessage;
190-
const opaque = asyncLocalStorage.getStore();
191-
if (!opaque?.[symbols.kRequestId]) {
192-
debug('[%s] opaque not found', name);
194+
const store = asyncLocalStorage.getStore();
195+
if (!store?.requestId) {
196+
debug('[%s] store not found', name);
193197
return;
194198
}
195199

196-
debug('[%s] Request#%d get response body and trailers', name, opaque[symbols.kRequestId]);
200+
debug('[%s] Request#%d get response body and trailers', name, store.requestId);
197201

198-
if (!opaque[symbols.kEnableRequestTiming]) return;
199-
opaque[symbols.kRequestTiming].contentDownload = performanceTime(opaque[symbols.kRequestStartTime]);
202+
if (!store.enableRequestTiming) return;
203+
store.requestTiming.contentDownload = performanceTime(store.requestStartTime);
200204
});
201205

202206
// This message is published if the request is going to error, but it has not errored yet.

src/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
} from './Request.js';
3939
import { RawResponseWithMeta, SocketInfo } from './Response.js';
4040
import { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
41-
import { asyncLocalStorage } from './asyncLocalStorage2.js';
41+
import { asyncLocalStorage } from './asyncLocalStorage.js';
4242

4343
const debug = debuglog('urllib:fetch');
4444

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ export {
6868
} from './IncomingHttpHeaders.js';
6969
export * from './HttpClientError.js';
7070
export { FetchFactory, fetch } from './fetch.js';
71-
export { asyncLocalStorage } from './asyncLocalStorage2.js';
71+
export { asyncLocalStorage } from './asyncLocalStorage.js';
7272

7373
export default {
7474
request,

src/types.ts

Whitespace-only changes.

src/utils.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import { performance } from 'node:perf_hooks';
44
import { ReadableStream } from 'node:stream/web';
55
import { Blob } from 'node:buffer';
66
import type { FixJSONCtlChars } from './Request.js';
7-
import { SocketInfo } from './Response.js';
7+
import type { InternalStore, SocketInfo } from './Response.js';
88
import symbols from './symbols.js';
9-
import { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
9+
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
1010

1111
const JSONCtlCharsMap: Record<string, string> = {
1212
'"': '\\"', // \u0022
@@ -157,8 +157,8 @@ export function isReadable(stream: any) {
157157
&& typeof stream._readableState === 'object';
158158
}
159159

160-
export function updateSocketInfo(socketInfo: SocketInfo, internalOpaque: any, err?: any) {
161-
const socket = internalOpaque[symbols.kRequestSocket] ?? err?.[symbols.kErrorSocket];
160+
export function updateSocketInfo(socketInfo: SocketInfo, internalStore: InternalStore, err?: any) {
161+
const socket = internalStore.requestSocket ?? err?.[symbols.kErrorSocket];
162162

163163
if (socket) {
164164
socketInfo.id = socket[symbols.kSocketId];

0 commit comments

Comments
 (0)