Skip to content

Let a middleware prepare with the request's services and refuse a request - #1311

Closed
Jamesr51d wants to merge 581 commits into
IABTechLab:mainfrom
jwrosewell:split/23-prepare
Closed

Jamesr51d wants to merge 581 commits into
IABTechLab:mainfrom
jwrosewell:split/23-prepare

Conversation

@Jamesr51d

@Jamesr51d Jamesr51d commented Oct 12, 2026 •

Copy link
Copy Markdown

Stacks on making a file the origin does not have (#1309). The additions that follow the seven pull requests open here are listed in #777.

Compare split/22-creating with split/23-prepare

to see only this pull request's change.

What this pull request does

A middleware whose decision needs the request's services, or has to wait on the store or the network, answers Middleware::prepare with a future. The request's handler runs it before the document is processed, as the middleware's module with the permissions it declares, and what it answers is kept under the middleware's name and phase for create in that phase to take.

fn prepare<'a>(&'a self, call: ModuleCall<'a>, context: &'a MiddlewareContext<'a>) -> Option<Preparation<'a>> {

    Some(Box::pin(async move {

        let lines = self.lines_for(call, context.request_host).await?;

        Ok(Box::new(lines) as Box<dyn Any + Send>)

    }))

}



fn create(&self, context: &MiddlewareContext<'_>) -> MiddlewareAction {

    let lines: Vec<String> = context.take_prepared(MODULE).unwrap_or_default();

    MiddlewareAction { stream: Some(Box::new(AppendLines::new(lines))), ..MiddlewareAction::pass() }

}

A preparation may answer a Refusal instead, with a status, an optional Retry-After in whole seconds rounded up, and a message, and the request is answered with that status, no document is processed, and nothing is stored or served from the store.

Why

create is synchronous, is given no services, and is asked inside the synchronous streaming paths that the Fastly adapter needs. A middleware that makes a well known file from what the store holds or a service answers, as robots.txt does, could not decide there, and could not fail closed when it had nothing to give, which for robots.txt matters because an empty file tells crawlers to crawl everything. The preparation runs where the handler is asynchronous on every adapter, and only its answer, a Send value, travels to the streaming paths, so create, the plan and every streaming path are as they were.

The rules

  • A middleware that prepares nothing is asked create as before, and nothing of it changes.

  • On a document fetched for the request that will not be stored, both phases are prepared, fetch then serve. On a document that may be stored, only the fetch phase is prepared, so the serve middleware of the reader it was fetched for find nothing prepared. On a copy read from the store, the serve phase alone. The preparations of a phase run in the order the entry names the middleware.

  • The first refusal ends the preparations of a document, and the response it makes is marked private and unstorable.

  • A fetch preparation of a document that may be stored is handed the request's services and nothing of the reader, neither its permissions, location nor Edge Cookie, because its answer is stored with the document.

What breaks

MiddlewareContext, HtmlProcessorConfig and ReaderDocument each gain a required pub field prepared, so every struct literal of them outside this repository stops compiling.

How it was verified

CI passed on 1c0c244f7 on the fork. Not exercised: no preparation has run against a real store or network, because the fixtures answer from what they are called with. The preparation on a copy read from the store has no test, because the test services have no template cache.

References #1281.

An integration that leaves core still bounds a request body and an upstream
response, registers a backend, strips the headers an upstream must not set
on a first-party response, matches a host against the allowed domains, marks
a synthesized response private, reads a list setting and rewrites a bare
host. Those were private to the crate because every caller lived in it.
They are public, each with the reason on it.

The platform test doubles are offered under the `test-utils` feature, where
the adapters already take core's other fixtures, so a moved integration's
tests keep the stub HTTP client and stores they were written against.
The feature turns on the platform library's feature of the same name,
because the doubles are built on that library's own.
The Lockr identity module is `identity.lockr`, and it now lives in
`crates/identity/lockr`. The stock list offers it in the place its hooks
ran before, and the settings that select and configure it do not change.

Three of core's tests used Lockr as their example of a built-in module.
The duplicate id test and the standalone script test use core's own
modules, the JavaScript asset proxy and the creative browser module. The
test that the documented settings template's module tables validate moves
to the stock list crate, where each module's settings type is in reach,
and the two template helpers it shares with core's own tests are offered
from core's test support.
The Permutive audience module is `audience.permutive`, and it now lives in
`crates/audience/permutive`. The stock list offers it in the place its
hooks ran before, and the settings that select and configure it do not
change.
The Sourcepoint consent management module is `cmp.sourcepoint`, and it now
lives in `crates/cmp/sourcepoint`. The stock list offers it in the place
its hooks ran before, and the settings that select and configure it do not
change.

Its tests build a registry with the crate's own builder, through
`IntegrationRegistry::with_registrations`, which is offered under the
`test-utils` feature for that. The test that a selected consent mirror's
browser script loads with the page moves here from core, as Osano's half
of it did.
The Didomi consent management module is `cmp.didomi`, and it now lives in
`crates/cmp/didomi`. The stock list offers it in the place its hooks ran
before, and the settings that select and configure it do not change.

Core's test that a module's table survives the configuration blob round
trip used Didomi's settings as its example. It uses an example module of
its own, with a flag set away from its default.
The Google Tag Manager module is `tag.google-tag-manager`, and it now
lives in `crates/tag/google-tag-manager`. The stock list offers it in the
place its hooks ran before, and the settings that select and configure it
do not change.

One of its tests runs the HTML pipeline over the publisher page fixture
core's own HTML tests use. Core's test support offers that page, so the
half megabyte file stays in one place.
The Testlight example integration is `testing.testlight`, and it now lives
in `crates/testing/testlight`. The stock list offers it in the place its
hooks ran before, and the settings that select and configure it do not
change.

It reads the Edge Cookie a request carries through
`edge_cookie::recognized_ec_id`, so that module is public. The test that
serves a page through the HTML pipeline and checks Testlight's script is
replaced moves with it. The registry test that lists the modules a
deployment does supply names a probe builder of its own.
The Next.js module is `framework.nextjs`, and it now lives in
`crates/framework/nextjs`, with its six source files and two fixtures. The
stock list offers it in the place its hooks ran before, and the settings
that select and configure it do not change.

Five of core's page pipeline tests used Next.js as their example of an
integration that rewrites script payloads in two passes, a script rewriter
that leaves placeholders and a stream processor that fills them. They test
core's part of that (the document state, the hold on an unresolved group,
the auction seam, the template cache), so they stay in core and select a
stand-in that does the same two passes with no vendor's format. The
stand-in is compiled for core's tests only. The test of Next.js's own
overflow handling moves to its crate, and the registry test that used
Next.js as an integration with no browser script uses a probe.

Two of Google Tag Manager's tests check that it leaves another module's
script rewrite alone, with Next.js as that module, so its crate takes the
Next.js crate as a test dependency. The stock list crate gains the test
that every stock module refuses a setting it does not know, which core's
own list can no longer show for a module that has left it.
The Google Publisher Tags module is `ad-tag.google`, and it now lives in
`crates/ad-tag/google` with the bootstrap script it injects. The stock list
offers it, and the settings that select and configure it do not change. Its
diagnostics module, `ad-tag.google.diagnostics`, belongs to the same crate
and stays in core until the page path can carry a module's own decision for
a request.

Five of core's tests used this module as their example of an integration
that tags a page: the HTML processor's test of the attribute an injector
asks for on the publisher bundle tag, the streaming test of the head
arriving before the origin finishes, and the JavaScript asset proxy's three
tests of its place ahead of an integration's own rewriter. They select a
second stand-in compiled for core's tests only, which inserts a head
script, asks for a bundle attribute and rewrites the address of its own
script. The core test that a selected module with invalid settings fails
startup uses an example module of its own.

The browser tests and the initial render harness read the bootstrap script
from its new place.
Two modules decide something about one request and act on it later, and the
page path named both. It called the Google Publisher Tags diagnostics module
to prepare the request and to finish the response, carried that module's
decision to the HTML processor, which wrote its two scripts, and carried a
flag for DataDome's tag beside it. Neither module can leave core while core
does that for it.

A request preparer or a request filter now leaves a value in the request's
`IntegrationRequestState` under its integration id. The page path copies
those values into the document's state before parsing starts, so the
module's own head injector reads them, and hands them to the response
finalizer a builder declares with `with_response_finalizer`. A head injector
gets a second place to write, straight after the script bundle, which is
where the diagnostics module loads its standalone script. The diagnostics
module and DataDome both use these and core calls neither.

A request that carries any value keeps to the origin path, so its document
is never read from a shared template and never stored as one, and its HTML
response is sent `private, no-store`. That was true of both modules before,
each by its own route. It is now one rule that holds for any module, checked
where the request is routed and again where the document is built.

The registry runs the request preparers once for a request, so the page
path asks it to prepare a request that reached it unprepared and names no
module to do it.

