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",
|
"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",
|
"description": "Self-hosted AI chat dashboard for Hermes Agent — multi-model (Claude, GPT, Gemini, DeepSeek) web UI with Telegram, Discord, Slack, WhatsApp integration",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -316,9 +316,20 @@ async function callSummarizer(
|
|||||||
}, timeoutMs)
|
}, timeoutMs)
|
||||||
|
|
||||||
const eventsUrl = new URL(`${upstream}/v1/runs/${run_id}/events`)
|
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) => {
|
source.onmessage = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -571,9 +571,20 @@ export class ChatRunSocket {
|
|||||||
|
|
||||||
// Stream upstream events via EventSource — survives socket disconnect
|
// Stream upstream events via EventSource — survives socket disconnect
|
||||||
const eventsUrl = new URL(`${upstream}/v1/runs/${runId}/events`)
|
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) => {
|
source.onmessage = (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -87,9 +87,20 @@ export class GatewaySummarizer implements GatewayCaller {
|
|||||||
}, this.timeoutMs)
|
}, this.timeoutMs)
|
||||||
|
|
||||||
const eventsUrl = new URL(`${upstream}/v1/runs/${runId}/events`)
|
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) => {
|
source.onmessage = async (event: MessageEvent) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -333,9 +333,21 @@ class AgentClient {
|
|||||||
|
|
||||||
// Stream events from Hermes
|
// Stream events from Hermes
|
||||||
const eventsUrl = new URL(`${upstream}/v1/runs/${run_id}/events`)
|
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}`)
|
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 = ''
|
let fullContent = ''
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user