Skip to content

fix: derive express subdomain offset from the configured root domain - #3575

Open
MannXo wants to merge 1 commit into
HeyPuter:mainfrom
MannXo:fix/3561-subdomain-offset
Open

fix: derive express subdomain offset from the configured root domain#3575
MannXo wants to merge 1 commit into
HeyPuter:mainfrom
MannXo:fix/3561-subdomain-offset

Conversation

@MannXo

@MannXo MannXo commented Aug 14, 2026

Copy link
Copy Markdown

Summary

Express derives req.subdomains by dropping a fixed number of labels from the right of the hostname, and that number defaults to 2. Any deployment whose domain has more than two labels reads its own root domain as an active subdomain, which sends the root origin into the user-site redirect and back again. This sets the offset from the label count of config.domain.

Context

Reported in #3561 for puter.bhhaihuan.com. It is not an exotic setup. doc/self-hosting.md tells self-hosters to use exactly that shape ("If users reach the box at puter.example.com, then domain must be puter.example.com").

With domain: puter.example.com, a request to the bare root domain parses as subdomain puter. Two things follow. createUserSubdomainRedirect sees a non-reserved subdomain and 302s to the static hosting domain, which redirects back. And every root-only route stops matching, because #materializeRoute gates those on req.subdomains.length === 0. The redirect runs ahead of the routes, so the whole deployment is unreachable, not just /.

Eight call sites read req.subdomains, and all of them reason about labels relative to the main domain, so the main domain's own label count is the right offset for all of them.

Changes

  • New subdomainOffsetForDomain() in src/backend/util/subdomains.ts. It counts the root domain's labels and tolerates casing, surrounding space, a port and a leading dot, falling back to express's 2 when domain is unset.
  • src/backend/server.ts sets subdomain offset alongside trust proxy.
  • Tests for the helper, plus an integration suite in server.test.ts that boots a server on a three-label domain.

Two-label domains (puter.com, puter.localhost) resolve to 2, so nothing changes for existing deployments.

Testing

CI=true npm run test:backend -- --coverage

 Test Files  218 passed | 19 skipped (237)
      Tests  5632 passed | 21 skipped (5653)

npm run typecheck reports no new errors (83 known, baselined).

The regression test fails without the fix:

FAIL  src/backend/server.test.ts > PuterServer subdomain routing on a multi-label root domain
      > serves the root origin instead of redirecting it to the hosting domain
AssertionError: expected 302 not to be 302

I also ran the real server rather than only the suite, with PUTER_CONFIG_PATH pointing at an override that sets domain: puter.example.localhost and the hosting domains under it, then curled it with matching Host headers.

Before, both the root path and /healthcheck bounce off the root origin:

$ curl -so /dev/null -w "%{http_code} %{redirect_url}\n" -H "Host: puter.example.localhost" http://127.0.0.1:4100/
302 http://site.puter.example.localhost/

After:

Host: puter.example.localhost        /            200 (desktop HTML)
Host: alice.puter.example.localhost  /            302 http://alice.site.puter.example.localhost/
Host: api.puter.example.localhost    /version     200

Notes for reviewers

The offset is a single global value, so a deployment whose hosting domain has fewer labels than its main domain would compute an empty req.subdomains on hosting hosts. That path is already safe. puterSite matches hosting hosts by suffix itself and terminates the request before any route runs, and the documented layout (site.<domain>) never produces a hosting domain shorter than the main one.

I did not add the env-var override from the issue's workaround. config.domain is already the authoritative value, so a second knob would only be another thing to set inconsistently.

Fixes #3561

Express derives `req.subdomains` by dropping a fixed number of labels
from the right of the hostname, defaulting to 2. A deployment whose
`domain` has more than two labels therefore reads its own root domain as
an active subdomain, so the user-site redirect sends the root origin to
the static hosting domain, which sends it back. Self-hosting docs
recommend exactly that shape (`puter.example.com`).

Set the offset from the label count of `config.domain`. Two-label
domains keep the express default, so existing deployments are unchanged.

Fixes HeyPuter#3561
@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

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.

subdomain offset=2

2 participants