Skip to content

Commit f627005

Browse files
http: fix perf_hooks detail.req.url port and proxied path
The perf_hooks HTTP client entry built the reported URL from the bare hostname, dropping non-default ports and IPv6 brackets, and appended the request path even after it had been rewritten to absolute-form for proxying, duplicating the protocol and authority. Report the connection authority captured at request creation and, for proxied requests, use the rewritten absolute-form target as-is. Fixes: #59625
1 parent c7ddda9 commit f627005

3 files changed

Lines changed: 86 additions & 7 deletions

File tree

lib/_http_client.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
121121
const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/;
122122
const kError = Symbol('kError');
123123
const kPath = Symbol('kPath');
124+
const kAuthority = Symbol('kAuthority');
125+
const kProxyRewrittenToAbsolute = Symbol('kProxyRewrittenToAbsolute');
124126

125127
const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request';
126128

@@ -318,6 +320,7 @@ function rewriteForProxiedHttp(req, reqOptions, proxyAuthority, userHostHeader,
318320
if (requestURL !== undefined) {
319321
// Convert the path to absolute-form. The authority is built from options.
320322
req.path = requestURL.href;
323+
req[kProxyRewrittenToAbsolute] = true;
321324
}
322325
debug(`updated request for HTTP proxy ${href} with ${req.path} `, req[kOutHeaders]);
323326
return true;
@@ -479,6 +482,7 @@ function ClientRequest(input, options, cb) {
479482
this.reusedSocket = false;
480483
this.host = host;
481484
this.protocol = protocol;
485+
this[kProxyRewrittenToAbsolute] = false;
482486

483487
if (this.agent) {
484488
// If there is an agent we should default to Connection:keep-alive,
@@ -509,6 +513,9 @@ function ClientRequest(input, options, cb) {
509513
if (port && +port !== defaultPort) {
510514
hostHeaderFromOptions += ':' + port;
511515
}
516+
// Preserve the request authority (with the port when non-default) so that
517+
// the perf_hooks entry can report a faithful URL.
518+
this[kAuthority] = hostHeaderFromOptions;
512519
const headersArray = ArrayIsArray(options.headers);
513520
if (!headersArray) {
514521
if (options.headers) {
@@ -627,7 +634,11 @@ ClientRequest.prototype._finish = function _finish() {
627634
detail: {
628635
req: {
629636
method: this.method,
630-
url: `${this.protocol}//${this.host}${this.path}`,
637+
// If the path has been rewritten to absolute-form for proxying,
638+
// it is already a full URL.
639+
url: this[kProxyRewrittenToAbsolute] ?
640+
this.path :
641+
`${this.protocol}//${this[kAuthority]}${this.path}`,
631642
headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
632643
},
633644
},
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// This tests that when a request path is rewritten to absolute-form for
2+
// proxying, the perf_hooks HTTP entries report it as the URL as-is, instead
3+
// of appending it to the protocol and authority again.
4+
// Refs: https://github.com/nodejs/node/issues/59625
5+
import * as common from '../common/index.mjs';
6+
import assert from 'node:assert';
7+
import { once } from 'events';
8+
import http from 'node:http';
9+
import { PerformanceObserver } from 'node:perf_hooks';
10+
import { createProxyServer } from '../common/proxy-server.js';
11+
12+
const entries = [];
13+
const obs = new PerformanceObserver(common.mustCallAtLeast((items) => {
14+
entries.push(...items.getEntries());
15+
}));
16+
obs.observe({ type: 'http' });
17+
18+
// Start a server to process the final request.
19+
const server = http.createServer(common.mustCall((req, res) => {
20+
res.end('Hello world');
21+
}));
22+
server.on('error', common.mustNotCall((err) => { console.error('Server error', err); }));
23+
server.listen(0);
24+
await once(server, 'listening');
25+
26+
// Start a minimal proxy server.
27+
const { proxy, logs } = createProxyServer();
28+
proxy.listen(0);
29+
await once(proxy, 'listening');
30+
31+
const requestUrl = `http://localhost:${server.address().port}/test`;
32+
const agent = new http.Agent({
33+
proxyEnv: {
34+
HTTP_PROXY: `http://localhost:${proxy.address().port}`,
35+
},
36+
});
37+
38+
const res = await new Promise((resolve, reject) => {
39+
http.request(requestUrl, { agent }, resolve).on('error', reject).end();
40+
});
41+
res.resume();
42+
await once(res, 'end');
43+
44+
// Verify that the request went through the proxy.
45+
assert.strictEqual(logs.length, 1);
46+
assert.strictEqual(logs[0].url, requestUrl);
47+
48+
proxy.close();
49+
server.close();
50+
51+
process.on('exit', () => {
52+
// Two HttpClient entries are expected: one for the proxied request, one for
53+
// the request the proxy makes to forward it. Both should report the full
54+
// URL, including the port.
55+
const clientUrls = entries.filter((entry) => entry.name === 'HttpClient')
56+
.map((entry) => entry.detail.req.url);
57+
assert.deepStrictEqual(clientUrls, [requestUrl, requestUrl]);
58+
// Two HttpRequest entries are expected: the proxy server receives the
59+
// request target in absolute-form, the final server in origin-form.
60+
const requestUrls = entries.filter((entry) => entry.name === 'HttpRequest')
61+
.map((entry) => entry.detail.req.url).sort();
62+
assert.deepStrictEqual(requestUrls, ['/test', requestUrl].sort());
63+
});

test/parallel/test-http-perf_hooks.js

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,18 @@ const server = http.Server(common.mustCall((req, res) => {
3636
}));
3737
}, 2));
3838

39+
let port;
3940
server.listen(0, common.mustCall(async () => {
41+
port = server.address().port;
4042
await Promise.all([
4143
makeRequest({
42-
port: server.address().port,
44+
port,
4345
path: '/',
4446
method: 'POST',
4547
data: expected
4648
}),
4749
makeRequest({
48-
port: server.address().port,
50+
port,
4951
path: '/',
5052
method: 'POST',
5153
data: expected
@@ -63,14 +65,17 @@ process.on('exit', () => {
6365
assert.strictEqual(typeof entry.duration, 'number');
6466
if (entry.name === 'HttpClient') {
6567
numberOfHttpClients++;
68+
// The reported URL must include the port when it is non-default.
69+
// Refs: https://github.com/nodejs/node/issues/59625
70+
assert.strictEqual(entry.detail.req.url, `http://localhost:${port}/`);
6671
} else if (entry.name === 'HttpRequest') {
6772
numberOfHttpRequests++;
73+
assert.strictEqual(entry.detail.req.url, '/');
6874
}
69-
assert.strictEqual(typeof entry.detail.req.method, 'string');
70-
assert.strictEqual(typeof entry.detail.req.url, 'string');
75+
assert.strictEqual(entry.detail.req.method, 'POST');
7176
assert.strictEqual(typeof entry.detail.req.headers, 'object');
72-
assert.strictEqual(typeof entry.detail.res.statusCode, 'number');
73-
assert.strictEqual(typeof entry.detail.res.statusMessage, 'string');
77+
assert.strictEqual(entry.detail.res.statusCode, 200);
78+
assert.strictEqual(entry.detail.res.statusMessage, 'OK');
7479
assert.strictEqual(typeof entry.detail.res.headers, 'object');
7580
}
7681
assert.strictEqual(numberOfHttpClients, 2);

0 commit comments

Comments
 (0)