Skip to content

Commit 55a634c

Browse files
authored
feat: impl fetch (#542)
- keep urllib:request, urllib:response channel message - add urllib:fetch:request, urllib:fetch:response channel message <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes - **New Features** - Introduced a `FetchFactory` class for enhanced HTTP request management with diagnostics. - Added a new `FetchOpaque` type for tracking request metadata. - Enhanced `HttpClient` with new timing and diagnostics capabilities. - **Bug Fixes** - Improved error handling for socket-related issues in `HttpClient`. - **Documentation** - Expanded public exports for better usability, including new interfaces and constants. - **Tests** - Added unit tests for the new `fetch` functionality to ensure reliability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 7c8aff0 commit 55a634c

File tree

8 files changed

+461
-47
lines changed

8 files changed

+461
-47
lines changed

src/FetchOpaqueInterceptor.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// const { AsyncLocalStorage } = require('node:async_hooks');
2+
import { AsyncLocalStorage } from 'node:async_hooks';
3+
import symbols from './symbols.js';
4+
import { Dispatcher } from 'undici';
5+
6+
// const RedirectHandler = require('../handler/redirect-handler')
7+
8+
export interface FetchOpaque {
9+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
10+
// @ts-ignore
11+
[symbols.kRequestId]: number;
12+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
13+
// @ts-ignore
14+
[symbols.kRequestStartTime]: number;
15+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
16+
// @ts-ignore
17+
[symbols.kEnableRequestTiming]: number;
18+
}
19+
20+
// const internalOpaque = {
21+
// [symbols.kRequestId]: requestId,
22+
// [symbols.kRequestStartTime]: requestStartTime,
23+
// [symbols.kEnableRequestTiming]: !!(init.timing ?? true),
24+
// [symbols.kRequestTiming]: timing,
25+
// // [symbols.kRequestOriginalOpaque]: originalOpaque,
26+
// };
27+
28+
export interface OpaqueInterceptorOptions {
29+
opaqueLocalStorage: AsyncLocalStorage<FetchOpaque>;
30+
}
31+
32+
export function fetchOpaqueInterceptor(opts: OpaqueInterceptorOptions) {
33+
const opaqueLocalStorage = opts?.opaqueLocalStorage;
34+
return (dispatch: Dispatcher['dispatch']): Dispatcher['dispatch'] => {
35+
return function redirectInterceptor(opts: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandlers) {
36+
const opaque = opaqueLocalStorage?.getStore();
37+
(handler as any).opaque = opaque;
38+
return dispatch(opts, handler);
39+
};
40+
};
41+
}

src/HttpAgent.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import {
88

99
export type CheckAddressFunction = (ip: string, family: number | string, hostname: string) => boolean;
1010

11-
export type HttpAgentOptions = {
11+
export interface HttpAgentOptions extends Agent.Options {
1212
lookup?: LookupFunction;
1313
checkAddress?: CheckAddressFunction;
1414
connect?: buildConnector.BuildOptions,
1515
allowH2?: boolean;
16-
};
16+
}
1717

1818
class IllegalAddressError extends Error {
1919
hostname: string;
@@ -36,9 +36,10 @@ export class HttpAgent extends Agent {
3636

3737
constructor(options: HttpAgentOptions) {
3838
/* eslint node/prefer-promises/dns: off*/
39-
const _lookup = options.lookup ?? dns.lookup;
40-
const lookup: LookupFunction = (hostname, dnsOptions, callback) => {
41-
_lookup(hostname, dnsOptions, (err, ...args: any[]) => {
39+
const { lookup = dns.lookup, ...baseOpts } = options;
40+
41+
const lookupFunction: LookupFunction = (hostname, dnsOptions, callback) => {
42+
lookup(hostname, dnsOptions, (err, ...args: any[]) => {
4243
// address will be array on Node.js >= 20
4344
const address = args[0];
4445
const family = args[1];
@@ -63,7 +64,8 @@ export class HttpAgent extends Agent {
6364
});
6465
};
6566
super({
66-
connect: { ...options.connect, lookup, allowH2: options.allowH2 },
67+
...baseOpts,
68+
connect: { ...options.connect, lookup: lookupFunction, allowH2: options.allowH2 },
6769
});
6870
this.#checkAddress = options.checkAddress;
6971
}

src/HttpClient.ts

Lines changed: 28 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ import { HttpAgent, CheckAddressFunction } from './HttpAgent.js';
3737
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
3838
import { RequestURL, RequestOptions, HttpMethod, RequestMeta } from './Request.js';
3939
import { RawResponseWithMeta, HttpClientResponse, SocketInfo } from './Response.js';
40-
import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable } from './utils.js';
40+
import { parseJSON, digestAuthHeader, globalId, performanceTime, isReadable, updateSocketInfo } from './utils.js';
4141
import symbols from './symbols.js';
4242
import { initDiagnosticsChannel } from './diagnosticsChannel.js';
4343
import { HttpClientConnectTimeoutError, HttpClientRequestTimeoutError } from './HttpClientError.js';
@@ -47,7 +47,28 @@ type UndiciRequestOption = Exists<Parameters<typeof undiciRequest>[1]>;
4747
type PropertyShouldBe<T, K extends keyof T, V> = Omit<T, K> & { [P in K]: V };
4848
type IUndiciRequestOption = PropertyShouldBe<UndiciRequestOption, 'headers', IncomingHttpHeaders>;
4949

50-
const PROTO_RE = /^https?:\/\//i;
50+
export const PROTO_RE = /^https?:\/\//i;
51+
52+
export interface UnidiciTimingInfo {
53+
startTime: number;
54+
redirectStartTime: number;
55+
redirectEndTime: number;
56+
postRedirectStartTime: number;
57+
finalServiceWorkerStartTime: number;
58+
finalNetworkResponseStartTime: number;
59+
finalNetworkRequestStartTime: number;
60+
endTime: number;
61+
encodedBodySize: number;
62+
decodedBodySize: number;
63+
finalConnectionTimingInfo: {
64+
domainLookupStartTime: number;
65+
domainLookupEndTime: number;
66+
connectionStartTime: number;
67+
connectionEndTime: number;
68+
secureConnectionStartTime: number;
69+
// ALPNNegotiatedProtocol: undefined
70+
};
71+
}
5172

5273
function noop() {
5374
// noop
@@ -137,9 +158,11 @@ export type RequestContext = {
137158
requestStartTime?: number;
138159
};
139160

140-
const channels = {
161+
export const channels = {
141162
request: diagnosticsChannel.channel('urllib:request'),
142163
response: diagnosticsChannel.channel('urllib:response'),
164+
fetchRequest: diagnosticsChannel.channel('urllib:fetch:request'),
165+
fetchResponse: diagnosticsChannel.channel('urllib:fetch:response'),
143166
};
144167

145168
export type RequestDiagnosticsMessage = {
@@ -631,7 +654,7 @@ export class HttpClient extends EventEmitter {
631654
}
632655
res.rt = performanceTime(requestStartTime);
633656
// get real socket info from internalOpaque
634-
this.#updateSocketInfo(socketInfo, internalOpaque);
657+
updateSocketInfo(socketInfo, internalOpaque);
635658

636659
const clientResponse: HttpClientResponse = {
637660
opaque: originalOpaque,
@@ -707,7 +730,7 @@ export class HttpClient extends EventEmitter {
707730
res.requestUrls.push(requestUrl.href);
708731
}
709732
res.rt = performanceTime(requestStartTime);
710-
this.#updateSocketInfo(socketInfo, internalOpaque, rawError);
733+
updateSocketInfo(socketInfo, internalOpaque, rawError);
711734

712735
channels.response.publish({
713736
request: reqMeta,
@@ -729,40 +752,4 @@ export class HttpClient extends EventEmitter {
729752
throw err;
730753
}
731754
}
732-
733-
#updateSocketInfo(socketInfo: SocketInfo, internalOpaque: any, err?: any) {
734-
const socket = internalOpaque[symbols.kRequestSocket] ?? err?.[symbols.kErrorSocket];
735-
if (socket) {
736-
socketInfo.id = socket[symbols.kSocketId];
737-
socketInfo.handledRequests = socket[symbols.kHandledRequests];
738-
socketInfo.handledResponses = socket[symbols.kHandledResponses];
739-
if (socket[symbols.kSocketLocalAddress]) {
740-
socketInfo.localAddress = socket[symbols.kSocketLocalAddress];
741-
socketInfo.localPort = socket[symbols.kSocketLocalPort];
742-
}
743-
if (socket.remoteAddress) {
744-
socketInfo.remoteAddress = socket.remoteAddress;
745-
socketInfo.remotePort = socket.remotePort;
746-
socketInfo.remoteFamily = socket.remoteFamily;
747-
}
748-
socketInfo.bytesRead = socket.bytesRead;
749-
socketInfo.bytesWritten = socket.bytesWritten;
750-
if (socket[symbols.kSocketConnectErrorTime]) {
751-
socketInfo.connectErrorTime = socket[symbols.kSocketConnectErrorTime];
752-
if (Array.isArray(socket.autoSelectFamilyAttemptedAddresses)) {
753-
socketInfo.attemptedRemoteAddresses = socket.autoSelectFamilyAttemptedAddresses;
754-
}
755-
socketInfo.connectProtocol = socket[symbols.kSocketConnectProtocol];
756-
socketInfo.connectHost = socket[symbols.kSocketConnectHost];
757-
socketInfo.connectPort = socket[symbols.kSocketConnectPort];
758-
}
759-
if (socket[symbols.kSocketConnectedTime]) {
760-
socketInfo.connectedTime = socket[symbols.kSocketConnectedTime];
761-
}
762-
if (socket[symbols.kSocketRequestEndTime]) {
763-
socketInfo.lastRequestEndTime = socket[symbols.kSocketRequestEndTime];
764-
}
765-
socket[symbols.kSocketRequestEndTime] = new Date();
766-
}
767-
}
768755
}

src/Request.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { EventEmitter } from 'node:events';
33
import type { Dispatcher } from 'undici';
44
import type { IncomingHttpHeaders } from './IncomingHttpHeaders.js';
55
import type { HttpClientResponse } from './Response.js';
6+
import { Request } from 'undici';
67

78
export type HttpMethod = Dispatcher.HttpMethod;
89

@@ -161,3 +162,8 @@ export type RequestMeta = {
161162
ctx?: unknown;
162163
retries: number;
163164
};
165+
166+
export type FetchMeta = {
167+
requestId: number;
168+
request: Request,
169+
};

0 commit comments

Comments
 (0)