Skip to content

Call a geo module with the request - #1374

Open
jwrosewell wants to merge 51 commits into
feature/32-cache-featuresfrom
feature/33-geo-call
Open

jwrosewell wants to merge 51 commits into
feature/32-cache-featuresfrom
feature/33-geo-call

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/headers/geo 3 files, 652 lines added, 0 removed
crates/trusted-server-core/src 15 files, 588 lines added, 296 removed
docs/superpowers 1 file, 325 lines added, 0 removed
crates/trusted-server-adapter-fastly/src 5 files, 204 lines added, 535 removed
crates/trusted-server-adapter-spin/tests 1 file, 66 lines added, 13 removed
docs/guide 6 files, 58 lines added, 21 removed
crates/trusted-server-adapter-axum/tests 2 files, 45 lines added, 8 removed
crates/trusted-server-adapter-spin/src 3 files, 43 lines added, 76 removed
crates/trusted-server-adapter-cloudflare/tests 1 file, 42 lines added, 8 removed
crates/trusted-server-adapter-axum/src 2 files, 40 lines added, 40 removed
crates/trusted-server-integration-tests/tests 3 files, 40 lines added, 34 removed
crates/cmp/didomi 1 file, 22 lines added, 2 removed
crates/testing/seam-probe 1 file, 21 lines added, 5 removed
crates/trusted-server-adapter-cloudflare/src 1 file, 18 lines added, 74 removed
Cargo.lock 1 file, 16 lines added, 0 removed
trusted-server.example.toml 1 file, 7 lines added, 0 removed
.cargo/config.toml 1 file, 6 lines added, 6 removed
crates/trusted-server-cli/src 1 file, 6 lines added, 3 removed
crates/trusted-server-modules/src 1 file, 5 lines added, 0 removed
CHANGELOG.md 1 file, 3 lines added, 0 removed
crates/auction/prebid-server 1 file, 3 lines added, 1 removed
crates/loading-cache/51degrees 1 file, 3 lines added, 1 removed
Cargo.toml 1 file, 2 lines added, 0 removed
AGENTS.md 1 file, 1 line added, 0 removed
crates/trusted-server-modules/Cargo.toml 1 file, 1 line added, 0 removed

Closes

Closes #1321

Spec

Spec: geo-module-request

Test plan

CI on this pull request at 79a0421 passed all 29 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 79a0421

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

