Skip to content

fix: guard localStorage.setItem('hermes-webui-model') against QuotaExceededError#1712

Closed
24601 wants to merge 1 commit into
nesquena:masterfrom
24601:fix/localstorage-quota-exceeded
Closed

fix: guard localStorage.setItem('hermes-webui-model') against QuotaExceededError#1712
24601 wants to merge 1 commit into
nesquena:masterfrom
24601:fix/localstorage-quota-exceeded

Conversation

@24601
Copy link
Copy Markdown
Contributor

@24601 24601 commented May 5, 2026

Problem

On some setups (localStorage near quota), the bare localStorage.setItem('hermes-webui-model', ...) call throws an unhandled DOMException:

Failed to execute 'setItem' on 'Storage': Setting the value of 'hermes-webui-model' exceeded the quota.

This surfaces as a fatal exception that breaks model selection and prevents the chat UI from loading on every new chat or page load.

Fix

Wrap both call-sites in try/catch so the error is logged to the console as a console.warn instead of crashing the UI. The stored value (a model ID string) is tiny — the quota failure is from overall localStorage pressure, not this key — so graceful degradation (fall back to server-side model state on next load) is the right behaviour.

Files changed:

  • static/boot.js$('modelSelect').onchange handler
  • static/onboarding.js_saveOnboardingDefaults()

Test

  1. Fill localStorage to near-quota in DevTools
  2. Change model in the selector → no thrown exception, console shows warning
  3. Reload → UI loads normally

…ceededError

On some setups the localStorage quota is exhausted; the bare setItem
call throws an unhandled DOMException that breaks model selection and
prevents the chat UI from loading.

Wrap both call-sites (boot.js model-select onChange, onboarding.js
_saveOnboardingDefaults) in try/catch so the error is logged to the
console as a warning instead of surfacing as a fatal exception.

Fixes: 'Failed to execute setItem on Storage: Setting the value of
hermes-webui-model exceeded the quota.'
@24601 24601 force-pushed the fix/localstorage-quota-exceeded branch from ac73170 to 646d48e Compare May 5, 2026 17:26
@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Thanks for the report and the patch — the QuotaExceededError on localStorage.setItem('hermes-webui-model', ...) is real, and graceful degradation is the right call. But after reading the diff against static/boot.js:815 and static/onboarding.js:468, then walking the actual call sites that touch this key, I think this PR misses the two most likely places the error actually fires from. As-is it's almost a no-op.

What's already wrapped vs what's not

The canonical write helper is _writePersistedModelState() in static/ui.js:457-469:

function _writePersistedModelState(model, modelProvider){
  const value=String(model||'').trim();
  ...
  localStorage.setItem('hermes-webui-model', value);          // line 465 — UNGUARDED
  try{
    localStorage.setItem(MODEL_STATE_KEY, JSON.stringify({model:value,model_provider:provider||null}));
  }catch(_){}
}

Whoever wrote this wrapped the second setItem (the JSON MODEL_STATE_KEY write, which is large and the actual quota risk) but not the first. Under quota pressure, line 465 is what throws. Every other module in the codebase routes through this helper, so wrapping callers without wrapping the helper itself doesn't help.

Why the boot.js change is effectively dead code

The patched line is the else branch:

if(typeof _writePersistedModelState==='function') _writePersistedModelState(modelState.model,modelState.model_provider);
else try{localStorage.setItem('hermes-webui-model',modelState.model)}catch{}

_writePersistedModelState is defined in static/ui.js, which is loaded before static/boot.js in the bundle. The typeof === 'function' branch is always taken in practice; the else fallback only existed for paranoia. Guarding the fallback while leaving the helper unguarded means the actual flow still throws.

The other unguarded site the PR misses

static/messages.js:233 writes the same key during stream start:

if(startData.effective_model && S.session){
  S.session.model=startData.effective_model;
  ...
  localStorage.setItem('hermes-webui-model', startData.effective_model);   // unguarded
  if(typeof _writePersistedModelState==='function') _writePersistedModelState(...);

This fires on every send when the gateway resolves a different effective model — which on near-quota setups is going to throw before the onboarding/onchange paths even get a chance.

Suggested fix

Push the try/catch one level down into the helper, then drop the now-redundant outer wraps:

// static/ui.js:465
try{ localStorage.setItem('hermes-webui-model', value); }catch(_){}

And add the same guard at static/messages.js:233. The onboarding.js wrap you already added stays useful (that path runs before _writePersistedModelState exists during first-boot wizard, in some load orders), so keep that one. The boot.js change can revert since the else branch is unreachable.

Verification

  • DevTools → Application → Local Storage, fill to quota with a junk key
  • Send a message that the gateway re-resolves to a different model → check messages.js:233 no longer throws
  • Change model in selector → _writePersistedModelState no longer throws
  • Reload, run onboarding → still wraps correctly

CI passing here just means the existing tests don't cover quota-exceeded paths (none do). A unit test would need a localStorage stub that throws on setItem; happy to point at where to add one if you want to extend the patch.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Closed by the v0.51.5 release in PR #1713 (merged at 0ea3dfb, deployed to production). Thanks!

Live on production: https://github.com/nesquena/hermes-webui/releases/tag/v0.51.5

🚀

githb-ac pushed a commit to githb-ac-org/hermes-webui that referenced this pull request May 5, 2026
githb-ac pushed a commit to githb-ac-org/hermes-webui that referenced this pull request May 5, 2026
4 PRs (1 surface addition, 3 fixes):
- nesquena#1688 VPS resource health Insights panel (@Michaelyklam, closes nesquena#693)
- nesquena#1709 preserve scroll on stream completion (@Michaelyklam, closes nesquena#1690)
- nesquena#1711 hide rename tooltip on folders (@nesquena-hermes, closes nesquena#1710)
- nesquena#1712 guard localStorage.setItem against QuotaExceededError (@24601)

Tests: 4504 → 4527 (+23). Opus: SHIP, 6/6 verification clean.

Held back: nesquena#1686 (Docker enhance) — Opus flagged sibling-repo dep that
breaks standalone clones. Left open for follow-up.

Co-authored-by: Michael Lam <Michaelyklam1@gmail.com>
Co-authored-by: 24601 <noreply@github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants