v0.0.1-alpha.26
Pre-releaseOvercast v0.0.1-alpha.26
Docker Images
Full image with the web management console:
- Pull:
ghcr.io/neaox/overcast:0.0.1-alpha.26 - Channel tag:
ghcr.io/neaox/overcast:alpha - Registry: overcast package
Headless slim image for CI pipelines:
- Pull:
ghcr.io/neaox/overcast-slim:0.0.1-alpha.26 - Channel tag:
ghcr.io/neaox/overcast-slim:alpha - Registry: overcast-slim package
docker run --rm -p 4566:4566 -p 4567:4567 ghcr.io/neaox/overcast:0.0.1-alpha.26
docker run --rm -p 4566:4566 ghcr.io/neaox/overcast-slim:0.0.1-alpha.26Native Binaries
Download a binary for your platform from the assets below and verify it with SHA256SUMS.
| Asset | SHA256 |
|---|---|
overcast-darwin-amd64 |
6ac8c65defc1d343242f685f47909e47695b80b31c9796c9b0db623acdf0a4b8 |
overcast-darwin-arm64 |
db81a245b0e3c19ea3d166369eec25351d814c6287fd200456c4dfcf74e5a51b |
overcast-linux-amd64 |
a77c1142aeeee065fdc3e24ad553f490c61e2cb6ee7792b864a16eb1ddec8af3 |
overcast-linux-arm64 |
0c7c7ddb128fba194c27f9df1332fea2978a8f241bbe41c7f9ddf7db7d832905 |
overcast-windows-amd64.exe |
bb7626ad41a9884e751267e3893ebd9ca41227d404fad3233a40c4513aef9326 |
overcastd-darwin-amd64 |
519c45f17baf1a51002dcb14b9f4d5ad26e4a9d9663868a2f84f514fe9e62b2f |
overcastd-darwin-arm64 |
9897333aa399166c1c50839bce9840f6d4b4a244ea7bbfc0d95282c82a1713b3 |
overcastd-linux-amd64 |
93b89bf81d1a0c41bfd3fff198acdae7522d85c3a57c03a85a2cee9768109ccf |
overcastd-linux-arm64 |
a90e7ad1baa6da0460910463c9ddf931937192d24d2df6c6986e70406c753bf8 |
overcastd-windows-amd64.exe |
4c10dec6cbc0ee6d47ef255183504eadb8d34f9164900319c3819e09eee5f50e |
Release Notes
Added
-
CI (release candidates) — every build of a release PR now publishes the exact images CI built to GHCR as
ghcr.io/neaox/overcast[-slim]:<version>-rc.<n>(linux/amd64,<n>auto-increments per build, earlier RCs stay pullable), uploads all ten cross-built native binaries as workflow artifacts, and maintains a single release-notes-shaped bot comment on the PR with pull commands, image digests, and the artifact table. Until now the PR workflow built images and binaries only as checks and discarded them, so pre-release smoke testing always ran against local rebuilds rather than the candidate bits themselves. Release-candidate detection is one shared predicate rather than a branch-name convention: a same-repo PR whoseVERSIONcarries nov<VERSION>tag yet (scripts/release-candidate-check.sh) — which also covers follow-up PRs after a failed release workflow, a case arelease/*branch test is blind to; the release workflow's changelog validation now uses the same predicate and so gains the same coverage. Fork PRs and ordinary PRs are unaffected. The ten-asset binary matrix, previously duplicated between the CI and release workflows and drifting one edit at a time, is now stated once in a sharedworkflow_call(build-binaries.yml) used by both — so the binaries checked on the release PR are built by the same definition that publishes them. -
HTTPS / HTTP/2 (
OVERCAST_TLS=auto,overcast https) — Overcast can now serve both the API and the web UI over browser-trusted HTTPS with certificates minted from a local overcast CA, unlocking HTTP/2 in browsers. This fixes the web console starving under load: browsers cap HTTP/1.1 at six connections per origin (localhost included) and never negotiate cleartext HTTP/2, so the console's SSE feed, Lambda invoke progress streams, S3 transfers and polling could exhaust the sockets and make navigation hang; over TLS the browser multiplexes everything on one connection via ALPN. Newovercast https enable|disable|statusdoes the whole setup in one command (create the CA under<data dir>/ca, install it into the system trust store — user-scope certificate store on Windows, login keychain on macOS, system CA bundle on Linux — and mint a leaf coveringlocalhost,127.0.0.1,::1,localhost.overcast.sh,*.localhost.overcast.sh,*.s3.localhost.overcast.sh, the other wildcard DNS domains,OVERCAST_HOSTNAME, andOVERCAST_SPLIT_HORIZON_HOSTS);overcast trust install|uninstall|status, previously a stub on every platform, now actually manages the CA in the trust store.OVERCAST_TLS=automakesovercast servemint/reuse that leaf at startup (re-minted automatically when the name set changes or expiry nears; leaves live 825 days, the CA 10 years, and re-minting never invalidates the trust-store install). ExplicitOVERCAST_TLS_CERT/OVERCAST_TLS_KEYnow applies to the web UI listener too, not just the API; the SPA bootstrap, minted client-facing URLs, and init-hook environment (AWS_ENDPOINT_URL, plus a newAWS_CA_BUNDLE) all sayhttpswhen TLS is on; the Docker HEALTHCHECK probes https as a fallback; plain-HTTP defaults are unchanged, including h2c for SDK clients.serve --bridgeis skipped with a warning while TLS is on (the port-80 proxy speaks plain HTTP). See the new docs/https.md. Docker setup is now two commands with no shared volume: the daemon serves its CA certificate (public half only) atGET /_overcast/ca.pem(404 until a CA exists; mirrored on the web UI as/api/ca.pem), andovercast https enable --endpoint http://localhost:4566fetches it, validates it actually is a CA certificate, caches it under<data dir>/ca-remote/<host_port>/(kept separate from the local CA sostatus/disable --endpointfind exactly what was installed), and installs it into the system trust store —http://spellings auto-negotiate to a TLS-only daemon, non-loopback endpoints are refused without an explicit--trust-remoteacknowledgement, and the same--endpointworks onovercast trust install|status|uninstall. A containerized daemon logs the exact--endpointcommand at startup. Both Docker images serve TLS + HTTP/2 (the slim image on its API listener; both health checks handle https), and on Linux installed anchors are now fingerprint-named so a local CA and fetched daemon CAs coexist (anchors installed by earlier builds under the old fixed name are still recognised and cleaned up). -
Lambda (concurrency) — a function now scales out to one execution environment per concurrent invocation instead of sharing a single warm container, and provisioned concurrency actually allocates environments.
PutProvisionedConcurrencyConfigpre-initializes the requested number in the background (IN_PROGRESS→READY), holds them open against the idle sweep, replenishes them when one is lost, rebuilds them against the new configuration after a code/config update, restores them across a restart, and marks their containersAWS_LAMBDA_INITIALIZATION_TYPE=provisioned-concurrency;Allocated/Availableare reported from the environments that actually exist. It behaves as a floor rather than a ceiling, so invocations beyond the reservation spill over into on-demand capacity with a cold start rather than throttling, matching AWS. Previously the config was stored, echoed back asREADY, and nothing was ever pre-warmed.DeleteProvisionedConcurrencyConfigandListProvisionedConcurrencyConfigsare now implemented, and all four operations moved to the/2019-09-30/path AWS actually serves them on — the AWS SDKs' calls previously missed the handlers entirely and fell through to the S3 catch-all.Reason: FAILEDwith aStatusReasonis now reported when Docker is unavailable, instead of claiming capacity that cannot exist. -
Lambda (concurrency limits) —
ReservedConcurrentExecutionsis now enforced: exceeding it returns AWS's 429TooManyRequestsException(Type/message/Reason/retryAfterSecondsbody,X-Amzn-ErrortypeandRetry-Afterheaders) withReason: ReservedFunctionConcurrentInvocationLimitExceeded, so throttle-handling code and the "reserve 0 to disable a function" idiom now work as they do on AWS; it was previously stored and returned but never applied. The three reserved-concurrency operations also moved to the API versions AWS actually serves them on —PutFunctionConcurrencyandDeleteFunctionConcurrencyon/2017-10-31/,GetFunctionConcurrencyon/2019-09-30/— having been registered on Lambda's/2015-03-31/base, where no AWS SDK or CLI call could reach them: they fell through to the S3 catch-all and came back as 404NoSuchBucketnaming the API version as the bucket, so reserved concurrency could not be set at all through a real client.GetFunctionCodeSigningConfighad the same defect and moved to/2020-06-30/. A disabled Lambda now also reports "service disabled" on every API version it serves rather than letting the non-base ones fall through to S3. NewLAMBDA_MAX_INSTANCES,LAMBDA_MAX_INSTANCES_PER_FUNCTIONandLAMBDA_MAX_WARM_INSTANCES(default 10) bound how many containers Overcast will run on your machine; when unset, the first two — along withLAMBDA_DOCKER_MAX_CONCURRENT_STARTS— are sized to the machine Docker actually runs containers on (itsGET /infoNCPU/MemTotal, which is the Docker Desktop VM or remote daemon, not necessarily where the Overcast process runs): concurrent startsclamp(NCPU/2, 2, 8)because each start bursts ~2 CPUs during INIT, instancesclamp(MemTotal×0.65 / 256 MiB, 4, 32), per-functionclamp(maxInstances/2, 2, maxInstances), logged once at startup, with the previous fixed defaults (4/25/10) applying when/infocannot be read. NewLAMBDA_MAX_MEMORY_MBadditionally budgets memory in bytes rather than containers: a new container is admitted only while ΣMemorySizeof live containers stays inside the budget (default 65% of the Docker host'sMemTotal; unlimited when/infocannot be read) — a real bound, since every container is hard-capped at itsMemorySizewith swap disabled — using the same reclaim → queue → throttle-at-timeout ladder, warning once (naming the env var) when first exhausted, and preferring to destroy rather than keep warm a just-finished container while the pool sits above ~90% of the budget; provisioned environments are never taken and running invocations are never interrupted. The pressure is visible at the default log level without per-container spam: entering the high-water regime warns once (budget, reserved, threshold — expect cold starts), recovering logs once with how many containers were shed and for how long, and a memory-bound idle-container reclaim warns where the routine count-cap reclaim stays at info — an invocation that cannot get one reclaims the least-recently-used idle container (never a provisioned one), then queues, and is throttled withReason: ConcurrentInvocationLimitExceededonly if it is still waiting when the function's timeout expires. Asynchronous invocations are never throttled back to the caller (they were already answered 202) and are retried internally instead; a throttled event source mapping batch is left in flight so its messages return to the queue on the visibility timeout. Account-wide quotas and requests-per-second limits remain out of scope. -
Lambda (code signing) — the code signing configuration a function is created with is now stored and read back.
GetFunctionCodeSigningConfigwas a stub that reported "no config" for every function, and nothing accepted or stored aCodeSigningConfigArnanywhere, so a CDKFunctionwithcodeSigningConfigset had it silently dropped.CreateFunctionnow accepts the ARN, CloudFormation passes the property through so the association survives a deploy, andPutFunctionCodeSigningConfig/DeleteFunctionCodeSigningConfigare implemented on the/2020-06-30/path AWS serves them on — they previously fell through to the S3 catch-all and returnedNoSuchBucket. The ARN is validated against AWS's documented pattern. Code signing is optional and that path is unchanged: a function without a configuration still answersResourceNotFoundException, which is what AWS's own model requires, sinceCodeSigningConfigArnis a required member of the success response and so "no config" cannot be expressed as one. The configuration itself is now a real resource:CreateCodeSigningConfig,Get,Update,Delete,ListCodeSigningConfigsandListFunctionsByCodeSigningConfigare implemented on/2020-04-22/, along with theAWS::Lambda::CodeSigningConfigCloudFormation resource type, so a CDK stack that declares aCodeSigningConfigand points aFunctionat it deploys and reads back.AllowedPublishersis required andUntrustedArtifactOnDeploymentdefaults toWarn, both as AWS models them; a function naming a configuration that does not exist is rejected withCodeSigningConfigNotFoundException, and deleting one still referenced by a function returnsResourceConflictException. Signature validation itself is deliberately not emulated — Overcast is not a security boundary. -
Web UI — the system map now shows a function's real concurrency: one card per execution environment rather than one per function, ordered so environments doing work appear first, with a header summary of how many are running/idle/provisioned, a "+N more · scroll" hint when the list overflows, and provisioned environments marked and shown as reserved rather than counting down an idle TTL they are exempt from.
Changed
-
Web UI (connection status) — losing the emulator is now reported by a persistent toast in the bottom-right corner instead of a bar across the top of the shell, and it says what the bar could not: which reconnect attempt is pending, the seconds until it runs, when the data on screen was last live, and that writes are paused until the connection returns.
retry nowfires the pending attempt immediately,view eventsgoes to the one page that still reads without the emulator, and the return is announced by a toast of its own, so a reconnect that happened while you were reading something else leaves a trace rather than being a disappearance you had to catch. Reconnection is Overcast's own now, not the browser's: anEventSourceretries a dropped connection on a schedule it never exposes, and that is the common case rather than an edge one — a stopped emulator is a network-level failure, which leaves the connection inCONNECTING, and only a bad response closes it outright — so nothing could say when the next attempt was due and "retry now" had nothing to pre-empt. The connection is now closed on the first error either way and every attempt is scheduled by the SharedWorker that owns it, so all open tabs count down to the same moment, and one backoff (1s, 2s, 4s, then 5s) replaces the copy the worker and its no-SharedWorker fallback each kept. Resuming is unaffected: the last event id already travelled as a query parameter for exactly the reconnects a browser does not manage for us, so nothing is replayed twice or missed. The toast shares one bottom-right dock with ordinary toasts — it holds the corner and transient ones stack above it — and the topbar's endpoint dot goes amber for a drop, matching the toast that explains it, rather than the grey it uses while a first connection is still outstanding. -
Web UI (tabs and sidebar) — the browser tab now carries the icon of the service you are on, drawn from the same registry entry the sidebar renders and inked light or dark from the tab strip's colour scheme, so a wall of open Overcast tabs is readable at a glance; a run narrowed with
OVERCAST_SERVICESstarts with its enabled services pinned in the sidebar instead of an empty pinned list, and those defaults are only written to local storage once you actually pin, unpin, or reorder them; a pinned service that this run has switched off is dimmed rather than hidden and stays clickable, so its page can explain why it is disabled; and CloudWatch's sub-navigation lists Logs before Metrics. -
OVERCAST_SERVICESremoved — every service now runs, always; a leftover value is ignored rather than rejected, so an existing compose file or CI job keeps working. The variable never saved resources: services were constructed either way and their constructors are barred from store reads and I/O, so a disabled service cost about what an enabled idle one costs. What it did cost was 85 branches in the wiring, eight of them pairs — a Lambda function withoutlogsran but wrote no invocation logs, one withoutec2started with no VPC attachment, S3 notifications withoutsqswere never delivered. Those failure modes are now unreachable rather than merely documented. Calls to a service you did not ask for no longer return503 ServiceDisabled, because there is no such state; the web console no longer dims services or seeds the sidebar from a narrowed run. -
Build from source —
git clone && go build ./...now works with only the Go toolchain: the docs search index (internal/docssearch/index.gen.go,web/src/docs-index.gen.ts) is committed rather than generated on every build, and a committedweb/dist/.gitkeepkeeps//go:embed all:web/distresolving before the SPA is built. A binary compiled without the SPA serves the API normally and returns an explanatory 503 (namingmake build-web) on the web UI port instead of a bare 500; Docker and release builds are unaffected and still assert a real SPA.
Fixed
-
TLS (containers) — Lambda functions and ECS tasks can now reach Overcast when it serves TLS. With
OVERCAST_TLSenabled the API listener speaks only TLS, but containers were still handedhttp://endpoints, so every SDK call from every function failed with Go's plaintext rejection ("Client sent an HTTP request to an HTTPS server") surfaced as SDK deserialization garbage. Container endpoints now follow the listener's scheme (containerendpoint.BaseURL, shared by both runtimes), the trust root — the local CA in auto mode, the operator's certificate chain in explicit mode — is injected into every container at/opt/overcast/ca.pemby the sameCopyToContainermechanism as function code (a dockerized Overcast has no host path to bind-mount), andAWS_CA_BUNDLE/NODE_EXTRA_CA_CERTS/SSL_CERT_FILE/REQUESTS_CA_BUNDLEpoint at it, covering the CLI, botocore, Go, Node, Ruby and python-requests stacks. The Java SDK reads only its own truststore — documented caveat indocs/https.md. Under TLS, environment/payload URL rewriting targets the SAN-covered hostname rather than the raw container address, which would trade a scheme error for a certificate error. Plain-HTTP deployments are unchanged: no CA material is injected and endpoints stayhttp. -
Networking (client-facing URLs) / AppSync / API Gateway / Lambda / Cognito / CloudFormation — every URL Overcast mints now carries the port the caller reached it on, so it is dialable by the party it was handed to. With
OVERCAST_HOSTNAMEset and the API port remapped (docker run -p 4652:4566), AppSync'suris, API Gateway v2'sapiEndpoint, Lambda function URLs and the Cognito issuer all carried the listen port and were undialable from the host — measured: SQS queue URLs said:4652while AppSync said:4566on the same instance. The repo had four hand-kept base-URL precedences (serviceutil, SQS, CloudFormation, Cognito's typed path) and two disagreed on the port; they now share one implementation,serviceutil.ClientBaseURLFromOrigin: configured hostname (authoritative, #351), the caller's port, and a TLS-aware scheme that upgrades and never downgrades. The Cognito issuer following the caller's port is what OIDC Discovery 1.0 §4.3 requires —issuermust equal the URL the configuration was retrieved from — and Overcast's own token validation is port-agnostic (the pool ID is read from the issuer path, guarded by test); the typed CBOR path used to mirror this precedence by hand and had drifted to the config port, so a caller's wire protocol changed their issuer — both paths now call the same function. SQS wire responses deliberately keep echoing the caller's exact origin (SDKs dial theQueueUrlitself; documented in code so a tidy-up cannot "unify" it away), and ECR'srepositoryUristays on the configured host:port because the docker daemon, not the API caller, dials it.containerendpointcorrespondingly rewrites split-horizon hostnames carrying the published port to the listen port on the way into function and task environment and invoke payloads — the name resolves inside a container but that port is bound only on the host; the patterns are precomputed so the per-invoke miss path allocates nothing. Full design and per-service constraints:docs/plans/client-facing-url-minting.md; user-facing summary indocs/networking.md. -
S3 (POST on an object) —
POST /{bucket}/{key}with no subresource now answers405 MethodNotAllowed, as S3 does, instead of501 NotImplemented. POST is only an operation on an object when a subresource selects one (?uploads,?uploadId=,?restore,?select); without one there is no such AWS operation, so the two responses make different claims.NotImplemented— with itsx-emulator-unsupportedheader — tells a client this emulator is incomplete and invites a workaround, for a request real S3 refuses too. That misdirection had a cost: it was the response surfaced when a CloudFront distribution routed a POST to an S3 origin, and it sent the investigation after a missing feature rather than a misrouted request. The subresource operations dispatch unchanged. -
CloudFront (cache behaviors, origins) — two defects that sent a viewer request to the wrong origin, both silently. Path patterns now follow the documented
CacheBehavior.PathPatternsemantics:*matches across/, and the leading/is optional as AWS states ("CloudFront behavior is the same with or without"). The matcher special-cased a trailing*and otherwise used Go'spath.Match, whose*stops at a/. So AWS's own*.jpgexample matched nothing below the root, a wildcard in the middle (/api/*/detail) never matched, and — worst — a pattern written without a leading slash (api/*,jobs*) could never match anything at all, because a request path always has one. Such a behavior was dead: its requests fell through to the DEFAULT behavior and were served by whichever origin that named, with nothing logged to say so. Origin resolution now recognises any endpoint this emulator serves, by deferring to the sameHostClassifierthe router uses for inbound requests and dialling locally with the origin's own Host preserved. Only the{bucket}.s3.{...}.amazonaws.comspelling had been recognised, by a bespoke prefix check; an S3 website endpoint, a legacy dash-region bucket ({bucket}.s3-{region}.amazonaws.com), or an origin naming any non-S3 service fell through to "custom origin" and was dialled at its literal domain — so a distribution fronting an emulated service reached out to real AWS over the internet instead of being served locally. API Gateway, Lambda function URLs and AppSync origins work as a result, and a service becomes usable as an origin as soon as it becomes routable, rather than needing its own case here. Genuine third-party origins are unaffected and still dialled as configured. -
CloudFront (origin groups) / Web UI — a cache behavior whose
TargetOriginIdnames an origin group rather than an origin is now served.CreateDistributionhas always accepted the shape —validateOriginRefsadds everyOriginGroupId to the set of legal targets, citing AWS's origin failover documentation — but the proxy resolved the target againstOriginsonly, so every viewer request returned502 Origin not found. CDK emits exactly this when aDistributionis given an origin group, so a stack could deploy clean and then fail on every request, with the distribution's own config offering no hint that the target was legal but unreachable. Group members are now tried in declared order, with failover on the response codes the group'sFailoverCriterialists and on a connection failure to the member. Failover is restricted toGET,HEADandOPTIONS, as AWS restricts it — for any other method CloudFront returns the primary's response as-is, and a request whose body has already been streamed to the first origin cannot be replayed to the second anyway. A member naming an origin that does not exist is skipped rather than failing the lookup; a group with no resolvable member still reportsOrigin not found, so a genuinely broken distribution does not become a silent one. The console's distribution detail page gained an Origin Groups table alongside Origins, listing each group's primary, its failover members and the status codes that trigger them — the origin list on its own could not explain where a distribution's traffic actually goes. -
API Gateway / AppSync / ECR (advertised URLs) — the URLs Overcast hands back now use the grammar AWS clients expect wherever the address they are minted on can carry them. The web console's REST v1 invoke URL — what its copy button yields, and what its test panel calls — was built path-style as
{base}/restapis/{apiId}/{stage}/_user_request_{path}, a shape no AWS SDK produces and nothing outside Overcast understands, even on a deployment whose every subdomain resolves; it is nowhttp://{apiId}.execute-api.{region}.{base}/{stage}{path}. AppSync'suris.GRAPHQLanduris.REALTIMEwere path-style for the same reason and are now host-routed too, which also fixes CloudFormation'sFn::GetAtt GraphQLUrlandRealtimeUrl, since those pass the value straight through. Both fall back to the path-style form — still served, so nothing holding one breaks — only when the base cannot carry a subdomain (a barelocalhost, whose*.localhostdoes not resolve on Windows or macOS, or an IP literal); that condition is now one predicate shared by the server and the console rather than a per-service judgement, soOVERCAST_HOSTNAME=localhost.overcast.shgets the AWS shape everywhere and a default install keeps the URL that works there. A custom domain'sappsyncDomainNamewas minted asd-{hex}.appsync-api.{region}.amazonaws.com, a name that resolves to real AWS and can never reach the emulator — the same defectdns.GRAPHQLcarried — and is now on the configured hostname.Fn::GetAtt RepositoryUrionAWS::ECR::Repositoryrebuilt{account}.dkr.ecr.{region}.amazonaws.com/{name}instead of reading therepositoryUriECR had just returned, so a stack published a registry nodocker pushin this environment can reach whileaws ecr describe-repositorieson the same repository returned the working one; neitherdkrnorecris a host-route label, so the stack-output re-hosting could not correct it after the fact. Unchanged and deliberate: LambdaFunctionUrl, API Gateway v2apiEndpoint, AppSyncdns.*and CloudFrontDomainNamewere already host-routed; SQS queue URLs and Cognito's OIDC issuer stay path-style because AWS addresses those as service endpoints too. Seedocs/plans/host-routing-precedence.md§8. -
CloudFront (ViewerProtocolPolicy) — a distribution set to
redirect-to-httpsno longer redirects to a scheme this server does not answer. Overcast listens for TLS only whenOVERCAST_TLS_CERTandOVERCAST_TLS_KEYare both set, but the policy was enforced unconditionally, so on a default (plain HTTP) run every viewer request was answered with301 https://{host}:4566/…— a URL that gives the browser a TLS handshake against an HTTP listener.redirect-to-httpsis a common template default, so a distribution could be completely unusable in the browser with nothing in the response explaining why.https-onlyhad the same defect one step earlier, returning a403 HTTPS is requiredthat no client could ever satisfy, making the distribution permanently unreachable. Both are now enforced only when TLS is actually configured, and otherwise served asallow-allwith a one-time warning naming the two environment variables; a TLS-terminating proxy in front that setsX-Forwarded-Proto: httpscontinues to satisfy the policy as before. This is a deliberate divergence from AWS, which always serves HTTPS and can therefore always satisfy both — pointing a caller at an endpoint the emulator cannot answer is the worse of the two, the same trade already recorded for AppSync'surismap and Cognito's OIDC issuer scheme. -
Networking (host-based addressing) — a Host header is now matched case-insensitively in every part, so the same address in any case reaches the same service. A hostname is case-insensitive (RFC 4343; RFC 3986 §3.2.2 for the URI authority), but only the base domain was folded — the
.s3.separator, the bucket name, the service label and the region segment were all compared verbatim, so casing decided which service answered a request. An upper-case service label missed the dispatch table entirely and S3 virtual-hosted addressing then took the whole{id}.{label}.{region}as a bucket name, soUrlId.Lambda-Url.US-East-1.localhost— a Lambda function URL — came back as S3'sNoSuchBucket, and the API Gateway and AppSync invoke hosts did the same. This is not a hand-typed edge case: a browser lower-cases the Host before sending it, so it is what a user gets by pasting an address Overcast minted into the address bar. CloudFront is where it surfaced, because its distribution IDs are the only upper-case ones Overcast mints (E+ 13 upper-case alphanumerics) and the lookup was verbatim: pasting a mintedDomainNameanswered502 Bad Gateway—Distribution "e…" not found— while the identical request withcurl -H Host:succeeded. Host-derived distribution IDs are re-canonicalised to upper case, the only form the store holds. An upper-case region segment was worse than a misroute, because that region becomes the state store's key prefix: a request on…execute-api.US-East-1.…partitioned its resources into a region no other request could name. S3 virtual-hosted addressing is fixed in both forms (MyBucket.localhostandmybucket.S3.localhostnow resolve, where a bucket name is lower-case-only by AWS rule so an upper-case Host segment could only ever have meant a case-folded name). The same rule now applies on the way out, so what Overcast mints is canonical rather than an echo of however the caller typed the Host: every client-facing URL funnels through one helper (serviceutil.RequestBaseURL), so a queue URL, invoke endpoint or stack output created overMixedCase.localhost:4566came back carrying that casing and was not string-equal to the same resource's URL fetched over the lowercase host, despite addressing one endpoint.requestContext.domainNameanddomainPrefix— data function code can branch on — and CloudFront'sredirect-to-httpsLocationare folded for the same reason. One shared implementation now serves both directions, since two copies of a case rule is exactly how this arose. Deliberately not folded: the Host used to verify a SigV4 signature, which must stay byte-identical to what the client signed. Paths are untouched and stay case-sensitive, matching AWS. Classification remains allocation-free on every path — folding is pay-per-use, so a host that is already lower-case is returned unchanged — at a measured cost of 4–13 ns per request, well inside the existing budget; seedocs/plans/host-routing-precedence.md§4 and §7. Separately, a host-routed invoke now resolves in the region its own URL names: the region hint was read off the Host by a second, older service list that had drifted from the routing table and knew nothing ofappsync-api,appsync-realtime-apiorlambda-url, so an AppSync API or Lambda function URL created outside the default region was invoked against the default region's partition of a region-scoped store and answeredNotFoundException: GraphQL API … not found/404 Function not found. These are exactly the requests with no SigV4 to fall back on — AppSync host-routed invokes authenticate with anx-api-keyheader and a Lambda function URL with nothing at all — so the Host was the only region evidence they carried. Lambda's case survived a comment asserting the lookup was region-agnostic, which held only for resolving the URL config: thegetFunctioncall immediately after it is region-scoped. Region extraction now reads the same label table the router dispatches on, so registering a host-routed service can no longer leave it resolving in the wrong region, and the documented precedence is unchanged — an explicitX-Overcast-Region, then the SigV4 credential scope, then the Host — so a signed request's scope is never overridden by its Host. The same read also stopped taking the base hostname for a region when the address has none: an unsigned request to an ordinary S3 virtual-hosted address such asmybucket.s3.localhost.overcast.shresolved to the regionlocalhostand partitioned that request's state under a name nothing else could reach — the same failure as an unfolded region segment, and invisible to signed callers because the credential scope resolves ahead of the Host. -
Per-service storage overrides —
OVERCAST_STATE_<SERVICE>naming a service that does not exist is now a startup error instead of being silently ignored. The override was read by probingOVERCAST_STATE_<name>for each known service, so a variable that loop never thought to construct —OVERCAST_STATE_CLOUDWATCH_LOGSfor a service actually namedlogs, any typo, or a name that predated a rename — did nothing at all: no error, no warning, and the service quietly kept using the global backend while its owner believed otherwise. -
SQS (dead-letter queues) / Web UI —
ListDeadLetterSourceQueuesreturned its source-queue list underQueueUrls, but AWS names that one member lower-camelqueueUrls— the single SQS list response that does, whereListQueuesand the rest capitalise it. AWS SDKs decode an absent member as an empty list rather than erroring, so every SDK client, not just the console, saw a dead-letter queue with no source queues and had no way to distinguish that from a queue nothing dead-letters to. The Query-protocol XML was never affected and still renders repeated<QueueUrl>elements, because AWS models the member with thatxmlName. The web UI gates DLQ redrive on this call exactly as AWS does —StartMessageMoveTaskaccepts "only ARNs of dead-letter queues (DLQs) whose sources are other Amazon SQS queues" — so the queue detail page's redrive button had never appeared for any queue. It now does, and is no longer additionally disabled while the DLQ happens to be empty, a restriction AWS does not impose. Individual messages on a DLQ also gained a button that returns just that one message to the queue it failed on, routed by itsDeadLetterSourceQueueArnthe same way a whole-queue redrive routes each message back to its own source. -
Lambda (Runtime API) — an invocation is no longer stranded when a function has more than one execution environment.
GET /2018-06-01/runtime/invocation/nextis a long poll, and the server tracked one waiting container per function ARN, so the second container to poll replaced the first one's channel. The displaced container stayed blocked on a channel nothing would ever send to and never re-checked the queue, whileSubmitInvocationdelivered into the surviving buffered channel — a send that succeeds even when the reader has gone. Either way the invocation was never handed to a running container: the function loggedSTART, produced no output for its entire configured timeout, and was reported as a timeout that the handler had no part in. The invocation also stayed in the function's queue after the caller gave up, becauseCancelInvocationonly removed it from the pending map, so the next container to start picked up work that had already been reported as failed and ran the handler for real — side effects included — under a dead request ID, its response then discarded as "invocation not found". A second, later invocation would find the queue still holding the stale one and take that instead, so once a function tipped into this state it stayed there. Waiters are now tracked per container in FIFO order, an invocation claimed by a poll that unwinds is redelivered rather than lost, cancelled invocations are dropped from the queue and skipped at pickup, and a container's parked polls are released when it is unregistered. The Extensions API had the same hazard on/2020-01-01/extension/event/next— an event handed to an unwinding poll vanished, and a lostSHUTDOWNis an extension never told to flush — and is now requeued the same way. Functions with a layered extension hit this most often (the AWS Parameters and Secrets Lambda Extension being the common case) because a longer, more variable cold start widens the window in which two containers poll at once. -
Web UI (event stream) — the Events page now opens with the history that preceded it instead of starting from whenever you first looked at it. The server has always replayed its rolling history buffer to every new SSE client, and the app shell has always opened that connection on load, but the captured events were being thrown away before they could be shown. The subscription writes into the React Query cache with
setQueryData, and nothing observes that query unless a consumer is mounted — so away from the Events page it was an unobserved query, garbage collected after the default five minutes, andsetQueryDatadoes not reschedule that timer. Opening the Events page afterwards then created the query afresh and ran itsqueryFn, which resolves to an empty list, wiping whatever had accumulated since the last collection. Both stream queries are now pinned for the lifetime of the page, registered as query defaults so the retention holds from the first event written rather than from whenever something first subscribed. Retention was also being undone from the other end: the SharedWorker kept the last 1,000 events and each tab the last 5,000, both as flat FIFOs, so the UI's own polling — every request publishes arequest:Received— filled them with telemetry and pushed out exactly the state changes a developer was looking for. Every client-side buffer now holds 10,000 events, matching the server's own capacity, and evicts the way the server does: oldest request telemetry and heartbeats first, oldest overall only once no noise is left. Backscroll is also the first thing given back under memory pressure — the working capacity starts lower on a low-RAM device and halves while the JS heap is near its limit, recovering as room returns. Events are now delivered from the worker in batches rather than one message per frame, which matters most on the path that was already correct: a fresh load replaying thousands of events cost a postMessage, a cache write, a re-sort of the whole buffer and a full query-invalidation pass each, with the event→query-key map rebuilt every time. Ordering is fixed too — events were sorted by comparing timestamps as strings, and Go's RFC3339Nano drops trailing zeros, so an event at…:05.5Zsorted before one at…:05Z. Finally,/_eventsnow gives every frame an SSEidand honours theLast-Event-IDa reconnecting client sends back (or alast_event_idquery parameter, for the reconnects a browser does not manage for us, where a freshEventSourcehas no memory of the id — the BFF forwards both). Without it every reconnect replayed the whole buffer and a client that had kept its own copy ended up holding several: a laptop waking from sleep was enough. The id names the run as well as the position, so a token minted before a restart is discarded rather than believed — sequence numbers restart with the process, and honouring a stale one would silently skip the beginning of the new run. The resume point is the highest sequence number below which everything has been sent, not the newest id written, because the bus dispatches through a worker pool and events reach one subscriber out of order; that costs a handful of duplicate events at a drop and can never lose one. -
Web UI (navigation under load) — clicking to another service now responds even while the emulator is busy. Routes are code-split, so a first visit to a page fetches its JavaScript chunk from the same HTTP/1.1 origin that carries the app-wide SSE stream, Lambda invoke progress streams, S3 transfers and polling queries — and a browser allows six connections per origin, so under a Lambda burst the chunk fetch could not start and the click looked dropped, with nothing painted to say otherwise. Every route's chunk is now warmed through the router's own chunk loader while the browser is idle after first paint — code only, one chunk at a time; loaders and queries still run on intent and navigation — so navigating stops needing the network once the tab has had a quiet moment. When a navigation does still wait (the first seconds of a session, or a loader in flight), the content area now paints the design system's static skeleton rows almost immediately (after 50 ms, held ≥ 300 ms once shown) instead of nothing. The burst itself is tamed at both ends: the event stream's flush window now widens 50→400 ms under sustained pressure and narrows back when traffic subsides, on the same halve/double hysteresis the buffer already uses for heap pressure, and each query key is invalidated at most once per second with a trailing call — so a burst no longer refetches instance lists and the topology graph twenty times a second, returning those sockets and that main-thread time to the page, while sparse events stay exactly as fresh as before.
-
Web UI (pinned sidebar items) — dragging a pinned service to reorder it no longer navigates to that service, and no longer scrolls the sidebar sideways. Two separate defects, both in the drag interaction. First, dropping a row — inside the sidebar or anywhere else on the page — followed the link under the pointer as a full page load. Once a drag activates, dnd-kit adds a capture-phase
clicklistener on the document that calls onlystopPropagation(); that is enough to stop React's syntheticonClick, which is exactly where TanStack Router's<Link>would have calledpreventDefault()and routed client-side, but the anchor's default action was never cancelled, so the browser navigated itself. Because the sortable translates the row rather than rendering a drag overlay, the link under the release point was often a neighbouring pinned service, so the reload frequently went somewhere the user had not dragged from. A guard now cancels the click that terminates a drag; it is armed fromonDragStart, which fires only once the sensor's 6px activation distance is met, so a click without a drag is untouched, and a freshpointerdowndisarms it so a drag that never produced a click (Escape, released off-window) cannot swallow an unrelated click later. Second, the dragged row was given a horizontal translate that chased the pointer even though the list only reorders vertically. That pushed it past the nav's right edge, which grew the nav's scrollable width, which stopped dnd-kit's auto-scroller seeing the nav as fully scrolled and set it scrolling sideways — and that scroll was fed back into the drag transform, pushing the row further right again. Dragging right and holding tookscrollLeftpast 15,000px.overflow-x: hiddendoes not prevent this, as the element is still a scroll container with no visible scrollbar. The drag is now pinned to the vertical axis, which removes the feedback loop at its source rather than damping the auto-scroller. -
Lambda (invoke) / web console — a function that runs longer than 30 seconds now completes when invoked from the console's Test tab. The BFF proxied the invoke progress stream through the shared HTTP client, whose 30-second
Timeoutcovers reading the response body, so any longer invocation had its upstream request cancelled mid-run: the browser saw the stream close with no result and reported "Stream ended without result", and the emulator recorded the cancellation as if the function had overrun — logginglambda invoke timed outand writingStatus: timeoutinto the CloudWatchREPORTline for a function whose configured timeout, up to AWS's 15-minute maximum, had not been reached. That pointed straight at the handler for a fault that was in the proxy, and was most visible on functions doing real work during INIT — fetching secrets through the AWS Parameters and Secrets Lambda extension, for instance — where the wall clock includes cold start. The stream is now proxied with the streaming client, and emits SSE keepalive frames every 15 seconds while the handler runs so a long invocation does not sit on an idle connection that proxies and browsers drop. A caller disconnecting is now reported as a cancellation withStatus: error, distinct from a timeout; a function that genuinely overruns returns AWS's payload —<timestamp> <request id> Task timed out after N.NN seconds— rather than a Go error string wrapped inRuntime.ExitError. Container teardown after either outcome stops the container with a 5-second grace period, so thedocker:ContainerDiedevent that follows reports exit code 137 whenever the runtime does not handleSIGTERM; that is the teardown, not the cause. -
Networking (container DNS) / Lambda / ECS — containers Overcast starts can now reach it by every hostname it advertises, not just the exact ones. The split-horizon names were pinned with Docker's
--add-host, and/etc/hostsis an exact-match table with no wildcard syntax, so a subdomain —{bucket}.s3.localhost.overcast.sh,{apiId}.execute-api.{region}.localhost.overcast.sh, the forms AWS SDKs actually build — missed the table, fell through to public DNS, and resolved to127.0.0.1, which inside a container is the container itself. Because the lookup succeeded, it surfaced as a refused connection rather than an unknown host, and virtual-hosted S3 addressing and API Gateway invoke URLs were unusable from function code even though Overcast has always routed those Hosts server-side. Enumerating entries could not fix it: bucket names and API IDs are minted after a container starts, and a container's/etc/hostsis fixed at creation. Overcast now serves the zone itself (internal/dns, UDP and TCP) and points containers at it withHostConfig.Dns; Docker keeps its embedded resolver in front and uses Overcast's as an upstream, so container-name service discovery and external names are unaffected. Answers are per caller, because Overcast attaches toovercast_lambdaandovercast_ecsby default and has an address on each.AAAAfor an owned name is answered NODATA rather than forwarded, since the public record is::1and a dual-stack client would take it and dial its own loopback; forwarding is refused for peers outside loopback and RFC1918, so a published port 53 cannot become an open resolver. Disable withOVERCAST_DNS=false; a failed bind is logged rather than fatal, as port 53 needs privilege outside a container, and either way the/etc/hostsentries still cover the exact hostnames. -
Lambda / ECS (endpoint) —
AWS_ENDPOINT_URLinside containers is now a hostname (http://localhost.overcast.sh:4566) rather than Overcast's raw address. An IP endpoint forces every AWS SDK to path-style addressing and makes the virtual-hosted URLs an SDK derives from the endpoint unusable, and an address baked into a warm execution environment stops working if Overcast's container is recreated on a different one./etc/hostsand the resolver still target the address, so the endpoint resolves whether or not the resolver is running. Invoke payloads now get the same URL rewriting function environment already receives: a queue URL minted by a host-side caller and handed toInvokewas the last route by which an unreachable origin reached a container, because AWS SDKs resolve the SQS endpoint from theQueueUrlrather than fromAWS_ENDPOINT_URL. -
CloudFormation (intrinsic functions) —
Fn::JoinandFn::Selectnow resolve a list argument that is itself an intrinsic, andFn::FindInMap,Fn::Base64andFn::Cidrare implemented.Fn::Select/Fn::Joinasserted their second argument was already a list, soFn::Select [n, {"Fn::Split": [...]}]resolved to an empty string andFn::Joinformatted the unresolved Go map into its output verbatim. That broke every CDK stack containing a nested stack: CDK builds the nested template's S3 key asFn::Select [n, Fn::Split ["||", ...]], so the key came out empty, theTemplateURLcollapsed to/<bucket>/, S3 answered that as aListObjectsV2with HTTP 200, and the stack failed three layers away withcannot unmarshal string into cloudformation.Template. The same defect brokeFn::Select [0, {"Fn::GetAZs": ""}], the standard idiom for placing a subnet in an availability zone.Fn::FindInMapwas never implemented at all —Mappingswas parsed and carried on the resolve context but never read — so region-keyed lookups resolved to the raw map; a miss now yields an empty string rather than embedding Go's map formatting into a resource property (real CloudFormation rejects the template, which the resolver has no way to signal).Fn::Base64was recognised by the YAML short-form parser but had no resolver.Fn::Cidris new, IPv4 only.Fn::ToJsonStringandFn::Lengthremain unimplemented; both require theAWS::LanguageExtensionstransform. With these, the CDK compat suite goes from 4 passed/1 failed/30 skipped to 35 passed, 0 failed across the full bootstrap → synth → deploy → verify → update → destroy lifecycle. -
CloudFormation (
AWS::Events::EventBus) —Refnow returns the event bus name, as AWS documents, rather than its ARN. Consumers build an ARN asarn:aws:events:<region>:<account>:event-bus/+ name, so aRefpassed onward produced a doubled ARN (…:event-bus/arn:aws:events:…:event-bus/my-bus).Fn::GetAtt Arnstill returns the ARN. This was invisible until the nested-stack fix above let a CDK deploy get far enough to exercise it. -
Router (unimplemented AWS operations) — 675 more modeled AWS REST operations now return a protocol-correct
501 NotImplementedinstead of an S3 error. A REST path shared by several AWS services —GET /tags/{resourceArn}is bound by roughly seventy of them,DELETE /workspaces/{workspaceId}by a handful — was treated as unclaimable and handed to S3's catch-all, so an SDK calling one gotNoSuchBucketrather than an honest "not emulated". The ambiguity only ever decided which service to name, never whether S3 owned the path: the fallback already requires a SigV4 credential scope that is present and not S3's, and no AWS SDK signs an S3 request as another service. That scope is therefore sufficient evidence on its own, and an exact modeled method-and-path match now returns the modeled 501 whether or not one service can be singled out. Requests that are unsigned, or signed for S3 or an S3-family service (s3-object-lambda,s3-outposts,s3express, which speak the S3 API under their own signing names and were previously only partly recognised), still reach S3 unchanged. Two further APIs that the credential scope alone cannot place are now resolved from the evidence they do carry: S3 Control shares S3's signing name but sendsx-amz-account-idon every operation, which S3 never does; CodeCatalyst models no SigV4 name at all and authenticates with a bearer token, an auth scheme S3 has no mode for. Both are only consulted when the scope is otherwise indistinguishable from S3's, so S3 traffic pays one header read and never reaches the operation trie. No modeled non-S3 operation now falls through to S3, and a router-level corpus test over all 18,720 of them keeps it that way. -
CloudFormation (stack outputs) — a stack output naming one of Overcast's own path-style URLs is now re-minted on the origin the caller reached Overcast on, so it is dialable by whoever asked for it. Resource handlers build these URLs while provisioning, from the configured origin — provisioning runs through internal requests, so there is no caller to derive one from — and that value was then handed to every later caller unchanged. With the API port remapped (
docker run -p 4600:4566, asscripts/run-test-instance.shdoes) a stack's queue-URL output named port 4566 while the same server'sGetQueueUrlreturned 4600, and the output was undialable: AWS SDKs resolve the SQS endpoint from theQueueUrlrather than from client configuration, so a JS SDK client reading the stack output failed with the queue URL's origin substituted for its endpoint. The AWS CLI masked this by honouringAWS_ENDPOINT_URL. Host-routed outputs were already corrected this way; this extends the same treatment to path-style URLs whose origin is recognisably Overcast's own, leaving ARNs, ECR URIs and third-party endpoints untouched. -
CloudFormation — stack outputs that name a host-routed AWS endpoint are now returned on an origin the caller can reach. CDK composes an API Gateway invoke URL in the template itself, with the scheme as a literal and no port (
["https://", {"Ref":"Api"}, ".execute-api.us-east-1.", {"Ref":"AWS::URLSuffix"}, ...]), soDescribeStackshanded backhttps://abc.execute-api.us-east-1.amazonaws.com/prod/— a URL that resolves to real AWS.AWS::URLSuffixcannot express a scheme or a port, so it still resolves toamazonaws.comas it does on AWS; the correction happens at output emission, where the finished string is parsed with the same grammar that routes inbound requests and re-minted through the same helper every service uses. Outputs that are not host-routed AWS endpoints — ECR registry URIs, S3 URLs, ARNs, plain strings — are left untouched, because Overcast does not serve those hostnames and must not claim to. -
Networking (host-based addressing) — three more AWS hostname forms now reach the right service, and the recognised-domain list is no longer duplicated.
{apiId}.appsync-realtime-api.{region}.{host}was not registered, so it was claimed as an S3 bucket named{apiId}.appsync-realtime-api.{region}— a subscription to the hostname AWS actually serves, and the one Amplify derives by substituting into the GraphQL URL, landed on the S3 handler. It now routes to the same endpoint asappsync-api, with the query string preserved because AppSync carries connection auth there.{distributionId}.cloudfront.netnow reaches the distribution proxy instead of falling through, andDistribution.DomainNameis minted on the hostname the caller reached Overcast on ({id}.cloudfront.{host}) rather than the literalcloudfront.net, which is a fixed AWS domain Overcast cannot serve without a DNS override. Andlocalhost.floci.iowas missing from the S3 virtual-hosted base list whileinternal/containerendpointalready advertised it, so a bucket was unreachable on a domain Overcast tells users resolves to it; both now derive from one list ininternal/config, with a test that fails if they diverge again.docs/networking.mdgains an inventory of every known AWS resource subdomain and whether Overcast routes it — AWS publishes no such list, and the SDK endpoint rulesets and SmithyendpointPrefixcover control-plane endpoints only. -
API Gateway (HTTP v2) / AppSync — the endpoint URLs these services report are now ones Overcast can actually serve. API Gateway v2 never populated
apiEndpointat all, so CloudFormation'sFn::GetAtt ApiEndpointresolved to an empty string; it now returns the canonical{apiId}.execute-api.{region}.{host}endpoint on the hostname the caller reached Overcast on, and is omitted whenDisableExecuteApiEndpointis set, as AWS does. AppSync'sdnsmap hardcodedamazonaws.com, advertising names that resolve to real AWS and can never reach the emulator; it now carries the configured hostname.dns.REALTIMEdeliberately reports the same host asdns.GRAPHQL, because Overcast colocates the GraphQL and realtime endpoints andappsync-apiis the host that routes — anappsync-realtime-apiname would be one Overcast hands out but cannot serve. AppSync'surismap deliberately stays path-style: the host-routed form needs*.localhostto resolve, which it does not on Windows by default, and advertising a URL the caller may be unable to dial is worse in practice than the shape difference. All three services now mint through one shared helper, so a URL Overcast returns is by construction one its own router resolves back to that resource. -
S3 (bucket naming) — bucket-name validation now matches AWS's documented rules in both directions. Names AWS accepts were being rejected: any name containing a period (
example.com,www.example.comandmy.example.s3.bucketare AWS's own documented valid examples — periods are discouraged, because they break the*.s3.<region>.amazonaws.comwildcard certificate for virtual-hosted-style addressing over HTTPS, but they are legal), and any name with consecutive hyphens, which AWS permits — it forbids two adjacent periods, and its own reserved suffixes--ol-s3,--x-s3and--table-s3contain double hyphens. Rejecting a valid name is the worse direction, since it fails a stack locally that deploys fine against AWS. Names AWS rejects were being accepted: the reserved prefixesxn--,sthree-andamzn-s3-demo-, the reserved suffixes-s3alias,--ol-s3,.mrap,--x-s3and--table-s3, and names containing two adjacent periods. Because periods are now accepted, a bucket whose name carries a host-routed service label (execute-api,lambda-url,appsync-api) as a second-or-later dot segment is reachable path-style and as{bucket}.s3.{host}but not in the bare{bucket}.{host}form, where it parses as a service address;CreateBucketlogs a warning naming the label and both alternatives, and still creates the bucket, as AWS does. -
Networking (host-based addressing) — every Host-routed invoke URL on a hostname Overcast can actually resolve now reaches its service. API Gateway (REST v1 and HTTP v2), Lambda function URLs and AppSync GraphQL were all reachable only on an
.amazonaws.comHost, which does not resolve to the emulator without a hosts-file entry; onlocalhost,localhost.overcast.sh,localhost.localstack.cloudor a configuredOVERCAST_HOSTNAMEthey returned403 Missing Authentication Token. S3 virtual-hosted addressing and host-routed service dispatch were separate middlewares that both claimed the same request: the S3 matcher took everything in front of the base as a bucket name, soabc123.execute-api.us-east-1.localhostyielded a bucket calledabc123.execute-api.us-east-1which was prepended to the path before the service rewrite prepended its own prefix on top, and API Gateway then read the mangled first segment as a stage name. Lambda was the sharpest case, sinceCreateFunctionUrlConfigminted exactly such a URL and Overcast then refused to serve it. The two are now one decision with a fixed precedence derived from the host grammar rather than from middleware registration order, so a bucket and a service can never both claim a Host. Classification also became allocation-free and runs once instead of twice: a plain path-style request costs 31.9 ns and no heap allocations, down from 230 ns and four (Go 1.24.13, linux/amd64, AMD Ryzen 9 5900X, median of 3). -
Cognito (OIDC issuer) — the
issclaim in every minted JWT, and every endpoint in the OIDC discovery document, now come from the configured external origin instead of the Host header the authenticating caller happened to send.OVERCAST_HOSTNAMEwas ignored outright, so a token minted for a host CLI claimed an issuer a sibling container could not dial, and vice versa — and because an OIDC client validatesissand fetches signing keys from{iss}/.well-known/jwks.json, the mismatch failed token validation rather than degrading. The scheme was hardcodedhttpas well, so withOVERCAST_TLS_CERT/OVERCAST_TLS_KEYset the advertised issuer andjwks_uripointed at a scheme the server does not answer. Both now resolve through the same helper every other client-facing URL uses, which derives host, port and scheme together from config;authorization_endpoint,token_endpoint,userinfo_endpointandrevocation_endpointmoved with them. Token validation is unaffected — the pool ID is read from the issuer's trailing path segment, never compared against a recomputed string — so tokens issued before this change still verify. Separately, a client speaking Smithy RPC v2 CBOR was given a different issuer again: CBOR dispatches to Cognito's typed operation table, whose handlers receive a context rather than a request, and the issuer built there was a bare{region}/{poolId}with no scheme or host at all — unusable for OIDC key discovery and rejected outright by an API Gateway JWT authorizer, which compares the claim exactly. All twelve typed auth flows now resolve the same origin the JSON path does, so a caller's wire protocol no longer changes the issuer their token carries. -
Lambda / API Gateway / CloudFront (account IDs) — resources and invoke events now report the configured
OVERCAST_ACCOUNT_IDrather than the default account.requestContext.accountIdwas the literal000000000000in Lambda function-URL events and in all three API Gateway proxy-event shapes (REST v1, HTTP v2, and v2 routes on the v1 payload format), so customer handler code reading it — to compose an ARN, or to assert which account it is running in — saw the wrong account on any server started with a different one. CloudFront function and realtime-log-config ARNs were hardcoded the same way, which meant a CloudFront function and a CloudFront distribution created on the same server reported different accounts, since distribution ARNs already read config correctly. -
SMTP (mail capture) — the mock SMTP server no longer hangs on shutdown when a connection arrives while it is stopping. The accept loop's
breakon a cancelled context ended its enclosingselectrather than the loop, so the server closed the connection and then started a session handler for it anyway. That handler released a concurrency slot it had never acquired: with no other sessions in flight the release blocked forever,Servenever returned, and shutdown stalled until the caller's timeout fired; with sessions in flight it took a live session's slot instead, permanently lowering how many connections the server would go on to accept. Reaching it needed a connection accepted in the window between the context being cancelled and the listener closing, and that was roughly a coin flip per connection rather than a rare race, since Go chooses at random when bothselectcases are ready. -
Lambda (event source mappings) — shutting the service down no longer hangs waiting for ESM delivery goroutines that have no stop signal to observe.
StopAllcancelled only the per-mapping delivery contexts it had registered, but the delivery manager's base context wascontext.Background(), so theReloadAllgoroutine that resumes persisted mappings at startup — tracked by the very WaitGroup the shutdown drain waits on — had nothing that could cancel it; parked waiting for a persistent store to become ready, it held the drain open indefinitely. AReloadAllthat instead woke up mid-teardown could register a fresh SQS poller after the drain had begun, stranding a poller nothing would stop and adding to a WaitGroup already being waited on. Both windows only opened whileReloadAllwas still running at shutdown, so this surfaced as a rare, load-dependent stall rather than a reliable failure. The base context is now cancelled byStopAll, delivery started after shutdown begins is refused, and the drain is bounded by the caller's shutdown context — logging loudly instead of blocking forever, the waycmd/overcast's bounded store close already did. -
Router / REST APIs — modeled REST JSON and REST XML operations that no configured handler claims now return protocol-correct
501 NotImplementedinstead of falling through to S3. The generated path trie is consulted only after explicit routes, and SigV4's modeled service scope disambiguates AWS SDK/CLI/CDK traffic from legitimate S3 bucket and object paths. -
Router / AWS JSON API — modeled AWS JSON targets that no configured service dispatcher claims now return
501 NotImplementedwith the standard unsupported-operation marker and request ID, rather than the legacy400 UnknownOperationException; existing service dispatchers and legitimate S3 traffic retain precedence. -
Router / Smithy RPC v2 — modeled RPC v2 CBOR operations, including services that advertise CBOR alongside AWS JSON, now return a CBOR
501 NotImplementedwhen no configured dispatcher implements them instead of415 UnsupportedProtocol; a known disabled service returns503 ServiceDisabledbefore modeled-operation ownership is considered; RPC v2 JSON uses the same generated ownership path when AWS publishes modeled operations for it, while headerless S3 multipart requests whose bucket/key happens to match the RPC path grammar remain with S3. -
CloudFormation — a stack can now contain more than one resource of a type that its template does not name, and a failed update no longer destroys the resource it was replacing. Physical names for unnamed resources were built from the stack name alone (
{StackName}-Function), so every unnamed resource of a type in a stack got the same name and the second collided with the first —ResourceConflictException: Function already exist. Since CDK almost never names its resources, leaving CloudFormation to generate one, a stack with two Lambda functions could not deploy at all; the same fallback affected queues, tables, topics, buckets, roles, policies, instance profiles, secrets, log groups, log streams, SSM parameters, layer versions and event buses. Generated names now follow CloudFormation's own shape,{StackName}-{LogicalID}-{RANDOM}, truncated to each service's limit with the unique suffix preserved. Separately, replacing a resource deleted the old one before creating its replacement, so if the update then failed anywhere the original was already gone and rollback had nothing to restore — the update destroyed the resource outright. Replacement now creates first and defers deleting the original to a cleanup phase that runs only once the whole update succeeds, so a failure rolls back to the intact original and removes the replacement instead; this is what the random name component is for, since the two must coexist. An unnamed resource also keeps its generated name across an update rather than being renamed into a needless replacement, and the removal phase is now reported as AWS reports it —UPDATE_COMPLETE_CLEANUP_IN_PROGRESSandUPDATE_ROLLBACK_COMPLETE_CLEANUP_IN_PROGRESSon the way to the matching terminal status, so a stack held up by a resource that will not delete is visibly in cleanup rather than looking mid-update. Two further rollback defects went with it: a rollback rebuilt the stack's resource list from a map the update had been emptying as it went, so every resource handled before the failure was dropped and the stack came back believing it owned nothing while those resources still existed — the next update would then try to create them on top of themselves; and a replacement underUpdateReplacePolicy: Retainleft its replacement behind on rollback, so the retained original and the abandoned replacement both survived with nothing recording which one the stack owned. -
Web UI (container port mapping) — the console now works when Overcast's ports are remapped, e.g.
docker run -p 4580:4566 -p 4581:4567. The SPA is handed its API endpoint by the server, which built it from the port Overcast listens on; inside a container that is the internal port, so a console opened on the published UI port was told the API was onlocalhost:4566, which is closed on the host, and every request from it failed. Overcast now asks Docker for its own container's port bindings and gives the browser the published host port, falling back to the listen port when there is no answer to be had — not containerised, no Docker socket mounted, or the port not published — so native binaries and 1:1 mappings are unchanged. The/api/*proxy the console calls server-side is unaffected by the same change: an endpoint naming Overcast's own published port on a loopback host is mapped back to the internal port before being dialled, since that port exists only outside the container. This affected every non-default port mapping, including the port pairscripts/run-test-instance.shpicks. -
Router / Query API — unclaimed AWS Query GET and POST actions now return
501 NotImplementedwith the standard unsupported-operation marker and
request ID, rather than GET falling through to S3 or POST returning a bare
400; requests for a disabled Query service now reportServiceDisabled. -
ECS — task containers can now actually reach Overcast.
AWS_ENDPOINT_URLwas hardcoded tohost.docker.internal, a name Docker Desktop synthesises but native Linux does not resolve at all, so on Linux hosts every task's AWS calls failed outright; it now carries a real address — Overcast's own IP on the ECS network when it runs in a container, otherwise the host address on the interface carrying the default route (skipping link-local169.254.0.0/16addresses and virtual switches a container cannot route to), withhost.docker.internalkept only as a last resort and paired with ahost-gateway/etc/hostsentry so it resolves off Docker Desktop too. Separately, AWS SDKs resolve the SQS endpoint from theQueueUrlrather than fromAWS_ENDPOINT_URL, so a queue URL baked into a task definition by a host-sidecdk deploysent the task's SQS client to its own loopback; loopback origins on Overcast's port are now rewritten in task-definition andRunTask-override environment values, and split-horizon hostnames (localhost.overcast.sh,localhost.localstack.cloud,localhost.floci.io, plusOVERCAST_HOSTNAMEand the new comma-separatedOVERCAST_SPLIT_HORIZON_HOSTS) are mapped to Overcast in each task container's/etc/hosts, so one URL works from both the host and inside the task. -
Web UI — expanded sidebar rows are clickable across their whole height and width. The row element carried the padding and the hover highlight while the link inside it only filled the content box, leaving a 10px horizontal / 7px vertical ring that highlighted on hover but swallowed the click; every row in the expanded sidebar was affected — nav links, pinned favourites, and the expand/collapse toggle on services with sub-pages — and the drag grip on a pinned favourite no longer blocks clicks on the row's left edge. Row dimensions and styling are unchanged. An object's name in the S3 bucket listing is now a link that opens the same object inspector the row's eye action does, instead of being inert text. Object rows also no longer show a pointer cursor or the interactive hover tint: those advertised a whole-row click that did nothing but toggle a selection highlight nothing else read. Folder rows, which do navigate on click, keep both. Tables and scrollable panels now share one subtle scrollbar treatment across Firefox, Chromium, and Safari, with the system's own scrollbars preserved in forced-colors mode. On the Metrics & Health page, the Advisories and Storage Activity sections no longer drop back to their loading skeletons every three seconds while debug mode is off, shifting the page below them on every poll:
GET /_debug/metricsreturning "debug disabled" is now an answer the poll carries rather than a fetch error, which stops the query from being rewound to its loading state on each refresh. The health strip's Journal Mode also no longer reports "Debug mode required" when debug mode is on and the backend is memory-only — there is no SQLite journal mode to report there, and it now says so. On the Lambda function detail page, the Test, Versions, Monitor, Configuration and Triggers tabs no longer open flush against the tab bar: each panel rendered at a 0px offset, so bare headings and text sat directly on the rule and a narrower card read as jammed under it. They now use the same 16px offset the ECS, Cognito and VPC detail pages already did. The Code tab keeps its flush alignment, since its editor is a full-width bordered slab whose top edge continues the rule. Every copy-to-clipboard control in the console now works outside a secure context:navigator.clipboardis gated on one, so it is absent — not failing, absent — whenever the console is served over plain HTTP from anything other thanlocalhost, and all thirteen copy buttons read it directly. Onhttp://localhost.overcast.sh, the hostname Overcast now points containers and docs at, on a LAN address, or on a container hostname, clicking Copy did nothing at all and said nothing about it. Copies now fall back to a hidden textarea driven bydocument.execCommand("copy"), which has no secure-context requirement, and both outcomes are reported as toasts rather than only the happy one — several controls (EC2, ECS and RDS detail pages, the system map) previously gave no feedback either way, so a silent no-op was indistinguishable from success. Copy is now one component across the console, which also gave the icon-only buttons the accessible names they never had, and added a copy action to each row of the event stream that yields the whole event envelope as formatted JSON. Reading the clipboard has no such fallback —execCommand("paste")is refused everywhere and Firefox never exposesreadTextto page scripts — so Lambda's "Paste .env" control is now disabled with an explanation where it cannot work, instead of appearing live and doing nothing. -
CloudFormation / Lambda — functions declared with inline code (
Code.ZipFile) are now packaged into a real deployment archive, so they can actually be invoked. A template'sCode.ZipFileis source text while the Lambda API's is a base64 zip; the source was passed straight through, so every inline-code function created cleanly and then failed at invoke withbuild code tar: open zip: zip: not a valid zip file. AffectsAWS::Lambda::Functionon both stack create and update, and therefore any CDK construct that injects an inline handler — includingBucketNotificationsHandler, which CDK adds automatically to any stack whose bucket has notifications, so stacks that declared no Lambda of their own were affected too. The console also no longer presents its example "Hello from Lambda!" stub as a function's real code: unreadable packages are labelled as examples, and code stored unpackaged by earlier versions is now shown as-is. Existing broken functions are repaired by redeploying the stack. -
Lambda — changing a function's configuration now retires its warm execution environments immediately instead of leaving the old containers serving invocations until the 15-minute idle timeout. Previously only
UpdateFunctionCodeinvalidated the warm container, and only lazily — on the next invocation — so editing environment variables (in the web UI, via the AWS CLI/SDK, or through CloudFormation/CDK) appeared to have no effect: the next invoke was served by the container started with the old values. Now any update that changes what the container was built with — environment variables, memory, timeout, handler, runtime, layers, VPC config, image config, architecture, or code — destroys idle containers as soon as the update is stored, and lets containers that are mid-invocation finish before destroying them, matching AWS, which never interrupts an in-flight invocation. Updates that change nothing the container can observe, such as the description or role, keep the warm containers, so cosmetic edits cost no cold start. The same retirement applies to code refreshed reactively from S3.GET /_lambda/instancesand the system map now report one entry per execution environment. They were keyed by function name, so a function running five concurrent invocations reported a single instance; the web UI had been built for multiple all along but the data could never contain them. Two related leaks are fixed alongside: deleting a function stopped its containers but left phantom instances listed for 15 minutes, and a retired environment stayed listed as reusable until the idle sweeper ran. a warm container that disappears without Overcast asking — removed withdocker rm -f, OOM-killed, or lost to a Docker restart — is now dropped from the warm set as soon as the Docker event stream reports it. Previously the pool kept treating it as warm, so the next invocation was handed the dead container, submitted the event to a Runtime API nothing was polling, and hung for the function's entire configured timeout before failing (only the invocation after that cold started). Docker container-died events are now also subscribed reliably: the subscription raced Docker probing, and if probing won, exit detection was silently disabled for the process lifetime, meaning a container that crashed mid-invocation also hung until the timeout instead of failing immediately. -
SQS, Lambda — queue URLs are now minted per request, on the origin the caller reached Overcast on, so a host CLI and a sibling Lambda container each get a URL they can actually dial. This matters because AWS SDKs resolve the SQS endpoint from the
QueueUrlrather than from client configuration andAWS_ENDPOINT_URLdoes not override that (see the ECS entry above for the mechanism), so a single server-wide hostname could only ever satisfy one side: anSQSClientinside a Lambda function was sent to its own container by alocalhost:4566queue URL, while the documented Docker Compose setup handed host-side clients anovercast:4566URL that does not resolve on the host.OVERCAST_HOSTNAMEbecomes the fallback for callers without a usable origin rather than a server-wide override, requests arriving on a real AWS hostname still fall back to it, and queue URLs minted on any origin remain accepted since only the queue name is read from them. Lambda containers additionally get the same environment-rewriting and split-horizon/etc/hoststreatment as ECS tasks, so a queue URL baked into function environment by a host-sidecdk deployreaches Overcast too. That rewriting now also recognises URLs minted against the host port Overcast is published on, not only the port it listens on: a container started with remapped ports (docker run -p 4580:4566, asscripts/run-test-instance.shdoes) listens on 4566 while host callers mint queue URLs on 4580, so a baked-in URL was previously left untouched and the function's SQS client dialled its own loopback on a dead port. URLs a function fetches itself were never affected.
Release: https://github.com/Neaox/overcast/releases/tag/v0.0.1-alpha.26