Repository navigation
Address requests by path before choosing settings - #1376
Open
jwrosewell wants to merge 44 commits into
Open
jwrosewell wants to merge 44 commits into
jwrosewell wants to merge 44 commits into
Conversation
…on is chosen
Core offers RequestAddressing, asked about each request before the adapter
chooses its app-config blob and before routing. It answers either that the
request is addressed by its host, as before, or three things: the selector
values that take the place of {host} in the __KEY selector, the path the
request continues with, and the path prefix it removed. ByHostAddressing is
the default, so an adapter given no hook serves every request as before.
The Fastly adapter takes the hook through run_with_addressing beside the
integration builders. It reads the first selector value whose blob exists,
keyed and checked as a host is, and answers 421 when none has one, never
from a key the hook did not name. The request continues at the given path
with its query kept and carries the prefix as a RemovedPathPrefix
extension. A selector without {host} ignores the values.
The Axum, Cloudflare and Spin adapters take the hook through
TrustedServerApp::routes_with_addressing, which wraps the router they build
with core's addressed_router. They read one blob, so the values are ignored
and the path and prefix apply.
Logs never print the path or the values, and the config store lookup error
no longer carries the key. Putting the prefix back into responses is the
deployment's job, through middleware. The design is in
docs/superpowers/specs/2026-10-13-request-addressing-design.md.
…apart A refusal logs how many values the hook named, not how many were tried. Publishers on one host are kept from sharing a stored page by the origin and the settings digest in the stored template's key, and share the host's cookies unless the deployment's prefix module names them apart. The hook is asked about /health wherever an adapter routes it.
# Conflicts: # crates/trusted-server-core/src/lib.rs
…to it Purging an addressed page also purges the page at the same continuing path of every other publisher on the host, and publishers on one host share its cookies unless the prefix module names them apart. The design page's rules list gave way to a pointer to the guide and the rustdoc that state them.
FirstSegment, NamesSelectorValues and status_and_type live in the test_support addressing module behind test-utils, and the tests use ByHostAddressing in place of a hook of their own that answers by host.
…gives The probe a request asks for is read by debug_probe, which tests drive through the addressing. The two blob choices log a refusal through one helper, and the rustdoc of run_with_addressing says only what a caller needs.
…ack and its limits Review round 1 found that putting the prefix back with a module's request state leaves the page unstored and sent private, no-store, and that publishers on one host share the consent cookies, the sharedId cookie and the forwarded Cookie header. The design now says both, gives the host each adapter hands the hook, the answers on failure, the rollout order and the two paths with no test, and records the review round.
Review round 2 found four small points, none blocking. The cost of
putting the removed path prefix back no longer says every such page is
fetched from the origin for each reader, because the platform's cache
can still answer the origin request unless origin readthrough is on or
the ad stack runs. The rollout order, which rests on the 421 for a
missing blob, is limited to a Fastly service whose selector carries
{host}. The round 1 entry of the review log says the copy of the design
on split/6-integration-seam is brought up to date in the specs pull
request. The removed path prefix is named in full wherever the text
means it, and the prefix module is left as it was. The review log and
the revision record carry round 2.
address_request handed the hook the request's Host read with get_header_str, which panics on a value whose bytes are not valid UTF-8, and a client chooses those bytes. It now reads Host through request_host, as the selection by host and both module requests do, so the hook is handed None for such a value, and every read of Host in the adapter goes through the one function that cannot panic. Tests: a_hook_is_handed_no_host_for_a_value_that_is_not_utf8 records the host a hook is handed for a Host of the bytes b"\xffa.example" and checks it is None and that the request, addressed by host, is refused 421. a_request_addressed_by_its_path_with_a_host_that_is_not_utf8_is_served_without_a_panic addresses a request for /publisher.example/news/today with such a Host by the selector value current-kid, which the local jwks_store holds, and checks the request is served from that key and that the module request and the kept request read no host.
A selector value whose key would be longer than the 255 characters a
Fastly config store entry takes is passed over without a lookup, as a
value that is not a host name is, and the search goes on to the next
value. The rule came with the merge of the config by host change, which
never looks up a host key that long, into the search over selector values.
Test: an_addressed_value_whose_key_is_too_long_is_passed_over names the
longest host name, whose key under {host}_v2 is 256 characters, and then
a.example, and checks a.example_v2 is read and is the only key the store
is asked about. The guide says the selector where it said the __KEY
selector, and states the rule.
This was referenced Oct 17, 2026
`RequestInfo::from_request` read the host from `Forwarded`, then `X-Forwarded-Host`, then `Host`, and gave an empty host when none of them was sent. An HTTP/2 client may send its `:authority` and no `Host` header, and core runs no middleware for a request that names no host, so such a request's page went back unmodified, with no URL rewriting, middleware, injected script or auction, and only a warning said so. The host now comes last from the request URI's authority, read as `address_request` and the attestation page's address already read it, so its port is kept as a `Host` header would carry it and each header still takes precedence. The tests read the host from the authority, with its port, when no header names one, find no host when nothing names one, show each of `Host`, `X-Forwarded-Host` and `Forwarded` winning over the authority, and send a request with no `Host` header through the publisher path to find the entry's middleware run for the host the URI names. The changelog and the request addressing design record the change. The fault was reported in #1118.
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:23
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-core/srccrates/trusted-server-adapter-fastly/srcdocs/superpowersdocs/guidecrates/trusted-server-adapter-cloudflare/testscrates/trusted-server-adapter-spin/testscrates/trusted-server-adapter-axum/testscrates/trusted-server-adapter-axum/srccrates/trusted-server-adapter-cloudflare/srccrates/trusted-server-adapter-spin/srcCHANGELOG.mdCloses
Closes #1328
Spec
Spec: request-addressing
Test plan
CI on this pull request at c61a4f4 passed all 29 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!)On top of the publisher's assets held through the loading cache (#1375). Issue: #1328.
What this adds
What changes.
One host may serve several publishers, each under a path of its own and from its own app-config blob. Core offers one extension point,
RequestAddressing, which the adapter asks about each request before it chooses the blob and before it routes. The hook reads the request's host and path and answers either that the request is addressed by its host, as every request is today, or three things and nothing else: the selector values that take the place of{host}in the__KEYselector, the path the request continues with, and the prefix it removed.A deployment hands its hook to
trusted_server_adapter_fastly::run_with_addressingbeside its integration builders, or toTrustedServerApp::routes_with_addressingon Axum, Cloudflare and Spin.run_withandroutes_with_registrationsare unchanged, so no existing caller changes, and an adapter given no hook usesByHostAddressing, which serves every request as before. On a Fastly service whose__KEYselector is{host}, a hook that takes the first path segment as the publisher gives these results.https://front.example/publisher-a.example/news?p=2publisher-a.example/news?p=2/publisher-a.examplehttps://front.example/publisher-b.example/publisher-b.example//publisher-b.examplehttps://front.example/unknown.example/news421 Misdirected Request, not to be storedhttps://front.example/not_a_host/news421 Misdirected Requesthttps://front.example/healthon Fastly200, before the hook is askedHow it works.
{host}in turn and is keyed as a host is, lowercased with a port and a trailing dot removed. A value that is not a host name is passed over without a lookup, so a value cannot name the logical store ID or a part of a stored blob. A miss on every value is answered421, as a host with no blob is, and never from a key the hook did not name, which includes the blob of the host the request arrived on. A lookup that fails refuses the request500and no later value is tried.RemovedPathPrefixextension. Core never writes it into a response, and a deployment whose pages must carry it puts it back with a serve phase middleware of its own.addressed_routerwraps the router they build, applying the path and the prefix for every path and method the inner router registers./healthfirst, then asks the hook, then serves the debug probes, so the probes read the blob and the path the request is addressed to, which a test drives through the hook.Why
A Fastly service reads the app-config blob its
__KEYselector names, and a selector that carries{host}names a blob for each host, so every publisher needs a host of its own. A middleware cannot change that, because a middleware is named by settings, so it runs after a blob has been chosen and after the request has been routed. Only a step the adapter takes before both can choose the blob and change the path the router sees, so the extension point is in core, and the policy that uses it, such as which part of a path names a publisher, is the deployment's.What breaks
Nothing. A deployment that supplies no hook is served exactly as before, and the existing entry points are unchanged.
The commits
The other commits merge the branch below into this one, and through it Tech Lab main at 6ef07ae.
What is not in this pull request
ts cache purge --pagewith that address also purges the page at the same path of every other publisher on the host./healthunder a prefix on Fastly, which is addressed like any other path. The probe at the root is answered before the hook is asked.The design page is
docs/superpowers/specs/2026-10-13-request-addressing-design.md.