From 91a9939cb883e79e1839a25a06c490f1f6efe080 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger Date: Tue, 28 Jul 2026 14:59:15 -0700 Subject: [PATCH 1/2] 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 | 10 ++++---- backend/test/routes/ansibletower.test.ts | 17 +++++++------ 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, 57 insertions(+), 55 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 d83e4091245..428636168d7 100644 --- a/backend/test/routes/aggregator.test.ts +++ b/backend/test/routes/aggregator.test.ts @@ -17,7 +17,7 @@ import type { IResource } from '../../src/resources/resource' describe(`aggregator Route`, function () { it(`should page Unfiltered Applications`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -63,7 +63,7 @@ describe(`aggregator Route`, function () { }) it(`should page Filtered Applications`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -93,7 +93,7 @@ describe(`aggregator Route`, function () { }) it(`should return application counts`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -114,9 +114,9 @@ describe(`aggregator Route`, function () { expect(res.statusCode).toEqual(200) expect(await parseResponseJsonBody(res)).toEqual(responseCount) }) - it(`should return ui data`, async function () { + it(`should return appset data`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + nock(process.env.CLUSTER_API_URL).head('/api').reply(200) // initialize events resources.forEach((resource) => cacheResource(resource)) diff --git a/backend/test/routes/ansibletower.test.ts b/backend/test/routes/ansibletower.test.ts index 2c6b77bf982..0cb7e3d5c62 100644 --- a/backend/test/routes/ansibletower.test.ts +++ b/backend/test/routes/ansibletower.test.ts @@ -23,8 +23,8 @@ function nockCredentialSecret(host: string) { } describe(`ansibletower Route`, function () { - it(`should list Ansible TowerJobs`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + it(`should list Ansible Automation controller Jobs`, async function () { + 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', { @@ -115,8 +115,8 @@ describe(`ansibletower Route`, function () { expect(res.statusCode).toEqual(400) }) - it(`when bad things happen to Ansible TowerJobs 1`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + it(`when bad things happen to Ansible Automation controller Jobs 1`, async function () { + 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', { @@ -128,8 +128,8 @@ describe(`ansibletower Route`, function () { expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({})) }) - it(`when bad things happen to Ansible TowerJobs 2`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(200) + it(`when bad things happen to Ansible Automation controller Jobs 2`, async function () { + 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', { @@ -140,9 +140,10 @@ describe(`ansibletower Route`, function () { expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({})) }) - it(`when bad things happen to Ansible TowerJobs 3`, async function () { - nock(process.env.CLUSTER_API_URL).get('/apis').reply(400) + it(`when bad things happen to Ansible Automation controller Jobs 3`, async function () { + 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 1406bd176a8ec7acac4fafb944fc76123c2e4265 Mon Sep 17 00:00:00 2001 From: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:11:06 -0700 Subject: [PATCH 2/2] 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 | 9 ++++---- backend/test/routes/aggregator.test.ts | 8 +++---- backend/test/routes/ansibletower.test.ts | 22 +++++++++---------- 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, 46 insertions(+), 47 deletions(-) diff --git a/backend/src/lib/token.ts b/backend/src/lib/token.ts index 203280b55ed..263cc0d923f 100644 --- a/backend/src/lib/token.ts +++ b/backend/src/lib/token.ts @@ -28,15 +28,14 @@ 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 428636168d7..287f2e3c2d3 100644 --- a/backend/test/routes/aggregator.test.ts +++ b/backend/test/routes/aggregator.test.ts @@ -17,7 +17,7 @@ import type { IResource } from '../../src/resources/resource' describe(`aggregator Route`, function () { it(`should page Unfiltered Applications`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -63,7 +63,7 @@ describe(`aggregator Route`, function () { }) it(`should page Filtered Applications`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -93,7 +93,7 @@ describe(`aggregator Route`, function () { }) it(`should return application counts`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events await Promise.all(resources.map((resource) => cacheResource(resource))) @@ -116,7 +116,7 @@ describe(`aggregator Route`, function () { }) it(`should return appset data`, async function () { resetApplicationCache() - nock(process.env.CLUSTER_API_URL).head('/api').reply(200) + nock(process.env.CLUSTER_API_URL).get('/api').reply(200) // initialize events resources.forEach((resource) => cacheResource(resource)) diff --git a/backend/test/routes/ansibletower.test.ts b/backend/test/routes/ansibletower.test.ts index 0cb7e3d5c62..cad82fe7288 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(`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', @@ -103,7 +103,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 }) @@ -116,7 +116,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', { @@ -129,7 +129,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', { @@ -141,7 +141,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()