got
Version:
Human-friendly and powerful HTTP request library for Node.js
163 lines (162 loc) • 6.9 kB
JavaScript
import { errorMonitor } from 'node:events';
import { types } from 'node:util';
import deferToConnect from './defer-to-connect.js';
const timer = (request) => {
if (request.timings) {
return request.timings;
}
const timings = {
start: Date.now(),
socket: undefined,
lookup: undefined,
connect: undefined,
secureConnect: undefined,
upload: undefined,
response: undefined,
end: undefined,
error: undefined,
abort: undefined,
phases: {
wait: undefined,
dns: undefined,
tcp: undefined,
tls: undefined,
request: undefined,
firstByte: undefined,
download: undefined,
total: undefined,
},
};
request.timings = timings;
const handleError = (origin) => {
origin.once(errorMonitor, () => {
timings.error = Date.now();
timings.phases.total = timings.error - timings.start;
});
};
handleError(request);
const onAbort = () => {
timings.abort = Date.now();
timings.phases.total = timings.abort - timings.start;
};
request.prependOnceListener('abort', onAbort);
const onSocket = (socket) => {
timings.socket = Date.now();
timings.phases.wait = timings.socket - timings.start;
if (types.isProxy(socket)) {
// HTTP/2: The socket is a proxy, so connection events won't fire.
// We can't measure connection timings, so leave them undefined.
// This prevents NaN in phases.request calculation.
return;
}
// Check if socket is already connected (reused from connection pool)
const socketAlreadyConnected = socket.writable && !socket.connecting;
if (socketAlreadyConnected) {
// Socket reuse detected: the socket was already connected from a previous request.
// For reused sockets, set all connection timestamps to socket time since no new
// connection was made for THIS request. But preserve phase durations from the
// original connection so they're not lost.
timings.lookup = timings.socket;
timings.connect = timings.socket;
if (socket.__initial_connection_timings__) {
// Restore the phase timings from the initial connection
timings.phases.dns = socket.__initial_connection_timings__.dnsPhase;
timings.phases.tcp = socket.__initial_connection_timings__.tcpPhase;
timings.phases.tls = socket.__initial_connection_timings__.tlsPhase;
// Set secureConnect timestamp if there was TLS
if (timings.phases.tls !== undefined) {
timings.secureConnect = timings.socket;
}
}
else {
// Socket reused but no initial timings stored (e.g., from external code)
// Set phases to 0
timings.phases.dns = 0;
timings.phases.tcp = 0;
}
return;
}
const lookupListener = () => {
timings.lookup = Date.now();
timings.phases.dns = timings.lookup - timings.socket;
};
socket.prependOnceListener('lookup', lookupListener);
deferToConnect(socket, {
connect() {
timings.connect = Date.now();
if (timings.lookup === undefined) {
// No DNS lookup occurred (e.g., connecting to an IP address directly)
// Set lookup to socket time (no time elapsed for DNS)
socket.removeListener('lookup', lookupListener);
timings.lookup = timings.socket;
timings.phases.dns = 0;
}
timings.phases.tcp = timings.connect - timings.lookup;
// If lookup and connect happen at the EXACT same time (tcp = 0),
// DNS was served from cache and the dns value is just event loop overhead.
// Set dns to 0 to indicate no actual DNS resolution occurred.
// Fixes https://github.com/szmarczak/http-timer/issues/35
if (timings.phases.tcp === 0 && timings.phases.dns && timings.phases.dns > 0) {
timings.phases.dns = 0;
}
// Store connection phase timings on socket for potential reuse
if (!socket.__initial_connection_timings__) {
socket.__initial_connection_timings__ = {
dnsPhase: timings.phases.dns,
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion -- TypeScript can't prove this is defined due to callback structure
tcpPhase: timings.phases.tcp,
};
}
},
secureConnect() {
timings.secureConnect = Date.now();
timings.phases.tls = timings.secureConnect - timings.connect;
// Update stored timings with TLS phase timing
if (socket.__initial_connection_timings__) {
socket.__initial_connection_timings__.tlsPhase = timings.phases.tls;
}
},
});
};
if (request.socket) {
onSocket(request.socket);
}
else {
request.prependOnceListener('socket', onSocket);
}
const onUpload = () => {
timings.upload = Date.now();
// Calculate request phase if we have connection timings
const secureOrConnect = timings.secureConnect ?? timings.connect;
if (secureOrConnect !== undefined) {
timings.phases.request = timings.upload - secureOrConnect;
}
// If both are undefined (HTTP/2), phases.request stays undefined (not NaN)
};
if (request.writableFinished) {
onUpload();
}
else {
request.prependOnceListener('finish', onUpload);
}
request.prependOnceListener('response', (response) => {
timings.response = Date.now();
timings.phases.firstByte = timings.response - timings.upload;
response.timings = timings;
handleError(response);
response.prependOnceListener('end', () => {
request.off('abort', onAbort);
response.off('aborted', onAbort);
if (timings.phases.total !== undefined) {
// Aborted or errored
return;
}
timings.end = Date.now();
timings.phases.download = timings.end - timings.response;
timings.phases.total = timings.end - timings.start;
});
response.prependOnceListener('aborted', onAbort);
});
return timings;
};
export default timer;