Skip to content

fix(urls): mint every client-facing URL on the caller's port - #380

Merged
Neaox merged 3 commits into
mainfrom
docs/client-base-url-finding
Jul 29, 2026
Merged

fix(urls): mint every client-facing URL on the caller's port#380
Neaox merged 3 commits into
mainfrom
docs/client-base-url-finding

Conversation

@Neaox

@Neaox Neaox commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Started as a docs-only finding; now the fix, built from a researched plan with per-service constraints. Design doc: docs/plans/client-facing-url-minting.md — the rule, each service's requirements tested against it, the special-case verdicts, performance, and the methodology for new services.

Problem

With OVERCAST_HOSTNAME set and the API port remapped (-p 4652:4566), measured live:

Advertised URL Port Dialable from host
SQS QueueUrl 4652
AppSync uris.GRAPHQL 4566
API Gateway v2 apiEndpoint 4566
Lambda FunctionUrl / Cognito issuer & discovery 4566

Root cause: four hand-kept base-URL precedences (serviceutil, SQS, CloudFormation, Cognito's typed path), two disagreeing on the port.

The rule, one implementation

serviceutil.ClientBaseURLFromOrigin: configured hostname (authoritative, #351) on the caller's port (their request is the only proof of a dialable port — Overcast can't see its own port mapping), scheme upgrades to https (config TLS or https caller) and never downgrades. CFN's and Cognito-typed's private copies are now one-line delegations — the typed CBOR path had drifted to the config port, so a caller's wire protocol changed their issuer; now impossible by construction.

The Cognito re-analysis (the "special case" that wasn't)

Per-caller issuer ports are required by OIDC Discovery 1.0 §4.3issuer must equal the URL configuration was retrieved from. The config-port issuer failed spec validation for every remapped-port caller and pointed jwks_uri at a port they couldn't dial. What actually needs guarding is Overcast's own validation staying path-based, never literal — now protected by a code comment at the validation site and TestPoolIDFromIssuer_ignoresTheOrigin, so a DRY pass can't silently revert it.

Deliberate divergences (documented in code + plan, functional reasons only)

  • SQS wire echo: the QueueUrl is dialed by the requesting client itself, so verbatim echo carries zero resolution assumptions; hostname substitution would turn the documented OVERCAST_HOSTNAME=localhost misconfig into a hard container failure.
  • ECR repositoryUri: the docker daemon dials it, not the API caller. Deferred, constraint documented.

Container boundary

containerendpoint now rewrites split-horizon names carrying the published port to the listen port entering function/task env and invoke payloads (origin + virtual-hosted forms) — the name resolves inside, the port doesn't exist there. Patterns precomputed in WithPublishedPort.

Performance

  • Minting: 274 ns / 196 B / 3 allocs per minted URL — response path only, never the routing path.
  • Payload rewrite miss path (runs per invoke): 313 ns / 0 B / 0 allocs on a 1 KiB payload.

Verification (all live, remapped port, OVERCAST_HOSTNAME=localhost.overcast.sh)

  • Every symptom-table URL carries :4652 and answers a real request
  • Discovery issuer == fetch origin (§4.3); jwks_uri returns 200
  • In-Lambda probe: bare virtual-hosted S3, SQS via env/GetQueueUrl/payload, DDB, SNS, wildcard DNS — all pass
  • A QUEUE_URL deliberately baked as :4652 arrives inside the function as :4566 and works
  • go test ./... clean, golangci-lint clean, CDK lifecycle suite 35 passed / 0 failed

Docs

  • docs/networking.md: user-facing "which host and port a URL carries, and why" — including the one irreducible caveat (no single host:port dials from both sides of a remap, so literal cross-boundary iss comparison fails under any policy; 1:1 mapping resolves it) so nobody burns time chasing it.
  • AGENTS.md: pointer to the doctrine so future services follow it and tidy-ups read the constraints first.

Neaox added 2 commits July 29, 2026 23:58
…d a resource URL

With OVERCAST_HOSTNAME set and the API port remapped, AppSync's uris, API
Gateway v2's apiEndpoint and Lambda function URLs carry the listen port and are
undialable from the host. SQS queue URLs are correct on the same instance,
because they mint per caller.

serviceutil.ClientBaseURL takes cfg.Port unconditionally once a hostname is set,
and cfg.PublishedPort is never referenced in the package at all.

Taking the caller's port instead — matching CloudFormation's own clientBaseURL
and SQS — fixes those URLs and passes the whole suite except Cognito, whose OIDC
issuer was deliberately changed in this same release to come from the configured
origin rather than the caller's Host. A per-caller iss breaks token validation
across the container boundary, which is the failure that fix removed.

One helper, two opposite requirements. Attempted, measured, reverted rather than
shipped: splitting the two notions of "the external base" is a design decision,
not a release patch.

Pre-existing — the responsible line is in v0.0.1-alpha.25, so not an alpha.26
regression, and the default configuration is unaffected.
With OVERCAST_HOSTNAME set and the API port remapped (docker run -p 4652:4566),
AppSync's uris, API Gateway v2's apiEndpoint, Lambda function URLs and the
Cognito issuer all carried the listen port and were undialable from the host —
while SQS queue URLs on the same instance were correct, because they mint per
caller. The repo had four hand-kept base-URL precedences (serviceutil, SQS,
CloudFormation, Cognito's typed path); two disagreed on the port, which was
exactly the bug.

One implementation now: serviceutil.ClientBaseURLFromOrigin — configured
hostname (authoritative, #351), the caller's port (their request is the only
proof of a dialable one; Overcast cannot see its own port mapping), and a
TLS-aware scheme that upgrades and never downgrades. ClientBaseURL wraps it for
request-shaped callers; CloudFormation's and Cognito's private copies become
one-line delegations, so the typed CBOR path can no longer drift from the JSON
path — it had, to the config port, making a caller's wire protocol change the
issuer their token carried.

The Cognito issuer following the caller's port is what OIDC Discovery 1.0 §4.3
requires: issuer MUST equal the URL the configuration was retrieved from, so
the old config-port issuer failed spec-compliant validation for every
remapped-port caller and pointed jwks_uri at a port they could not dial.
Overcast's own validation is port-agnostic — the pool ID is read from the
issuer path, never compared literally — and that accommodation is now guarded
by comment and test so a tidy-up cannot silently revert it.

Two deliberate divergences survive, each commented in place with its functional
reason: SQS wire responses echo the caller's exact origin (SDKs dial the
QueueUrl itself; substitution would amplify the OVERCAST_HOSTNAME=localhost
misconfiguration into a container-breaking failure), and ECR's repositoryUri
stays canonical (the docker daemon, not the API caller, dials it).

containerendpoint correspondingly rewrites split-horizon hostnames carrying the
published port to the listen port on the way into function/task environment and
invoke payloads — the name resolves inside a container, but that port is bound
only on the host. Patterns are precomputed in WithPublishedPort because the
rewrite runs per invoke payload: measured 313 ns and zero allocations on a 1 KiB
miss. URL minting itself measures 274 ns per minted URL, response-path only.

Verified live on OVERCAST_HOSTNAME=localhost.overcast.sh with -p 4652:4566:
every symptom-table URL carries :4652 and answers; discovery issuer equals the
fetch origin and jwks_uri returns 200; the in-Lambda probe passes (bare
virtual-hosted S3, SQS via env/discovery/payload, wildcard DNS); a :4652 URL
baked into function env arrives inside as :4566 and works. Full go test ./...
passes, golangci-lint clean, CDK lifecycle suite 35 passed / 0 failed.

Design, per-service requirements and constraints, and the methodology for new
services: docs/plans/client-facing-url-minting.md. User-facing summary:
docs/networking.md. The one irreducible caveat — no single host:port is
dialable from both sides of a port remap, so cross-boundary literal iss
comparison under a remap fails under every possible policy — is documented in
both, with the 1:1 mapping workaround.
@Neaox Neaox changed the title docs(plans): ClientBaseURL cannot serve both a stable issuer and a dialable resource URL fix(urls): mint every client-facing URL on the caller's port Jul 29, 2026
make docs-check gates on the committed index matching the docs content; the
new 'Which host and port a URL carries' section made it stale.
@Neaox
Neaox merged commit f5ca3ac into main Jul 29, 2026
42 checks passed
@Neaox
Neaox deleted the docs/client-base-url-finding branch July 29, 2026 21:30
Neaox added a commit that referenced this pull request Jul 29, 2026
Rebuilt after the client-facing URL minting fix (#380) landed. 47 entries:
4 Added, 4 Changed, 39 Fixed. Bullet set byte-identical to main's [Unreleased].
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