From 6e5f15fd66516d45e6dad8805c050a81c9f97fd4 Mon Sep 17 00:00:00 2001 From: ekko <152005280+EKKOLearnAI@users.noreply.github.com> Date: Wed, 29 Apr 2026 23:09:18 +0800 Subject: [PATCH] fix(sse): use Authorization header instead of query token for EventSource (#318) * fix(sse): use Authorization header instead of query token for EventSource Fixes #315 - EventSource connection lost when Hermes Gateway requires Bearer token authentication. Problem: - Web UI used `?token=` for SSE event streaming - Hermes Gateway expects `Authorization: Bearer ` header (like other API endpoints) - Mismatch caused 'EventSource connection lost' errors on longer runs Solution: - Use eventsource library's `fetch` override to pass Authorization header - Apply fix to all 4 EventSource usage points: 1. chat-run-socket.ts - main chat run events 2. group-chat/agent-clients.ts - agent run events 3. context-compressor/index.ts - compression events 4. context-engine/gateway-client.ts - context engine events Benefits: - Consistent authentication across all API endpoints - Better compatibility with Hermes Gateway - Fixes SSE stream disconnections Note: Added @ts-ignore comments because eventsource library types are stricter than actual fetch API capabilities. Co-Authored-By: Claude Sonnet 4.6 * chore: bump version to 0.5.2 Includes fix for EventSource Authorization header (issue #315) --------- Co-authored-by: Claude Sonnet 4.6 --- package.json | 2 +- .../server/src/lib/context-compressor/index.ts | 15 +++++++++++++-- .../src/services/hermes/chat-run-socket.ts | 15 +++++++++++++-- .../hermes/context-engine/gateway-client.ts | 15 +++++++++++++-- .../services/hermes/group-chat/agent-clients.ts | 16 ++++++++++++++-- 5 files changed, 54 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 142d6be..b42189a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hermes-web-ui", - "version": "0.5.1", + "version": "0.5.2", "description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model (Claude, GPT, Gemini, DeepSeek) web UI with Telegram, Discord, Slack, WhatsApp integration", "repository": { "type": "git", diff --git a/packages/server/src/lib/context-compressor/index.ts b/packages/server/src/lib/context-compressor/index.ts index 944a24a..35756c4 100644 --- a/packages/server/src/lib/context-compressor/index.ts +++ b/packages/server/src/lib/context-compressor/index.ts @@ -316,9 +316,20 @@ async function callSummarizer( }, timeoutMs) const eventsUrl = new URL(`${upstream}/v1/runs/${run_id}/events`) - if (apiKey) eventsUrl.searchParams.set('token', apiKey) - const source = new EventSource(eventsUrl.toString()) + // Use Authorization header instead of query parameter for better compatibility + const eventSourceInit: any = apiKey ? { + fetch: (url: string, init: any = {}) => fetch(url, { + ...init, + headers: { + ...(init.headers || {}), + Authorization: `Bearer ${apiKey}`, + }, + }), + } : {} + + // @ts-ignore - eventsource library types are too strict + const source = new EventSource(eventsUrl.toString(), eventSourceInit) source.onmessage = (event: MessageEvent) => { try { diff --git a/packages/server/src/services/hermes/chat-run-socket.ts b/packages/server/src/services/hermes/chat-run-socket.ts index c6a2f7f..c6e0d17 100644 --- a/packages/server/src/services/hermes/chat-run-socket.ts +++ b/packages/server/src/services/hermes/chat-run-socket.ts @@ -571,9 +571,20 @@ export class ChatRunSocket { // Stream upstream events via EventSource — survives socket disconnect const eventsUrl = new URL(`${upstream}/v1/runs/${runId}/events`) - if (apiKey) eventsUrl.searchParams.set('token', apiKey) - const source = new EventSource(eventsUrl.toString()) + // Use Authorization header instead of query parameter for better compatibility + const eventSourceInit: any = apiKey ? { + fetch: (url: string, init: any = {}) => fetch(url, { + ...init, + headers: { + ...(init.headers || {}), + Authorization: `Bearer ${apiKey}`, + }, + }), + } : {} + + // @ts-ignore - eventsource library types are too strict + const source = new EventSource(eventsUrl.toString(), eventSourceInit) source.onmessage = (event: MessageEvent) => { try { diff --git a/packages/server/src/services/hermes/context-engine/gateway-client.ts b/packages/server/src/services/hermes/context-engine/gateway-client.ts index f96525a..55e145e 100644 --- a/packages/server/src/services/hermes/context-engine/gateway-client.ts +++ b/packages/server/src/services/hermes/context-engine/gateway-client.ts @@ -87,9 +87,20 @@ export class GatewaySummarizer implements GatewayCaller { }, this.timeoutMs) const eventsUrl = new URL(`${upstream}/v1/runs/${runId}/events`) - if (apiKey) eventsUrl.searchParams.set('token', apiKey) - const source = new EventSource(eventsUrl.toString()) + // Use Authorization header instead of query parameter for better compatibility + const eventSourceInit: any = apiKey ? { + fetch: (url: string, init: any = {}) => fetch(url, { + ...init, + headers: { + ...(init.headers || {}), + Authorization: `Bearer ${apiKey}`, + }, + }), + } : {} + + // @ts-ignore - eventsource library types are too strict + const source = new EventSource(eventsUrl.toString(), eventSourceInit) source.onmessage = async (event: MessageEvent) => { try { diff --git a/packages/server/src/services/hermes/group-chat/agent-clients.ts b/packages/server/src/services/hermes/group-chat/agent-clients.ts index 014c4e4..0d09ce8 100644 --- a/packages/server/src/services/hermes/group-chat/agent-clients.ts +++ b/packages/server/src/services/hermes/group-chat/agent-clients.ts @@ -333,9 +333,21 @@ class AgentClient { // Stream events from Hermes const eventsUrl = new URL(`${upstream}/v1/runs/${run_id}/events`) - if (apiKey) eventsUrl.searchParams.set('token', apiKey) logger.debug(`[AgentClients] ${this.name}: streaming events from ${eventsUrl}`) - const source = new EventSource(eventsUrl.toString()) + + // Use Authorization header instead of query parameter for better compatibility + const eventSourceInit: any = apiKey ? { + fetch: (url: string, init: any = {}) => fetch(url, { + ...init, + headers: { + ...(init.headers || {}), + Authorization: `Bearer ${apiKey}`, + }, + }), + } : {} + + // @ts-ignore - eventsource library types are too strict + const source = new EventSource(eventsUrl.toString(), eventSourceInit) let fullContent = ''