Open Closed

LeptonX SideMenuLayout throws JSException ('abp' undefined / abp.utils.* not a function) on InteractiveWebAssembly(prerender: false) pages during WASM warm-up #10738


User avatar
0
rcalv002 created

Summary

On a Blazor WebApp page rendered with InteractiveWebAssembly(prerender: false), the LeptonX SideMenuLayout calls JS-interop functions from the abp / LeptonX script bundle during component initialization, before that bundle has loaded. This throws repeated Microsoft.JSInterop.JSExceptions that are caught by the router's ErrorBoundary, which auto-recovers and retries — re-mounting the whole layout dozens of times for ~10–15s until the WASM script bundle finishes loading, then self-heals.

This is reproducible in a clean template (not specific to our code) and is caused by two framework components together:

  1. AbpScripts.razor emits the WebAssembly script bundle (the one that defines window.abp and the LeptonX globals) only when OperatingSystem.IsBrowser() is true — i.e. after the WASM runtime boots and the component re-renders.
  2. SideMenuLayout (and LeptonXStyleProvider) call into that bundle from OnInitializedAsync and OnAfterRenderAsync, with no guard for the script not yet being loaded.

On a prerender:false page there is no server-prerendered copy of the bundle, so (2) runs before (1) has injected/loaded the scripts.

Pages rendered with prerender (the default) do not hit this, because the bundle is written into the initial HTML and window.abp exists before the layout becomes interactive.

Environment

  • ABP: 10.4.1
  • LeptonX (Blazor) theme: 5.4.1
  • .NET: 10.0
  • UI: Blazor WebApp (Interactive Auto / WebAssembly islands), Side-menu layout
  • Auth: non-tiered, single host (OpenIddict in-host)
  • Render mode of the affected page: new InteractiveWebAssemblyRenderMode(prerender: false)

Steps to reproduce

  1. Create a Blazor WebApp (non-tiered, LeptonX, side-menu layout).
  2. Add a page rendered WebAssembly-only with prerender disabled, e.g.: <MyPage @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" /> (or set prerender: false on the component/route). Keep the default App.razor wiring of <AbpScripts ... WebAssemblyScriptFiles="..." @rendermode="..." />.
  3. Wrap the router in an ErrorBoundary (as the default template does) so the exception is observable rather than fatal.
  4. Hard-reload that page on a cold cache (Ctrl+Shift+R) so the WASM runtime boots fresh.
  5. Watch the browser console during the WASM warm-up window.

Expected: the side-menu layout initializes once the page is interactive, with no exceptions.

Actual: repeated JSExceptions during warm-up (see below), each caught by the ErrorBoundary and auto-recovered, re-mounting the layout many times until the abp/LeptonX bundle loads.

Exception message and full stack trace

(1) During OnInitializedAsync:

Microsoft.JSInterop.JSException: Could not find 'abp.utils.getCookieValue' ('abp' was undefined).
Error: Could not find 'abp.utils.getCookieValue' ('abp' was undefined).
at Microsoft.JSInterop.JSRuntime.<InvokeAsync>d__23`1[[System.String, System.Private.CoreLib]].MoveNext()
at Volo.Abp.AspNetCore.Components.Web.CookieService.GetAsync(String key)
at Volo.Abp.AspNetCore.Components.WebAssembly.LeptonXTheme.LeptonXStyleProvider.GetSideMenuStateAsync()
at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenuLayout.OnInitializedAsync()
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

(2) During OnAfterRenderAsync (after (1) is worked around, or in parallel):

Microsoft.JSInterop.JSException: The value 'abp.utils.addClassToTag' is not a function.
Error: The value 'abp.utils.addClassToTag' is not a function.
at Microsoft.JSInterop.JSRuntime.&lt;InvokeAsync&gt;d__23`1[[Microsoft.JSInterop.Infrastructure.IJSVoidResult, Microsoft.JSInterop]].MoveNext()
at Microsoft.JSInterop.JSRuntimeExtensions.InvokeVoidAsync(IJSRuntime jsRuntime, String identifier, Object[] args)
at Volo.Abp.AspNetCore.Components.Web.LeptonXTheme.Components.ApplicationLayout.SideMenuLayout.OnAfterRenderAsync(Boolean firstRender)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

SideMenuLayout.OnAfterRenderAsync also invokes initLeptonX and afterLeptonXInitialization (theme initialization) from the same bundle, which fail the same way until the bundle is loaded.

Root cause

AbpScripts.razor resolves WebAssemblyScriptFiles only under if (OperatingSystem.IsBrowser() && WebAssemblyScriptFiles != null), so on a prerender:false page the script bundle is injected only after WASM activation. SideMenuLayout/LeptonXStyleProvider then call abp.utils.getCookieValue, abp.utils.addClassToTag, initLeptonX, and afterLeptonXInitialization from OnInitializedAsync/OnAfterRenderAsync without verifying the bundle is loaded, so they throw until it is.

