Skip to content

Denied localStorage aborts Flux initialization before Alpine providers register #2841

Description

@cyppe

Flux version

v2.20.0 (livewire/flux and livewire/flux-pro)

Livewire version

v4.4.5

Tailwind version

v4.3.3

Browser and Operating System

Chromium on Linux; reproduced with both debug and production Flux Pro bundles. The denied-storage condition below is deliberately injected before initialization, so reproducing it does not depend on a particular browser privacy setting.

What is the problem?

An optional saved appearance preference can prevent unrelated Flux components from initializing when access to localStorage throws SecurityError.

There are two boundaries: the initial appearance read in Flux's alpine:init listener (and its fallback cleanup), and the reads/writes in @fluxAppearance. With storage denied, initialization throws before providers such as fluxModal register. A page can therefore load all its JavaScript successfully but still have unusable modals.

Condition Current behavior Expected behavior
Reading localStorage throws SecurityError Initialization throws; modal fails to open Use system appearance and finish registering components
Removing the saved preference throws SecurityError System-appearance initialization can throw Apply system appearance without persistence
Saving dark/light preference throws SecurityError Appearance change throws before applying the class Apply the choice in memory; skip persistence
Storage is available Works Keep existing persistence behavior

Code snippets to replicate the problem

In an otherwise working Flux/Livewire application, render this standalone Blade view. There are no models, application variables or third-party scripts:

<!doctype html>
<html>
<head>
    <script>
        // Deterministic simulation of browser storage being unavailable.
        Object.defineProperty(window, 'localStorage', {
            get() {
                throw new DOMException('Storage denied', 'SecurityError');
            },
        });
    </script>
    @fluxAppearance
</head>
<body>
    <div x-data>
        <button type="button" x-on:click="$flux.modal('storage-test').show()">
            Open dialog
        </button>
        <button type="button" x-on:click="$flux.appearance = 'dark'">Dark</button>
        <button type="button" x-on:click="$flux.appearance = 'light'">Light</button>

        <flux:modal name="storage-test">
            <p>Storage independent modal</p>
            <flux:modal.close>
                <flux:button>Close dialog</flux:button>
            </flux:modal.close>
        </flux:modal>
    </div>
    @fluxScripts
    @livewireScripts
</body>
</html>
  1. Load the view and inspect the console. Click Open dialog.
  2. Remove the injection script, reload, and click again: this is the normal-storage control and the modal works.
  3. Restore the injected getter and remove only @fluxAppearance: the runtime's own initial read still fails. This isolates the runtime from the inline appearance script.
  4. To exercise cleanup separately, replace the injected getter with:
Storage.prototype.removeItem = function () {
    throw new DOMException('Storage denied', 'SecurityError');
};

Use a fresh browser context with no saved appearance. For the write case, override setItem instead, restore @fluxAppearance, and click Dark / Light.

Console evidence / before and after

The unpatched getter case produces SecurityError: Storage denied, followed by missing-provider errors such as fluxModal is not defined; Open dialog fails. This is an initialization failure rather than a visual rendering discrepancy.

Our real-browser matrix runs getter denial, removal denial, write denial and an available-storage control against both debug and production Pro bundles, including @fluxAppearance:

  • Before: 6 failing cases; 2 normal-storage controls passing.
  • After guarding the relevant storage operations: 8 passing cases, 40 assertions, covering modal open/close, dark/light application and absence of JavaScript errors.

The same unguarded runtime operations are present in the public free bundles, but the executed browser matrix above is specifically for Pro plus the shared appearance directive.

How do you expect it to work?

A failure to persist a theme preference should not prevent modal/input providers from registering or prevent a theme choice being applied for the current page.

Suggested scope: catch only SecurityError around these optional storage operations, use system when the initial read is unavailable, and still apply appearance changes in memory when persistence fails. Re-throw unrelated exceptions so real implementation defects remain visible. Avoid wrapping the entire initialization handler in a catch.

The current public appearance directive contains the unguarded get/remove/set operations; the public dist/flux-lite.min.js and dist/flux.min.js also contain the unguarded runtime appearance read and fallback cleanup.

Please confirm

  • I have provided easy and step-by-step instructions to reproduce the bug.
  • I have provided code samples as text and NOT images.
  • I understand my bug report will be closed if I have not met the criteria above.

I searched open/closed issues, pull requests and discussions for localStorage, SecurityError and denied-storage initialization. The appearance reports I found concerned configuration, flashing or cross-subdomain persistence, rather than this failure.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions