Skip to content

fix(web): serve static web assets, and fix a DI error Production was masking (#63) - #65

Merged
ChrisonSimtian merged 1 commit into
mainfrom
fix/dev-hosting-static-assets
Jul 28, 2026
Merged

fix(web): serve static web assets, and fix a DI error Production was masking (#63)#65
ChrisonSimtian merged 1 commit into
mainfrom
fix/dev-hosting-static-assets

Conversation

@ChrisonSimtian

Copy link
Copy Markdown
Contributor

Fixes #63. Two bugs — and the second was hiding the first.

1. No framework JavaScript, so the whole UI was inert

GET /_framework/blazor.web.js -> HTTP 500, 0 bytes
GET /app.css (compressed)     -> HTTP 200, 0 bytes

No Blazor circuit could start, so every interactive page — Search, Activity, and the new Settings page — rendered its initial HTML then did nothing. Compressed CSS came back empty too, so the UI was unstyled. Nothing logged an obvious error; it just looked broken.

System.IO.FileNotFoundException: Could not find file '.../wwwroot/_framework/blazor.web.js'
   at StaticAssetDevelopmentRuntimeHandler.AttachRuntimePatching

Root cause: the framework calls UseStaticWebAssets() only in Development, and none of our hosts run as Development — there are no launch profiles, and Aspire doesn't set one either. Without it the static-web-assets manifest is never loaded, so relative asset paths resolve against wwwroot, where framework assets and the content-hashed compressed files don't exist.

The manifest was correct all along — blazor.web.js → the microsoft.aspnetcore.app.internal.assets package, app.css.gzobj/<config>/net10.0/compressed/<hash>.gz. Nothing was stale or missing; it simply was never consulted.

Fix: call builder.WebHost.UseStaticWebAssets() explicitly. No-op when the manifest is absent, i.e. in a published app where assets are copied into wwwroot — so production behaviour is unchanged.

Rejected alternative: launchSettings.json with ASPNETCORE_ENVIRONMENT=Development on each host. It fixes the standalone case but is the wrong lever — it flips Development on for anything running the built binary, and an applicationUrl in those profiles interfered with Aspire's endpoint allocation. Fixing the asset loading directly is narrower and environment-independent. (I tried this first; it's in the branch history as a dead end.)

2. IArrClient was never registered anywhere

Trying to run in Development to test the theory immediately surfaced:

Unable to resolve service for type 'IArrClient'
  while attempting to activate 'TestArrConnectionHandler'

AddApplication() registered TestArrConnectionHandler unconditionally, but no host called AddArrClient()IArrClient had no registration at all. Every host would fail to start under DI validate-on-build, and in Production (validation off) the Test button would simply have thrown the moment a user clicked it.

This one is mine, from #61. Fixed by following the pattern this codebase already uses for RunDownloadHandler ("registered by the Downloader host only — it needs IDownloadProvider"): the handler is registered by the Web host alongside AddArrClient(), because the two can't be separated.

Worth stating plainly why it went unnoticed: running as Production disables DI validate-on-build, so the very bug that made the UI inert also suppressed the startup error that would have pointed straight at the missing registration.

Verified on both run paths

before after
standalone blazor.web.js 500 / 0b 200 / 200,645b (gz 200 / 55,644b)
standalone app.css gz 200 / 0b 200 / 4,831b
Aspire fleet blazor.web.js 500 / 0b 200 / 200,645b (gz 200 / 55,644b)
Aspire fleet app.css gz 200 / 0b 200 / 4,831b

Aspire verified on a clean environment (leftover containers and processes removed first — a stale environment sent me chasing a phantom "children aren't binding ports" problem that turned out to be my own grep -i dotnet missing processes named Krautwatc).

The Web host also now starts under DI validation, which it previously could not.

Tests: App 88 / Arch 11 / Infra 102 green, 0 warnings.

Next

This unblocks the browser test-ride of #64 (the settings page), which is why it went first.

🤖 Generated with Claude Code

…masking (#63)

Two bugs, and the second was hiding the first.

## 1. No framework JavaScript, so the UI was inert

  GET /_framework/blazor.web.js -> HTTP 500, 0 bytes
  GET /app.css (compressed)     -> HTTP 200, 0 bytes

  System.IO.FileNotFoundException: Could not find file
    '.../src/Presentation/Web/wwwroot/_framework/blazor.web.js'
     at StaticAssetDevelopmentRuntimeHandler.AttachRuntimePatching

No Blazor circuit could start, so every interactive page — Search, Activity, and the new
Settings page — rendered its initial HTML and then did nothing. Compressed CSS came back
empty too, so the UI was unstyled. Nothing logged an obvious error; it just looked broken.

Root cause: the framework calls UseStaticWebAssets() only in Development, and none of our
hosts run as Development — there are no launch profiles, and Aspire does not set one either.
Without it the static-web-assets manifest is never loaded, so relative asset paths resolve
against wwwroot, where framework assets and the content-hashed compressed files do not
exist. The manifest itself was correct all along: blazor.web.js pointed at the
microsoft.aspnetcore.app.internal.assets package and app.css.gz at
obj/<config>/net10.0/compressed/<hash>.gz.

Fixed by calling builder.WebHost.UseStaticWebAssets() explicitly. It is a no-op when the
manifest is absent, i.e. in a published app where assets are copied into wwwroot, so
production behaviour is unchanged.

Rejected alternative: adding launchSettings.json with ASPNETCORE_ENVIRONMENT=Development to
each host. It fixes the standalone case but is the wrong lever — it would also flip
Development on for anything that runs the built binary, and an applicationUrl in those
profiles interfered with Aspire's own endpoint allocation. Fixing the asset loading directly
is narrower and works regardless of environment.

## 2. IArrClient was never registered anywhere

Trying to run in Development to test the theory surfaced this:

  Unable to resolve service for type 'Krautwatch.Domain.Interfaces.IArrClient'
    while attempting to activate 'TestArrConnectionHandler'

AddApplication() registered TestArrConnectionHandler unconditionally, but no host called
AddArrClient(), so IArrClient had no registration at all. Every host would fail to start
under DI validate-on-build, and in Production — where validation is off — the Test button
would simply have thrown at the moment a user clicked it.

Mine, from #61. Fixed by following the pattern this codebase already uses for
RunDownloadHandler ("registered by the Downloader host only — it needs IDownloadProvider"):
the handler is registered by the Web host, alongside AddArrClient(), because the two cannot
be separated.

Worth noting why this went unnoticed: running as Production disables DI validate-on-build,
so the very bug that made the UI inert also suppressed the startup error that would have
pointed at the missing registration.

## Verified on both run paths

Standalone (dotnet run --project src/Presentation/Web):
  blazor.web.js  plain 200/200645b   gz 200/55644b
  app.css        plain 200/4798b     gz 200/4831b

Aspire fleet (dotnet run --project src/Presentation/AppHost), on a clean environment:
  Web host on :51578
  blazor.web.js  plain 200/200645b   gz 200/55644b
  app.css        plain 200/4798b     gz 200/4831b

Both were 500/0b and 200/0b before. The Web host also now starts under DI validation.

Tests: App 88 / Arch 11 / Infra 102 green, 0 warnings.

Closes #63.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ChrisonSimtian
ChrisonSimtian merged commit f26a92d into main Jul 28, 2026
1 check passed
@ChrisonSimtian
ChrisonSimtian deleted the fix/dev-hosting-static-assets branch July 28, 2026 07:43
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.

Web host serves no framework JS (500 on blazor.web.js) — all interactive pages are inert

1 participant