The token an auction publishes with its winning bids was made when the
diagnostics module ran. A builder now declares with `with_auction_token`
that its browser script reads the token, the auction plan records whether a
selected module does, and the page path asks the orchestrator.

Core's tests of all this use a third stand-in compiled for its tests only.
It marks a document navigation that carries its query, writes a head script
and its standalone module's tag for a marked request, and sets a cookie on
the response. The template cache tests that set a diagnostics decision by
hand, the template rendering tests and the auction token tests use it. The
tests that drive either module itself still do, and hand the page path the
request state where they handed it a decision or a flag.
The diagnostics module is `ad-tag.google.diagnostics`, the second module of
the crate at `crates/ad-tag/google`, and it now lives there beside the
Google Publisher Tags module. The stock list offers it last, where its hooks
ran before, and the settings that select it do not change. It prepares the
request, writes its two scripts and finishes the response through the hooks
any module has, so core calls nothing of its own.

Its tests went with it, and four were added for what it now does through
those hooks: that only a decision which acts is left for the page path,
that the finalizer acts only on such a decision, and that a document gets
both scripts in order when diagnostics is active and neither when it is
not. The test that the built-in list attached its request preparer is now a
test that the stock list does.

Core's tests that used the module as their example use core's stand-in: the
HTML processor's test of what is written around the bundle, the registry's
two tests of a standalone browser module, and the page path's tests of a
query a module acts on, a warm template, an inactive ad stack and a
standalone module's own path. The Fastly adapter's test that a late filter
cannot make such a response public marks the response the way a module's
finalizer does and names no module.

One of core's template cache tests depended on this module without saying
so. The module's preparer writes the Cookie header of every request out
again as it strips its own cookie, dropping a field it cannot read, and the
test expected a request with such a field to be stored. The stand-in now
reserves a cookie and does the same, so the test still shows that the cache
classifies a request as a preparer left it. A stock build behaves as
before, because the stock list still runs the module's preparer for every
request.

The page path's two request classifiers the module uses, for a prefetch and
for a crawler, are public. The changelog's list of moved modules had been
garbled by the full stop in Next.js and reads correctly again.
A setting that holds a secret holds the name of its key in a pushed
configuration and the secret once the settings load. Core named DataDome's
two such settings in three places: the list of secret paths, the step that
clears a reference nothing uses before the lookup, and the deploy check that
a reference in use names a key. A module with a secret could not leave core
while core listed its settings by name.

A builder now declares them with `with_secret_settings`, each as a path
inside the module's own table and a rule for when the table as written puts
it to use. Core does the three things from the builders it is given, for
any module. DataDome declares its server-side key as in use when protection
is on, and its test bypass credential when the bypass is on as well, which
are the rules core applied for it.

The settings load takes the declarations from the builders it already
validates against, so a module a deployment added has its secrets looked up
as a stock one does. A leaf listed twice is looked up once, because the
fixed list and the load can both name a module core itself offers, and a
second lookup would read the secret as the name of a key.

A module's table sits under the name its section selects it by, which may
be the short name or the full one. The paths are listed and cleared at both,
where the code this replaces looked only at `bot-protection.datadome`.
DataDome is the module `bot-protection.datadome`, and it now lives in
`crates/bot-protection/datadome` with its Protection API filter and its
protection scope. The stock list offers it in the place its hooks ran, and
the settings that select and configure it do not change. It leaves its tag
suppression for one request, and declares its two secret settings, through
the hooks any module has.

Its 59 tests went with it and eight were added from what core tested for it:
that a suppressed document keeps and rewrites the publisher's own tag, that
an ordinary one gets the client configuration, that the settings load looks
up both secrets when both are in use and clears each one that is not, the
deploy check of an invalid test bypass, and the refusal of a removed
setting. The stock list crate checks that a registered list puts DataDome's
two settings among the leaves a push treats as naming a key.

Core's tests that used DataDome use core's stand-in, or the neutral marker
they were about. The stand-in gains a request filter that leaves its mark
for a header, so core still tests state a filter leaves reaching the
document, which is the route DataDome takes. The scheduling test's origin
document gains a head. It asserted that DataDome's tag was absent from a
document that had no head for any tag to be written into, so it could not
fail, and it now asserts that what a module left on the request is written.
The tests of a personalized response say personalized where they said
suppressed, and no longer set DataDome's marker beside the neutral one.
Core's configuration tests use a module that exists only in the test.

