diff --git a/nix/modules/hive-gateway.nix b/nix/modules/hive-gateway.nix index 74af4d26..d73b5ce1 100644 --- a/nix/modules/hive-gateway.nix +++ b/nix/modules/hive-gateway.nix @@ -147,6 +147,35 @@ in enable = true; recommendedProxySettings = true; recommendedOptimisation = true; + # SPA-fallback target keyed on the `Accept` request header + # (#686, mara + damocles on PR #729). This decides whether a + # `/matrix/...` miss falls through to `index.html` (route + # navigation) or returns a clean 404 (asset miss) — see the + # `/matrix/` location comment below for the full rationale. + # + # Top-frame browser navigations always send + # `Accept: text/html,...` (chrome/firefox/safari are + # consistent on this). Asset fetches from script tags / img + # / fetch() / XHR send asset-typed Accepts (`image/*`, + # `application/javascript`, `*/*`) without `text/html`. + # Mapping is purely on the header → no extension allowlist + # to keep in sync with whatever the SPA ships, no regex + # heuristic to false-positive on dot-segment routes. + # + # `$matrix_spa_target` defaults to a sentinel nonexistent + # path so `try_files` falls through to the trailing `=404` + # for asset misses. Browser navigations route to + # `/matrix/index.html` where the SPA's client-side router + # takes over. + # + # Only emitted when the matrix GUI is on (saves a no-op + # `map` directive otherwise). + appendHttpConfig = lib.optionalString (matrixCfg.enable && matrixCfg.gui.enable) '' + map $http_accept $matrix_spa_target { + default "/__matrix_spa_no_html_fallback"; + "~*text/html" "/matrix/index.html"; + } + ''; virtualHosts."_" = { listen = [ { @@ -158,35 +187,31 @@ in # Matrix GUI: when the operator has flipped both # `services.hyperhive.matrix.enable` and `matrix.gui.enable` # on, nginx serves fluffychat-web (or whatever override) - # as a static dist at `/matrix/`. fluffychat is a SPA - # — fall back to its index.html on deep links. + # as a static dist at `/matrix/`. + # + # SPA fallback (iris/#643, rewritten in #686 per mara + # + damocles on PR #729): the original + # `try_files $uri $uri/ /matrix/index.html;` shape + # silently masked missing assets — flutter's bootstrap + # requesting e.g. `/matrix/native_executor.js` got + # `index.html` (Content-Type: text/html, status 200) + # when the file was absent from the dist, so the JS + # runtime never loaded and `/matrix/` rendered blank + # without any visible error. + # + # The followup #729 narrowed it with an extension + # allowlist; this version uses `$matrix_spa_target` + # (defined in the `appendHttpConfig` above, keyed on + # the `Accept` header) so the decision lives in HTTP + # semantics rather than a maintained extension list. + # Navigations (Accept: text/html) fall to index.html; + # asset fetches (Accept: */*, image/*, etc.) get a + # clean 404 via the trailing `=404`. lib.optionalAttrs (matrixCfg.enable && matrixCfg.gui.enable) { "/matrix/" = { alias = "${matrixCfg.gui.package}/"; extraConfig = '' - try_files $uri $uri/ @matrix_spa_fallback; - ''; - }; - # SPA fallback (iris/#643). The naive - # `try_files $uri $uri/ /matrix/index.html;` shape - # silently masked missing static assets — flutter's - # bootstrap requesting e.g. `/matrix/native_executor.js` - # got `index.html` (Content-Type: text/html, status - # 200) when the file was absent from the dist, so - # the JS runtime never loaded and `/matrix/` rendered - # blank without any visible error. - # - # Asset-shaped URIs (anything with a `.` segment) - # get an explicit 404 so the SPA + browser see the - # missing-asset error cleanly. Only route-shaped URIs - # (no extension) fall through to index.html for SPA - # client-side routing. - "@matrix_spa_fallback" = { - extraConfig = '' - if ($uri ~ "\.[A-Za-z0-9]+$") { - return 404; - } - rewrite ^ /matrix/index.html last; + try_files $uri $uri/ $matrix_spa_target =404; ''; }; }