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.
This commit is contained in:
deboste
2026-03-31 14:35:45 +00:00
parent a9ae0b0a83
commit 96547f68a3
4 changed files with 7 additions and 6 deletions

View File

@@ -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);}

View File

@@ -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));
}

View File

@@ -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);

View File

@@ -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();