Skip to content

Run request filters and the device module on all adapters - #1410

Open
jwrosewell wants to merge 27 commits into
feature/51-config-migratefrom
feature/52-request-filters
Open

jwrosewell wants to merge 27 commits into
feature/51-config-migratefrom
feature/52-request-filters

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-adapter-spin/src 3 files, 1426 lines added, 275 removed
crates/trusted-server-adapter-cloudflare/src 3 files, 1388 lines added, 159 removed
crates/trusted-server-adapter-axum/src 3 files, 1318 lines added, 153 removed
docs/superpowers 3 files, 195 lines added, 59 removed
docs/guide 9 files, 43 lines added, 33 removed
crates/middleware/51degrees 1 file, 8 lines added, 9 removed
CHANGELOG.md 1 file, 2 lines added, 1 removed

Closes

Part of #777

Spec

Spec: request-filters-on-every-adapter

Test plan

CI on this pull request at 18e8765 passed all 27 checks.

  • cargo test-fastly && cargo test-axum
  • cargo clippy-fastly && cargo clippy-axum
  • cargo fmt --all -- --check
  • JS tests: cd crates/trusted-server-js/lib && npx vitest run
  • JS format: cd crates/trusted-server-js/lib && npm run format
  • Docs format: cd docs && npm run format
  • WASM build: cargo build --package trusted-server-adapter-fastly --release --target wasm32-wasip1
  • Manual testing via fastly compute serve
  • Other: the full workflow set of this repository, run on this pull request at 18e8765

Checklist

  • Changes follow AGENTS.md conventions
  • No unwrap() in production code (clippy denies unwrap_used in CI)
  • Uses log macros as AGENTS.md asks (not println!)
  • New code has tests
  • No secrets or credentials committed

