fix(urls): mint every client-facing URL on the caller's port - #380
Merged
Conversation
…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.
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
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].
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_HOSTNAMEset and the API port remapped (-p 4652:4566), measured live:QueueUrluris.GRAPHQLapiEndpointFunctionUrl/ Cognito issuer & discoveryRoot 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.3 —
issuermust equal the URL configuration was retrieved from. The config-port issuer failed spec validation for every remapped-port caller and pointedjwks_uriat 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 andTestPoolIDFromIssuer_ignoresTheOrigin, so a DRY pass can't silently revert it.Deliberate divergences (documented in code + plan, functional reasons only)
QueueUrlis dialed by the requesting client itself, so verbatim echo carries zero resolution assumptions; hostname substitution would turn the documentedOVERCAST_HOSTNAME=localhostmisconfig into a hard container failure.repositoryUri: the docker daemon dials it, not the API caller. Deferred, constraint documented.Container boundary
containerendpointnow 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 inWithPublishedPort.Performance
Verification (all live, remapped port,
OVERCAST_HOSTNAME=localhost.overcast.sh):4652and answers a real requestissuer== fetch origin (§4.3);jwks_urireturns 200GetQueueUrl/payload, DDB, SNS, wildcard DNS — all passQUEUE_URLdeliberately baked as:4652arrives inside the function as:4566and worksgo test ./...clean,golangci-lintclean, CDK lifecycle suite 35 passed / 0 failedDocs
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-boundaryisscomparison 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.