From dcc43205e0f7af7989c8b3f1f461be3480e2b148 Mon Sep 17 00:00:00 2001 From: Anso Date: Sun, 17 May 2026 05:06:45 -0400 Subject: [PATCH] refactor(mesh): route inspectStackServices through proxyFetch (#1082) inspectStackServices was constructing its Authorization, x-sencho-tier, and x-sencho-variant headers inline. proxyFetch is the centralized helper used by listStacksOnNode, pushOverrideToNode, triggerRemeshRedeploy, and removeOverrideFromNode. Equivalent today for a GET-no-body call, but any future header added to proxyFetch (audit context, license fingerprint, request id) would silently miss inspectStackServices. Wire output is byte-identical: same URL, same method, same three headers, same 5 s timeout. The new catch arm converts MeshError('push_failed') back to the existing warn + return [] contract callers (optInStack, refreshAliasCache) rely on. Pattern mirrors listStacksOnNode. --- backend/src/services/MeshService.ts | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/backend/src/services/MeshService.ts b/backend/src/services/MeshService.ts index 5ca6aab0..cddccff1 100644 --- a/backend/src/services/MeshService.ts +++ b/backend/src/services/MeshService.ts @@ -1180,19 +1180,14 @@ export class MeshService extends EventEmitter implements MeshForwarderHost { if (!node) return []; if (node.type !== 'remote') return this.inspectLocalStackServices(stackName); - const target = NodeRegistry.getInstance().getProxyTarget(nodeId); - if (!target) { - console.warn(`[MeshService] inspectStackServices: no proxy target for node ${nodeId} (${sanitizeForLog(node.name)})`); - return []; - } try { - const url = `${target.apiUrl.replace(/\/$/, '')}/api/mesh/local-services/${encodeURIComponent(stackName)}`; - const headers: Record = {}; - if (target.apiToken) headers['Authorization'] = `Bearer ${target.apiToken}`; - const proxyHeaders = LicenseService.getInstance().getProxyHeaders(); - headers[PROXY_TIER_HEADER] = proxyHeaders.tier; - headers[PROXY_VARIANT_HEADER] = proxyHeaders.variant || ''; - const res = await fetch(url, { headers, signal: AbortSignal.timeout(5_000) }); + const res = await this.proxyFetch( + nodeId, + 'GET', + `/api/mesh/local-services/${encodeURIComponent(stackName)}`, + undefined, + 5_000, + ); if (!res.ok) { console.error(`[MeshService] inspectStackServices: HTTP ${res.status} from node ${nodeId} (${sanitizeForLog(node.name)})`); return []; @@ -1200,6 +1195,13 @@ export class MeshService extends EventEmitter implements MeshForwarderHost { const body = await res.json() as { services?: Array<{ service: string; ports: number[] }> }; return body.services ?? []; } catch (err) { + // proxyFetch throws MeshError('push_failed') when getProxyTarget + // returns null (e.g. pilot tunnel offline). Treat the same as a + // non-OK response: empty list. + if (err instanceof MeshError && err.code === 'push_failed') { + console.warn(`[MeshService] inspectStackServices: no proxy target for node ${nodeId} (${sanitizeForLog(node.name)})`); + return []; + } console.error('[MeshService] inspectStackServices remote unreachable:', sanitizeForLog((err as Error).message)); return []; }