Core's settings no longer remove DataDome's two deprecated secret store
selectors from its table as they are read. DataDome accepts and discards
both, as it did, so a configuration that carries one still loads, and a
pushed configuration now keeps them.
`ts audit` turns the integrations it finds on a page into the section that
selects each one's module and the name written there. It took each module's
name from `trusted_server_core::integrations`, so it stopped compiling when
the first module left core, and no local check showed that because the `ts`
tool cannot be built on the machine this branch was made on.

The stock list crate now answers the question by integration id from the
builders it already lists, with a test of every id the generator knows, and
the generator asks it. The generator names no module of a vendor's. Prebid
is the one exception, because it is registered through the auction plan and
no builder carries its name, and it stays in core for now.
Prebid and APS each register a page integration that depends on the
compiled auction plan, so the registry called both by name before it ran
any builder, recorded Prebid's id by hand, and listed Prebid's module name
among the ones a section may select. Deploy validation called Prebid's two
checks by name for the same reason. Neither module can leave core while
core does that.

A builder now registers from the plan with `with_plan_registration` and
checks its settings against the plan with `with_plan_validator`. A
registration from the plan is made whether or not a section selects the
module, because the function decides from the plan and the settings whether
the module runs, and its hooks go ahead of every module a section selects,
which is where Prebid's and APS's ran. Prebid is a builder like any other,
selected in `[auction]`, and APS's implementation builder carries its
registration. The registry and the deploy check name neither.

The page path read the identifier a renderer picks its bid by with APS's
type tag and APS's payload key. The code that builds a renderer descriptor
now states which payload key holds that identifier, with `picking_bid_by`,
and the page path asks the descriptor. A descriptor built without the
statement has no such identifier whatever its payload holds, so a key of the
same name under another renderer's tag is still not read, which a test
pins. The statement is no part of what is sent to a page.
The demonstration ad server is the implementation `ad-server.mock`, and it
now lives in `crates/ad-server/mock`. The stock list offers it, so
`[ad-server] module = "mock"` selects it as before and its settings do not
change. Its 22 tests went with it.

Core's tests of the ad server seam selected the mock because it was the one
ad server there was. They select a stand-in compiled for core's tests only,
`ad-server.fixture`, which reads an endpoint and a timeout, refuses any
other setting and answers every request with no bid. That covers the plan
compiler's tests of how a name resolves within `[ad-server]` and how a
table's settings are checked, the registry's refusal of an auction
implementation named in a section, and three tests that built a mock
provider only because they needed a provider.

One orchestrator test ran APS through the mock ad server and checked that
the winner kept its seat, its bidder name and its renderer. It is a test of
the two together, so it is in the stock list crate, where both are on
offer, and it drives the orchestrator through its public interface.

The bid dimension helpers the mock ad server admits a bid with are public,
so an auction implementation in a crate of its own applies the rules core's
response reader applies.
The Amazon Publisher Services demand implementation is `auction.aps`, and
it now lives in `crates/auction/aps`. The stock list offers it, so a
`[demand.<name>]` table with `implementation = "auction.aps"` selects it as
before and its settings do not change. Its builder registers the page
support the APS renderer needs when the auction plan selects an APS source.

Its 38 tests went with it, and 24 more came from core, where APS had been
the example the engine was tested with:

- three tests of the renderer descriptor APS writes,
- eight that run an APS source through the orchestrator and the response
  reader,
- four of the plan compiler and the page registration,
- six of the request APS builds, with both golden requests carried byte for
  byte,
- three of the deploy check of APS's own settings.

Those tests run against core's engine from APS's crate, so core offers what
they need under its `test-utils` feature. That is the helpers that build an
auction context, a demand table and a plan, two drivers that build a
request for a plan's first source and read a response as that source, and
the backend double in the platform test doubles.

Core's own tests of routing, the plan compiler, the request builder, the
response format and the page's bid map use core's plain OpenRTB source, or
a renderer descriptor of the test's own tagged `example`, in place of APS.

`ERROR_TYPE_HTTP_STATUS` and `ignored_bidder_params_count` are public, so
an implementation in a crate of its own reports a failed call and counts
ignored bidder parameters the way core does.
`integrations/prebid.rs` held three things: the page integration, the
request, response and bidder parameter override code the Prebid Server
demand runs, and the provider and combined configuration kept for the test
build. The shipped code falls into two parts with nothing shared between
them, so the file is divided along that line and nothing it does changes.

