From 1c3cbe521cb0f097126feda066211ad89e49dfde Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger Date: Tue, 28 Jul 2026 14:59:15 -0700 Subject: [PATCH 1/3] ACM-38826 fix(backend): use HEAD /api for token validation to eliminate response body drain (#6537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit getAuthenticatedToken() called isAuthenticated() on every authenticated request, which issued GET /apis to the kube API. The response body (up to several MB on CRD-heavy clusters) was never consumed on the success path, preventing the socket from returning to the keepAlive pool. Under sustained load, native (external) memory accumulated proportionally to request volume. Replace GET /apis with HEAD /api: - HEAD responses have no message body by HTTP spec — nothing to drain - /api (core group) is ~200 bytes of headers; it does not grow with CRDs - Returns HTTP status so callers preserve 401/403/5xx distinctions - No client-side caching required: OpenShift oauth-apiserver caches valid tokens ~30 seconds server-side; failures are not cached isAuthenticated() now returns Promise (HTTP status) instead of Promise so callers preserve upstream status codes. authenticated.ts updated accordingly. All route tests updated to mock HEAD /api instead of GET /apis. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-authored-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 --- backend/src/lib/authenticated.ts | 5 ++-- backend/src/lib/token.ts | 25 +++++++++++-------- backend/test/routes/aggregator.test.ts | 8 +++--- backend/test/routes/ansibletower.test.ts | 9 ++++--- backend/test/routes/apiPath.test.ts | 2 +- backend/test/routes/hub.test.ts | 4 +-- backend/test/routes/hypershift-status.test.ts | 2 +- backend/test/routes/metricsProxy.test.ts | 4 +-- backend/test/routes/operatorCheck.test.ts | 6 ++--- backend/test/routes/search.test.ts | 4 +-- .../routes/upgrade-risks-prediction.test.ts | 2 +- backend/test/routes/username.test.ts | 5 ++-- backend/test/routes/userpreference.test.ts | 4 +-- .../test/routes/virtualMachineProxy.test.ts | 22 ++++++++-------- 14 files changed, 52 insertions(+), 50 deletions(-) diff --git a/backend/src/lib/authenticated.ts b/backend/src/lib/authenticated.ts index 7d3bf1f1b0b..f9e1dd2caee 100644 --- a/backend/src/lib/authenticated.ts +++ b/backend/src/lib/authenticated.ts @@ -7,9 +7,8 @@ export function authenticated(req: Http2ServerRequest, res: Http2ServerResponse) const token = getToken(req) if (!token) return unauthorized(req, res) isAuthenticated(token) - .then((response) => { - res.writeHead(response.status).end() - void response.blob() + .then((status) => { + res.writeHead(status).end() }) .catch(catchInternalServerError(res)) } diff --git a/backend/src/lib/token.ts b/backend/src/lib/token.ts index 9b339f2a7a1..203280b55ed 100644 --- a/backend/src/lib/token.ts +++ b/backend/src/lib/token.ts @@ -28,10 +28,16 @@ export function getToken(req: Http2ServerRequest): string | undefined { return token } -export async function isAuthenticated(token: string) { - return fetchRetry(process.env.CLUSTER_API_URL + '/apis', { +// HEAD /api returns headers only — no response body — so no drain is needed and +// the payload is ~200 bytes regardless of how many CRDs are registered. +// Returns the HTTP status so callers can distinguish 401 (invalid token) from +// 403 (valid token, insufficient permission) and 5xx (transient upstream error). +export async function isAuthenticated(token: string): Promise { + const response = await fetchRetry(process.env.CLUSTER_API_URL + '/api', { + method: 'HEAD', headers: { [HTTP2_HEADER_AUTHORIZATION]: `Bearer ${token}` }, }) + return response.status } export const isHttp2ServerResponse = ( @@ -51,22 +57,19 @@ export async function getAuthenticatedToken( const token = getToken(req) if (token) { - const authResponse = await isAuthenticated(token) + const status = await isAuthenticated(token) /* istanbul ignore if */ - if (authResponse.status === constants.HTTP_STATUS_OK) { + if (status === constants.HTTP_STATUS_OK) { if (process.env.NODE_ENV === 'development') { const localStorage = new LocalStorage(LOCAL_STORAGE) localStorage.setItem(ADMIN_TOKEN, token) } return token + } + if (isHttp2ServerResponse(resOrSocket)) { + resOrSocket.writeHead(status).end() } else { - if (isHttp2ServerResponse(resOrSocket)) { - resOrSocket.writeHead(authResponse.status).end() - } else { - resOrSocket.destroy() - } - - void authResponse.blob() + resOrSocket.destroy() } } else if (isHttp2ServerResponse(resOrSocket)) { unauthorized(req, resOrSocket) diff --git a/backend/test/routes/aggregator.test.ts b/backend/test/routes/aggregator.test.ts index 909584bb57c..59c77228382 100644 --- a/backend/test/routes/aggregator.test.ts +++ b/backend/test/routes/aggregator.test.ts @@ -51,7 +51,7 @@ describe(`aggregator Route`, function () { }) it(`should page Unfiltered Applications`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -98,7 +98,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseNoFilter) }) it(`should page Filtered Applications`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -129,7 +129,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseFiltered) }) it(`should return application counts`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -153,7 +153,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseCount) }) it(`should return appset data`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { diff --git a/backend/test/routes/ansibletower.test.ts b/backend/test/routes/ansibletower.test.ts index c76345cd2b1..e8058039609 100644 --- a/backend/test/routes/ansibletower.test.ts +++ b/backend/test/routes/ansibletower.test.ts @@ -24,7 +24,7 @@ function nockCredentialSecret(host: string) { describe(`ansibletower Route`, function () { it(`should list Ansible Automation controller Jobs`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -94,7 +94,7 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 1`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -107,7 +107,7 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 2`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -119,8 +119,9 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 3`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(400) + nock(process.env.CLUSTER_API_URL).head('/api').reply(401) const res = await request('POST', '/ansibletower') + expect(res.statusCode).toEqual(401) expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({})) }) }) diff --git a/backend/test/routes/apiPath.test.ts b/backend/test/routes/apiPath.test.ts index 75732fcfc28..ac98bbd0513 100644 --- a/backend/test/routes/apiPath.test.ts +++ b/backend/test/routes/apiPath.test.ts @@ -15,7 +15,7 @@ describe(`apiPath Route`, function () { nock(process.env.CLUSTER_API_URL).get(paths[0]).reply(200, response) - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, paths: response, }) diff --git a/backend/test/routes/hub.test.ts b/backend/test/routes/hub.test.ts index d2d47b3c4fc..82604050646 100644 --- a/backend/test/routes/hub.test.ts +++ b/backend/test/routes/hub.test.ts @@ -5,9 +5,7 @@ import { request } from '../mock-request' describe('global hub', function () { it('should return the boolean', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { - status: 200, - }) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .get('/apis/apiextensions.k8s.io/v1/customresourcedefinitions') // .reply(200, { isGlobalHub: true }) .reply(200, { diff --git a/backend/test/routes/hypershift-status.test.ts b/backend/test/routes/hypershift-status.test.ts index 24aa6478438..9cb7280e05d 100644 --- a/backend/test/routes/hypershift-status.test.ts +++ b/backend/test/routes/hypershift-status.test.ts @@ -4,7 +4,7 @@ import { parseResponseJsonBody } from '../../src/lib/body-parser' import nock from 'nock' describe('hypershift-status Route', function () { - const mockAuth = () => nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { status: 200 }) + const mockAuth = () => nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200 }) const mockMCE = (hypershiftEnabled = true, localHostingEnabled = true) => nock(process.env.CLUSTER_API_URL) diff --git a/backend/test/routes/metricsProxy.test.ts b/backend/test/routes/metricsProxy.test.ts index c6a55a6d0fc..6b4b03f8909 100644 --- a/backend/test/routes/metricsProxy.test.ts +++ b/backend/test/routes/metricsProxy.test.ts @@ -4,14 +4,14 @@ import { request } from '../mock-request' describe('metrics proxy route', function () { it('Successfully calls prometheus endpoint', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) const res = await request('GET', '/prometheus/query') expect(res.statusCode).toEqual(200) }) it(`Successfully calls observability endpoint`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) const res = await request('GET', '/observability/query') diff --git a/backend/test/routes/operatorCheck.test.ts b/backend/test/routes/operatorCheck.test.ts index 2af878d2412..29dfa68f819 100644 --- a/backend/test/routes/operatorCheck.test.ts +++ b/backend/test/routes/operatorCheck.test.ts @@ -23,7 +23,7 @@ const subscriptionOperators = { describe(`operatorCheck Route`, function () { it(`returns valid response with version for installed operator`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -38,7 +38,7 @@ describe(`operatorCheck Route`, function () { }) }) it(`returns valid response for not-installed operator`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -52,7 +52,7 @@ describe(`operatorCheck Route`, function () { }) }) it(`returns bad request for arbitrary operator`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) diff --git a/backend/test/routes/search.test.ts b/backend/test/routes/search.test.ts index 0a50d6c469b..9b7663b64c5 100644 --- a/backend/test/routes/search.test.ts +++ b/backend/test/routes/search.test.ts @@ -4,7 +4,7 @@ import nock from 'nock' describe(`search Route`, function () { it(`uses search-api in the namespace of the MultiClusterHub`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -27,7 +27,7 @@ describe(`search Route`, function () { //expect(res.statusCode).toEqual(200) }) it(`uses search-api in namespace of pod if no MultiClusterHub`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL).get('/apis/operator.open-cluster-management.io/v1/multiclusterhubs').reply(200, { diff --git a/backend/test/routes/upgrade-risks-prediction.test.ts b/backend/test/routes/upgrade-risks-prediction.test.ts index 6792470b2ad..6352ef9f8c9 100644 --- a/backend/test/routes/upgrade-risks-prediction.test.ts +++ b/backend/test/routes/upgrade-risks-prediction.test.ts @@ -5,7 +5,7 @@ import { request } from '../mock-request' describe('Upgrade risks prediction Route', function () { it('should return the upgrade risks', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .get('/api/v1/namespaces/openshift-config/secrets') .reply(200, { diff --git a/backend/test/routes/username.test.ts b/backend/test/routes/username.test.ts index dbb12649116..59adfec4fd3 100644 --- a/backend/test/routes/username.test.ts +++ b/backend/test/routes/username.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' describe('username Route', function () { it('should return the username', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -23,7 +23,7 @@ describe('username Route', function () { expect(body).toEqual({ username: 'testuser' }) }) it('should return empty string if no username provided', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -39,6 +39,7 @@ describe('username Route', function () { expect(body).toEqual({ username: '' }) }) it('should handle errors', async function () { + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL).post('/apis/authentication.k8s.io/v1/tokenreviews').replyWithError('failed') const res = await request('GET', '/username') expect(res.statusCode).toEqual(500) diff --git a/backend/test/routes/userpreference.test.ts b/backend/test/routes/userpreference.test.ts index b6b2b0456ea..503b76ed2b2 100644 --- a/backend/test/routes/userpreference.test.ts +++ b/backend/test/routes/userpreference.test.ts @@ -5,7 +5,7 @@ import { request } from '../mock-request' describe('userpreference Route', function () { it('should return the userpreference', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post('/apis/authentication.k8s.io/v1/tokenreviews') .reply(200, { @@ -53,7 +53,7 @@ describe('userpreference Route', function () { savedSearches: [{ description: '', id: '1678205878189', name: 'testing', searchText: 'kind:Pod' }], }, } - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post('/apis/authentication.k8s.io/v1/tokenreviews') .reply(200, { diff --git a/backend/test/routes/virtualMachineProxy.test.ts b/backend/test/routes/virtualMachineProxy.test.ts index c7786318c80..0348d087f35 100644 --- a/backend/test/routes/virtualMachineProxy.test.ts +++ b/backend/test/routes/virtualMachineProxy.test.ts @@ -10,7 +10,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully call start action', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -57,7 +57,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully call pause action', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -104,7 +104,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully take snapshot action', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -175,7 +175,7 @@ describe('Virtual Machine actions', function () { }) it('should error on start action request', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -220,7 +220,7 @@ describe('Virtual Machine actions', function () { }) it('should fail with invalid route and secret', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -252,7 +252,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully restore a snapshot', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -323,7 +323,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully get VM', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -366,7 +366,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully get VM snapshot', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -409,7 +409,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully delete VM', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -457,7 +457,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully delete VM Snapshot', async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -507,7 +507,7 @@ describe('Virtual Machine actions', function () { describe('vmResourceUsageProxy', () => { beforeEach(() => { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) }) afterEach(() => { nock.cleanAll() From 9f0f67558d874b6cad347d3526d85c029d086e2c Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:13:47 -0700 Subject: [PATCH 2/3] ACM-38826 fix(backend): use GET /api with body drain instead of HEAD MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HEAD /api returns 405 on clusters where the API server or proxy chain rejects HEAD requests, causing every auth check to fail. Switch to GET /api with explicit body drain — the response is ~200 bytes (core API group only), drained immediately so the socket returns to the keepAlive pool. This preserves the memory optimization while restoring compatibility. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 --- backend/src/lib/token.ts | 13 ++++++----- backend/test/routes/aggregator.test.ts | 8 +++---- backend/test/routes/ansibletower.test.ts | 18 +++++++-------- backend/test/routes/apiPath.test.ts | 2 +- backend/test/routes/hub.test.ts | 2 +- backend/test/routes/hypershift-status.test.ts | 2 +- backend/test/routes/metricsProxy.test.ts | 4 ++-- backend/test/routes/operatorCheck.test.ts | 6 ++--- backend/test/routes/search.test.ts | 4 ++-- .../routes/upgrade-risks-prediction.test.ts | 2 +- backend/test/routes/username.test.ts | 6 ++--- backend/test/routes/userpreference.test.ts | 4 ++-- .../test/routes/virtualMachineProxy.test.ts | 22 +++++++++---------- 13 files changed, 48 insertions(+), 45 deletions(-) diff --git a/backend/src/lib/token.ts b/backend/src/lib/token.ts index 203280b55ed..b50cc523416 100644 --- a/backend/src/lib/token.ts +++ b/backend/src/lib/token.ts @@ -28,15 +28,18 @@ export function getToken(req: Http2ServerRequest): string | undefined { return token } -// HEAD /api returns headers only — no response body — so no drain is needed and -// the payload is ~200 bytes regardless of how many CRDs are registered. -// Returns the HTTP status so callers can distinguish 401 (invalid token) from -// 403 (valid token, insufficient permission) and 5xx (transient upstream error). + + + + +// GET /api returns the core API group (~200 bytes) — unlike /apis which grows +// with every installed CRD. The response body is drained so the socket returns +// to the keepAlive pool immediately and native memory does not accumulate. export async function isAuthenticated(token: string): Promise { const response = await fetchRetry(process.env.CLUSTER_API_URL + '/api', { - method: 'HEAD', headers: { [HTTP2_HEADER_AUTHORIZATION]: `Bearer ${token}` }, }) + response.body?.on('error', () => undefined).resume() return response.status } diff --git a/backend/test/routes/aggregator.test.ts b/backend/test/routes/aggregator.test.ts index 59c77228382..7b1ed38c07e 100644 --- a/backend/test/routes/aggregator.test.ts +++ b/backend/test/routes/aggregator.test.ts @@ -51,7 +51,7 @@ describe(`aggregator Route`, function () { }) it(`should page Unfiltered Applications`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -98,7 +98,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseNoFilter) }) it(`should page Filtered Applications`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -129,7 +129,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseFiltered) }) it(`should return application counts`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { @@ -153,7 +153,7 @@ describe(`aggregator Route`, function () { expect(await parseResponseJsonBody(res)).toEqual(responseCount) }) it(`should return appset data`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events - cache sequentially to ensure deterministic order for (const resource of resources) { diff --git a/backend/test/routes/ansibletower.test.ts b/backend/test/routes/ansibletower.test.ts index e8058039609..daa7409adab 100644 --- a/backend/test/routes/ansibletower.test.ts +++ b/backend/test/routes/ansibletower.test.ts @@ -24,7 +24,7 @@ function nockCredentialSecret(host: string) { describe(`ansibletower Route`, function () { it(`should list Ansible Automation controller Jobs`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -37,7 +37,7 @@ describe(`ansibletower Route`, function () { }) it(`should reject body-supplied tower hostname`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) const res = await request('POST', '/ansibletower', { towerHost: TOWER_HOST + ansiblePaths[0], token: '12345', @@ -46,7 +46,7 @@ describe(`ansibletower Route`, function () { }) it(`should preserve the query string for paginated requests`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).query({ page: '2', page_size: '20' }).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -59,7 +59,7 @@ describe(`ansibletower Route`, function () { }) it(`should reject an external absolute URL in ansiblePath`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) const res = await request('POST', '/ansibletower', { secretNamespace: SECRET_NS, @@ -70,7 +70,7 @@ describe(`ansibletower Route`, function () { }) it(`should reject a network-path reference in ansiblePath`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) const res = await request('POST', '/ansibletower', { secretNamespace: SECRET_NS, @@ -81,7 +81,7 @@ describe(`ansibletower Route`, function () { }) it(`should fail closed when caller cannot read the credential secret`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .get(`/api/v1/namespaces/${SECRET_NS}/secrets/${SECRET_NAME}`) .reply(403, { kind: 'Status', apiVersion: 'v1', status: 'Failure', reason: 'Forbidden', code: 403 }) @@ -94,7 +94,7 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 1`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -107,7 +107,7 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 2`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nockCredentialSecret(TOWER_HOST) nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response) const res = await request('POST', '/ansibletower', { @@ -119,7 +119,7 @@ describe(`ansibletower Route`, function () { }) it(`when bad things happen to Ansible Automation controller Jobs 3`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(401) + nock(process.env.CLUSTER_API_URL).get('/api').reply(401) const res = await request('POST', '/ansibletower') expect(res.statusCode).toEqual(401) expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({})) diff --git a/backend/test/routes/apiPath.test.ts b/backend/test/routes/apiPath.test.ts index ac98bbd0513..2d24f1f7d36 100644 --- a/backend/test/routes/apiPath.test.ts +++ b/backend/test/routes/apiPath.test.ts @@ -15,7 +15,7 @@ describe(`apiPath Route`, function () { nock(process.env.CLUSTER_API_URL).get(paths[0]).reply(200, response) - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, paths: response, }) diff --git a/backend/test/routes/hub.test.ts b/backend/test/routes/hub.test.ts index 82604050646..5319fd2cc65 100644 --- a/backend/test/routes/hub.test.ts +++ b/backend/test/routes/hub.test.ts @@ -5,7 +5,7 @@ import { request } from '../mock-request' describe('global hub', function () { it('should return the boolean', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .get('/apis/apiextensions.k8s.io/v1/customresourcedefinitions') // .reply(200, { isGlobalHub: true }) .reply(200, { diff --git a/backend/test/routes/hypershift-status.test.ts b/backend/test/routes/hypershift-status.test.ts index 9cb7280e05d..10467df915f 100644 --- a/backend/test/routes/hypershift-status.test.ts +++ b/backend/test/routes/hypershift-status.test.ts @@ -4,7 +4,7 @@ import { parseResponseJsonBody } from '../../src/lib/body-parser' import nock from 'nock' describe('hypershift-status Route', function () { - const mockAuth = () => nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200 }) + const mockAuth = () => nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200 }) const mockMCE = (hypershiftEnabled = true, localHostingEnabled = true) => nock(process.env.CLUSTER_API_URL) diff --git a/backend/test/routes/metricsProxy.test.ts b/backend/test/routes/metricsProxy.test.ts index 6b4b03f8909..6539faee920 100644 --- a/backend/test/routes/metricsProxy.test.ts +++ b/backend/test/routes/metricsProxy.test.ts @@ -4,14 +4,14 @@ import { request } from '../mock-request' describe('metrics proxy route', function () { it('Successfully calls prometheus endpoint', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) const res = await request('GET', '/prometheus/query') expect(res.statusCode).toEqual(200) }) it(`Successfully calls observability endpoint`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) const res = await request('GET', '/observability/query') diff --git a/backend/test/routes/operatorCheck.test.ts b/backend/test/routes/operatorCheck.test.ts index 29dfa68f819..c94327efee4 100644 --- a/backend/test/routes/operatorCheck.test.ts +++ b/backend/test/routes/operatorCheck.test.ts @@ -23,7 +23,7 @@ const subscriptionOperators = { describe(`operatorCheck Route`, function () { it(`returns valid response with version for installed operator`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -38,7 +38,7 @@ describe(`operatorCheck Route`, function () { }) }) it(`returns valid response for not-installed operator`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -52,7 +52,7 @@ describe(`operatorCheck Route`, function () { }) }) it(`returns bad request for arbitrary operator`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) diff --git a/backend/test/routes/search.test.ts b/backend/test/routes/search.test.ts index 9b7663b64c5..f4fc879f29f 100644 --- a/backend/test/routes/search.test.ts +++ b/backend/test/routes/search.test.ts @@ -4,7 +4,7 @@ import nock from 'nock' describe(`search Route`, function () { it(`uses search-api in the namespace of the MultiClusterHub`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -27,7 +27,7 @@ describe(`search Route`, function () { //expect(res.statusCode).toEqual(200) }) it(`uses search-api in namespace of pod if no MultiClusterHub`, async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL).get('/apis/operator.open-cluster-management.io/v1/multiclusterhubs').reply(200, { diff --git a/backend/test/routes/upgrade-risks-prediction.test.ts b/backend/test/routes/upgrade-risks-prediction.test.ts index 6352ef9f8c9..c1966bfbdb1 100644 --- a/backend/test/routes/upgrade-risks-prediction.test.ts +++ b/backend/test/routes/upgrade-risks-prediction.test.ts @@ -5,7 +5,7 @@ import { request } from '../mock-request' describe('Upgrade risks prediction Route', function () { it('should return the upgrade risks', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .get('/api/v1/namespaces/openshift-config/secrets') .reply(200, { diff --git a/backend/test/routes/username.test.ts b/backend/test/routes/username.test.ts index 59adfec4fd3..2f9c24eec5f 100644 --- a/backend/test/routes/username.test.ts +++ b/backend/test/routes/username.test.ts @@ -5,7 +5,7 @@ import nock from 'nock' describe('username Route', function () { it('should return the username', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -23,7 +23,7 @@ describe('username Route', function () { expect(body).toEqual({ username: 'testuser' }) }) it('should return empty string if no username provided', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { + nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200, }) nock(process.env.CLUSTER_API_URL) @@ -39,7 +39,7 @@ describe('username Route', function () { expect(body).toEqual({ username: '' }) }) it('should handle errors', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL).post('/apis/authentication.k8s.io/v1/tokenreviews').replyWithError('failed') const res = await request('GET', '/username') expect(res.statusCode).toEqual(500) diff --git a/backend/test/routes/userpreference.test.ts b/backend/test/routes/userpreference.test.ts index 503b76ed2b2..8f53479cf89 100644 --- a/backend/test/routes/userpreference.test.ts +++ b/backend/test/routes/userpreference.test.ts @@ -5,7 +5,7 @@ import { request } from '../mock-request' describe('userpreference Route', function () { it('should return the userpreference', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post('/apis/authentication.k8s.io/v1/tokenreviews') .reply(200, { @@ -53,7 +53,7 @@ describe('userpreference Route', function () { savedSearches: [{ description: '', id: '1678205878189', name: 'testing', searchText: 'kind:Pod' }], }, } - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post('/apis/authentication.k8s.io/v1/tokenreviews') .reply(200, { diff --git a/backend/test/routes/virtualMachineProxy.test.ts b/backend/test/routes/virtualMachineProxy.test.ts index 0348d087f35..98f96d3b254 100644 --- a/backend/test/routes/virtualMachineProxy.test.ts +++ b/backend/test/routes/virtualMachineProxy.test.ts @@ -10,7 +10,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully call start action', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -57,7 +57,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully call pause action', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -104,7 +104,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully take snapshot action', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -175,7 +175,7 @@ describe('Virtual Machine actions', function () { }) it('should error on start action request', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -220,7 +220,7 @@ describe('Virtual Machine actions', function () { }) it('should fail with invalid route and secret', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -252,7 +252,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully restore a snapshot', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -323,7 +323,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully get VM', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -366,7 +366,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully get VM snapshot', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -409,7 +409,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully delete VM', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -457,7 +457,7 @@ describe('Virtual Machine actions', function () { }) it('should successfully delete VM Snapshot', async function () { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) nock(process.env.CLUSTER_API_URL) .post( '/apis/authorization.k8s.io/v1/selfsubjectaccessreviews', @@ -507,7 +507,7 @@ describe('Virtual Machine actions', function () { describe('vmResourceUsageProxy', () => { beforeEach(() => { - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) }) afterEach(() => { nock.cleanAll() From 4d8176200127e8555338d4961920874abe871fd0 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:32:07 -0700 Subject: [PATCH 3/3] ACM-38826 fix(backend): remove blank lines left by sed comment strip Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 --- backend/src/lib/token.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/backend/src/lib/token.ts b/backend/src/lib/token.ts index b50cc523416..263cc0d923f 100644 --- a/backend/src/lib/token.ts +++ b/backend/src/lib/token.ts @@ -28,10 +28,6 @@ export function getToken(req: Http2ServerRequest): string | undefined { return token } - - - - // GET /api returns the core API group (~200 bytes) — unlike /apis which grows // with every installed CRD. The response body is drained so the socket returns // to the keepAlive pool immediately and native memory does not accumulate.