From dc09f57ad8d78208d75e7ea7084147a3966d4261 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Mon, 17 Aug 2026 14:03:22 +0300 Subject: [PATCH] fix(web): stop redirecting visitors to the vhost's internal port MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Any route asked for without a trailing slash sent the visitor to a port nothing answers on from outside: $ curl -sSI https://rob.tn/activity HTTP/2 301 location: https://rob.tn:14443/activity/ `try_files $uri $uri/` 301s a slash-less directory URL to add the slash, and nginx builds that Location as an absolute URL from its own $server_port. This vhost listens on WEB_LISTEN — 127.0.0.1:14443, behind the edge's stream SNI router — so the redirect advertised 14443 instead of the 443 the client used. The browser then sat on a TCP connect that never completes and gave up only after its own timeout, 60s+, before showing an error. It reads as the site hanging. `absolute_redirect off` makes the Location relative, so the client keeps whatever scheme, host and port it actually used. `port_in_redirect off` would drop the port too, but this also stops nginx asserting a scheme and host it cannot know from behind the router. Every /activity, /blog, /cv and /project/... request without the trailing slash was affected — external links, bookmarks, typed URLs, crawlers. In-app navigation never round-trips to the server, and `/` needs no directory redirect, which is why the homepage always loaded fine and this stayed hidden. It is not a regression from any recent change; it follows from the vhost listening on a shifted port. Verified by reproducing the port leak in a container listening on 8081 published as 18081 — `Location: http://127.0.0.1:8081/activity/` before the directive, `Location: /activity/` after — then rendering the real template through script/render-site-conf.py (9 placeholders in, none surviving) and passing `nginx -t` on the result. Closes https://git.lair.cafe/grenade/moments/issues/9 --- asset/nginx/site.conf.tmpl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/asset/nginx/site.conf.tmpl b/asset/nginx/site.conf.tmpl index 05594f0..a519e6d 100644 --- a/asset/nginx/site.conf.tmpl +++ b/asset/nginx/site.conf.tmpl @@ -18,6 +18,16 @@ server { root {{WEB_ROOT}}; index index.html; + # Emit relative Location headers. `try_files $uri $uri/` 301s a slash-less + # directory URL (/activity -> /activity/), and by default nginx builds that + # redirect from its own $server_port — WEB_LISTEN's 14443, not the 443 the + # client used, because this vhost sits behind the edge's stream SNI router. + # So /activity redirected visitors to https://rob.tn:14443/activity/, where + # nothing answers from outside, and the browser hung until its connect + # timeout (60s+) before failing. Relative redirects keep whatever scheme, + # host and port the client actually used. + absolute_redirect off; + # Compress text responses on the wire. text/html is always compressed when # gzip is on (nginx won't let it be listed in gzip_types); the prerendered # pages are large — the dashboard bakes the full all-time activity dataset —