From 573d98c372667e9495785f0e5fdd67a8cbb443c0 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 22 Jul 2026 19:07:45 -0400 Subject: [PATCH 1/4] Fix Test Tool v2 Blazor UI non-interactive on .NET 10 (static assets 404) On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS bundle are served through the endpoint-routing MapStaticAssets API backed by the *.staticwebassets.endpoints.json manifest, not the classic static-files middleware. The tool only registered a wwwroot-only UseStaticFiles provider, so on net10 those assets returned 404, window.Blazor was never defined, no interactive server circuit was established, and the whole UI rendered statically (buttons/@onclick/@bind non-functional). Add a NET9_0_OR_GREATER-guarded app.MapStaticAssets() so net9+ serves the framework + scoped assets via the endpoints manifest while net8.0 keeps its existing path. Also disable the build-manifest dev-time runtime-patching handler (ReloadStaticAssetsAtRuntime=false), which otherwise probes those assets through the physical wwwroot provider and threw FileNotFoundException (HTTP 500) for _framework/* under dotnet run/build. --- .../156dd8b1-1af2-45fc-b051-b94dc51fc56f.json | 11 ++++++++ .../Processes/TestToolProcess.cs | 25 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 .autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json diff --git a/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json new file mode 100644 index 000000000..1b6c1abd8 --- /dev/null +++ b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.TestTool", + "Type": "Patch", + "ChangelogMessages": [ + "Fix Blazor web UI being non-interactive on .NET 9\u002B/net10.0 (framework static assets 404) by serving them via MapStaticAssets" + ] + } + ] +} \ No newline at end of file diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index 5da9d349b..80380b805 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -40,6 +40,19 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT Utils.ConfigureWebApplicationBuilder(builder); +#if NET9_0_OR_GREATER + // On .NET 9+ framework assets are served through MapStaticAssets (see below). When the + // build-time static web assets manifest is in use (dotnet build / dotnet run), ASP.NET Core + // attaches a development-time runtime-patching handler that probes each asset via the app's + // physical WebRootFileProvider (wwwroot) to detect local edits. The framework assets + // (_framework/*) and the scoped-CSS bundle do not physically exist under wwwroot, so that + // probe throws FileNotFoundException and the assets return HTTP 500. Disabling the runtime + // reload makes MapStaticAssets serve directly from the manifest's content roots, which is + // exactly what the installed global tool (publish manifest) already does. This tool manages + // its own static file serving, so runtime hot-reload of wwwroot is not needed. + builder.Configuration["ReloadStaticAssetsAtRuntime"] = "false"; +#endif + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -100,6 +113,18 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT FileProvider = wwwrootFileProvider }); +#if NET9_0_OR_GREATER + // On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS + // bundle (Amazon.Lambda.TestTool.styles.css) are served through the endpoint-routing + // static assets API backed by the *.staticwebassets.endpoints.json manifest, not the + // classic static-files middleware above. Without this call those assets return 404, so + // window.Blazor is never defined and the interactive server circuit is never established, + // leaving the entire Blazor UI non-interactive. This manifest is generated relative to the + // published output, so it works for the installed global-tool scenario. Guarded so net8.0 + // (which serves these assets via the wwwroot provider above) keeps its existing behavior. + app.MapStaticAssets(); +#endif + app.UseAntiforgery(); app.MapRazorComponents() From 390c54c975ddd6988611ee0a6b7c09eae00533eb Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 22 Jul 2026 20:05:56 -0400 Subject: [PATCH 2/4] Serve net10 Blazor framework assets with real bytes (fix 0-byte 200 responses) The previous fix made _framework/blazor.web.js return HTTP 200 but with an empty (0-byte) body, so window.Blazor was still never defined and the UI stayed non-interactive. Two root causes, both fixed here: 1. MapStaticAssets serves asset bytes from IWebHostEnvironment.WebRootFileProvider. Under dotnet run the framework files live in the NuGet cache and are mapped in via *.staticwebassets.runtime.json, but ASP.NET Core only composes that manifest into the web root automatically in the Development environment. The tool runs as Production by default, so the web root was a bare wwwroot provider and the invoker caught FileNotFoundException and returned an empty 200. Fixed by calling builder.WebHost.UseStaticWebAssets(), which composes the manifest regardless of environment (and is a harmless no-op for the installed tool, where the framework files are published directly into wwwroot). 2. As an installed global tool the process launches from an arbitrary working directory, and the default content root (hence WebRootFileProvider = contentRoot/wwwroot) followed the cwd, so MapStaticAssets looked in a nonexistent wwwroot and returned empty 200s for every asset. Fixed by pinning ContentRootPath to AppContext.BaseDirectory. Replaces the earlier ReloadStaticAssetsAtRuntime=false workaround, which only suppressed the dev hot-reload 500 but left MapStaticAssets serving 0 bytes. Verified on net10 with byte counts and a real browser (Playwright window.Blazor): - dotnet run: blazor.web.js=200575 bytes, styles.css=2024, bootstrap=232808; window.Blazor=true on / and /documentation. - installed global tool launched from a foreign cwd: same byte counts; window.Blazor=true on / and /documentation. net8 still builds and stays interactive (window.Blazor=true). --- .../156dd8b1-1af2-45fc-b051-b94dc51fc56f.json | 4 +- .../Processes/TestToolProcess.cs | 45 +++++++++++++------ 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json index 1b6c1abd8..f590f864f 100644 --- a/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json +++ b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json @@ -4,8 +4,8 @@ "Name": "Amazon.Lambda.TestTool", "Type": "Patch", "ChangelogMessages": [ - "Fix Blazor web UI being non-interactive on .NET 9\u002B/net10.0 (framework static assets 404) by serving them via MapStaticAssets" + "Fix Blazor web UI being non-interactive on .NET 9+/net10.0. Framework static assets (_framework/blazor.web.js) and the scoped-CSS bundle are now served via MapStaticAssets, the static web assets manifest is composed regardless of hosting environment, and the content root is pinned to the tool's install directory so the assets resolve correctly when running as an installed global tool." ] } ] -} \ No newline at end of file +} diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index 80380b805..d5735a702 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -36,21 +36,37 @@ public class TestToolProcess /// public static TestToolProcess Startup(RunCommandSettings settings, CancellationToken cancellationToken = default) { - var builder = WebApplication.CreateBuilder(); + var builder = WebApplication.CreateBuilder(new WebApplicationOptions + { + // Pin the content root to the tool's install directory rather than the current + // working directory. As an installed global tool the process is launched from an + // arbitrary cwd, and the default content root is the cwd. On .NET 9+ the framework + // and scoped-CSS assets are served by MapStaticAssets from the WebRootFileProvider, + // which is derived from the content root (contentRoot/wwwroot). If the content root + // is a foreign cwd, that provider points at a nonexistent wwwroot and MapStaticAssets + // returns empty (0-byte) 200 responses, leaving the Blazor UI non-interactive. + ContentRootPath = AppContext.BaseDirectory + }); Utils.ConfigureWebApplicationBuilder(builder); #if NET9_0_OR_GREATER - // On .NET 9+ framework assets are served through MapStaticAssets (see below). When the - // build-time static web assets manifest is in use (dotnet build / dotnet run), ASP.NET Core - // attaches a development-time runtime-patching handler that probes each asset via the app's - // physical WebRootFileProvider (wwwroot) to detect local edits. The framework assets - // (_framework/*) and the scoped-CSS bundle do not physically exist under wwwroot, so that - // probe throws FileNotFoundException and the assets return HTTP 500. Disabling the runtime - // reload makes MapStaticAssets serve directly from the manifest's content roots, which is - // exactly what the installed global tool (publish manifest) already does. This tool manages - // its own static file serving, so runtime hot-reload of wwwroot is not needed. - builder.Configuration["ReloadStaticAssetsAtRuntime"] = "false"; + // On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS + // bundle (Amazon.Lambda.TestTool.styles.css) are served by the endpoint-routing static + // assets pipeline (app.MapStaticAssets, added below), which reads the bytes from the web + // host's WebRootFileProvider. When running from a build output (dotnet run / dotnet build) + // those framework assets do not physically live under wwwroot; they live in the NuGet + // cache and are mapped in via the *.staticwebassets.runtime.json manifest. ASP.NET Core + // only composes that manifest into the WebRootFileProvider automatically in the + // Development environment, but this tool runs in the Production environment by default, so + // without the call below the WebRootFileProvider is a bare wwwroot provider, MapStaticAssets + // cannot find the framework files, and it serves an empty (0-byte) HTTP 200 response. That + // leaves window.Blazor undefined and the interactive server circuit never starts, so the + // whole UI renders as non-interactive. Calling UseStaticWebAssets forces the manifest to be + // composed regardless of environment. When running as an installed global tool the manifest + // is absent (the framework files are published directly into wwwroot instead), so this is a + // harmless no-op there and MapStaticAssets serves the files straight from wwwroot. + builder.WebHost.UseStaticWebAssets(); #endif builder.Services.AddSingleton(); @@ -119,9 +135,10 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT // static assets API backed by the *.staticwebassets.endpoints.json manifest, not the // classic static-files middleware above. Without this call those assets return 404, so // window.Blazor is never defined and the interactive server circuit is never established, - // leaving the entire Blazor UI non-interactive. This manifest is generated relative to the - // published output, so it works for the installed global-tool scenario. Guarded so net8.0 - // (which serves these assets via the wwwroot provider above) keeps its existing behavior. + // leaving the entire Blazor UI non-interactive. The bytes are resolved from the web host's + // WebRootFileProvider, which is why UseStaticWebAssets() is also required above (see the + // comment there). Guarded so net8.0 (which serves these assets via the classic static-web- + // assets middleware) keeps its existing behavior. app.MapStaticAssets(); #endif From cdeb3dc947b9afe9b368320ab7fd337158d6cde5 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Wed, 22 Jul 2026 21:45:12 -0400 Subject: [PATCH 3/4] Only MapStaticAssets when the manifest exists (fix net10 unit tests) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TestToolProcess.Startup is invoked directly by unit tests (RuntimeApiTests, RunCommandTests) inside the xUnit test host. MapStaticAssets() throws InvalidOperationException when the *.staticwebassets.endpoints.json manifest is absent, and the manifest is named after the entry assembly — under the test host that's 'testhost', so the tool's manifest isn't present and every test that calls Startup failed on net10 (net8 was unaffected as it doesn't call MapStaticAssets). Guard the call so it only maps static assets when the expected manifest exists: the running tool has it (assets serve, UI is interactive); the test host does not (skipped, and those tests only exercise the Runtime API). Verified: the previously-failing net10 tests pass, and the real tool still serves _framework/blazor.web.js with real bytes. --- .../Processes/TestToolProcess.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index d5735a702..7dfe0f022 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -139,7 +139,19 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT // WebRootFileProvider, which is why UseStaticWebAssets() is also required above (see the // comment there). Guarded so net8.0 (which serves these assets via the classic static-web- // assets middleware) keeps its existing behavior. - app.MapStaticAssets(); + // + // MapStaticAssets() throws if the manifest is absent. That manifest is named after the + // entry assembly, so when Startup is invoked from within a test host (unit tests call it + // directly) the tool's manifest isn't next to the running 'testhost' assembly. Only map + // static assets when the expected manifest exists — the running tool has it; the test host + // does not (and those tests only exercise the Runtime API, not the Blazor assets). + var staticAssetsManifest = Path.Combine( + AppContext.BaseDirectory, + $"{System.Reflection.Assembly.GetEntryAssembly()?.GetName().Name}.staticwebassets.endpoints.json"); + if (File.Exists(staticAssetsManifest)) + { + app.MapStaticAssets(); + } #endif app.UseAntiforgery(); From 4e578f11c181126c1fffb22d86f6c90dc70313f2 Mon Sep 17 00:00:00 2001 From: Garrett Beatty Date: Thu, 23 Jul 2026 15:58:52 -0400 Subject: [PATCH 4/4] Fix RCL _content/** assets 404 on .NET 8 (dotnet run) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Blazor web UI's static-asset serving was only fully fixed for .NET 9+. On .NET 8, running from a build output (dotnet run / dotnet build) left Razor class library content — notably BlazorMonaco's _content/BlazorMonaco/** editor assets — returning 404, so the request/response code editor never initialized and selecting an example request could not populate the input. Root cause: those RCL assets are not physically under wwwroot in a build output; they are surfaced through the static-web-assets runtime manifest. UseStaticWebAssets() (which composes that manifest into the WebRootFileProvider) was guarded to net9+, and the classic UseStaticFiles middleware was pinned to an explicit bare-wwwroot provider that never reads the composed manifest. On net9+ MapStaticAssets reads the WebRootFileProvider so it worked there; net8 had no such reader. Fix: - Compose the static-web-assets manifest on all target frameworks (make UseStaticWebAssets() unconditional). - On .NET 8, add a second UseStaticFiles pass over the WebRootFileProvider so manifest-mapped _content/** assets resolve. For an installed global tool the WebRootFileProvider is the same wwwroot (assets published there directly), so it is a harmless second lookup. Verified across the full matrix — .NET 8/.NET 10 x Development/Production x dotnet-run/installed-tool (8/8 serve _framework, _content/BlazorMonaco, and app.css with non-zero bytes). --- .../156dd8b1-1af2-45fc-b051-b94dc51fc56f.json | 3 +- .../Processes/TestToolProcess.cs | 65 +++++++++++++------ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json index f590f864f..0bb927899 100644 --- a/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json +++ b/.autover/changes/156dd8b1-1af2-45fc-b051-b94dc51fc56f.json @@ -4,7 +4,8 @@ "Name": "Amazon.Lambda.TestTool", "Type": "Patch", "ChangelogMessages": [ - "Fix Blazor web UI being non-interactive on .NET 9+/net10.0. Framework static assets (_framework/blazor.web.js) and the scoped-CSS bundle are now served via MapStaticAssets, the static web assets manifest is composed regardless of hosting environment, and the content root is pinned to the tool's install directory so the assets resolve correctly when running as an installed global tool." + "Fix Blazor web UI being non-interactive on .NET 9+/net10.0. Framework static assets (_framework/blazor.web.js) and the scoped-CSS bundle are now served via MapStaticAssets, the static web assets manifest is composed regardless of hosting environment, and the content root is pinned to the tool's install directory so the assets resolve correctly when running as an installed global tool.", + "Fix Razor class library content (e.g. the BlazorMonaco code-editor assets under _content/**) returning 404 on .NET 8 when running from a build output (dotnet run), which left the request/response editor uninitialized so selecting an example request could not populate the input. The static web assets manifest is now composed on all target frameworks, and .NET 8 additionally serves static files from the web root file provider so manifest-mapped _content/** assets resolve. Verified across .NET 8/.NET 10, Development/Production, and dotnet-run/installed-tool." ] } ] diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs index 7dfe0f022..6e8c8771b 100644 --- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs +++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/TestToolProcess.cs @@ -50,24 +50,26 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT Utils.ConfigureWebApplicationBuilder(builder); -#if NET9_0_OR_GREATER - // On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS - // bundle (Amazon.Lambda.TestTool.styles.css) are served by the endpoint-routing static - // assets pipeline (app.MapStaticAssets, added below), which reads the bytes from the web - // host's WebRootFileProvider. When running from a build output (dotnet run / dotnet build) - // those framework assets do not physically live under wwwroot; they live in the NuGet - // cache and are mapped in via the *.staticwebassets.runtime.json manifest. ASP.NET Core - // only composes that manifest into the WebRootFileProvider automatically in the - // Development environment, but this tool runs in the Production environment by default, so - // without the call below the WebRootFileProvider is a bare wwwroot provider, MapStaticAssets - // cannot find the framework files, and it serves an empty (0-byte) HTTP 200 response. That - // leaves window.Blazor undefined and the interactive server circuit never starts, so the - // whole UI renders as non-interactive. Calling UseStaticWebAssets forces the manifest to be - // composed regardless of environment. When running as an installed global tool the manifest - // is absent (the framework files are published directly into wwwroot instead), so this is a - // harmless no-op there and MapStaticAssets serves the files straight from wwwroot. + // Static web assets (the *.staticwebassets.runtime.json manifest) map two kinds of files + // that do NOT physically live under wwwroot when running from a build output (dotnet run / + // dotnet build): the Blazor framework files (_framework/blazor.web.js) and Razor class + // library content such as BlazorMonaco's _content/BlazorMonaco/** editor assets. Both live + // in the NuGet cache and are surfaced via that manifest. ASP.NET Core only composes the + // manifest into the WebRootFileProvider automatically in the Development environment, but + // this tool runs in the Production environment by default, so without the call below the + // WebRootFileProvider is a bare wwwroot provider and those assets are unreachable. + // + // On .NET 9+ that leaves MapStaticAssets (added below) unable to find the framework files — + // it serves an empty (0-byte) HTTP 200, window.Blazor is never defined, and the whole UI is + // non-interactive. On .NET 8 the framework files are served by Blazor's own middleware, but + // the RCL _content/** assets are served through UseStaticFiles + WebRootFileProvider, so + // without the manifest the Monaco editor assets 404 and the code editor never initializes + // (e.g. selecting an example request cannot populate the input). Either way the fix is the + // same, so this runs on all target frameworks. + // + // When running as an installed global tool the manifest is absent (the framework and RCL + // content are published directly into wwwroot instead), so this is a harmless no-op there. builder.WebHost.UseStaticWebAssets(); -#endif builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -120,15 +122,36 @@ public static TestToolProcess Startup(RunCommandSettings settings, CancellationT app.UseDeveloperExceptionPage(); } - // Always use the explicit file provider to serve static files from the tool's install - // directory. Without this, non-Production environments attempt to use the static web - // assets manifest which contains absolute paths from the build machine and will fail - // when running as an installed global tool on a different machine. + // Serve classic static files from the tool's install directory (wwwroot). This is the + // authoritative provider for an installed global tool, where every asset — app.css, the + // Blazor framework files, and RCL _content/** (e.g. BlazorMonaco) — is published directly + // into wwwroot. Pinning an explicit provider (rather than the default WebRootFileProvider) + // also avoids depending on the static-web-assets manifest, whose absolute build-machine + // paths would not resolve on another machine. app.UseStaticFiles(new StaticFileOptions { FileProvider = wwwrootFileProvider }); +#if !NET9_0_OR_GREATER + // On .NET 8 there is no MapStaticAssets endpoint pipeline (added below for net9+), and the + // explicit provider above sees only the physical wwwroot. When running from a build output + // (dotnet run / dotnet build), RCL _content/** assets are NOT physically in wwwroot — they + // are surfaced through the static-web-assets manifest that UseStaticWebAssets() composes + // into the WebRootFileProvider. Without a second pass over that provider, BlazorMonaco's + // _content/BlazorMonaco/** editor assets 404 and the code editor never initializes (so, for + // example, selecting an example request cannot populate the input). Serve from the + // WebRootFileProvider as well to cover that case. For an installed tool the WebRootFile + // provider resolves to the same wwwroot as above, so this is a harmless second lookup. + if (app.Environment.WebRootFileProvider is not null) + { + app.UseStaticFiles(new StaticFileOptions + { + FileProvider = app.Environment.WebRootFileProvider + }); + } +#endif + #if NET9_0_OR_GREATER // On .NET 9+ the Blazor framework files (_framework/blazor.web.js) and the scoped-CSS // bundle (Amazon.Lambda.TestTool.styles.css) are served through the endpoint-routing