Repository navigation
Serve asset routes on every adapter - #1412
Open
jwrosewell wants to merge 27 commits into
Open
jwrosewell wants to merge 27 commits into
jwrosewell wants to merge 27 commits into
Conversation
Each adapter's dispatch_fallback now checks a GET or HEAD against [[proxy.asset_routes]] where the Fastly adapter does, once robots.txt, the Edge Cookie context, the request filters, tsjs and the module routes have had their turn and before the publisher's origin. dispatch_asset_fallback follows the Fastly function of that name: the registry's source for the route, handle_asset_proxy_request, which locates the asset, signs the request with the route's signer and answers from a held copy before it asks the origin, the response's cache policy in its extensions and the request filters' effects, and no Edge Cookie state. FinalizeResponseMiddleware applies the cache policy after the finalize headers with the adapter's own shared cache header, before the Edge Cookie and the filters' effects. None of the three HTTP clients streams, so the asset body is held whole. The Cloudflare and Spin clients already refuse a request carrying Image Optimizer metadata, which only an asset route sets, and the Axum client now refuses it too where it sent the request unchanged. The Cloudflare client's note no longer says asset routes are not routed to it. src/app/request_cycle_tests.rs in each adapter checks, with a stub origin and an asset route whose source signs with the stock s3_sigv4 signer, that a request under the route reaches the route's origin with an AWS4-HMAC-SHA256 signature and that one outside it reaches the publisher's origin unsigned. The Axum platform tests check that a request with Image Optimizer metadata is refused while its origin would have answered. cargo test-axum (1,365 passed), cargo test-cloudflare (68) and cargo test-spin (102) pass, and so do the cross-adapter parity tests (17), the Cloudflare wasm32 check, the Spin native and wasm32-wasip1 checks and its release build, clippy with warnings denied on the three adapters and cargo fmt.
docs/superpowers/specs/2026-10-17-asset-routes-on-every-adapter-design.md sets out how the Fastly adapter serves [[proxy.asset_routes]], the dispatch and finalize steps the Axum, Cloudflare and Spin adapters now run, where each holds the route's answers, that their bodies are held whole, and that a route enabling the Image Optimizer answers with an error there.
…pter The changelog gains a line under Changed. The route table of the API reference lists the asset routes as served on every adapter, the asset routes guide says what the Axum, Cloudflare and Spin adapters do with the body and the Image Optimizer, and their own guides say the routes are served there.
jwrosewell
added this pull request to stack #1408
October 18, 2026 01:31
Spin sends the address's own host as the Host of every request, because WASI HTTP takes the authority from the address and forbids a host header, so the Spin HTTP client dropped the Host that core writes for a source's host_header. A route whose source names another host therefore reached its origin with the wrong Host, and a signed one with a signature made for the Host it named. The publisher's own source has the same fault. NoopBackend::ensure now refuses a backend asked for a Host other than the address's own host, with its port where that is not the scheme's default, so core answers the request with 502 before anything is sent, the signing HEAD of a route that uses the Image Optimizer included. The backend is the one the adapter's own services use, and it is pub(crate) so the request cycle tests can send requests through it. The platform tests cover another name, another port and the same host written with its default port or in other letter case. Through that backend, a request under a route whose source names another host answers 502 with no-store, private while a route without one reaches its origin, and a page request to a publisher source that names another host answers 502 with its origin never asked. With the refusal disabled, a_backend_asked_to_send_another_host_is_refused fails at its expect_err and both request cycle tests fail, because their requests reach the origins and answer 200, while a_backend_asked_to_send_the_address_s_own_host_is_named still passes.
The PlatformHttpRequest contract asks a client that cannot honor Image Optimizer metadata to refuse the request rather than send it untransformed. The Axum client refused it on send, which asset routes use, but send_async built and sent its own request with no check. Both paths now call refuse_image_optimizer_metadata first. a_request_with_image_optimizer_metadata_is_refused sends through send and send_async to an origin that would answer. With the send_async check taken out the test fails at its send_async assertion, while send still refuses.
…Image Optimizer The HTTP clients of those adapters refuse Image Optimizer metadata, so a route that enables the Image Optimizer answers each request it would optimize with 502, and nothing at startup said so. Each adapter's state builder now logs one warning naming the prefix of every such route, once for each instance, as the identity graph warning does. The route is still served rather than refused, because its SVG paths, its debug requests and, where it signs, its origin's own errors are answered, and a refusal would stop every route and page of a deployment that shares its settings with a Fastly service whose routes optimize images. startup_warns_when_an_asset_route_enables_the_image_optimizer in each adapter's request cycle tests builds the router with such a route and checks that the warning was logged. With the call taken out of the state builder the test fails on each adapter.
…ength The comment copied from the Fastly adapter said that a HEAD answer keeps the origin's Content-Length. The Cloudflare client holds every response whole and writes the length of the body it read, which for a HEAD is 0, and the comment now says so.
…dge Cookie Nothing proved, on the Axum, Cloudflare or Spin adapter, that FinalizeResponseMiddleware applies an asset response's cache policy, that a failed asset request is marked no-store, private, or that an asset response carries no Edge Cookie. Each adapter's request cycle tests now check, with a stub origin, that a [[cache.asset_rules]] rule is written after the finalize headers in the adapter's own shared cache header, s-maxage on Axum and Spin and cloudflare-cdn-cache-control on Cloudflare, that an origin that fails answers 502 with no-store, private, and that a browser's page navigation under the route gets no Edge Cookie where its permissions would grant one on a page. Taking the cache policy step out of FinalizeResponseMiddleware fails the cache rule and error tests on each adapter, the rule then giving public, max-age=60 with no shared lifetime and the error no Cache-Control, and the Spin route refusal test too, which also checks no-store, private. Taking NoStorePrivate out of the error arm fails the error test on each adapter, and giving the asset response the publisher path's Edge Cookie state writes a ts-ec cookie and fails the Edge Cookie test on each adapter. With the fixes of this round, cargo test-axum (1,371 passed, 1 ignored), cargo test-cloudflare (74 passed, 1 ignored), cargo test-spin (112 passed, 1 ignored) and the cross-adapter parity tests (17) pass, and so do clippy-axum, clippy-cloudflare, clippy-cloudflare-wasm, clippy-spin-native and clippy-spin-wasm with warnings denied, and cargo fmt --all -- --check.
…oudflare and Spin The asset routes guide gives the body limits on Cloudflare and Spin, the 502 with no-store, private that a route enabling the Image Optimizer answers there, the startup warning, the HEAD a signed route sends first, the SVG and debug requests that pass, and what Spin and Cloudflare do with a source's host_header. The Axum, Cloudflare and Spin guides say the same for their adapter, and the Cloudflare guide gives the reason as the adapter not translating the metadata into Cloudflare's own image options, where it said Workers has none. The API reference no longer calls the route Fastly-only and names the 502 answers, and the configuration guide says Spin cannot send host_header and no longer calls an asset route Fastly's. The changelog entry says all of this, that an asset response neither writes nor expires the Edge Cookie, and that on Spin a request to a source whose host_header names another host, a route's or the publisher's, now answers 502.
The origin signing design said asset routes and their preflight run on the Fastly adapter alone, and now says every adapter runs them, with Spin refusing a source whose host_header names another host before it is signed. The asset cache design said an origin fetch has 15-second timeouts, and now says core asks for them and only the Fastly adapter applies them, the Axum client applying its own limits and the Cloudflare and Spin adapters none of their own.
The design names its pull request and states Spin's refusal of a source's host_header and what Cloudflare does with one, the startup warning for a route that enables the Image Optimizer and why it is a warning, the HEAD a signed route sends and the SVG and debug requests that pass, the Axum client's refusal on send_async, the startup refusal of a source that cannot be built, the per-request failures with their statuses, the body limits and timeouts, where answers are held, and that an asset response neither writes nor expires the Edge Cookie. It lists the new tests and how each was shown to fail, leaves the Cloudflare host_header and HEAD length as open questions, and ends with a review log entry for round 1.
EdgeZero's run_app installs no logger on Workers or Spin, because its init_logger does nothing on either at v0.0.9, and neither adapter installed one, so the log crate dropped every line the two adapters wrote: the warnings they log as they build their state, a startup error and the report behind each error answer, such as Spin's refusal of a source whose host_header names another host. Each entry point now calls init_logger before run_app. The Worker's logger writes each line through the worker crate's console macros by level, console.error, console.warn, console.log for info and console.debug below that, and the Spin component's writes each line to standard error. A line gives the level and the module before the message, both write info and above, and a second call in an instance finds the logger installed and changes nothing, because the entry point runs for every request. Each adapter's Hooks::owns_logging now answers true, so run_app does not try to install a logger of its own. The unit tests cover the console method for each level, the line and the level filter, and tests/logging.rs in each adapter, a binary of its own because a logger is global to a process, calls init_logger twice and checks that one logger is installed at info and above.
…y where Cloudflare and Spin log On Spin a request to a source whose host_header names another host answers 502 before its origin is asked, the publisher's source as well as an asset route's, so a Spin deployment whose publisher source sets one answers every page with 502. The changelog said so at the end of the asset routes entry, and now gives it a Breaking entry of its own with the check to make before upgrading. A Fixed entry says the Cloudflare and Spin adapters now install a logger and where each writes its lines. The Cloudflare and Spin guides gain a Logging section. The Worker's lines go to the console by level, which wrangler dev prints and wrangler tail, the dashboard's live logs and, where the Wrangler file enables observability, Workers Logs show. The component's go to standard error, which spin up prints and keeps in .spin/logs beside a local manifest. Both guides say the Image Optimizer warning is logged once for each instance, and the Spin guide that Spin can serve several requests from one instance, to a degree it does not guarantee, so there the warning appears each time Spin starts an instance. The asset routes and configuration guides say Spin refuses a host_header that names another host, where they said any host_header, because the backend accepts the address's own host, and the API reference's publisher fallback contract gives the Spin 502.
The contributing guide asks a spec to end with its review log, and both designs had it before the revision record. The two sections change places and their text is unchanged.
The summary, the problem and the first rollout item now say that the Spin refusal of a host_header naming another host reaches the publisher path as well as the asset routes, so a Spin deployment whose publisher source sets one answers every page with 502, a breaking change. A Logging paragraph says the Cloudflare and Spin adapters install a logger of their own and where each writes its lines, and Configuration says how often each adapter logs the Image Optimizer warning: once for the Axum process, once for each Workers instance, and once for each instance of the Spin component, which Spin can reuse for several requests to a degree it does not guarantee. It also says why such a route gets a warning where Cloudflare and Spin refuse an auction of more than one provider at startup, which is that such an auction could run nothing there. The design also states the reader headers a route's origin receives, the rise in key-value traffic after the upgrade, the signed source whose host_header names the address's own host in other letter case, the error lines each adapter now writes, what the warning test reads, which Spin client code the native tests reach, the logger tests and how each was shown to fail, and what is not in this change. The round 1 entry of the review log names the public branch and the round 1 revision by its commit, and a round 2 entry follows.
# Conflicts: # CHANGELOG.md # crates/trusted-server-adapter-axum/src/app.rs # crates/trusted-server-adapter-axum/src/app/request_cycle_tests.rs # crates/trusted-server-adapter-cloudflare/src/app.rs # crates/trusted-server-adapter-cloudflare/src/app/request_cycle_tests.rs # crates/trusted-server-adapter-spin/src/app.rs # crates/trusted-server-adapter-spin/src/app/request_cycle_tests.rs
# Conflicts: # docs/superpowers/specs/2026-10-15-asset-cache-design.md
jwrosewell
added this pull request to stack #1418
October 19, 2026 12:18
jwrosewell
removed this pull request from stack #1418
October 19, 2026 16:30
jwrosewell
added this pull request to stack #1421
October 19, 2026 17:38
jwrosewell
removed this pull request from stack #1421
October 19, 2026 18:29
jwrosewell
added this pull request to stack #1422
October 19, 2026 19:20
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:24
This was referenced Oct 20, 2026
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-spin/srccrates/trusted-server-adapter-cloudflare/srccrates/trusted-server-adapter-axum/srcdocs/superpowersdocs/guidecrates/trusted-server-adapter-spin/testscrates/trusted-server-adapter-cloudflare/testsCHANGELOG.mdCloses
Part of #777
Spec
Spec: asset-routes-on-every-adapter
Test plan
CI on this pull request at 1dde36e passed all 27 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 the Edge Cookie on every adapter (#1411).
What this pull request does
The design is asset-routes-on-every-adapter.
[[proxy.asset_routes]]were dispatched on the Fastly adapter alone, so on the Axum, Cloudflare and Spin adapters a request under a route went to the publisher's origin without the route's signing. Those adapters now dispatch asset routes as Fastly does, with the route's source module, its signer, its held answers and its cache policy, the request filters' effects kept and no Edge Cookie state.host_headernames another host answers502before its origin is asked, the publisher's[source.origin]as well as an asset route's source, because Spin can only send the host in the address. Before, the request reached its origin with that host in place of the onehost_headernamed. The changelog gives the check to make before upgrading.wrangler tailand Spin's console and.spin/logs, where before every line was lost.sendandsend_async, as the Cloudflare and Spin clients already did, where it sent the request unchanged.How it was verified
Locally on Windows at f429b64:
cargo test-cloudflare(79) andcargo test-spin(116) passed, with clippy refusing warnings on both adapters natively and for WebAssembly, andcargo fmt --all -- --check. At 76986f0cargo test-axum(1,371) and the parity tests (17) passed. A request under a route reaches the route's origin with an AWS4-HMAC-SHA256 signature from the stocks3_sigv4signer, one outside it reaches the publisher's origin unsigned, and on each adapter the route's cache rule, the502that no cache keeps and the missing Edge Cookie are tested. Each logger is installed once at Info and above, and each new test fails with its change reverted. With every row below merged in, this row passedcargo fmt --all -- --check, clippy on the four adapters natively and for WebAssembly, core's 2,989 tests,cargo test-axum,cargo test-cloudflare(90),cargo test-spin(127), the parity tests (17), the Fastly adapter tests under Viceroy (300) and the CLI's library suite (706) at 19d15b2.