Skip to content

Commit 9f0f675

Browse files
Randy424claude
andcommitted
ACM-38826 fix(backend): use GET /api with body drain instead of HEAD
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 <noreply@anthropic.com>
1 parent 1c3cbe5 commit 9f0f675

13 files changed

Lines changed: 48 additions & 45 deletions

backend/src/lib/token.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,18 @@ export function getToken(req: Http2ServerRequest): string | undefined {
2828
return token
2929
}
3030

31-
// HEAD /api returns headers only — no response body — so no drain is needed and
32-
// the payload is ~200 bytes regardless of how many CRDs are registered.
33-
// Returns the HTTP status so callers can distinguish 401 (invalid token) from
34-
// 403 (valid token, insufficient permission) and 5xx (transient upstream error).
31+
32+
33+
34+
35+
// GET /api returns the core API group (~200 bytes) — unlike /apis which grows
36+
// with every installed CRD. The response body is drained so the socket returns
37+
// to the keepAlive pool immediately and native memory does not accumulate.
3538
export async function isAuthenticated(token: string): Promise<number> {
3639
const response = await fetchRetry(process.env.CLUSTER_API_URL + '/api', {
37-
method: 'HEAD',
3840
headers: { [HTTP2_HEADER_AUTHORIZATION]: `Bearer ${token}` },
3941
})
42+
response.body?.on('error', () => undefined).resume()
4043
return response.status
4144
}
4245

backend/test/routes/aggregator.test.ts

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

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

5656
// initialize events - cache sequentially to ensure deterministic order
5757
for (const resource of resources) {
@@ -98,7 +98,7 @@ describe(`aggregator Route`, function () {
9898
expect(await parseResponseJsonBody(res)).toEqual(responseNoFilter)
9999
})
100100
it(`should page Filtered Applications`, async function () {
101-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
101+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
102102

103103
// initialize events - cache sequentially to ensure deterministic order
104104
for (const resource of resources) {
@@ -129,7 +129,7 @@ describe(`aggregator Route`, function () {
129129
expect(await parseResponseJsonBody(res)).toEqual(responseFiltered)
130130
})
131131
it(`should return application counts`, async function () {
132-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
132+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
133133

134134
// initialize events - cache sequentially to ensure deterministic order
135135
for (const resource of resources) {
@@ -153,7 +153,7 @@ describe(`aggregator Route`, function () {
153153
expect(await parseResponseJsonBody(res)).toEqual(responseCount)
154154
})
155155
it(`should return appset data`, async function () {
156-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
156+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
157157

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

backend/test/routes/ansibletower.test.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ function nockCredentialSecret(host: string) {
2424

2525
describe(`ansibletower Route`, function () {
2626
it(`should list Ansible Automation controller Jobs`, async function () {
27-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
27+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
2828
nockCredentialSecret(TOWER_HOST)
2929
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
3030
const res = await request('POST', '/ansibletower', {
@@ -37,7 +37,7 @@ describe(`ansibletower Route`, function () {
3737
})
3838

3939
it(`should reject body-supplied tower hostname`, async function () {
40-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
40+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
4141
const res = await request('POST', '/ansibletower', {
4242
towerHost: TOWER_HOST + ansiblePaths[0],
4343
token: '12345',
@@ -46,7 +46,7 @@ describe(`ansibletower Route`, function () {
4646
})
4747

4848
it(`should preserve the query string for paginated requests`, async function () {
49-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
49+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
5050
nockCredentialSecret(TOWER_HOST)
5151
nock(TOWER_HOST).get(ansiblePaths[0]).query({ page: '2', page_size: '20' }).reply(200, response)
5252
const res = await request('POST', '/ansibletower', {
@@ -59,7 +59,7 @@ describe(`ansibletower Route`, function () {
5959
})
6060

6161
it(`should reject an external absolute URL in ansiblePath`, async function () {
62-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
62+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
6363
nockCredentialSecret(TOWER_HOST)
6464
const res = await request('POST', '/ansibletower', {
6565
secretNamespace: SECRET_NS,
@@ -70,7 +70,7 @@ describe(`ansibletower Route`, function () {
7070
})
7171

7272
it(`should reject a network-path reference in ansiblePath`, async function () {
73-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
73+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
7474
nockCredentialSecret(TOWER_HOST)
7575
const res = await request('POST', '/ansibletower', {
7676
secretNamespace: SECRET_NS,
@@ -81,7 +81,7 @@ describe(`ansibletower Route`, function () {
8181
})
8282

8383
it(`should fail closed when caller cannot read the credential secret`, async function () {
84-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
84+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
8585
nock(process.env.CLUSTER_API_URL)
8686
.get(`/api/v1/namespaces/${SECRET_NS}/secrets/${SECRET_NAME}`)
8787
.reply(403, { kind: 'Status', apiVersion: 'v1', status: 'Failure', reason: 'Forbidden', code: 403 })
@@ -94,7 +94,7 @@ describe(`ansibletower Route`, function () {
9494
})
9595

9696
it(`when bad things happen to Ansible Automation controller Jobs 1`, async function () {
97-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
97+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
9898
nockCredentialSecret(TOWER_HOST)
9999
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
100100
const res = await request('POST', '/ansibletower', {
@@ -107,7 +107,7 @@ describe(`ansibletower Route`, function () {
107107
})
108108

109109
it(`when bad things happen to Ansible Automation controller Jobs 2`, async function () {
110-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
110+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
111111
nockCredentialSecret(TOWER_HOST)
112112
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
113113
const res = await request('POST', '/ansibletower', {
@@ -119,7 +119,7 @@ describe(`ansibletower Route`, function () {
119119
})
120120

121121
it(`when bad things happen to Ansible Automation controller Jobs 3`, async function () {
122-
nock(process.env.CLUSTER_API_URL).head('/api').reply(401)
122+
nock(process.env.CLUSTER_API_URL).get('/api').reply(401)
123123
const res = await request('POST', '/ansibletower')
124124
expect(res.statusCode).toEqual(401)
125125
expect(JSON.stringify(await parsePipedJsonBody(res))).toEqual(JSON.stringify({}))

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).head('/api').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).head('/api').reply(200)
8+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
99
nock(process.env.CLUSTER_API_URL)
1010
.get('/apis/apiextensions.k8s.io/v1/customresourcedefinitions') // .reply(200, { isGlobalHub: true })
1111
.reply(200, {

backend/test/routes/hypershift-status.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { parseResponseJsonBody } from '../../src/lib/body-parser'
44
import nock from 'nock'
55

66
describe('hypershift-status Route', function () {
7-
const mockAuth = () => nock(process.env.CLUSTER_API_URL).head('/api').reply(200, { status: 200 })
7+
const mockAuth = () => nock(process.env.CLUSTER_API_URL).get('/api').reply(200, { status: 200 })
88

99
const mockMCE = (hypershiftEnabled = true, localHostingEnabled = true) =>
1010
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).head('/api').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).head('/api').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).head('/api').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).head('/api').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).head('/api').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).head('/api').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).head('/api').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, {

backend/test/routes/upgrade-risks-prediction.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('Upgrade risks prediction Route', function () {
77
it('should return the upgrade risks', async function () {
8-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
8+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
99
nock(process.env.CLUSTER_API_URL)
1010
.get('/api/v1/namespaces/openshift-config/secrets')
1111
.reply(200, {

0 commit comments

Comments
 (0)