Skip to content

fix(multi-tenant): harden the unknown-host fix and pin it with tests - #397

Merged
mindsers merged 5 commits into
mainfrom
fix/unknown-host-followup
Oct 13, 2026
Merged

mindsers merged 5 commits into
mainfrom
fix/unknown-host-followup

Conversation

@mindsers

Copy link
Copy Markdown
Contributor

Description

Follow-up to #393, from a review of it after the merge. Requests that already got the right answer keep it; this pins the fix with tests and closes the gaps the review found.

Tests (first commit, no production code)

  • No test executed handleRequest, so putting the throwing locale lookup back in entry.server left everything green. A test now hands it the redirect resolveDocumentLocale returns and expects that response back.
  • The base-domain and look-alike-host tests asserted that a query was not made. They now let the slug lookup answer and assert what the caller gets, which fails on the pre-fix(multi-tenant): stop answering unknown hosts and paths with a 500 #393 code without a spy.
  • findCongregation was compared with a function that delegates to it; it asserts the mapped fields.
  • A subdomain is resolved with UNITAE_BASE_URL written with and without its scheme (the self-hosting guide writes it with one), and /livez is requested through the router in the E2E health spec.

Locale lookup

  • A failed lookup rendered in the default locale, and entry.server wrote that to the locale cookie: after one database error, a member of an English congregation read the app in French until their next full page load. A fallback now renders in the locale the visitor's cookie already holds, or the base one, and is not written back.
  • Only a 3xx with a Location is answered as a redirect. Any other Response thrown by the lookup was handed to the visitor as the page; it is logged and falls back like the other failures.
  • The locale is validated where it is resolved, so entry.server no longer checks it against a second default. The failure log carries the host and the path.

Base domain

  • The suffix a subdomain is matched against was UNITAE_BASE_URL with its scheme cut off. With a port or a trailing slash it matched no host, and every congregation subdomain was served as the platform root, silently. The hostname is now parsed out of the value: scheme, port, trailing slash and capitals all resolve. An empty value reads as unset, and validateEnv() warns about one that names no host.
  • Behaviour change: an instance whose UNITAE_BASE_URL carries a port or a trailing slash starts resolving its subdomains. Before, none of them did.
  • A host with nothing before the base domain's dot no longer yields an empty slug.

Comments and docs

  • Five comments described the missing-congregation branch as a session outliving its congregation, with a 500 on every page. The foreign key forbids an account without its congregation, so the branch only covers a deletion racing a request; the comments say so.
  • docs/self-hosting/health-monitoring.md: the Compose example probed with curl, which the runtime image does not ship, so the container reported unhealthy forever. It uses Node's fetch, as docker/docker-compose.yml does. A liveness restart takes several failed probes, not a brief outage, and the readiness delay the startup probe already covers is gone.

Verified against a production build in multi-tenant mode, with UNITAE_BASE_URL=https://unitae.test:8443/ and Host headers:

Request Answer
unknown.<domain>/anything 302 → /congregation-not-found
unknown.<domain>/congregation-not-found 200, no locale cookie written
<domain>/anything (base domain) 404
not<domain>/anything (look-alike host) 404
known.<domain>/anything 404
known.<domain>/login 200
/livez 200 ok, Cache-Control: no-store

With UNITAE_BASE_URL="", subdomains resolve against the default domain again. New behaviour was written test-first; unit tests for the touched files, lint and typecheck pass.

Type de changement

  • Bug fix
  • Nouvelle fonctionnalité
  • Refactoring (pas de changement fonctionnel)
  • Documentation
  • Autre : ___

Checklist

  • J'ai formaté mon code avant de pousser (pnpm build:format)
  • J'ai mis à jour la documentation si nécessaire

Issue liée

None.

… by outcome

Nothing executed handleRequest, so putting the throwing locale lookup back in
entry.server left every test green. A test now hands it the redirect
resolveDocumentLocale returns and expects that response back.

The base-domain and look-alike-host tests asserted that a query was not made.
They now let the slug lookup answer and assert what the caller gets, which
fails on the old code without a spy. findCongregation was compared with a
function that delegates to it; it asserts the mapped fields instead.

A subdomain is resolved with UNITAE_BASE_URL written with and without its
scheme, as the self-hosting guide writes it, and /livez is requested through
the router so losing its route registration is caught.
…nguage

resolveDocumentLocale answered a failed lookup with the default locale, and
entry.server wrote whatever it got to the locale cookie: after one database
error, a member of an English congregation read the app in French until
their next full page load. A fallback now renders in the locale the
visitor's cookie already holds, or the base one, and is not written back.

Only a redirect is answered as one. Any other Response thrown by the lookup
was handed to the visitor as the page; it is logged and falls back like the
other failures. The log line carries the host and the path, so a failure can
be tied to the tenant it happened on.

The locale is validated where it is resolved, so entry.server no longer
checks it again against a second default.
…r it is written

The suffix a subdomain is matched against was UNITAE_BASE_URL with its scheme
cut off. Written with a port or a trailing slash, it matched no host: every
congregation subdomain was served as the platform root, without the check
that a session belongs to the congregation in the URL, and with nothing in
the logs. Set but left empty, it did the same.

The hostname is now parsed out of the value, so a scheme, a port, a trailing
slash or capitals all resolve, and an empty value reads as unset. validateEnv
warns about a value that names no host. It does not refuse it: an instance
that only serves custom domains runs without a base domain.

A host with nothing before the base domain's dot no longer yields an empty
slug.
…and fix the probe guide

Five comments described the missing-congregation branch as a session
outliving its congregation, with a 500 on every page. The foreign key
forbids an account without its congregation, and once the accounts are gone
the guard already redirected: the branch only covers a deletion racing a
request. The comments say so now, and findCongregation's doc names its real
callers.

"As the loaders do" named no loader, and a test file repeated the JSDoc on
what React Router does with a throw in entry.server.

In the health-monitoring guide, the Compose example probed with curl, which
the runtime image does not ship, so the container reported unhealthy
forever. It uses Node's fetch, as docker/docker-compose.yml does. The guide
also says a liveness restart takes several failed probes, and drops a
readiness delay the startup probe already covers.
@mindsers
mindsers merged commit e77dedd into main Oct 13, 2026
7 checks passed
@mindsers
mindsers deleted the fix/unknown-host-followup branch October 13, 2026 19:41
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.

1 participant