Skip to content

Default numeric hosts to http, named hosts to https; add a README - #4

Merged
alcor merged 2 commits into
mainfrom
claude/zen-meitner-84wsuy
Sep 19, 2026
Merged

alcor merged 2 commits into
mainfrom
claude/zen-meitner-84wsuy

Conversation

@alcor

@alcor alcor commented Sep 19, 2026

Copy link
Copy Markdown
Member

Scheme defaulting

A target with no scheme now gets one chosen by what the host looks like. Numeric hosts, localhost and .local names are this machine or a device on the local network, which rarely have certificates. Everything else gets https.

Input Result
example.com https://example.com
example.com:8080 https://example.com:8080
192.168.1.50:8123 http://192.168.1.50:8123
localhost:3000 http://localhost:3000
nas1.local http://nas1.local

Applied in the three places that defaulted a scheme and previously disagreed with each other: index.html used http, forward.js and metadata.js used https.

Telling a host and port from a scheme takes care, since example.com is made of characters that are legal in a scheme. It reads as host-and-port only when the name is dotted or is localhost and what follows the colon is purely a number — so localhost:3000 is a host and tel:5551234 is still a scheme.

Four bugs fixed

Two were the reason for the change, two were found by testing it.

  1. index.html treated any colon as a scheme. if (url.indexOf(":") < 0) meant 192.168.1.50:8123 was left with no scheme at all — exactly the input this rule is for.
  2. metadata.js tested startsWith("http"). A hostname beginning with those letters, like httpbin.org, was emitted bare into og:url. Live in production, not hypothetical — before/after below.
  3. localhost:3000 got no scheme. My own first cut: the host-and-port rule only recognised dotted names, so localhost: read as a scheme. Same trap as Make og:image work for SVG: pasted markup, svg: payloads, and UTF-8 #1, one layer down.
  4. Short hostnames were mangled as base64. mylocal is accidentally valid base64 and decoded to binary, producing https://)hq©. A bare token is now only accepted as base64 when it decodes to printable text containing a dot or colon, which every real encoded URL does. This predates the PR; it only became visible once bare hostnames were being tested.

Testing

  • Unit, on normalizeTarget — 32 assertions: the local-host set (localhost, with port, with path, uppercase, *.localhost, .local, uppercase .LOCAL), numeric hosts with and without port and path, loopback, named hosts including near-misses (notlocalhost.com, local.example.com), accidental-base64 tokens (mylocal, test, intranet, wiki, abcd), real base64 that must still decode, and schemes that must survive (http, https, tel, sms, mailto, hass://, the :host shorthand).
  • End-to-end in Chromium against netlify dev: /view/ loads localhost:9001 and 127.0.0.1:9001 over http; example.com:8080 resolves to https; index.html's prompt() stores http://192.168.1.50:8123 for a numeric answer and https://example.com:8080 for a named one.
  • Regression: both existing suites clean — 8 /view/ and sender checks, 10 receiver dispatch checks including DashCast namespace semantics.

Edge function, verified on the deploy preview

metadata.js can't run locally (the sandbox can't fetch Deno's bootstrap), so it was checked against the preview with a Twitterbot user-agent, with production alongside for before/after:

Path …/u/<x>/ Production today This PR
httpbin.org og:url="httpbin.org" — no scheme https://httpbin.org
192.168.1.50:8123 https://192.168.1.50:8123 http://192.168.1.50:8123
localhost:3000 http://localhost:3000
nas1.local http://nas1.local
example.com:8080 https://example.com:8080
intranet mangled by base64 decode https://intranet
aHR0cHM6… https://example.com/a?b=c

README

Replaces the single-line file. Covers the path grammar and its three encodings, the route table, fallback probing, the Cast setup including the published app id CCAB7FD4 and the DashCast namespace compatibility, the repo layout, and local development.

It also writes down two routing constraints that are easy to break silently: rules in netlify.toml must stay above the /* catch-all with /cast/receiver above /cast/*, and the metadata edge function must keep skipping /view/ and /cast/ or it will treat them as link previews and redirect bots to their targets.

🤖 Generated with Claude Code

https://claude.ai/code/session_014nP4CW7aH6ei3eHbBb1oYd

A target with no scheme now gets one chosen by what the host looks like:
a numeric host is a device on the local network, which rarely has a
certificate, while a named host should be https.

  example.com          -> https://example.com
  example.com:8080     -> https://example.com:8080
  192.168.1.50:8123    -> http://192.168.1.50:8123

Applied in the three places that defaulted a scheme, which previously
disagreed: index.html used http, forward.js and metadata.js used https.

Two latent bugs fall out of the same change. index.html treated any colon
as a scheme, so a host and port such as 192.168.1.50:8123 was left without
one entirely. metadata.js tested startsWith("http"), so a hostname
beginning with those letters, like httpbin.org, was treated as already
carrying a scheme. Both now share hasScheme(), which distinguishes
"example.com:8080" from "tel:5551234" by whether the part before the colon
is dotted and the part after is purely a number.

The README covers the path grammar, the routes, the fallback probing, the
Cast setup including the published app id, and the routing-order
constraints in netlify.toml that are easy to break.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nP4CW7aH6ei3eHbBb1oYd
Numeric hosts already defaulted to http. localhost, *.localhost and
*.local names are this machine or a device on the local network and are
in the same position: they rarely have certificates, so https just fails
to connect. The host test is now case-insensitive.

Two bugs found while testing this.

localhost:3000 was getting no scheme at all. hasScheme only recognised a
host and port when the name was dotted, so "localhost:" read as a scheme,
the same trap "example.com:8080" fell into. The rule now also accepts
localhost as a host name, while tel:5551234 stays a scheme.

A short single-label hostname could be decoded as base64 and mangled:
"mylocal" decoded to binary and produced https://)hq. A bare token is now
only accepted as base64 when it decodes to printable text containing a dot
or a colon, which every real encoded URL does. This predates the scheme
work; it was only visible once bare hostnames were being tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014nP4CW7aH6ei3eHbBb1oYd
@alcor
alcor marked this pull request as ready for review September 19, 2026 14:06
@alcor
alcor merged commit d821de6 into main Sep 19, 2026
3 checks passed
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.

2 participants