From 96547f68a33cc61176f4def41c96e4e85d34f544 Mon Sep 17 00:00:00 2001 From: deboste Date: Tue, 31 Mar 2026 14:35:45 +0000 Subject: [PATCH 1/2] fix(frontend): use URL origin for fetch/EventSource to support reverse proxy auth When Hermes WebUI runs behind a reverse proxy with HTTP basic auth (e.g. Caddy basic_auth), browsers embed credentials in the page URL. The Fetch API and EventSource reject requests constructed from URLs that include credentials (per Fetch spec, all modern browsers). Fix: construct all fetch() and EventSource URLs via new URL(path, location.origin) which strips credentials from the base URL. Add credentials:"include" to ensure auth headers are forwarded on each request. --- static/boot.js | 2 +- static/messages.js | 4 ++-- static/ui.js | 4 ++-- static/workspace.js | 3 ++- 4 files changed, 7 insertions(+), 6 deletions(-) diff --git a/static/boot.js b/static/boot.js index da172b2..2f92890 100644 --- a/static/boot.js +++ b/static/boot.js @@ -2,7 +2,7 @@ async function cancelStream(){ const streamId = S.activeStreamId; if(!streamId) return; try{ - await fetch(`/api/chat/cancel?stream_id=${encodeURIComponent(streamId)}`); + await fetch(new URL(`/api/chat/cancel?stream_id=${encodeURIComponent(streamId)}`,location.origin).href,{credentials:'include'}); const btn=$('btnCancel');if(btn)btn.style.display='none'; setStatus('Cancelling…'); }catch(e){setStatus('Cancel failed: '+e.message);} diff --git a/static/messages.js b/static/messages.js index 5929d36..b1e4b0e 100644 --- a/static/messages.js +++ b/static/messages.js @@ -169,7 +169,7 @@ async function send(){ const st=await api(`/api/chat/stream/status?stream_id=${encodeURIComponent(streamId)}`); if(st.active){ setStatus('Reconnected'); - _wireSSE(new EventSource(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`)); + _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href)); return; } }catch(_){} @@ -214,7 +214,7 @@ async function send(){ if(!S.session||!INFLIGHT[S.session.session_id]){setBusy(false);setStatus('Error: Connection lost');} } - _wireSSE(new EventSource(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`)); + _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href)); } diff --git a/static/ui.js b/static/ui.js index dbd54aa..679c89e 100644 --- a/static/ui.js +++ b/static/ui.js @@ -11,7 +11,7 @@ async function populateModelDropdown(){ const sel=$('modelSelect'); if(!sel) return; try{ - const data=await fetch('/api/models').then(r=>r.json()); + const data=await fetch(new URL('/api/models',location.origin).href,{credentials:'include'}).then(r=>r.json()); if(!data.groups||!data.groups.length) return; // keep HTML defaults // Clear existing options sel.innerHTML=''; @@ -745,7 +745,7 @@ async function uploadPendingFiles(){ const f=S.pendingFiles[i];const fd=new FormData(); fd.append('session_id',S.session.session_id);fd.append('file',f,f.name); try{ - const res=await fetch('/api/upload',{method:'POST',body:fd}); + const res=await fetch(new URL('/api/upload',location.origin).href,{method:'POST',credentials:'include',body:fd}); if(!res.ok){const err=await res.text();throw new Error(err);} const data=await res.json(); if(data.error)throw new Error(data.error); diff --git a/static/workspace.js b/static/workspace.js index 9290871..fdcb30e 100644 --- a/static/workspace.js +++ b/static/workspace.js @@ -1,5 +1,6 @@ async function api(path,opts={}){ - const res=await fetch(path,{headers:{'Content-Type':'application/json'},...opts}); + const url=new URL(path,location.origin); + const res=await fetch(url.href,{credentials:'include',headers:{'Content-Type':'application/json'},...opts}); if(!res.ok)throw new Error(await res.text()); const ct=res.headers.get('content-type')||''; return ct.includes('application/json')?res.json():res.text(); From 1375ce0634ae56c08ebcd4955f556dbde09a3ecd Mon Sep 17 00:00:00 2001 From: Nathan Esquenazi Date: Wed, 1 Apr 2026 22:53:50 -0700 Subject: [PATCH 2/2] fix: add withCredentials to EventSource for reverse proxy auth The original PR correctly used new URL(path, location.origin) to strip credentials from fetch/EventSource URLs, and added credentials:'include' to all fetch() calls. However, EventSource requires { withCredentials: true } as a second constructor argument for cookies/auth headers to be forwarded. Without this, SSE streaming breaks behind a reverse proxy with basic auth. Co-Authored-By: Claude Opus 4.6 (1M context) --- static/messages.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/messages.js b/static/messages.js index b1e4b0e..c2413c2 100644 --- a/static/messages.js +++ b/static/messages.js @@ -169,7 +169,7 @@ async function send(){ const st=await api(`/api/chat/stream/status?stream_id=${encodeURIComponent(streamId)}`); if(st.active){ setStatus('Reconnected'); - _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href)); + _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href,{withCredentials:true})); return; } }catch(_){} @@ -214,7 +214,7 @@ async function send(){ if(!S.session||!INFLIGHT[S.session.session_id]){setBusy(false);setStatus('Error: Connection lost');} } - _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href)); + _wireSSE(new EventSource(new URL(`/api/chat/stream?stream_id=${encodeURIComponent(streamId)}`,location.origin).href,{withCredentials:true})); }