Repository navigation
Add one app-config blob for each host - #1352
Open
jwrosewell wants to merge 29 commits into
Open
jwrosewell wants to merge 29 commits into
jwrosewell wants to merge 29 commits into
Conversation
A sandbox that serves several requests kept the one application it built first and handed it to every request after. That is right while a service reads one app-config blob. The commit after this one lets a service read a blob for each host, which needs an application for each, and an application built from one publisher's blob must never answer a request for another's. The sandbox's payload is now `RetainedApps`, which keeps each application under the key of the blob it was built from and finds one by that key and by nothing else. It retains only a successful build and counts every attempt, as the framework's one slot did. The framework hands its payload out by shared reference and offers no way to replace it, so the holder is made once and its list sits behind a `RefCell`. It keeps the eight applications served most recently, because all of them share the sandbox's one memory bound. The build count a sandbox reports, in the counters probe, the response headers and the retirement line, is the holder's. A test now holds the count a response carries to the builds made, which nothing did before. Nothing changes for a service that reads one blob. It builds one application under one key.
One Fastly service can serve several publishers, each from an application
config of its own. A `__KEY` selector that contains `{host}` is a
template. The entry point puts the request's host in the placeholder's
place, lowercased and without a port or a trailing dot, before anything
reads settings, so the debug probes, the application build and the
finalizers all read the blob of the publisher the request is for.
A host with no blob is answered `421 Misdirected Request`, and the answer
is not to be stored. It is never answered from the logical store ID or
from another host's blob, because a shared fallback is how a host nobody
configured comes to be served another publisher's site. Only letters,
digits, hyphens and dots make a host name, so a `Host` header cannot be
made into the logical store ID or the key of a blob's chunk, each of
which carries an underscore. A lookup that fails is answered `500`,
because a request is not served while it is unknown which blob is its
own.
The placeholder goes in the selector that already chooses the key, and
not in a switch of its own, so that a staged version stays apart from
production. A staged deploy copies a service's runtime entries into a
store of its own and points only the key selector at
`<logical-store-id>_staging`. A separate switch would be copied with the
rest, and a staged version would then read the production blobs. The
selector is replaced, so a staged version reads the one staging blob.
A selector without the placeholder is the key as it stands and nothing is
looked up, so a service that reads one blob is unchanged.
`scripts/config-by-host-local-test.sh` pushes an app-config blob under each of two hosts and one at the logical store ID, each with a stub origin of its own, and runs the adapter under Viceroy. Without the placeholder every host reads the one blob. With it each host is answered from its own origin, in any letter case and with a port or a trailing dot, and a host with no blob, the logical store ID sent as a host and a longer name that ends in a publisher's host are each answered 421 with no publisher's page. Each response carries the sandbox's counters. In the default mode the fourth response is the first request of its sandbox, which has built one application. In `reuse` mode, built with the reusable sandbox, it is the fourth request of its sandbox, which has built two. Two publishers then shared a sandbox and never an application, which is the fault a unit test cannot show because it sits in the entry point. CI runs both modes in the Fastly job.
The configuration guide's section on the Fastly runtime config store gains "One blob for each host", with the push for each host, the selector entry, a table of what each request reads and is answered, and what the publishers on one service share. The Fastly guide points to it, and the changelog records the addition.
The host selector refuses a label that starts or ends with a hyphen, with a test. The guide says each key is pushed in lower case and that an IPv6 literal host is refused. The thing a host selects is called an app-config blob throughout, including the refusal log line the harness greps. Comments that restated a test, the guide or another comment are cut.
This was referenced Oct 15, 2026
…hat are not UTF-8
The selection and the module request read Host with get_header_str, which panics on a value that is not UTF-8. Both now read it with get_header and take it as text only when its bytes are valid UTF-8, so such a Host counts as no host. A selector that carries {host} then answers 421 with no lookup, and a module is shown no host. A selector without {host} no longer reads Host at all.
…asking the store
A Fastly config store key holds at most 255 characters, counted as EdgeZero counts them. A host may be 253 characters, so a selector such as {host}_v2 could build a key the store cannot hold, and the lookup then failed with KeyTooLong and answered 500. Such a key is now never looked up and counts as no blob, so the request is answered 421.
…linked as trusted_server_config
EdgeZero v0.0.9 removed the edgezero_runtime_env store that held the per-service __KEY selector, so a service that keeps a blob for each host would read the one blob at trusted_server_config for every host. The entry point now reads the selector from the TS__CONFIG__KEY entry of the app-config store on every request, beside the blobs it names and the sandbox bounds, with try_get so a failed lookup cannot panic. Without the entry the key is the logical store ID, as on main.
A selector that is empty, carries whitespace, a control character or {host} more than once, carries a brace outside its one {host}, or is longer than 255 characters is malformed and answered 500, never read as one key for every host. A store that cannot be opened or read is answered 500. The comments of main that said no selector reaches the Fastly runtime now say where it is read.
…-local EdgeZero v0.0.9 refuses any Fastly config key other than the logical store ID, for a local push as much as a remote one, so a blob for each host could no longer be pushed. ts config push --adapter fastly with another --key now goes to a writer of its own. It validates the blob as ts config validate does, wraps it in EdgeZero's envelope, splits a blob over 8,000 bytes into EdgeZero's chunk format with the chunks before the pointer, checks every key against 255 characters before writing anything, and writes the local server's store in the fastly.toml the manifest names. --dry-run lists the keys and writes nothing. Without --local the push is refused with a message that says so, because writing a live store needs the Fastly CLI runner that arrives with the signing key commands. --staging with --key is still refused when the arguments are parsed. A push of the logical store ID still goes through EdgeZero.
…ocal harness
The guide's section on one blob for each host writes the selector into TS__CONFIG__KEY in the store linked as trusted_server_config, pushes each host's blob with --local, lists the 421 and 500 answers and what makes a selector malformed, and points a service that kept its selector in edgezero_runtime_env at the upgrade checklist. The CLI guide and main's runtime config section say a selector entry may name another key.
The harness writes the selector into the local trusted_server_config store and starts Viceroy for each value, covering no entry, {host}, {host}_v2 beside a blob at the logical store ID, a malformed {Host}, and an old service-scoped selector in an edgezero_runtime_env store, which is not read. In reuse mode the sandbox bounds sit unscoped beside the blobs, and the harness uses main's trusted_server_secrets store.
The design now tests entry_request directly, with entry_request_reads_a_host_that_is_not_utf8_as_no_host in place of a test through the device module, and relies on the existing config_push_parses_staging_and_rejects_explicit_key for --staging with --key, so the extra parse test is removed. Core changes only in the comment on default_config_key, so the comment on default_config_store_name is main's again.
This was referenced Oct 17, 2026
# Conflicts: # scripts/README.md
This branch has not been deployed
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.
Summary
Changes
crates/trusted-server-adapter-fastly/srcscripts/config-by-host-local-test.shcrates/trusted-server-cli/srccrates/trusted-server-cli/testsdocs/guide.github/workflowsCargo.lockcrates/trusted-server-cli/Cargo.tomlCHANGELOG.mdcrates/trusted-server-core/srcscripts/README.mdCloses
Closes #1270
Spec
Spec: config-by-host, fastly-per-host-selector
Test plan
CI on this pull request at 08c3339 passed all 32 checks.
cargo test-fastly && cargo test-axumcargo clippy-fastly && cargo clippy-axumcargo fmt --all -- --checkcd crates/trusted-server-js/lib && npx vitest runcd crates/trusted-server-js/lib && npm run formatcd docs && npm run formatcargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1fastly compute serveChecklist
unwrap()in production code (clippy deniesunwrap_usedin CI)logmacros as AGENTS.md asks (notprintln!)Stacks on page changes as middleware (#1351).
One Fastly service can serve several publishers, each from an app-config blob of its own. The selector, the entry
TS__CONFIG__KEYin the config store linked astrusted_server_config, names a blob for each host when it contains{host}, and each request reads the blob stored under the host it names.What this pull request does
ts config pushwrites a key other thantrusted_server_configonly with--local, into the local store infastly.toml. The entries it writes are copied into a live store withfastly config-store-entry update, a long blob's chunks before its pointer.The entry point puts the request's host in the placeholder's place before anything reads settings, so the debug probes, the application build and the finalizers all read the blob of the publisher the request is for. Every request reads the selector from the config store before anything else, the counters and JA4 probes included, and with
{host}set it also asks the store whether the host's blob exists. Only/healthis answered first. Each key is pushed in lower case, because the host is lowercased and--keystores the key as typed.www.publisher-a.examplewww.publisher-a.exampleWWW.Publisher-A.example:443orwww.publisher-a.example.www.publisher-a.examplewww.publisher-b.examplewww.publisher-b.example421 Misdirected Request, not to be storedHostthat is not a host name, an IPv6 literal, not UTF-8, or noHost421 Misdirected Request421 Misdirected Request500/health, for any host200, before any config is read500A selector without the placeholder is the key as it stands and nothing is looked up, and without the entry every request reads
trusted_server_config, as on Tech Lab main. A selector is malformed when it is empty, carries whitespace, a control character or{host}more than once, carries a brace outside its{host}(such as{Host}), or is longer than 255 characters.What keeps one publisher's settings from answering for another
421Hostheader is written to name another key in the store, such as the logical store ID, the selector or a chunk of a blobtrusted_server_config, so a staging store of its own keeps staging's blobs apart, with a selector of its own such as{host}_staging500and not servedThe reusable sandbox
Sandboxwaslifecycle::Sandbox<RetainedApp>, one application for the life of the sandbox. It islifecycle::Sandbox<RetainedApps>, which keeps each application under its blob's key.Sandbox<T>holds one value, builds it only when empty and hands it out by shared reference, with no way to replace it. So the holder is made once and its list sits behind aRefCell. One callback runs to completion before the next begins and no borrow is held across a build.421once the store no longer holds it, even while a sandbox still keeps its application, because the store is asked on every request.What the publishers on one service share
What breaks
Nothing for a service that reads one blob. A service that wrote its selector as the service-scoped
__KEYentry ofedgezero_runtime_envwrites the same value intoTS__CONFIG__KEYbefore it deploys a build that reads that entry, as the guide's upgrade notes say, and removes any blob attrusted_server_configfrom a store that holds a blob for each host.Why
A Fastly service on Tech Lab main reads one app-config blob, at the logical store ID. An operator who runs Trusted Server for several publishers therefore runs a service for each, with its own domains, stores, deploys and rollbacks, although the code is the same and only the settings differ.
Two things stood between that and one service reading a blob for each host.
The selector is the entry
TS__CONFIG__KEYbeside the sandbox bounds, which Tech Lab main already reads from the same store, because EdgeZero v0.0.9 removed the runtime store that held a service's__KEYentry and refuses a deploy whose key names anything other than the logical store ID. It is read withtry_get, so a store that cannot be read is answered500and never panics, and a malformed selector is answered500and never read as one key for every host.Closes #1270.