Stacks on ts config migrate (#1409).

What this pull request does

The design is request-filters-on-every-adapter. The modules' request filters and the device module ran on the Fastly adapter alone. The Axum, Cloudflare and Spin adapters now run them in the order Fastly does: the request preparers, robots.txt, the device module, the Edge Cookie context with the device result on it, the request filters, then the route. A filter's answer stands in for the page or the named route, a filter's request header reaches the route, and its response header is written last, after the finalize headers, without making a private response less private. robots.txt, the inspection endpoints and the attestation pages are still answered without the filters. So the 51Degrees client hints middleware and DataDome run on every adapter.

  • On Spin, discovery, /verify-signature, the first-party routes and the page-bids preflight now run the modules' preparers, which they skipped before.
  • On every route in the cycle an Edge Cookie context that cannot be read answers an error before the filters run. A client's own Cookie value that is not UTF-8 is dropped whole by a preparer every adapter links, the Edge Cookie and consent cookies in it included, which is new on the Spin routes above.
  • A device module is shown the request's method, host, path and query, the client's IP address, its User-Agent and its cookies, and on these adapters it classifies every request the fallback proxies, subresources included.
  • [device] module = "platform" builds the built-in module on these adapters, because they supply no TLS or HTTP/2 signals.
  • These adapters' HTTP clients apply no timeout_ms to a filter's outbound call, and the design and the 51Degrees guide say what bounds the call on each.

How it was verified

Locally on Windows at 076f4ad: cargo test-axum (1,368), cargo test-cloudflare (72) and cargo test-spin (106) passed, with clippy refusing warnings on the three adapters and cargo fmt --all -- --check. On each of the three adapters, four tests fail when one of the guards that keep a filter's response headers from making a private response less private is taken away, and one test sends all 13 named route and method pairs in the cycle to a filter that answers, so taking build_ec_context or run_pre_route_filters out of the adapter's execute_named fails a test. Each new adapter test fails with its change reverted. At f778984 the cross-adapter parity tests (17) and the Cloudflare and Spin WebAssembly checks passed.

…e device module

Each of the three adapters now runs the request cycle the Fastly adapter
runs. After the modules' preparers, a named route other than the
inspection endpoints, and the publisher fallback, classify the device with
the module [device] selects (derive_device_signals), read the Edge Cookie
context with that classification (build_ec_context), and run the
integration request filters (run_pre_route_filters) before the route. A
filter that answers stands in for the route, its request headers reach
the route, and its response headers are left on the response for
FinalizeResponseMiddleware, which writes them after the finalize headers
and then restores the privacy guards (apply_terminal_response_effects).
robots.txt is answered once the preparers have run and before the Edge
Cookie steps and the filters, with an empty Edge Cookie context.

Axum's named routes and fallback go through execute_named and
execute_fallback where they went through execute_handler. Cloudflare and
Spin register the same paths and methods as before, each handled by
named_route_handler, and the fallback by execute_fallback. On Spin the
discovery, signature verification and first-party routes and the
page-bids preflight now run the preparers, which they skipped. These
adapters supply no TLS or HTTP/2 signals, so [device] module = "platform"
builds the built-in module there.

src/app/request_cycle_tests.rs in each adapter checks, through the router
its composition root builds and a stub origin, that a filter's answer
stands in for the page and the origin is not asked, that a filter's
request header reaches a module route and its response header is written
after the operator's, that the selected device module's classification
reaches a module route's call, and that robots.txt is answered with a
filter that answers everything registered. cargo test-axum (1,357 passed),
cargo test-cloudflare (61) and cargo test-spin (95) pass, and so do 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.
…ry adapter

docs/superpowers/specs/2026-10-17-request-filters-on-every-adapter-design.md
sets out what the Fastly adapter runs that the Axum, Cloudflare and Spin
adapters do not, the request cycle each of them now runs and the routes it
covers, how device classification there differs from the Fastly entry
point, the timeouts their HTTP clients give a filter's call, and what a
deployment sees change.
…pter

The changelog gains a line under Changed, and the 51Degrees client hints
line no longer says only the Fastly adapter runs request filters. The
51Degrees, DataDome, permission model, configuration and attestation
guides no longer limit the request filters, the order robots.txt is
answered in or the order the attestation endpoint is answered in to
Fastly, the 51Degrees guide gives what the other three HTTP clients wait
for, and the Axum, Cloudflare and Spin guides say the filters and the
device module run there and what platform classifies from.
@jwrosewell
jwrosewell added this pull request to stack #1408 October 18, 2026 01:31
…and Spin

Four tests on each adapter fail when one of the guards that
apply_terminal_response_effects runs after a filter's response headers is
taken away: a response core marks private stays no-store, private, a
filter's Set-Cookie keeps the response from shared caches, a shared cache
header a filter adds to a private response is removed, and the validators
an operator writes on a response core marks private are removed.

Four more show that platform classifies with the built-in module, that an
Edge Cookie context that cannot be read answers before the filters while a
client's unreadable Cookie header is rebuilt by a linked preparer first,
that robots.txt shows the device module none of a crawler's cookies, and
that the inspection endpoints and the attestation pages skip the filters.
robots.txt is answered with an empty Edge Cookie context, so its Edge
Cookie and consent are not read and no device module is shown its cookies,
but the publisher path still parses its Cookie header, as for every request.
…n every adapter

Every adapter now classifies a request with the device module [device]
selects, where a module was asked only for bid request attributes off
Fastly. DataDome's timeout_ms and the 51Degrees timeout_ms apply on the
Fastly adapter only, DataDome checks /auction and both page-bids paths, the
client tag suppression holds on every adapter, and a request filter's
response header is written after X-Robots-Tag. The middleware readme no
longer says only the Fastly adapter runs request filters.
The table of parts, the builder, the timeout_ms setting, consent, failure,
rollout, section 3 and the tests no longer say only the Fastly adapter runs
request filters, and a table gives what bounds the call on each adapter.
The review log moves after the revision record, as in the other designs.
It says what a selected device module did on Axum, Cloudflare and Spin
before, what DataDome's filter sends and sets with no permission gating
it, why no time limit is added to a filter's call, that the Fastly adapter
answers all three inspection endpoints before the preparers, which routes
stay out of the cycle, which of a crawler's cookies robots.txt reads, when
the Cookie check is reached, the configuration, the new tests, the open
questions, and ends with its review log.
… Axum, Cloudflare and Spin

A new test on each adapter sends every named route the cycle runs, being
discovery, /verify-signature, /auction, GET and OPTIONS on both page-bids
paths and the four first-party paths, to a filter that answers every
request, and asserts that each is the filter's answer with its effect
header. The spoiling preparer now also spoils the discovery route, so the
context test shows the preparers and the Edge Cookie context run for a
named route as well as for a page.

Taking run_pre_route_filters out of any adapter's execute_named now fails
the new test alone, and taking build_ec_context out of it, or on Spin the
preparers, fails the context test alone. The context test's comment says
the linked preparer drops a Cookie header value it cannot read, rather
than rebuilding the header from the values it can.
build_ec_context answers with an error for a Cookie header that
HeaderValue::to_str refuses, being one holding any byte outside visible
ASCII, an accented letter in UTF-8 among them, and not only one that is
not valid UTF-8, as the comment said on Axum, Cloudflare and Spin.
A device module a registration offers is shown the client's IP address as
well as the User-Agent and the cookies, in a module context that carries
the request's method, scheme, host, path and query.
Section 2.11 lists apply_terminal_response_effects on all four adapters,
at the Fastly entry point and in the other adapters'
FinalizeResponseMiddleware, and section 1 no longer calls the Fastly
adapter the only one that calls detect.
…sts skip

On Axum a service that cannot be reached fails within the client's 5
second connection limit and a refused connection fails at once, so only
a service that accepts the connection and then stalls holds the call for
up to 30 seconds. No alias runs the two 51Degrees crates' tests for the
WebAssembly target of any adapter, not only the Fastly adapter's, because
cargo test-cloudflare and cargo test-spin test the adapter crate alone.
The review of the change that has every adapter run the filter counts as
this design's third round, found nothing blocking and was answered in
e7417c8.
It lists the test that sends every named route in the cycle through the
filters and the context test's discovery request, names the tests each
removal fails, names the IP address and the request's address a device
module is shown and the subresources it classifies, says the diagnostics
preparer drops a Cookie header value whole, names the two DataDome designs
whose scope it lifts, says what the guards do with a platform's own cache
header with an open question on it, gives a true reason no shipped filter
is tested through an adapter, and logs the round.
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@jwrosewell jwrosewell closed this Oct 18, 2026
@jwrosewell jwrosewell reopened this Oct 18, 2026
@jwrosewell
jwrosewell added this pull request to stack #1418 October 19, 2026 12:18
@jwrosewell
jwrosewell removed this pull request from stack #1418 October 19, 2026 16:30
@jwrosewell jwrosewell closed this Oct 19, 2026
@jwrosewell jwrosewell reopened this Oct 19, 2026
@jwrosewell
jwrosewell added this pull request to stack #1421 October 19, 2026 17:38
@jwrosewell
jwrosewell removed this pull request from stack #1421 October 19, 2026 18:29
@jwrosewell
jwrosewell added this pull request to stack #1422 October 19, 2026 19:20
@jwrosewell jwrosewell changed the title Request filters and the device module run on every adapter Run request filters and the device module on all adapters Oct 19, 2026
@jwrosewell
jwrosewell marked this pull request as ready for review October 19, 2026 20:24
@jwrosewell jwrosewell mentioned this pull request Oct 20, 2026
13 of 14 tasks

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant