From 14a03ba847af9f7f8cc3eeadc409b6191a2b9b09 Mon Sep 17 00:00:00 2001 From: Yvon Morice Date: Wed, 1 Jul 2026 09:30:08 +0200 Subject: [PATCH 1/3] Replace usage of old node:url api with WHATWG one in pushgateway.js Signed-off-by: Yvon Morice --- lib/pushgateway.js | 54 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/lib/pushgateway.js b/lib/pushgateway.js index f7d4f757..c543cc98 100644 --- a/lib/pushgateway.js +++ b/lib/pushgateway.js @@ -45,10 +45,46 @@ class Pushgateway { return useGateway.call(this, 'DELETE', params.jobName, params.groupings); } } + +function legacyUrlResolve(from, to) { + const resolvedUrl = new URL(to, new URL(from, 'resolve://')); + if (resolvedUrl.protocol === 'resolve:') { + // `from` is a relative URL. + const { pathname, search, hash } = resolvedUrl; + return pathname + search + hash; + } + return resolvedUrl.toString(); +} + +function legacyUrlFromWhatwgUrl(whatwgUrl) { + const legacyUrl = { + hash: whatwgUrl.hash, + host: whatwgUrl.host, + href: whatwgUrl.href, + pathname: whatwgUrl.pathname, + port: whatwgUrl.port, + protocol: whatwgUrl.protocol, + search: whatwgUrl.search, + + // replaced + hostname: whatwgUrl.hostname.replace(/^\[|]$/, ''), + path: `${whatwgUrl.pathname}${whatwgUrl.search}`, + }; + + if (whatwgUrl.username) { + legacyUrl.auth = whatwgUrl.username; + + if (whatwgUrl.password) { + legacyUrl.auth += `:${whatwgUrl.password}`; + } + } + + return legacyUrl; +} + async function useGateway(method, job, groupings) { // `URL` first added in v6.13.0 - // eslint-disable-next-line n/no-deprecated-api - const gatewayUrlParsed = url.parse(this.gatewayUrl); + const gatewayUrlParsed = new url.URL(this.gatewayUrl); const gatewayUrlPath = gatewayUrlParsed.pathname && gatewayUrlParsed.pathname !== '/' ? gatewayUrlParsed.pathname @@ -58,14 +94,14 @@ async function useGateway(method, job, groupings) { : ''; const path = `${gatewayUrlPath}/metrics${jobPath}`; - // eslint-disable-next-line n/no-deprecated-api - const target = url.resolve(this.gatewayUrl, path); - // eslint-disable-next-line n/no-deprecated-api - const requestParams = url.parse(target); + const target = legacyUrlResolve(this.gatewayUrl, path); + const requestParams = new url.URL(target); const httpModule = isHttps(requestParams.href) ? https : http; - const options = Object.assign(requestParams, this.requestOptions, { - method, - }); + const options = Object.assign( + legacyUrlFromWhatwgUrl(requestParams), + this.requestOptions, + { method }, + ); return new Promise((resolve, reject) => { if (method === 'DELETE' && options.headers) { From 1079eb30eba6714d18eb14797d1a33362f5092dc Mon Sep 17 00:00:00 2001 From: Yvon Morice Date: Wed, 1 Jul 2026 09:49:20 +0200 Subject: [PATCH 2/3] Refactor to avoid parsing URL multiple times Signed-off-by: Yvon Morice --- lib/pushgateway.js | 20 ++++---------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/lib/pushgateway.js b/lib/pushgateway.js index c543cc98..60a7522f 100644 --- a/lib/pushgateway.js +++ b/lib/pushgateway.js @@ -46,16 +46,6 @@ class Pushgateway { } } -function legacyUrlResolve(from, to) { - const resolvedUrl = new URL(to, new URL(from, 'resolve://')); - if (resolvedUrl.protocol === 'resolve:') { - // `from` is a relative URL. - const { pathname, search, hash } = resolvedUrl; - return pathname + search + hash; - } - return resolvedUrl.toString(); -} - function legacyUrlFromWhatwgUrl(whatwgUrl) { const legacyUrl = { hash: whatwgUrl.hash, @@ -84,18 +74,16 @@ function legacyUrlFromWhatwgUrl(whatwgUrl) { async function useGateway(method, job, groupings) { // `URL` first added in v6.13.0 - const gatewayUrlParsed = new url.URL(this.gatewayUrl); + const requestParams = new url.URL(this.gatewayUrl); const gatewayUrlPath = - gatewayUrlParsed.pathname && gatewayUrlParsed.pathname !== '/' - ? gatewayUrlParsed.pathname + requestParams.pathname && requestParams.pathname !== '/' + ? requestParams.pathname : ''; const jobPath = job ? `/job/${encodeURIComponent(job)}${generateGroupings(groupings)}` : ''; - const path = `${gatewayUrlPath}/metrics${jobPath}`; + requestParams.pathname = `${gatewayUrlPath}/metrics${jobPath}`; - const target = legacyUrlResolve(this.gatewayUrl, path); - const requestParams = new url.URL(target); const httpModule = isHttps(requestParams.href) ? https : http; const options = Object.assign( legacyUrlFromWhatwgUrl(requestParams), From e9553ac5082b2b35cc59167562183f7a54e279ff Mon Sep 17 00:00:00 2001 From: Yvon Morice Date: Thu, 2 Jul 2026 09:54:41 +0200 Subject: [PATCH 3/3] Refactor to completely remove old url usage and manual interpolation Signed-off-by: Yvon Morice --- lib/pushgateway.js | 45 ++--------- .../pushgatewayWithPathTest.js.snap | 37 +++++++++ test/pushgatewayWithPathTest.js | 77 ++++++++++++------- 3 files changed, 90 insertions(+), 69 deletions(-) create mode 100644 test/__snapshots__/pushgatewayWithPathTest.js.snap diff --git a/lib/pushgateway.js b/lib/pushgateway.js index 60a7522f..6ac7f495 100644 --- a/lib/pushgateway.js +++ b/lib/pushgateway.js @@ -1,6 +1,5 @@ 'use strict'; -const url = require('url'); const http = require('http'); const https = require('https'); const { gzipSync } = require('zlib'); @@ -46,35 +45,9 @@ class Pushgateway { } } -function legacyUrlFromWhatwgUrl(whatwgUrl) { - const legacyUrl = { - hash: whatwgUrl.hash, - host: whatwgUrl.host, - href: whatwgUrl.href, - pathname: whatwgUrl.pathname, - port: whatwgUrl.port, - protocol: whatwgUrl.protocol, - search: whatwgUrl.search, - - // replaced - hostname: whatwgUrl.hostname.replace(/^\[|]$/, ''), - path: `${whatwgUrl.pathname}${whatwgUrl.search}`, - }; - - if (whatwgUrl.username) { - legacyUrl.auth = whatwgUrl.username; - - if (whatwgUrl.password) { - legacyUrl.auth += `:${whatwgUrl.password}`; - } - } - - return legacyUrl; -} - async function useGateway(method, job, groupings) { - // `URL` first added in v6.13.0 - const requestParams = new url.URL(this.gatewayUrl); + // `URL` is a global since Node.js v10 - no `require('url')` needed. + const requestParams = new URL(this.gatewayUrl); const gatewayUrlPath = requestParams.pathname && requestParams.pathname !== '/' ? requestParams.pathname @@ -84,18 +57,14 @@ async function useGateway(method, job, groupings) { : ''; requestParams.pathname = `${gatewayUrlPath}/metrics${jobPath}`; - const httpModule = isHttps(requestParams.href) ? https : http; - const options = Object.assign( - legacyUrlFromWhatwgUrl(requestParams), - this.requestOptions, - { method }, - ); + const httpModule = requestParams.protocol === 'https:' ? https : http; + const options = { ...this.requestOptions, method }; return new Promise((resolve, reject) => { if (method === 'DELETE' && options.headers) { delete options.headers['Content-Encoding']; } - const req = httpModule.request(options, resp => { + const req = httpModule.request(requestParams, options, resp => { let body = ''; resp.setEncoding('utf8'); resp.on('data', chunk => { @@ -153,8 +122,4 @@ function generateGroupings(groupings) { .join(''); } -function isHttps(href) { - return href.search(/^https/) !== -1; -} - module.exports = Pushgateway; diff --git a/test/__snapshots__/pushgatewayWithPathTest.js.snap b/test/__snapshots__/pushgatewayWithPathTest.js.snap new file mode 100644 index 00000000..f01f1700 --- /dev/null +++ b/test/__snapshots__/pushgatewayWithPathTest.js.snap @@ -0,0 +1,37 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`pushgateway with path and OpenMetrics registry global registry pins the http.request(url, options) call shape (no legacy options translation) 1`] = ` +{ + "options": { + "method": "PUT", + }, + "url": "http://192.168.99.100:9091/path/metrics/job/testJob", +} +`; + +exports[`pushgateway with path and OpenMetrics registry registry instance pins the http.request(url, options) call shape (no legacy options translation) 1`] = ` +{ + "options": { + "method": "PUT", + }, + "url": "http://192.168.99.100:9091/path/metrics/job/testJob", +} +`; + +exports[`pushgateway with path and Prometheus registry global registry pins the http.request(url, options) call shape (no legacy options translation) 1`] = ` +{ + "options": { + "method": "PUT", + }, + "url": "http://192.168.99.100:9091/path/metrics/job/testJob", +} +`; + +exports[`pushgateway with path and Prometheus registry registry instance pins the http.request(url, options) call shape (no legacy options translation) 1`] = ` +{ + "options": { + "method": "PUT", + }, + "url": "http://192.168.99.100:9091/path/metrics/job/testJob", +} +`; diff --git a/test/pushgatewayWithPathTest.js b/test/pushgatewayWithPathTest.js index df274eb4..7dd75427 100644 --- a/test/pushgatewayWithPathTest.js +++ b/test/pushgatewayWithPathTest.js @@ -37,27 +37,29 @@ describe.each([ instance.pushAdd({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('POST'); - expect(invocation.path).toEqual('/path/metrics/job/testJob'); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('POST'); + expect(requestUrl.pathname).toEqual('/path/metrics/job/testJob'); }); it('should use groupings', () => { instance.pushAdd({ jobName: 'testJob', groupings: { key: 'value' } }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('POST'); - expect(invocation.path).toEqual('/path/metrics/job/testJob/key/value'); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('POST'); + expect(requestUrl.pathname).toEqual( + '/path/metrics/job/testJob/key/value', + ); }); it('should escape groupings', () => { instance.pushAdd({ jobName: 'testJob', groupings: { key: 'va&lue' } }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('POST'); - expect(invocation.path).toEqual( + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('POST'); + expect(requestUrl.pathname).toEqual( '/path/metrics/job/testJob/key/va%26lue', ); }); @@ -68,18 +70,18 @@ describe.each([ instance.push({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('PUT'); - expect(invocation.path).toEqual('/path/metrics/job/testJob'); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('PUT'); + expect(requestUrl.pathname).toEqual('/path/metrics/job/testJob'); }); it('should uri encode url', () => { instance.push({ jobName: 'test&Job' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('PUT'); - expect(invocation.path).toEqual('/path/metrics/job/test%26Job'); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('PUT'); + expect(requestUrl.pathname).toEqual('/path/metrics/job/test%26Job'); }); }); @@ -88,9 +90,9 @@ describe.each([ instance.delete({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('DELETE'); - expect(invocation.path).toEqual('/path/metrics/job/testJob'); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('DELETE'); + expect(requestUrl.pathname).toEqual('/path/metrics/job/testJob'); }); }); @@ -111,27 +113,30 @@ describe.each([ instance.pushAdd({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('POST'); - expect(invocation.auth).toEqual(auth); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('POST'); + expect(requestUrl.username).toEqual(USERNAME); + expect(requestUrl.password).toEqual(PASSWORD); }); it('push should send PUT request with basic auth data', () => { instance.push({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('PUT'); - expect(invocation.auth).toEqual(auth); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('PUT'); + expect(requestUrl.username).toEqual(USERNAME); + expect(requestUrl.password).toEqual(PASSWORD); }); it('delete should send DELETE request with basic auth data', () => { instance.delete({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.method).toEqual('DELETE'); - expect(invocation.auth).toEqual(auth); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.method).toEqual('DELETE'); + expect(requestUrl.username).toEqual(USERNAME); + expect(requestUrl.password).toEqual(PASSWORD); }); }); @@ -149,8 +154,22 @@ describe.each([ instance.push({ jobName: 'testJob' }); expect(mockHttp).toHaveBeenCalledTimes(1); - const invocation = mockHttp.mock.calls[0][0]; - expect(invocation.headers).toEqual({ 'unit-test': '1' }); + const [, requestOptions] = mockHttp.mock.calls[0]; + expect(requestOptions.headers).toEqual({ 'unit-test': '1' }); + }); + + it('pins the http.request(url, options) call shape (no legacy options translation)', () => { + instance.push({ jobName: 'testJob' }); + + expect(mockHttp).toHaveBeenCalledTimes(1); + const [requestUrl, requestOptions] = mockHttp.mock.calls[0]; + + expect(requestUrl).toBeInstanceOf(URL); + + expect({ + url: requestUrl.toString(), + options: requestOptions, + }).toMatchSnapshot(); }); }; describe('global registry', () => {