The page integration stays in `prebid.rs`. The override rules and their
engine, the transport headers, the response reader and the upstream error
text join the demand in `prebid_server.rs`, with the test-only provider
that the tests compare the demand with. Neither file refers to the other.

The combined test configuration is divided the same way, so each file
keeps the fields its own code reads. The page's is `LegacyPrebidConfig`,
the browser settings and the bidder list a plan would supply. The demand's
keeps the name `LegacyPrebidServerConfig`.

Of the file's 178 tests, 57 stay with the page integration and 120 go
with the demand. One is not carried:
`register_rejects_invalid_bid_param_override_rule` checked that the page
registration built from the combined configuration refused a bad override
rule, and that registration no longer compiles override rules. The
provider's own test of the same refusal stays. `validate_config_for_startup`
was test-only and nothing called it, so it is gone.
The Prebid page integration is the module `auction.prebid`, and it now
lives in `crates/auction/prebid`. `[auction] modules = ["prebid"]` selects
it as before and its settings do not change. The stock list offers it
first, so what it registers from the auction plan still runs ahead of
APS's page support and of every module a section selects.

Its 57 tests went with it, and seven came from core, where they checked
rules that are Prebid's own: the removed `server_url` setting is refused,
the two deploy rules for the external bundle, deploy and runtime
validation both reaching Prebid's table, a bidder owned by the browser and
by the server being refused only while the auction is on, and the shim
loading deferred with the bundle route registered. The two bidder tests
ran through the configuration blob in core and call runtime validation
with Prebid's builder here, which is the step of the blob load they test.

Core's shared test settings selected Prebid, so every test of core and of
every other module ran with Prebid in it. They now select no auction
module, and Prebid's tests select Prebid themselves. Core's tests that
used Prebid as the one deferred browser module there was, or as the
integration whose settings reach a page template, run on a stand-in
compiled for core's tests only, `testing.deferred-fixture`, which carries
a deferred module and writes its two settings into `<head>`. The settings
tests of a leftover `enabled` key and of a module named twice use the
example module they already had.

`AuctionPlan::browser_bidder_codes` and `AuctionPlan::with_enabled` are
public, so a page module in a crate of its own reads the plan's bidder
routes and a test switches a compiled plan off.

The `ts` audit generator asks the stock list where Prebid is selected, as
it does for every other module, and the `ts prebid server inspect` tests
read Prebid's configuration type from Prebid's crate.
The Prebid Server demand implementation is `auction.prebid-server`, and it
now lives in `crates/auction/prebid-server` with its request extensions,
its bidder parameter overrides, its transport headers, its response reader
and the provider kept for its tests. The stock list offers it, so a
`[demand.<name>]` table with `implementation = "auction.prebid-server"`
selects it as before and its settings do not change.

Its 124 tests went with it. Sixteen more are in its crate, run through
core's driver from outside core:

- eight that core held for it, which are the body consent by source and
  forwarding mode, the debug query fragment, the ordered overrides with the
  stored request as fallback, each impression paired with its slot's
  parameters, the stored intent applied after overrides, empty parameters
  falling back to a stored request, the exact request golden, and two
  sources run through the orchestrator,
- eight for the Prebid Server half of a test core keeps, which are the
  consent matrix, the language tag, a disabled empty candidate, the signed
  request golden, the unsigned request identity, the endpoint completed
  through the plan, the refusal of all-eligible routing and the default
  timeout.

Both request goldens are carried byte for byte.

Core's auction tests used Prebid Server as the implementation that departs
from the plain one. They run on a stand-in compiled for core's tests only,
`auction.fixture`, which serves stored requests, forbids all-eligible
routing, takes bidder parameters, keeps the unsigned request identity and
completes an endpoint that names a host alone. Twenty-nine tests of
routing, the plan, the formats and the driver pass on it with the name
alone changed, and six read the stand-in's extension or its endpoint where
they read Prebid Server's. Their names, labels and messages say what they
test.

`ResponseAdmissionDiagnostics`, `build_bid_dimension_index`,
`apply_notification_policy` and `ProviderSlotInput::allows_stored_fallback`
are public, so an implementation in a crate of its own admits bids, applies
the notification policy and reads a slot's stored request intent the way
core does. Two test drivers are added under `test-utils`,
`route_to_first_source` and `routed_transport`.

Prebid's test of the browser injection compiles its plan with Prebid
Server's builder, the `ts` tool reads the implementation's name from its
crate, and the stock list states the two rules its order carries.
…list