Suggested fix

SideMenuLayout / LeptonXStyleProvider should make their JS interop resilient to the script bundle not yet being present under InteractiveWebAssembly(prerender: false) — e.g. feature-detect window.abp before calling, await a "scripts ready" signal, or defer the cookie/class/init interop until the abp + LeptonX bundle is guaranteed loaded.

Workarounds we evaluated

  • Shimming the individual abp.utils.* functions early (in a plain <head> script) does not work: it just moves the failure to the next call, and initLeptonX is the theme's real initialization, which cannot be stubbed.
Markdown supported.
Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)

5 Answer(s)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    We can't reproduce the JSException window on a default Blazor WebApp + LeptonX template — would you mind sharing some details so we can spot what's different in your setup?

    For reference, the default template wires the bundle like this:

    // App.razor — Blazor host project
    <AbpScripts BundleName="@BlazorMudBlazorLeptonXThemeBundles.Scripts.Global"
                WebAssemblyScriptFiles="GlobalScripts"
                @rendermode="InteractiveAuto" />
    
    @code {
        private List<string> GlobalScripts => ["global.js"];
    }
    
    // BlazorModule.cs
    Configure<AbpBundlingOptions>(options =>
    {
        options.Parameters.InteractiveAuto = true;
        // style + script bundle contributors ...
    });
    

    With this setup, the server-side bundle (resolved from BundleName) is rendered into the initial HTML during the prerender pass. That bundle inherits from BlazorStandardBundles.Scripts.Global, which already includes abp.js via BlazorGlobalScriptContributor, so window.abp is defined before the WebAssembly runtime activates and SideMenuLayout reads abp.utils.getCookieValue. No race window.

    If @rendermode on <AbpScripts> is anything that skips server prerender (for example new InteractiveWebAssemblyRenderMode(prerender: false)), or Parameters.InteractiveAuto = true is missing from the host project's bundling options, the bundle that defines window.abp only lands in the DOM after WASM activates — and that gap is the JSException window you're seeing.

    Could you share:

    1. Your Blazor host project's full App.razor (the file containing <AbpScripts>)
    2. The Configure<AbpBundlingOptions> block in your BlazorModule
    3. The exact abp new command (or abp-studio template options) you used to generate the project

    That should let us pin down where your setup diverges from the default.

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    rcalv002 created

    we are on Blazor WebApp with Parameters.InteractiveAuto = true and the standard bundle wiring, exactly as your reference, we checked by creating new project in web app mode to check before we ported from server to wasm pages. The divergence is a single page that is intentionally WebAssembly-only (new InteractiveWebAssemblyRenderMode(prerender: false)) rather than Interactive Auto, and that's the page that hits the race.

    Why this page is WASM-only (prerender:false), not Auto by design, not oversight.

    This page must not do the Interactive Auto server-first → WASM transition:

    • Its page-local UI services are registered only in the .Blazor.Client project. An Interactive Auto page renders server-first on a cold cache, which would require those client-only services (one of them an internal type) to also be registered server-side — which we can't do without a .Blazor.Server → .Blazor.Client reference we deliberately don't have.
    • The page is also required to run with no server-side /_blazor circuit it should be pure WebAssembly, not a server circuit that later hands off to WASM.

    So InteractiveWebAssemblyRenderMode(prerender: false) is the correct, intended render mode for it. That is precisely the mode in which the abp/LeptonX bundle is not prerendered, so on this page <AbpScripts> (whose @rendermode follows the page's) emits the bundle only after WASM activation. Your template doesn't reproduce it only because the default has no genuinely WASM-only page; the moment a page is WASM-only, SideMenuLayout races the bundle.

    (1) App.razor — <AbpScripts> wiring:

    <AbpScripts BundleName="@BlazorLeptonXThemeBundles.Scripts.Global"
                WebAssemblyScriptFiles="GlobalScripts"
                @rendermode="PageRenderMode" />
    
    @code {
        // Per-route render mode. The affected page is WASM-only, so for it
        // PageRenderMode resolves to InteractiveWebAssemblyRenderMode(prerender:false).
        private IComponentRenderMode PageRenderMode =>
            IsWasmOnlyRoute ? new InteractiveWebAssemblyRenderMode(prerender: false)
            : IsAutoRoute   ? InteractiveAuto
                            : InteractiveServer;
    
        private List<string> GlobalScripts => ["global.js", "global-scripts.js"];
    }
    

    The routed page renders the same way: <ClientRoutes @rendermode="@(new InteractiveWebAssemblyRenderMode(prerender: false))" />.

    (2) Configure<AbpBundlingOptions> note InteractiveAuto = true is present:

    Configure<AbpBundlingOptions>(options =>
    {
        options.Parameters["LeptonXTheme.Layout"] = "side-menu";
        options.Parameters.InteractiveAuto = true;   // present
    
        options.ScriptBundles.Configure(
            BlazorLeptonXThemeBundles.Scripts.Global,
            bundle => bundle.AddFiles("/global-scripts.js"));
    
        options.StyleBundles.Configure(
            BlazorLeptonXThemeBundles.Styles.Global,
            bundle => bundle.AddFiles("/blazor-global-styles.css"));
        // (MVC bundles omitted)
    });
    

    (3) Template: Blazor WebApp (non-tiered), LeptonX side-menu, OpenIddict in-host, .NET 10, ABP 10.4.1, LeptonX 5.4.1

    A minimal repro isolating the framework layout.

    To rule out our own code, we ran a controlled A/B: two trivial pages with identical content and the same InteractiveWebAssemblyRenderMode(prerender:false) only the layout differs:

    • Under SideMenuLayout → crashes on cold direct load (the JSException storm → fatal error bar).
    • Under a bare/empty layout → loads cleanly, no exceptions.

    Same render mode, same bundle wiring; only the layout differs which isolates SideMenuLayout / LeptonXStyleProvider as the cause, independent of our app code.

    What we're asking.

    a. Framework hardening: can SideMenuLayout / LeptonXStyleProvider feature-detect window.abp (and the LeptonX globals), or await a "scripts ready" signal, before the cookie / addClassToTag / initLeptonX interop so a genuinely WASM-only page initializes the layout once, cleanly, after the bundle lands?

    b. Supported workaround meanwhile: is there a supported way to get the abp/LeptonX bundle into the initial HTML for a WASM-only page without introducing a /_blazor server circuit on it (which, per above, this page must not have)? For example, prerendering only the <AbpScripts> tags while the routed page stays prerender:false WASM-only. If decoupling <AbpScripts>'s render mode from the page is the intended approach, we'd want to confirm it doesn't create a circuit or an asset-swap conflict.

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Hi,

    <AbpScripts>'s @rendermode doesn't have to follow the page's. Decouple them on the WASM-only route only, and you keep the page on prerender: false (no /_blazor circuit) while letting <AbpScripts> server-prerender the bundle into the initial HTML.

    <AbpScripts BundleName="@BlazorLeptonXThemeBundles.Scripts.Global"
                WebAssemblyScriptFiles="GlobalScripts"
                @rendermode="AbpScriptsRenderMode" />
    
    @code {
        // Page render mode — unchanged.
        private IComponentRenderMode PageRenderMode =>
            IsWasmOnlyRoute ? new InteractiveWebAssemblyRenderMode(prerender: false)
            : IsAutoRoute   ? InteractiveAuto
                            : InteractiveServer;
    
        // AbpScripts — only the WASM-only branch differs: prerender on the server,
        // activate in WASM (no SignalR), so the bundle lands in the initial HTML.
        private IComponentRenderMode AbpScriptsRenderMode =>
            IsWasmOnlyRoute ? InteractiveWebAssembly   // prerender: true by default
            : PageRenderMode;                          // Auto / Server unchanged
    }
    

    InteractiveWebAssembly here is static SSR prerender + WASM activation; it doesn't open a SignalR circuit. The server prerender resolves BundleName, whose BlazorStandardBundles.Scripts.Global base contributes abp.js via BlazorGlobalScriptContributor, so window.abp is defined before WASM activates and SideMenuLayout reads abp.utils.getCookieValue. Auto and Server routes are untouched.

    Verified on a fresh Blazor WebApp + LeptonX + Blazorise template (the same wiring you have) with a page on InteractiveWebAssemblyRenderMode(prerender: false):

    | <AbpScripts> @rendermode | Bundle in initial HTML | /_blazor circuit | window.abp on page render | JSException | |---|---|---|---|---| | InteractiveAuto (default) | yes (Blazor.LeptonXTheme.Global.<hash>.js) | opened (negotiate?negotiateVersion=1) | ready | none | | InteractiveWebAssemblyRenderMode(prerender: false) (current) | no | not opened | undefined | reproduced (Could not find 'abp.utils.removeOidcUser' ('abp' was undefined)) | | InteractiveWebAssembly (prerender: true, suggested) | yes (Blazor.LeptonXTheme.Global.<hash>.js) | not opened | ready | none |

    Could you give the AbpScriptsRenderMode split a try and let us know whether the JSException window goes away?

    Thanks

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    rcalv002 created

    Hi Mali,

    Thanks for your suggestion. AbpScriptsRenderMode worked as suggestion and the exception is gone

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
  • User Avatar
    0
    maliming created
    Support Team Fullstack Developer

    Good news. 👍

    Markdown supported.
    Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)
Boost Your Development
ABP Live Training
Packages
See Trainings
Mastering ABP Framework Book
The Official Guide
Mastering
ABP Framework
Learn More
Mastering ABP Framework Book
Made with ❤️ on ABP v10.8.0-preview. Updated on September 24, 2026, 12:09
1
ABP Assistant
🔐 You need to be logged in to use the chatbot. Please log in first.