fix(desktop): stop calling a 3xx HTML reply a missing endpoint

The JSON guard in fetchJson/fetchPublicJson blamed every HTML reply on a
missing backend endpoint. A 3xx HTML body is an access proxy redirecting
to its login page: the endpoint exists, the credentials never arrived.
Besides misleading the user, the wording is the capability signal that
isMissingHealthEndpointError and the renderer's gateway-rpc predicate key
on, so an auth redirect was silently classified as "endpoint missing" and
routed onto compatibility paths instead of surfacing as an error.

Build the error in api-transport.ts (where the sibling httpStatusError
lives) and pick the hint by status: 3xx names the redirect and points at
the saved token/extra headers; everything else keeps the endpoint-missing
wording the predicates rely on.

Part of #112072
This commit is contained in:
teknium1
2026-09-15 12:08:59 -07:00
committed by Teknium
parent e65ddf1c7b
commit 47c3683f23
3 changed files with 37 additions and 12 deletions
@@ -19,6 +19,7 @@ import { afterAll, describe, expect, it } from 'vitest'
import {
destroyKeepaliveAgents,
downloadAgentFor,
htmlResponseError,
httpStatusError,
isIdempotentMethod,
isTransientTransportError,
@@ -404,3 +405,16 @@ describe('httpStatusError', () => {
expect(httpStatusError(0, 'boom').statusCode).toBe(500)
})
})
describe('htmlResponseError', () => {
it('names an auth redirect for 3xx HTML and keeps the endpoint-missing capability wording otherwise', () => {
const redirected = htmlResponseError('https://gateway.example.com/api/profiles', 302).message
expect(redirected).toContain('status 302')
expect(redirected).toMatch(/redirected/)
expect(redirected).not.toContain('endpoint is likely missing')
expect(htmlResponseError('https://gateway.example.com/api/missing', 404).message).toContain(
'endpoint is likely missing'
)
})
})
+20
View File
@@ -194,9 +194,29 @@ function readStatusCode(error: unknown): number {
return Number(error && typeof error === 'object' ? (error as { statusCode?: unknown }).statusCode : NaN)
}
/**
* Error for a JSON endpoint that answered with HTML. A 2xx/404 HTML body is
* the SPA index.html for an unregistered /api path, and downstream capability
* probes (isMissingHealthEndpointError, gateway-rpc) key on the "endpoint is
* likely missing" wording. A 3xx is an access proxy bouncing the request to
* its login page instead — the endpoint exists, the credentials never
* arrived — so it must neither carry that wording nor blame the backend.
*/
function htmlResponseError(url: string, statusCode: unknown) {
const status = Number(statusCode)
const hint =
status >= 300 && status < 400
? 'The request was redirected, usually by an authentication proxy in front of the gateway; check the saved token and extra gateway headers.'
: 'The endpoint is likely missing on the Hermes backend.'
return new Error(`Expected JSON from ${url} but got HTML (status ${statusCode}). ${hint}`)
}
export {
destroyKeepaliveAgents,
downloadAgentFor,
htmlResponseError,
httpStatusError,
isIdempotentMethod,
isTransientTransportError,
+3 -12
View File
@@ -33,6 +33,7 @@ import { classifyActiveRuntime } from './active-runtime-state'
import {
destroyKeepaliveAgents,
downloadAgentFor,
htmlResponseError,
httpStatusError,
jsonAgentFor,
readStatusCode,
@@ -5469,12 +5470,7 @@ function fetchJson(url, token, options: any = {}) {
const contentType = String(res.headers['content-type'] || '')
if (looksHtml || contentType.includes('text/html')) {
reject(
new Error(
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
'The endpoint is likely missing on the Hermes backend.'
)
)
reject(htmlResponseError(url, res.statusCode))
return
}
@@ -5631,12 +5627,7 @@ function fetchPublicJson(url, options: any = {}) {
const contentType = String(res.headers['content-type'] || '')
if (looksHtml || contentType.includes('text/html')) {
reject(
new Error(
`Expected JSON from ${url} but got HTML (status ${res.statusCode}). ` +
'The endpoint is likely missing on the Hermes backend.'
)
)
reject(htmlResponseError(url, res.statusCode))
return
}