The adapter's test settings select Prebid and a Prebid Server demand
source, as a deployment does. Two test helpers compiled the auction plan,
and one of them built the registry, from core's built-in modules alone,
which held while both lived in core. With Prebid and the Prebid Server
demand in crates of their own the helpers failed, and the adapter's test
binary stopped at the first of them.

Both helpers now take the stock list's builders, which is what the
adapter's own entry point does. The two commits before this one left the
Fastly tests failing, and they pass again here (3,572 in the Fastly build).
The integration guide, the integrations overview, the configuration guides
and the onboarding page still described an integration as code inside
`trusted-server-core`, or as "still in core" with the name its crate would
have. Every page integration now has a crate of its own, so they say that:
an integration is a crate under `crates/<type>/<vendor>/` here or in a
repository of the vendor's, and a module's name is that crate's path.

The agent notes list the module crate folders in the workspace layout, and
the onboarding table gains the rows for a module crate and for the stock
list.
Core's error type had a `Prebid` variant that only Prebid raised. Prebid's
page integration and the Prebid Server demand raise `Integration` under
their own names now, as every other module does, and core's error type
names no vendor's variant.

A reader sees no difference. Both variants answer 502 with the generic
message, and the detail is logged as "Integration error (prebid)" or
"Integration error (auction.prebid-server)" where it was "Prebid error".
`models.rs` held `AdResponse` and `Callback`, the response of one ad
server, kept with dead code allowed "for external ad-server
compatibility". Nothing in the workspace reads either type, so the file
goes, with the ten tests of how the two types deserialize.

The `Gam` error variant named Google Ad Manager and nothing raised it, so
it goes from the error type and from the tables that list every variant.
Core's OpenRTB model held `RequestExt`, `PrebidExt`, `ImpExt`,
`PrebidImpExt` and `ImpStoredRequest`, the typed `ext.prebid` objects of a
request and an impression. Core's driver writes an implementation's
extensions as the objects the implementation hands it and uses none of
these types. Only the provider that Prebid Server's crate keeps for its
tests builds a request through them, so they sit beside it there, compiled
for its tests alone.
The limit on the zone a browser names for a slot was
`MAX_PREBID_ZONE_BYTES`, and the admitted request headers were described
as approved for "Prebid transport forwarding". Routing admits both for
every demand source, so the limit is `MAX_ZONE_BYTES` and the headers are
approved for forwarding to a demand source.
Each module crate says who maintains it in `[package.metadata.maintainers]`
of its manifest, the way Prebid.js requires a named maintainer of every
adapter. Three crates did not: the seam probe, the stock list and, in part,
Testlight, which named an owner and no status. They do now, and the
demonstration ad server's entry, which read "No vendor has not yet adopted
this crate", says that the crate is the project's own.

A status is one of three. `vendor owned` is a crate its vendor has adopted,
`seeking vendor owner` is one the Trusted Server maintainers hold for a
vendor, and `project owned` is one that is the project's own.

A test in the stock list crate reads the manifest of every crate under
`crates/<type>/<vendor>/`, and its own, and fails for one with no
declaration, no owner or a status that is not one of the three. The
integration guide states the convention and its checklist asks for it.
Jamesr51d and others added 23 commits October 13, 2026 16:39
The wasm clock lint reaches core's build script, which runs on the host.
Core's version_header module is removed, with its call from the finalize
step of the Axum, Cloudflare and Spin adapters and from the Fastly adapter's
health probe and its settings fallback, so no response carries x-ts-version
unless a [[serve]] entry names the service_version middleware. That
middleware now writes the git version the build compiled in from
TRUSTED_SERVER__GIT_VERSION or local git as X-TS-Version, when the build
knows one, and the version the adapter reports for the service as
X-TS-Service-Version, when it reports one. HEADER_X_TS_FASTLY_VERSION, which
nothing wrote, goes with the module.

The health tests of the three adapters prove the probe carries no version,
the middleware's own tests prove each header is written when its version is
known and nothing when neither is, and the publisher tests prove both
headers reach a buffered page and neither reaches a page no entry names the
middleware for. The configuration guide, the getting started guide, the
EdgeZero guide and the changelog say the same.
# Conflicts:
#	docs/guide/configuration.md
@jwrosewell

Copy link
Copy Markdown
Collaborator

Moved into this repository as #1364, one branch of the stack, so the review continues there. This pull request is closed in its favor and its comments stay here for the record.

@jwrosewell jwrosewell closed this Oct 15, 2026
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.

2 participants