Skip to content

Commit 1406bd1

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 91a9939 commit 1406bd1

13 files changed

Lines changed: 46 additions & 47 deletions

backend/src/lib/token.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,15 +28,14 @@ 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+
// GET /api returns the core API group (~200 bytes) — unlike /apis which grows
32+
// with every installed CRD. The response body is drained so the socket returns
33+
// to the keepAlive pool immediately and native memory does not accumulate.
3534
export async function isAuthenticated(token: string): Promise<number> {
3635
const response = await fetchRetry(process.env.CLUSTER_API_URL + '/api', {
37-
method: 'HEAD',
3836
headers: { [HTTP2_HEADER_AUTHORIZATION]: `Bearer ${token}` },
3937
})
38+
response.body?.on('error', () => undefined).resume()
4039
return response.status
4140
}
4241

backend/test/routes/aggregator.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { IResource } from '../../src/resources/resource'
1717
describe(`aggregator Route`, function () {
1818
it(`should page Unfiltered Applications`, async function () {
1919
resetApplicationCache()
20-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
20+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
2121

2222
// initialize events
2323
await Promise.all(resources.map((resource) => cacheResource(resource)))
@@ -63,7 +63,7 @@ describe(`aggregator Route`, function () {
6363
})
6464
it(`should page Filtered Applications`, async function () {
6565
resetApplicationCache()
66-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
66+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
6767

6868
// initialize events
6969
await Promise.all(resources.map((resource) => cacheResource(resource)))
@@ -93,7 +93,7 @@ describe(`aggregator Route`, function () {
9393
})
9494
it(`should return application counts`, async function () {
9595
resetApplicationCache()
96-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
96+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
9797

9898
// initialize events
9999
await Promise.all(resources.map((resource) => cacheResource(resource)))
@@ -116,7 +116,7 @@ describe(`aggregator Route`, function () {
116116
})
117117
it(`should return appset data`, async function () {
118118
resetApplicationCache()
119-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
119+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
120120

121121
// initialize events
122122
resources.forEach((resource) => cacheResource(resource))

backend/test/routes/ansibletower.test.ts

Lines changed: 11 additions & 11 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(`should reject body-supplied tower hostname`, 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
const res = await request('POST', '/ansibletower', {
9999
towerHost: TOWER_HOST + ansiblePaths[0],
100100
token: '12345',
@@ -103,7 +103,7 @@ describe(`ansibletower Route`, function () {
103103
})
104104

105105
it(`should fail closed when caller cannot read the credential secret`, async function () {
106-
nock(process.env.CLUSTER_API_URL).get('/apis').reply(200)
106+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
107107
nock(process.env.CLUSTER_API_URL)
108108
.get(`/api/v1/namespaces/${SECRET_NS}/secrets/${SECRET_NAME}`)
109109
.reply(403, { kind: 'Status', apiVersion: 'v1', status: 'Failure', reason: 'Forbidden', code: 403 })
@@ -116,7 +116,7 @@ describe(`ansibletower Route`, function () {
116116
})
117117

118118
it(`when bad things happen to Ansible Automation controller Jobs 1`, async function () {
119-
nock(process.env.CLUSTER_API_URL).head('/api').reply(200)
119+
nock(process.env.CLUSTER_API_URL).get('/api').reply(200)
120120
nockCredentialSecret(TOWER_HOST)
121121
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
122122
const res = await request('POST', '/ansibletower', {
@@ -129,7 +129,7 @@ describe(`ansibletower Route`, function () {
129129
})
130130

131131
it(`when bad things happen to Ansible Automation controller Jobs 2`, 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
nockCredentialSecret(TOWER_HOST)
134134
nock(TOWER_HOST).get(ansiblePaths[0]).reply(200, response)
135135
const res = await request('POST', '/ansibletower', {
@@ -141,7 +141,7 @@ describe(`ansibletower Route`, function () {
141141
})
142142

143143
it(`when bad things happen to Ansible Automation controller Jobs 3`, async function () {
144-
nock(process.env.CLUSTER_API_URL).head('/api').reply(401)
144+
nock(process.env.CLUSTER_API_URL).get('/api').reply(401)
145145
const res = await request('POST', '/ansibletower')
146146
expect(res.statusCode).toEqual(401)
147147
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)