fix(web): stop redirecting visitors to the vhost's internal port
All checks were successful
deploy / Build api + worker (static musl) (push) Successful in 6m59s
deploy / Deploy moments-worker to frootmig (push) Successful in 19s
deploy / Deploy moments-api to nikola (push) Successful in 32s
deploy / Build prerendered web (push) Successful in 4m38s
deploy / Deploy web to oolon (push) Successful in 21s
refresh / Rebuild prerendered web (push) Successful in 4m51s
refresh / Deploy refreshed web to oolon (push) Successful in 39s

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 #9
This commit is contained in:
2026-08-17 14:03:22 +03:00
parent 6ee4cf5299
commit dc09f57ad8

View File

@@ -18,6 +18,16 @@ server {
root {{WEB_ROOT}}; root {{WEB_ROOT}};
index index.html; 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 # 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 # 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 — # pages are large — the dashboard bakes the full all-time activity dataset —