urllib-next
Version:
Help in opening URLs (mostly HTTP) in a complex world — basic and digest authentication, redirections, cookies and more. Base undici fetch API.
204 lines (203 loc) • 10.1 kB
JavaScript
import diagnosticsChannel from 'node:diagnostics_channel';
import { performance } from 'node:perf_hooks';
import { debuglog } from 'node:util';
import { Socket } from 'node:net';
import symbols from './symbols.js';
import { globalId, performanceTime } from './utils.js';
const debug = debuglog('urllib:DiagnosticsChannel');
let initedDiagnosticsChannel = false;
// https://undici.nodejs.org/#/docs/api/DiagnosticsChannel
// client --> server
// undici:request:create => { request }
// -> [optional] undici:client:connected => { socket } [first request will create socket]
// -> undici:client:sendHeaders => { socket, request }
// -> undici:request:bodySent => { request }
//
// server --> client
// undici:request:headers => { request, response }
// -> undici:request:trailers => { request, trailers }
function subscribe(name, listener) {
if (typeof diagnosticsChannel.subscribe === 'function') {
diagnosticsChannel.subscribe(name, listener);
}
else {
// TODO: support Node.js 14, will be removed on the next major version
diagnosticsChannel.channel(name).subscribe(listener);
}
}
function formatSocket(socket) {
if (!socket)
return socket;
return {
localAddress: socket[symbols.kSocketLocalAddress],
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) {
if (err) {
Object.defineProperty(err, symbols.kErrorSocket, {
// don't show on console log
enumerable: false,
value: this,
});
}
return this[kDestroy](err);
};
function getRequestOpaque(request, kHandler) {
if (!kHandler)
return;
const handler = request[kHandler];
// maxRedirects = 0 will get [Symbol(handler)]: RequestHandler {
// responseHeaders: null,
// opaque: {
// [Symbol(request id)]: 1,
// [Symbol(request start time)]: 465.0712921619415,
// [Symbol(enable request timing or not)]: true,
// [Symbol(request timing)]: [Object],
// [Symbol(request original opaque)]: undefined
// }
return handler?.opts?.opaque ?? handler?.opaque;
}
export function initDiagnosticsChannel() {
// makre sure init global DiagnosticsChannel once
if (initedDiagnosticsChannel)
return;
initedDiagnosticsChannel = true;
let kHandler;
// This message is published when a new outgoing request is created.
// Note: a request is only loosely completed to a given socket.
subscribe('undici:request:create', (message, name) => {
const { request } = message;
if (!kHandler) {
const symbols = Object.getOwnPropertySymbols(request);
for (const symbol of symbols) {
if (symbol.description === 'handler') {
kHandler = symbol;
break;
}
}
}
const opaque = getRequestOpaque(request, kHandler);
// ignore non HttpClient Request
if (!opaque || !opaque[symbols.kRequestId])
return;
debug('[%s] Request#%d %s %s, path: %s, headers: %o', name, opaque[symbols.kRequestId], request.method, request.origin, request.path, request.headers);
if (!opaque[symbols.kEnableRequestTiming])
return;
opaque[symbols.kRequestTiming].queuing = performanceTime(opaque[symbols.kRequestStartTime]);
});
// diagnosticsChannel.channel('undici:client:beforeConnect')
subscribe('undici:client:connectError', (message, name) => {
const { error, connectParams } = message;
let { socket } = message;
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;
}
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.message, formatSocket(socket));
}
else {
debug('[%s] connectError, connectParams: %o, error: %o', name, connectParams, error);
}
});
// This message is published after a connection is established.
subscribe('undici:client:connected', (message, name) => {
const { socket, connectParams } = message;
socket[symbols.kSocketId] = globalId('UndiciSocket');
socket[symbols.kSocketStartTime] = performance.now();
socket[symbols.kSocketConnectedTime] = new Date();
socket[symbols.kHandledRequests] = 0;
socket[symbols.kHandledResponses] = 0;
// 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));
});
// This message is published right before the first byte of the request is written to the socket.
subscribe('undici:client:sendHeaders', (message, name) => {
const { request, socket } = message;
const opaque = getRequestOpaque(request, kHandler);
if (!opaque || !opaque[symbols.kRequestId])
return;
socket[symbols.kHandledRequests]++;
// attach socket to opaque
opaque[symbols.kRequestSocket] = socket;
debug('[%s] Request#%d send headers on Socket#%d (handled %d requests, sock: %o)', name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledRequests], formatSocket(socket));
if (!opaque[symbols.kEnableRequestTiming])
return;
opaque[symbols.kRequestTiming].requestHeadersSent = performanceTime(opaque[symbols.kRequestStartTime]);
// first socket need to caculate the connected time
if (socket[symbols.kHandledRequests] === 1) {
// kSocketStartTime - kRequestStartTime = connected time
opaque[symbols.kRequestTiming].connected =
performanceTime(opaque[symbols.kRequestStartTime], socket[symbols.kSocketStartTime]);
}
});
subscribe('undici:request:bodySent', (message, name) => {
const { request } = message;
const opaque = getRequestOpaque(request, kHandler);
if (!opaque || !opaque[symbols.kRequestId])
return;
debug('[%s] Request#%d send body', name, opaque[symbols.kRequestId]);
if (!opaque[symbols.kEnableRequestTiming])
return;
opaque[symbols.kRequestTiming].requestSent = performanceTime(opaque[symbols.kRequestStartTime]);
});
// This message is published after the response headers have been received, i.e. the response has been completed.
subscribe('undici:request:headers', (message, name) => {
const { request, response } = message;
const opaque = getRequestOpaque(request, kHandler);
if (!opaque || !opaque[symbols.kRequestId])
return;
// get socket from opaque
const socket = opaque[symbols.kRequestSocket];
socket[symbols.kHandledResponses]++;
debug('[%s] Request#%d get %s response headers on Socket#%d (handled %d responses, sock: %o)', name, opaque[symbols.kRequestId], response.statusCode, socket[symbols.kSocketId], socket[symbols.kHandledResponses], formatSocket(socket));
if (!opaque[symbols.kEnableRequestTiming])
return;
opaque[symbols.kRequestTiming].waiting = performanceTime(opaque[symbols.kRequestStartTime]);
});
// This message is published after the response body and trailers have been received, i.e. the response has been completed.
subscribe('undici:request:trailers', (message, name) => {
const { request } = message;
const opaque = getRequestOpaque(request, kHandler);
if (!opaque || !opaque[symbols.kRequestId])
return;
debug('[%s] Request#%d get response body and trailers', name, opaque[symbols.kRequestId]);
if (!opaque[symbols.kEnableRequestTiming])
return;
opaque[symbols.kRequestTiming].contentDownload = performanceTime(opaque[symbols.kRequestStartTime]);
});
// This message is published if the request is going to error, but it has not errored yet.
// subscribe('undici:request:error', (message, name) => {
// const { request, error } = message as DiagnosticsChannel.RequestErrorMessage;
// const opaque = request[kHandler]?.opts?.opaque;
// if (!opaque || !opaque[symbols.kRequestId]) return;
// const socket = opaque[symbols.kRequestSocket];
// debug('[%s] Request#%d error on Socket#%d (handled %d responses, sock: %o), error: %o',
// name, opaque[symbols.kRequestId], socket[symbols.kSocketId], socket[symbols.kHandledResponses],
// formatSocket(socket), error);
// });
}