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=<query>` for SSE event streaming
- Hermes Gateway expects `Authorization: Bearer <token>` 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 <noreply@anthropic.com>

* chore: bump version to 0.5.2

Includes fix for EventSource Authorization header (issue #315)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
ekko
2026-04-29 23:09:18 +08:00
committed by GitHub
parent 037c2881d8
commit 6e5f15fd66
5 changed files with 54 additions and 9 deletions
+1 -1
View File
@@ -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",
@@ -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 {
@@ -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 {
@@ -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 {
@@ -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 = ''