Skip to content

Commit 64dc9b1

Browse files
Randy424claude
andcommitted
ACM-40502 fix(backend): use GET /api with body drain instead of GET /apis [release-2.14]
Backport of the memory leak fix to release-2.14. isAuthenticated() was calling GET /apis on every authenticated request and never consuming the response body on the success path. This left sockets stuck outside the keepAlive pool, causing unbounded native (external) memory growth in console-chart-console-v2 pods. Fix: switch to GET /api (~200 bytes vs 35+ KB for /apis which scales with CRDs), drain the response body via .resume() so the socket returns to the keepAlive pool immediately, and return the numeric status so callers no longer hold a Response reference. Signed-off-by: Randy Bruno Piverger <21374229+Randy424@users.noreply.github.com> Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent 9649814 commit 64dc9b1

14 files changed

Lines changed: 46 additions & 43 deletions

backend/src/lib/authenticated.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@ export function authenticated(req: Http2ServerRequest, res: Http2ServerResponse)
77
const token = getToken(req)
88
if (!token) return unauthorized(req, res)
99
isAuthenticated(token)
10-
.then((response) => {
11-
res.writeHead(response.status).end()
12-
void response.blob()
10+
.then((status) => {
11+
res.writeHead(status).end()
1312
})
1413
.catch(catchInternalServerError(res))
1514
}

backend/src/lib/token.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,26 +26,30 @@ export function getToken(req: Http2ServerRequest): string | undefined {
2626
return token
2727
}
2828

29-
export async function isAuthenticated(token: string) {
30-
return fetchRetry(process.env.CLUSTER_API_URL + '/apis', {
29+
// GET /api returns the core API group (~200 bytes) — unlike /apis which grows
30+
// with every installed CRD. The response body is drained so the socket returns
31+
// to the keepAlive pool immediately and native memory does not accumulate.
32+
export async function isAuthenticated(token: string): Promise<number> {
33+
const response = await fetchRetry(process.env.CLUSTER_API_URL + '/api', {
3134
headers: { [HTTP2_HEADER_AUTHORIZATION]: `Bearer ${token}` },
3235
})
36+
response.body?.on('error', () => undefined).resume()
37+
return response.status
3338
}
3439

3540
export async function getAuthenticatedToken(req: Http2ServerRequest, res: Http2ServerResponse): Promise<string> {
3641
const token = getToken(req)
3742
if (token) {
38-
const authResponse = await isAuthenticated(token)
43+
const status = await isAuthenticated(token)
3944
/* istanbul ignore if */
40-
if (authResponse.status === constants.HTTP_STATUS_OK) {
45+
if (status === constants.HTTP_STATUS_OK) {
4146
if (process.env.NODE_ENV === 'development') {
4247
const localStorage = new LocalStorage(LOCAL_STORAGE)
4348
localStorage.setItem(ADMIN_TOKEN, token)
4449
}
4550
return token
4651
} else {
47-
res.writeHead(authResponse.status).end()
48-
void authResponse.blob()
52+
res.writeHead(status).end()
4953
}
5054
} else {
5155
unauthorized(req, res)

backend/test/routes/aggregator.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ describe(`aggregator Route`, function () {
4949
})
5050

5151
it(`should page Unfiltered Applications`, async function () {
52-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
52+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
5353

5454
// initialize events - cache sequentially to ensure deterministic order
5555
for (const resource of resources) {
@@ -94,7 +94,7 @@ describe(`aggregator Route`, function () {
9494
expect(await parseResponseJsonBody(res)).toEqual(responseNoFilter)
9595
})
9696
it(`should page Filtered Applications`, async function () {
97-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
97+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
9898

9999
// initialize events - cache sequentially to ensure deterministic order
100100
for (const resource of resources) {
@@ -126,7 +126,7 @@ describe(`aggregator Route`, function () {
126126
expect(JSON.stringify(await parseResponseJsonBody(res))).toEqual(JSON.stringify(responseFiltered))
127127
})
128128
it(`should return application counts`, async function () {
129-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
129+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
130130

131131
// initialize events - cache sequentially to ensure deterministic order
132132
for (const resource of resources) {
@@ -150,7 +150,7 @@ describe(`aggregator Route`, function () {
150150
expect(JSON.stringify(await parseResponseJsonBody(res))).toEqual(JSON.stringify(responseCount))
151151
})
152152
it(`should return ui data`, async function () {
153-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
153+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
154154

155155
// initialize events - cache sequentially to ensure deterministic order
156156
for (const resource of resources) {

backend/test/routes/ansibletower.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const TOWER_HOST = 'https://ansible-tower.com'
88

99
describe(`ansibletower Route`, function () {
1010
it(`should list Ansible TowerJobs`, async function () {
11-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
11+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
1212
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
1313
const res = await request('POST', '/ansibletower', {
1414
towerHost: TOWER_HOST + ansiblePaths[0],
@@ -19,7 +19,7 @@ describe(`ansibletower Route`, function () {
1919
})
2020

2121
it(`when bad things happen to Ansible TowerJobs 1`, async function () {
22-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
22+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
2323
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
2424
const res = await request('POST', '/ansibletower', {
2525
towerHost: TOWER_HOST + '/badPath',
@@ -30,7 +30,7 @@ describe(`ansibletower Route`, function () {
3030
})
3131

3232
it(`when bad things happen to Ansible TowerJobs 2`, async function () {
33-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
33+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
3434
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
3535
const res = await request('POST', '/ansibletower', {
3636
token: '12345',
@@ -39,7 +39,7 @@ describe(`ansibletower Route`, function () {
3939
})
4040

4141
it(`when bad things happen to Ansible TowerJobs 3`, async function () {
42-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(400)
42+
nock(process.env.CLUSTER_API_URL).get('/api').reply(400)
4343
const res = await request('POST', '/ansibletower')
4444
expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({}))
4545
})

backend/test/routes/apiPath.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ describe(`apiPath Route`, function () {
1515

1616
nock(process.env.CLUSTER_API_URL).get(paths[0]).reply(200, response)
1717

18-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
18+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
1919
status: 200,
2020
paths: response,
2121
})

backend/test/routes/hub.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { request } from '../mock-request'
55

66
describe('global hub', function () {
77
it('should return the boolean', async function () {
8-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
8+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
99
status: 200,
1010
})
1111
nock(process.env.CLUSTER_API_URL)

backend/test/routes/metrics.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { request } from '../mock-request'
44

55
describe('metrics route', function () {
66
it('Should response with successful metrics GET', async function () {
7-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
7+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
88
status: 200,
99
})
1010
nock(process.env.CLUSTER_API_URL)
@@ -25,7 +25,7 @@ describe('metrics route', function () {
2525
})
2626

2727
it('Should response with successful metrics GET request with page param', async function () {
28-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
28+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
2929
status: 200,
3030
})
3131
nock(process.env.CLUSTER_API_URL)

backend/test/routes/metricsProxy.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { request } from '../mock-request'
44

55
describe('metrics proxy route', function () {
66
it('Successfully calls prometheus endpoint', async function () {
7-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
7+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
88
status: 200,
99
})
1010
const res = await request('GET', '/prometheus/query')
1111
expect(res.statusCode).toEqual(200)
1212
})
1313
it(`Successfully calls observability endpoint`, async function () {
14-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
14+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
1515
status: 200,
1616
})
1717
const res = await request('GET', '/observability/query')

backend/test/routes/operatorCheck.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const subscriptionOperators = {
2323

2424
describe(`operatorCheck Route`, function () {
2525
it(`returns valid response with version for installed operator`, async function () {
26-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
26+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
2727
status: 200,
2828
})
2929
nock(process.env.CLUSTER_API_URL)
@@ -38,7 +38,7 @@ describe(`operatorCheck Route`, function () {
3838
})
3939
})
4040
it(`returns valid response for not-installed operator`, async function () {
41-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
41+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
4242
status: 200,
4343
})
4444
nock(process.env.CLUSTER_API_URL)
@@ -52,7 +52,7 @@ describe(`operatorCheck Route`, function () {
5252
})
5353
})
5454
it(`returns bad request for arbitrary operator`, async function () {
55-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
55+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
5656
status: 200,
5757
})
5858
nock(process.env.CLUSTER_API_URL)

backend/test/routes/search.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import nock from 'nock'
44

55
describe(`search Route`, function () {
66
it(`uses search-api in the namespace of the MultiClusterHub`, async function () {
7-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
7+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
88
status: 200,
99
})
1010
nock(process.env.CLUSTER_API_URL)
@@ -27,7 +27,7 @@ describe(`search Route`, function () {
2727
//expect(res.statusCode).toEqual(200)
2828
})
2929
it(`uses search-api in namespace of pod if no MultiClusterHub`, async function () {
30-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200, {
30+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200, {
3131
status: 200,
3232
})
3333
nock(process.env.CLUSTER_API_URL).get('/apis/operator.open-cluster-management.io/v1/multiclusterhubs').reply(200, {

0 commit comments

Comments
 (0)