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:
+1
-1
@@ -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 = ''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user