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:
AbpScripts.razoremits the WebAssembly script bundle (the one that defineswindow.abpand the LeptonX globals) only whenOperatingSystem.IsBrowser()is true — i.e. after the WASM runtime boots and the component re-renders.SideMenuLayout(andLeptonXStyleProvider) call into that bundle fromOnInitializedAsyncandOnAfterRenderAsync, 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
- Create a Blazor WebApp (non-tiered, LeptonX, side-menu layout).
- Add a page rendered WebAssembly-only with prerender disabled, e.g.:
<MyPage @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />(or setprerender: falseon the component/route). Keep the defaultApp.razorwiring of<AbpScripts ... WebAssemblyScriptFiles="..." @rendermode="..." />. - Wrap the router in an
ErrorBoundary(as the default template does) so the exception is observable rather than fatal. - Hard-reload that page on a cold cache (Ctrl+Shift+R) so the WASM runtime boots fresh.
- 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.<InvokeAsync>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, andinitLeptonXis the theme's real initialization, which cannot be stubbed.
5 Answer(s)
-
0
Hi,
We can't reproduce the
JSExceptionwindow 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 fromBlazorStandardBundles.Scripts.Global, which already includesabp.jsviaBlazorGlobalScriptContributor, sowindow.abpis defined before the WebAssembly runtime activates andSideMenuLayoutreadsabp.utils.getCookieValue. No race window.If
@rendermodeon<AbpScripts>is anything that skips server prerender (for examplenew InteractiveWebAssemblyRenderMode(prerender: false)), orParameters.InteractiveAuto = trueis missing from the host project's bundling options, the bundle that defineswindow.abponly lands in the DOM after WASM activates — and that gap is theJSExceptionwindow you're seeing.Could you share:
- Your Blazor host project's full
App.razor(the file containing<AbpScripts>) - The
Configure<AbpBundlingOptions>block in yourBlazorModule - The exact
abp newcommand (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) - Your Blazor host project's full
-
0
we are on Blazor WebApp with
Parameters.InteractiveAuto = trueand 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.Clientproject. An Interactive Auto page renders server-first on a cold cache, which would require those client-only services (one of them aninternaltype) to also be registered server-side — which we can't do without a.Blazor.Server → .Blazor.Clientreference we deliberately don't have. - The page is also required to run with no server-side
/_blazorcircuit 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@rendermodefollows 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,SideMenuLayoutraces 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>noteInteractiveAuto = trueis 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 (theJSExceptionstorm → 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/LeptonXStyleProvideras the cause, independent of our app code.What we're asking.
a. Framework hardening: can
SideMenuLayout/LeptonXStyleProviderfeature-detectwindow.abp(and the LeptonX globals), or await a "scripts ready" signal, before the cookie /addClassToTag/initLeptonXinterop 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
/_blazorserver circuit on it (which, per above, this page must not have)? For example, prerendering only the<AbpScripts>tags while the routed page staysprerender:falseWASM-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) - Its page-local UI services are registered only in the
-
0
Hi,
<AbpScripts>'s@rendermodedoesn't have to follow the page's. Decouple them on the WASM-only route only, and you keep the page onprerender: false(no/_blazorcircuit) 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 }InteractiveWebAssemblyhere is static SSR prerender + WASM activation; it doesn't open a SignalR circuit. The server prerender resolvesBundleName, whoseBlazorStandardBundles.Scripts.Globalbase contributesabp.jsviaBlazorGlobalScriptContributor, sowindow.abpis defined before WASM activates andSideMenuLayoutreadsabp.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 |/_blazorcircuit |window.abpon 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
AbpScriptsRenderModesplit a try and let us know whether theJSExceptionwindow goes away?Thanks
Markdown supported.Copy, paste, or drag & drop images and files (max 100 MB per file, 100 MB total per post)