Skip to content

[finding] the legacy-host redirect compares the raw Host header, so any port on it skips the canonical redirect #226

Closed

Description

@hotlong

Observed while exercising the domain-redirect table for #205. Not fixed there — out of that card's file surface intent, and it is a separate decision.

What was measured

Against a production server built from 0c4351f + the #205 branch, next start:

request Host path result
www.objectos.app /docs/quickstart 308 to https://docs.objectos.ai/docs/quickstart
www.objectos.app:8080 /docs/quickstart 200 — no redirect, page served under the legacy host

Why

middleware.ts reads the Host header raw and compares it whole:

const host = request.headers.get('host');
if (host === LEGACY_HOST) {

The Host header carries authority, i.e. host[:port] — the port is present whenever it is not the scheme default. So www.objectos.app:8080 is simply not equal to www.objectos.app and falls through to locale negotiation, which serves the page under the legacy host instead of redirecting off it.

The asymmetry is what makes this worth recording rather than just noticing: the same block explicitly clears the port on the way out —

target.port = '';

— so the port was considered on the target side and not on the source side. That reads like an oversight rather than a decision, but only the maintainer can say.

Severity, honestly

Low, and possibly zero in production. The site sits behind Cloudflare on 443, where browsers omit the default port and the Host header arrives bare, so the redirect works for every real visitor today. The unhandled shape is reachable only by a request to the legacy host on a non-standard port, which that host does not serve.

If it is worth closing

Compare the hostname rather than the authority — split on : and take the first field, or parse it. One line, and the existing six-case redirect table plus a seventh row for the ported host would cover it. If instead the exact-match is deliberate (only ever answer the legacy host on its real port), a comment saying so would keep the next reader from "fixing" it.

Filed unassigned for triage.

Activity

  1. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Graded: pm:queue / Bug, low priority. Queued behind #227.

    The severity assessment is the part that makes this a good card rather than a noisy one: "Low, and possibly zero in production", with the reason — the site sits behind Cloudflare on 443, browsers omit the default port, so the Host header arrives bare and the redirect works for every real visitor today. Saying that plainly is what lets it be scheduled honestly instead of either inflated or buried.

    The asymmetry is the actual argument, and it is a good one. The same block clears the port on the way out:

    target.port = '';
    

    So the port was considered on the target side and not on the source side. That is a much stronger case than "the code does not handle a shape" — it is evidence about intent, drawn from the code's own behaviour two lines away.

    Direction, adjudicated

    Compare the hostname, not the authority. The Host header carries host[:port] by definition, so an exact match against a bare hostname is testing something narrower than it reads as testing.

    I am not treating the "maybe it was deliberate" branch as open. If answering the legacy host on a non-standard port were an intended behaviour, the same block would not be stripping the port from the target — and there is no scenario where serving pages under the legacy host is preferable to redirecting off it. Take the fix; the alternative the card offers (a comment explaining the exact match) would be documenting an inconsistency rather than resolving it.

    Scope

    One line in apps/docs/middleware.ts, plus the seventh row on the redirect table. That table has now been re-run by #204, #223, #225 and #227 — reuse it rather than rebuilding it, and add www.objectos.app:8080 alongside the existing six.

    Note middleware.ts has been rewritten four times today (#204, #223, #225, #227). Base on current main and read the file.

    Low priority and small: this is the last card in the chain and it is worth closing rather than leaving as the one dangling item, but nothing depends on it.


    Generated by Claude Code

  2. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Unblocked — #227 merged (f78a077). Dispatching. This is the last card in the chain.

    Claim: PM loop round 18
    Session: session_01G5zjYc2BoFV2NjKBBapC7C
    Branch: claude/issue-226-legacy-host-port
    Worktree: objectos-issue-226
    Domain: n/a (objectos carries no domain:* lanes)
    File surface: apps/docs/middleware.ts. Stop on breach; explain in the report.
    Container and model: S, mode:subagent, model: sonnet — the direction is adjudicated in the grading comment above and the change is one comparison.
    Clause-②: no
    Serial constraints cleared: nothing in flight. middleware.ts has been rewritten four times today — #204, #223, #225, #227. Base on current main and read the file rather than the card's snippet.

    The change

    Compare the hostname, not the authority. The Host header carries host[:port] by definition, so an exact match against a bare hostname tests something narrower than it reads as testing — which is why www.objectos.app:8080 gets a 200 under the legacy host instead of a 308 off it.

    The grading comment records why I am not treating "maybe the exact match was deliberate" as an open branch: the same block already strips the port from the target (target.port = ''), so the port was considered on one side and not the other, and there is no scenario where serving pages under the legacy host beats redirecting off it.

    Verification

    The redirect table has now been run by #204, #223, #225 and #227. Reuse it rather than rebuilding it, and add the ported host as a seventh row:

    • Host: www.objectos.app:8080 on /docs/quickstart → 308 to https://docs.objectos.ai/docs/quickstart, with no port on the target.
    • All six existing rows unchanged, including the query-preservation case and case 6, the ordering control (Host: www.objectos.app on /cn/docs/quickstart → the domain rule wins, /cn/ is handled on the next hop).
    • #225's controls intact: /zh-Hant/... 200, /zh-hant/... 308, /en/... 307.
    • A canonical host with a port (Host: docs.objectos.ai:3000) must not be redirected away — that is the row that catches a fix which strips the port everywhere instead of comparing hostnames. Say what it does before and after.

    node .github/scripts/check-locale-surface.mjs exit 0 at 409 URLs, and pnpm turbo run type-check / build / test all exit 0.

    One trap recorded by the #205 dev, so you do not lose time to it: running check-locale-surface.mjs from the wrong cwd exits 1 with MODULE_NOT_FOUND. That is an invocation error, not a red gate — run it from the repo root and read the verdict line, not just the exit code.

    The turbo cache is shared across worktrees and has genuinely replayed a sibling's green on this repo today; use --force where a comparison depends on it, capture exit codes before any pipe, and kill any server you start by explicit PID found via /proc — never pkill -f.


    Generated by Claude Code

  3. self-assigned this
    on Aug 26, 2026
  4. added a commit that references this issue on Aug 26, 2026
    6449bf3
  5. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    os-dev-report

    {
    "issue": 226,
    "status": "done",
    "branch": "claude/issue-226-legacy-host-port",
    "pr": "#228",
    "premise_still_valid": true,
    "summary": "apps/docs/middleware.ts compared the raw Host header (host[:port], the authority) against the bare LEGACY_HOST literal, so a ported request like www.objectos.app:8080 fell through to locale negotiation instead of redirecting off the legacy host. Fixed by comparing the hostname (host?.split(':')[0]) instead, one line plus a comment explaining the asymmetry with the existing target.port = '' strip. No change to file surface beyond apps/docs/middleware.ts (git diff --stat: 1 file, +11/-1).",
    "tests": "Reused the redirect table run by #204/#223/#225/#227 against next build + next start (server killed by explicit PID via /proc walk each time, never pkill -f). All 6 existing rows unchanged: (1) Host www.objectos.app /docs/quickstart -> 308 to https://docs.objectos.ai/docs/quickstart; (2) same host with query ?utm=x&ref=y -> 308, query preserved; (3) Host docs.objectos.ai /docs/quickstart -> 200; (4) /zh-Hant/docs/quickstart -> 200; (5) /zh-hant/docs/quickstart -> 308 to /zh-Hant/docs/quickstart; (6) ordering control, Host www.objectos.app /cn/docs/quickstart -> 308 to https://docs.objectos.ai/cn/docs/quickstart (domain rule wins, /cn/ handled next hop). New row 7: Host www.objectos.app:8080 /docs/quickstart -> 308 to https://docs.objectos.ai/docs/quickstart, no port on target (empirically verified BEFORE too: reverted middleware.ts to HEAD via git checkout HEAD -- <path>, rebuilt, restarted on a fresh port, confirmed 200/no-redirect reproducing the bug; then git apply'd the saved patch to restore the fix and rebuilt again for the final table -- restore verified by git diff --stat matching the original edit). Guard row: Host docs.objectos.ai:3000 /docs/quickstart -> 200 both before and after, proving the fix compares against LEGACY_HOST specifically rather than stripping the port from every Host. #225 controls re-checked: /en/docs/quickstart -> 307. Gates, all forced (no turbo cache reuse), exit codes captured before any pipe via PIPESTATUS: pnpm turbo run type-check --filter=@objectos/docs --force EXIT=0 ('Types generated successfully'); pnpm turbo run build --filter=@objectos/docs --force EXIT=0 (route table printed, Proxy/Middleware compiled); pnpm turbo run test --force EXIT=0 (@objectos/ci-scripts self-tests, '4 self-test(s) passed'); node .github/scripts/check-locale-surface.mjs from repo root EXIT=0, verdict line: 'every advertised URL has a source file and every source file is advertised', sitemap.xml 409/409 distinct/expected, 0 unexpected/missing/duplicated. next lint (not in the required gate list) fails repo-wide with 'Invalid project directory provided, no such directory: .../apps/docs/lint' both before and after this change -- pre-existing Next 16 CLI issue unrelated to this file, not filed as a finding since it's a whole-CLI break rather than a located defect and outside this card's scope.",
    "open_questions": [],
    "out_of_scope_findings": []
    }


    Generated by Claude Code

  6. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — merging #228. This closes the chain.

    CI 2/2 green on 6449bf3, one file, one line of behaviour change.

    The guard row did its job. Host: docs.objectos.ai:3000 staying 200 on both sides is what distinguishes the fix that was made from the one that would have been easier: comparing against LEGACY_HOST specifically, rather than "strip the port off every Host and compare." The second would have passed every row about the bug and quietly changed behaviour for the canonical host too. A table that only tests the new behaviour cannot tell those apart.

    And row 7's before column was reproduced, not inferred — the pre-fix file rebuilt, started on its own port, confirmed 200 for the ported legacy host, then restored and rebuilt for the final table. On a one-line fix with an obvious mechanism that is the step most easily skipped, and skipping it is how a card gets closed against a bug that was never there.

    The change is proportionate to the severity you assessed honestly in the first place: one line for a defect that is real, reachable, and almost certainly zero in production today.


    Chain closed

    Twenty-nine cards merged since the keyword-optimization question was asked. Nothing dispatchable remains.

    Still with the maintainer, and not for me to answer:


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions