From 08fe5e33beafd90063df9b6e0b46f85187d0e1b3 Mon Sep 17 00:00:00 2001 From: Alexandre <44178713+alexbelgium@users.noreply.github.com> Date: Wed, 19 Aug 2026 07:15:20 +0200 Subject: [PATCH] fix(seerr): version the rewritten asset paths so cached bundles expire (#2975) (#2993) * fix(seerr): version the rewritten asset paths so cached bundles expire (#2975) Seerr serves everything under /_next/static/ with "Cache-Control: public, max-age=31536000, immutable", and nginx's sub_filter strips ETag, Last-Modified and Content-Length from every response it rewrites. The HTML is served "no-store" but keeps naming the same chunk URLs, and all three 3.4.1.x add-on versions ship the same upstream build, so a browser that had loaded Seerr through ingress once kept replaying the JavaScript it cached then - for up to a year, with no request to revalidate it. That is why #2975 outlived two fixes: the reporter's https origin was still executing the 3.4.1/3.4.1.1 bundle, whose rewritten root link makes Next hard-navigate to /api/hassio_ingress/ without a trailing slash, which Home Assistant does not route and answers with its own "404: Not Found". An origin that had never cached it - the same instance over http://:8123 - already showed the fixed behaviour. The asset paths now carry the add-on version ("/ha-3-4-1-3/_next/..."), so every release has its own URLs, a poisoned cache is bypassed on the first load after an update, and any future change to a rewrite rule is actually delivered. njs strips the marker again before proxying, so Seerr still receives the paths it serves. Co-Authored-By: Claude Opus 5 * fix(seerr): tighten the cache-bust comments after review Co-Authored-By: Claude Opus 5 * fix(seerr): take the cache-bust marker straight from BUILD_VERSION bashio::addon.version is an indirection here: bashio-standalone.sh defines it as printf '%s' "${BUILD_VERSION:-1.0}", and the builder always passes BUILD_VERSION from config.yaml, which the Dockerfile bakes in as an ENV. Reading it directly drops a Supervisor round-trip and the fallback chain around it, for the same value. The sanitiser stays: it protects the sed replacement and the regex literal the marker lands in inside Seerr's bundle. Co-Authored-By: Claude Opus 5 --------- Co-authored-by: Claude Opus 5 --- seerr/CHANGELOG.md | 5 ++++- seerr/config.yaml | 2 +- .../etc/cont-init.d/32-nginx_ingress.sh | 19 +++++++++++++++- seerr/rootfs/etc/nginx/njs/ingress.js | 22 ++++++++++++++++--- seerr/rootfs/etc/nginx/servers/ingress.conf | 10 +++++++-- 5 files changed, 50 insertions(+), 8 deletions(-) diff --git a/seerr/CHANGELOG.md b/seerr/CHANGELOG.md index 3b780566ea..1451226fb2 100644 --- a/seerr/CHANGELOG.md +++ b/seerr/CHANGELOG.md @@ -1,4 +1,7 @@ - +## 3.4.1.3 (2026-08-18) + +- Fixed the `404: Not Found` on **Discover** persisting for browsers that had already opened Seerr through ingress, even after 3.4.1.1 and 3.4.1.2 were installed (#2975). Seerr serves its JavaScript bundle with `Cache-Control: public, max-age=31536000, immutable`, and the add-on's nginx rewrites that bundle to carry the ingress prefix - which strips the `ETag` and `Last-Modified` a browser would revalidate with. Since every add-on version served the same upstream build, the chunk URLs never changed either, so a browser kept replaying the broken 3.4.1/3.4.1.1 JavaScript from its own cache for up to a year and no fix could reach it. That is why the report persisted on the origin the reporter uses daily (`https:///`) while a browser that had never cached it (`http://:8123/`) already showed the fixed behaviour. The asset paths now carry the add-on version, so each release has its own URLs and the first page load after an update fetches the current bundle. Only ingress was affected; the directly published port 5055 always worked. + ## 3.4.1.2 (2026-08-18) - Fixed **Discover** in the sidebar still failing through ingress after 3.4.1.1 (#2975). The trailing slash added in 3.4.1.1 was also applied to the copy of the link inside Seerr's JavaScript bundle, and Next.js' client-side router strips a trailing slash before navigating: it then sent the click to a URL Home Assistant does not route, so it either landed on the same `404: Not Found` or threw `Invariant: attempted to hard navigate to the same URL` and did nothing at all. The bundle is no longer rewritten, so **Discover** routes inside the app exactly like **Requests**, **Issues** and **Settings** already did. The server-rendered link keeps its trailing slash. Only ingress was affected; the directly published port 5055 always worked. diff --git a/seerr/config.yaml b/seerr/config.yaml index f73c177f7a..b3eaffa55c 100644 --- a/seerr/config.yaml +++ b/seerr/config.yaml @@ -96,4 +96,4 @@ schema: slug: seerr udev: true url: https://github.com/alexbelgium/hassio-addons/tree/master/seerr -version: "3.4.1.2" +version: "3.4.1.3" diff --git a/seerr/rootfs/etc/cont-init.d/32-nginx_ingress.sh b/seerr/rootfs/etc/cont-init.d/32-nginx_ingress.sh index ab5f163b86..b6cae77afc 100755 --- a/seerr/rootfs/etc/cont-init.d/32-nginx_ingress.sh +++ b/seerr/rootfs/etc/cont-init.d/32-nginx_ingress.sh @@ -12,10 +12,27 @@ ingress_port=$(bashio::addon.ingress_port) ingress_interface=$(bashio::addon.ip_address) ingress_entry=$(bashio::addon.ingress_entry) +# Cache-busting marker for the rewritten JavaScript bundle. +# +# Seerr serves /_next/static/ as "public, max-age=31536000, immutable", and +# nginx's sub_filter strips ETag and Last-Modified off every response it +# rewrites, while the HTML naming those chunks is served "no-store" and keeps +# naming the same URLs. A browser therefore pins the bundle this add-on rewrote +# on its first visit for a year, with no request left that could deliver a +# later change to the sub_filter rules below - which is how #2975 outlived two +# fixes. Folding the version into the asset path gives every release its own +# URLs. njs/ingress.js strips the marker again before proxying. +# +# BUILD_VERSION is the add-on version baked in at build time (it is also what +# bashio::addon.version returns). Only [A-Za-z0-9-] survives: the marker ends up +# inside a regex literal in Seerr's own bundle, where a dot would be a wildcard. +asset_tag="ha-$(printf '%s' "${BUILD_VERSION:-0}" | tr -c 'A-Za-z0-9' '-')" + # Update ingress.conf with actual values sed -i "s|%%port%%|${ingress_port}|g" /etc/nginx/servers/ingress.conf sed -i "s|%%interface%%|${ingress_interface}|g" /etc/nginx/servers/ingress.conf sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf sed -i "s|%%ingress_entry_escaped%%|${ingress_entry//\//\\\\\/}|g" /etc/nginx/servers/ingress.conf +sed -i "s|%%asset_tag%%|${asset_tag}|g" /etc/nginx/servers/ingress.conf -bashio::log.info "Nginx ingress configured on ${ingress_interface}:${ingress_port}" +bashio::log.info "Nginx ingress configured on ${ingress_interface}:${ingress_port} (asset tag ${asset_tag})" diff --git a/seerr/rootfs/etc/nginx/njs/ingress.js b/seerr/rootfs/etc/nginx/njs/ingress.js index 70e5aefc51..b28a4c4db2 100644 --- a/seerr/rootfs/etc/nginx/njs/ingress.js +++ b/seerr/rootfs/etc/nginx/njs/ingress.js @@ -47,11 +47,27 @@ function encodePart(part) { } /* - * Returns the request URI with the path untouched byte-for-byte and only the - * query string repaired. Used as the proxy_pass target. + * The cache-busting marker servers/ingress.conf inserts in front of every + * rewritten "/_next" path, e.g. "/ha-3-4-1-3/_next/static/chunks/x.js". It + * gives each add-on release its own asset URLs - Seerr serves /_next/static/ as + * immutable for a year and sub_filter strips the validators, so identical URLs + * would pin the rewritten bundle in the browser forever. Seerr knows nothing + * about the marker, so it is removed again here, on the way in. + * + * Any marker is accepted, not just the one this container serves: a tab opened + * before an add-on update keeps requesting its dynamic chunks under the marker + * it was handed, and those have to keep working until it is reloaded. The + * lookahead keeps a real Seerr path that merely starts with "ha-" untouched. + */ +var ASSET_TAG = /^\/ha-[0-9A-Za-z-]+(?=\/_next(\/|$))/; + +/* + * Returns the request URI with the path untouched byte-for-byte apart from the + * cache-busting marker, and only the query string repaired. Used as the + * proxy_pass target. */ function uri(r) { - var raw = r.variables.request_uri; + var raw = r.variables.request_uri.replace(ASSET_TAG, ""); var split = raw.indexOf("?"); if (split < 0) { diff --git a/seerr/rootfs/etc/nginx/servers/ingress.conf b/seerr/rootfs/etc/nginx/servers/ingress.conf index 8ac559904a..e14048fb5b 100644 --- a/seerr/rootfs/etc/nginx/servers/ingress.conf +++ b/seerr/rootfs/etc/nginx/servers/ingress.conf @@ -77,8 +77,14 @@ server { # because the compiled output spells the prop 'href:"/"' and not # 'href="/"'. These are textual substitutions over someone else's minified # output: recheck them whenever Seerr or Next.js is upgraded. - sub_filter '\/_next' '%%ingress_entry_escaped%%\/_next'; - sub_filter '/_next' '$app/_next'; + # "%%asset_tag%%" is a cache-busting marker carrying the add-on version, + # substituted by 32-nginx_ingress.sh - which explains why it is needed. + # In short: without it a browser replays the bundle this file produced at + # the version it first loaded, for a year, and no later change to any + # rule here can reach it. njs/ingress.js strips the marker back off + # before proxying; the two belong together, do not change one alone. + sub_filter '\/_next' '%%ingress_entry_escaped%%\/%%asset_tag%%\/_next'; + sub_filter '/_next' '$app/%%asset_tag%%/_next'; sub_filter '/api/v1' '$app/api/v1'; sub_filter '/login/plex/loading' '$app/login/plex/loading'; sub_filter '/images/' '$app/images/';