diff --git a/apps/desktop/electron/main.cjs b/apps/desktop/electron/main.cjs index 17a37e15d6f..150f0cfe9b7 100644 --- a/apps/desktop/electron/main.cjs +++ b/apps/desktop/electron/main.cjs @@ -1617,10 +1617,29 @@ function fetchJson(url, token, options = {}) { reject(new Error(`${res.statusCode}: ${text || res.statusMessage}`)) return } + if (!text) { + resolve(null) + return + } + // A 2xx response whose body is HTML means the request fell through + // to the SPA index.html (e.g. an unregistered /api path). JSON.parse + // would throw an opaque `Unexpected token '<'` here, so surface a + // clear diagnostic with the offending URL instead. + const looksHtml = /^\s*<(?:!doctype|html)/i.test(text) + 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.' + ) + ) + return + } try { - resolve(text ? JSON.parse(text) : null) - } catch (error) { - reject(error) + resolve(JSON.parse(text)) + } catch { + reject(new Error(`Invalid JSON from ${url} (status ${res.statusCode}): ${text.slice(0, 200)}`)) } }) } diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index cd294681fc8..eee4710bbc3 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -4758,6 +4758,17 @@ def mount_spa(application: FastAPI): @application.get("/{full_path:path}") async def serve_spa(full_path: str, request: Request): prefix = _normalise_prefix(request.headers.get("x-forwarded-prefix")) + # An unmatched /api/* path is a missing/renamed endpoint, NOT a + # client-side route. Falling through to index.html here returns + # `` with status 200, which makes JSON clients (the + # desktop app's fetchJson, dashboard fetch wrappers) blow up with an + # opaque `SyntaxError: Unexpected token '<'`. Return a real 404 JSON + # so the caller sees a clear "no such endpoint" instead. + if full_path == "api" or full_path.startswith("api/"): + return JSONResponse( + {"detail": f"No such API endpoint: /{full_path}"}, + status_code=404, + ) file_path = WEB_DIST / full_path # Prevent path traversal via url-encoded sequences (%2e%2e/) if (