Repository navigation
Let a middleware prepare with the request's services and refuse a request - #1364
Open
jwrosewell wants to merge 40 commits into
Open
jwrosewell wants to merge 40 commits into
jwrosewell wants to merge 40 commits into
Conversation
`Middleware::prepare` answers a future for a document whose decision needs the request's services or has to wait, on the key-value store or the network. The request's handler runs it before the document is processed, once per phase the document goes through, as the middleware's module and with the permissions it declares, and keeps what it answers under the middleware's name in the context's `Prepared`, where `create` takes it as the type it prepared. `create` and every streaming path stay synchronous, so the Fastly adapter is as it was. A middleware that prepares nothing is asked `create` as before. A preparation may answer a `Refusal` instead, with a status, an optional `Retry-After` and a message. The handler answers the request with that status and the message as plain text, marks the response private and unstorable, and processes no document, so a middleware responsible for a file fails closed when it cannot make it. The first refusal ends the preparations of a document. A copy read from the store prepares its serve chain the same way before it is assembled. The middleware fixture gains one that prepares and one that refuses, and the tests cover what is prepared reaching `create` in either phase, the refusal's status, headers and body, the chain keeping each answer under its name, and the first refusal ending the preparation.
The section says when a middleware prepares, what it is called with, where its answer goes, how a refusal is answered, and what each phase's preparation must not depend on. The changelog records the addition.
# Conflicts: # CHANGELOG.md # crates/trusted-server-core/src/middleware.rs # crates/trusted-server-core/src/middleware/tests.rs # crates/trusted-server-core/src/publisher.rs
…e reader A document that may be stored is prepared with the request's services alone, so no reader permission or Edge Cookie reaches the shared template. Retry-After rounds up to whole seconds. TextDocument is renamed ResponseDocument, the prepared field docs use one phrase, and the guide states which phases are prepared.
A document that is not stored is prepared in both phases before either is planned, and both kept what they prepared in one store under the middleware's name alone. A middleware running in both phases had its fetch preparation replaced by its serve one, so the fetch create took the serve value and the serve create took nothing. Prepared is now keyed by phase and name, and MiddlewareContext::take_prepared takes what was prepared in the context's own phase.
# Conflicts: # crates/trusted-server-core/src/html_processor.rs # crates/trusted-server-core/src/integrations/registry.rs # crates/trusted-server-core/src/middleware.rs # crates/trusted-server-core/src/publisher.rs
This was referenced Oct 15, 2026
…signals What the fetch phase of a document that may be stored prepares is stored with the document and served to every later reader, so the preparation must not see anything of the reader whose request fetched it. Core already gave it neither the permissions nor the Edge Cookie, but it handed over the request's services whole, and those carry the reader's client IP, the TLS details and the host's client signals (TLS JA4 and HTTP/2). The fetch phase of such a document is now prepared with StoredDocumentServices, a copy of the services made with the new RuntimeServices::without_client, which empties the client metadata and drops the host's client signals. The serve phase, whose answer is this reader's alone, is prepared with the reader's state as before. The preparing fixture now writes what its preparation could read of the reader, and a_fetch_preparation_of_a_stored_page_reads_nothing_of_the_reader drives a page through handle_publisher_request with a template cache and a reader carrying a client IP, TLS details, client signals and a User-Agent. The stored page is prepared with nothing of the reader and none of those values reach it, and the same page in inline mode, which is not stored, is prepared with the reader's client IP. Also repairs an assertion message in the pairing test that carried a run of spaces where a line continuation was meant.
…error A middleware's refusal ended the request with no log line, and core wrote whatever status the refusal carried, a 200 or a 301 included, although a refusal gives no document. MiddlewareChain::prepare now passes the first refusal through Refusal::checked, which logs it once as a warning naming the middleware, the phase and the status, and answers a status that is not a client or server error with 500 Internal Server Error, saying so in the same line. The message is not logged. The middleware tests add a_refusal_is_logged_once_as_a_warning_naming_the_middleware and a_refusal_answers_with_a_client_or_server_error_alone, and the end-to-end refusal test now checks for the one warning. The tests read the log through test_support::logs, which keeps what a test logs on its own thread.
…gets The integration guide and the changelog entry say that the fetch phase of a document that may be stored is prepared with no client IP, TLS details or client signals, and that core logs a refusal as a warning naming the middleware and answers a status that is not a client or server error with 500. Both also said that a preparation's answer is kept under the middleware's name, where it is kept under the phase and the name.
The preparing middleware's marker, saying what its preparation could read of the reader, was added to the middleware fixture under the name the serve reader middleware's marker already had, so the core tests did not compile. It is now `prepared_reader_marker`, with its two calls.
…logger A test binary holds one logger. Core's test support installs one for the whole binary in `test_support::logs`, and the tests of the startup warning that an adapter sends no telemetry installed a second, so whichever was installed second failed its test. The warning's tests now read their lines through `test_support::logs::logged_by`, as the other tests of a log line do, and each line is its level and then its message.
This was referenced Oct 17, 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-core/srcdocs/guideCHANGELOG.mdcrates/trusted-server-core/benchesCloses
Closes #1281
Spec
Spec: middleware-prepare
Test plan
CI on this pull request at 06ca813 passed all 32 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 making a file the origin does not have (#1363).
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::preparewith 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 forcreatein that phase to take.A preparation may answer a
Refusalinstead, with a status, an optionalRetry-Afterin 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
createis 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, aSendvalue, travels to the streaming paths, socreate, the plan and every streaming path are as they were.The rules
A middleware that prepares nothing is asked
createas 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, both phases are prepared too, so the serve middleware of the reader it was fetched for find what they prepared. The fetch phase is handed the request's services and nothing of the reader, and the serve phase is handed the reader's state. 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,HtmlProcessorConfigandReaderDocumenteach gain a required pub fieldprepared, so every struct literal of them outside this repository stops compiling.How it was verified
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.