On top of a module declares the caches it loads through (#1373). Issue: #1321.

What this adds

A geo module is a GeoModule, called with a module call, the type a device module is called with, as locate(call), so it sees the request's evidence, the settings and the services, and a module whose one service answers location and device from the same request can answer both from one call. The evidence is wider than a device module's detect is shown, because five of the six callers pass every header of the request they serve. A module that resolves from the client address alone keeps implementing PlatformGeo and is offered through PlatformGeoModule::new(name, module), which hands the platform lookup the address the call carries and asks nothing when the call carries no services.

RuntimeServices::geo and IntegrationRegistration::with_geo_module take an Arc<dyn GeoModule>, build_geo_module returns one (platform wraps the host's lookup, anything else the disabled one), and the registry resolves [geo] module to a GeoModule. Core builds the call in one place, locate_request on dyn GeoModule, from the request's view, its headers, the settings and the services. The Edge Cookie path, the auction endpoint, the Didomi module and the seam probe call it with the request's headers. The Fastly adapter writes the x-geo-* headers on every response in two steps. The one in the router's middleware chain keeps the request's headers as the request arrived and calls the module the deployment selects, being the registry's module when [geo] module names one, and otherwise the host's own lookup or the disabled one, as build_geo_module chooses. The entry-point step, for a response that did not pass through the middleware chain, calls the host's own lookup or the disabled one with no headers, because the entry point keeps only the request's view.

#[async_trait::async_trait(?Send)]
impl GeoModule for OneService {
    fn id(&self) -> &'static str { "geo.example" }
    async fn locate(&self, call: ModuleCall<'_>) -> Result<Option<GeoInfo>, Report<PlatformError>> {
        let evidence = <&dyn RequestInfo>::from_module_context(&call)?;
        self.answer_for(evidence).await
    }
}

Why

A geo module was called as lookup(client_ip, services), with the address and nothing else of the request, while a device module is called with the request's evidence. A vendor whose one service answers location and device from the same request could not make the geo answer from the same call as the device answer, so either the two were asked separately or the geo answer depended on the device module having run first on the same adapter. The adapters resolved geo in four places with the request in hand and handed the module only the address.

What breaks

Crates outside core that passed a PlatformGeo to with_geo_module or RuntimeServices::geo wrap it in PlatformGeoModule::new(name, module), or implement GeoModule to see the request. The Didomi module's geo call is rewritten to go through locate_request, and reads the resolved GeoInfo as before. Nothing in a deployment's documents changes. On Fastly, a deployment whose [geo] module names a module from the registry now gets that module's location in the x-geo-* headers of every response the router's middleware finishes, where those responses carried x-geo-info-available: false before. The headers carry the module's city, coordinates, metro code and region as well as its country, and no permission guards them. Responses finished by the entry-point step are unchanged. The middleware also copies the request's method, headers and URI before the handler runs, on every request.

The commits

Commit What it does
d66b7ce A geo module is called with the request
dbaa36b The ts CLI's local services call the disabled geo as a module
9800980 Core builds the geo call in one helper
376746a The Fastly adapter resolves a response's geo headers with the module the deployment selects
c5b04c2 The geo module request design
c8bcb8b The geo module request design takes its round 1 review
01e76c1 The carried tests' services are handed a geo module

The other 13 commits merge the branch below into this one, and through it Tech Lab main at 6ef07ae.

What is not in this pull request

  • A geo module in the stock list that reads more than the address. The host platforms' lookups stay PlatformGeo and are wrapped. Fastly's resolves the client address, Cloudflare's reads the platform's cf-* request headers, and Axum's and Spin's resolve nothing.

jwrosewell and others added 14 commits October 13, 2026 03:48
locate_request on dyn GeoModule builds the call every geo call carries, the request as evidence from its headers and the client address, the settings when the caller has them, the services and the client, and the Edge Cookie path, the auction endpoint, the Didomi module and the seam probe call it. The Fastly adapter's two finalize paths call it without headers and say why: the module there is the host's own lookup or the disabled one, chosen by build_geo_module, which reads the address alone, so the middleware no longer clones the headers for it.
…the deployment selects

The finalize step that writes the x-geo-* headers on every response built
its geo module with build_geo_module alone, which answers the host's own
lookup for "platform" and the disabled module for every other name,
because it cannot see the registry. A deployment whose [geo] module names
a module from the registry therefore got no location on that path, and
every page carried x-geo-info-available: false, while the same module
answered the request path.

The adapter now hands the finalize step the registry's module when the
selector names one, through finalize_geo_module, and the host's own lookup
or the disabled module otherwise, as before. The step keeps the request's
headers before the handler takes the request and hands them to the module
as its evidence, so a module that reads the request, as a device module
does, is called the way the request path calls it, and the answer it holds
for the same evidence from earlier in the request is the one it gives. The
note on locate_request no longer says the finalize paths never call a
registry module.

A test proves the finalize step hands a geo module the request's headers,
with a module that records the user agent of the evidence it is called
with.
Describes how a geo module is called with the request's module call, as a device module is, in the series that adds GeoModule, PlatformGeoModule and locate_request. Covers each adapter, the [geo] settings, what request data a geo module now sees, failure handling, rollout for crates outside core, and the tests, with the joins that have none.

Written after the code, for draft pull request #1374, which answers issue #1321.
@jwrosewell
jwrosewell added this pull request to stack #1401 October 15, 2026 17:56
The page no longer says a geo module is called as a device module is.
On Fastly a device module's detect is shown the client address, the
User-Agent header and, for a registered module, the Cookie header, where
five of the six geo callers hand the module every request header.

The page now states that the Fastly router path keeps the request before
the request preparers run, so its call can differ from the request
path's call, and that the 401 rule stops only the second call. It also
adds the services graph of the finalize steps, the response headers that
no permission guards and the wait for the module, which the review found
missing. The code changes the review points to are listed as open
questions.

The changelog entry names the builder types and the registry items it
left out, and the integration guide's geo paragraph no longer splits the
paragraph about module names.
@jwrosewell
jwrosewell removed this pull request from stack #1401 October 15, 2026 20:51
The page now says why the Fastly entry-point step skips the registry, which responses carry the Edge Cookie state, what the Fastly device module reads and the type of the registration field, and names the permissions endpoint for the other three adapters. The changelog, the integration guide and four stale comments are listed under the open questions with their files and lines.
…erve middleware

Core and the adapters stop writing the x-geo-* headers and stop calling a
geo module when they finish a response. The serve middleware headers.geo,
in crates/headers/geo, writes the headers from the location the request
already resolved, on the responses a [[serve]] entry naming it covers.
The design also lists the other changes a finalize step makes to a
response, so each can move to a middleware later.
…eware

Core and the four adapters no longer write any x-geo-* header, including
x-geo-info-available, and the Fastly adapter no longer asks a geo module a
second time when it finishes a response. The module headers.geo, in
crates/headers/geo, supplies a serve middleware that writes the headers
from the location the request already resolved, which its preparation
takes from the module call, so the headers appear only on the responses a
[[serve]] entry naming it covers. A document that selects the module and
names it in no [[serve]] entry is refused.

GeoInfo::set_response_headers and the HEADER_X_GEO_* constants move from
core to the crate. The Fastly services builder that only the entry point's
device module now uses is renamed for that use, and the request copy and
the 401 rule that existed for the finalize lookup are removed with it.
The changelog marks as breaking that no response carries an x-geo-*
header until a deployment selects headers.geo and names it in a [[serve]]
entry. The configuration guide's geo section shows the two lines, the
Fastly guide's client address check says where the headers appear, and
AGENTS.md lists the headers type folder.
This branch hands the services a geo module, which is called with the
request, where they held a platform geo lookup. The helper that builds the
services for the three tests the demo Edge Cookie module fix brought up
from split/4-client-resolve still handed it NoopGeo, so core's tests
stopped compiling here. It now wraps NoopGeo in a PlatformGeoModule named
"noop", as the noop services in test_support do.
@jwrosewell jwrosewell changed the title A geo module is called with the request, as a device module is A geo module is called with the request Oct 17, 2026
@jwrosewell
jwrosewell added this pull request to stack #1408 October 17, 2026 09:05
`[geo] module = "platform"` selects the host's own geo lookup. The Axum
dev server's lookup and Spin's resolve nothing, so on those two adapters
the selection located no request and still counted as a geo module,
which let a deployment running an Edge Cookie module skip the
`assume_single_jurisdiction` acknowledgement that a deployment with no
geo module must make.

Each of the two adapters now refuses the selection at startup, naming
the setting and saying the host has no geo lookup. Services a caller
supplies carry a lookup of their own for `platform` to select, as the
Next.js auction fixture's do, so the check applies only to the services
the adapter builds. A test on each adapter checks the refusal.

The integration tests' shared fixture selects `platform` for Viceroy,
so the Axum dev server they start now has `[geo] module` unset and
single-jurisdiction operation acknowledged, which locates no request as
its own lookup did.

The configuration guide says what `platform` selects on each adapter,
and with the Cloudflare guide that Cloudflare places a visitor in a
region only from the `cf-region-code` request header. The architecture
table, the Spin guide, the changelog and the geo design record the
refusal.
# Conflicts:
#	docs/guide/cloudflare.md
@jwrosewell jwrosewell closed this Oct 18, 2026
@jwrosewell jwrosewell reopened this Oct 18, 2026
@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 changed the title A geo module is called with the request Call a geo module with the request Oct 19, 2026
@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 marked this pull request as ready for review October 19, 2026 20:23

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.

A geo module is asked with the client address alone, so it cannot share what a device module learned from the same request

2 participants