Skip to content

Make a file the origin does not have from the middleware named for its path - #1363

Open
jwrosewell wants to merge 38 commits into
feature/21-cmp-phasesfrom
feature/22-creating
Open

jwrosewell wants to merge 38 commits into
feature/21-cmp-phasesfrom
feature/22-creating

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 3 files, 617 lines added, 0 removed
crates/trusted-server-adapter-axum/tests 1 file, 89 lines added, 3 removed
crates/ads-txt/sellers 2 files, 46 lines added, 11 removed
docs/guide 2 files, 24 lines added, 3 removed
CHANGELOG.md 1 file, 2 lines added, 1 removed

Closes

Closes #1280

Spec

Spec: middleware-creates-a-missing-file

Test plan

CI on this pull request at 0b7c6a6 passed all 32 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 0b7c6a6

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 the consent prompt phases (#1362).

What this pull request does

When the origin answers 404 Not Found for a path under the path of a [[fetch]] entry for a media type other than HTML, core discards the origin's body and headers and runs that entry's middleware on an empty document of the entry's media type, and its output is the file. The prefix is compared as an entry's path always is, so path = "/ads.txt" also makes /ads.txt.bak. The made file carries only Content-Type. It is not stored as a template, which is HTML alone, and no cache policy applies unless an asset policy names the path, so it is made again on every request. So a publisher whose origin has no ads.txt gets one made of the sellers lines alone.

[ads-txt]
modules = ["sellers"]

[ads-txt.sellers]
lines = ["ssp.example, 12345, DIRECT, f08c47fec0942fa0"]

[[fetch]]
media_type = "text/plain"
path = "/ads.txt"
middleware = ["ads-txt.sellers"]

The rule

A well known file is made where middleware is configured for its path, and is the origin's answer where none is.

  • An entry for HTML, or without a path, makes nothing, because a page the origin does not have is the origin's answer.
  • The entry taken is the one with the longest path covering the request, of those for a media type other than HTML, so neither an HTML entry nor one for a shorter path shadows it.
  • Any answer from the origin other than 404 stands, an error among them.
  • A GET and a HEAD are answered alike, the HEAD with no body.

The middleware is an ordinary one, given an empty document. A middleware that adds to a file writes the same on an empty one, with nothing before its first line, which the sellers module already did.

How it was verified

Not exercised: no file has been made through an adapter against a real origin. The rule is checked through the publisher path with a stub origin answering 404, and the made file is streamed through the entry's middleware.

References #1280.

jwrosewell and others added 11 commits October 10, 2026 22:40
…ts path

When the origin answers `404 Not Found` for a path a `[[fetch]]` entry with
a path names middleware for, core runs that middleware on an empty document
of the entry's media type and its output is the file, stored and served as a
fetched one is. A GET and a HEAD are answered alike. With no such entry, on
any other answer from the origin, or where only the entry covering every
path applies, the origin's answer stands, because a page the origin does not
have is the origin's answer.

`PhaseEntries::creating` finds the entry. The sellers crate says its file is
made from the lines alone where the publisher has none, which its middleware
already does on an empty document.
The configuration guide says it with the entries for other media types and
in the ads.txt section, the integration guide tells a middleware it may be
run on an empty document, and the changelog records the rule.
# Conflicts:
#	CHANGELOG.md
#	crates/ads-txt/sellers/src/lib.rs
#	docs/guide/configuration.md
PhaseEntries::creating is now making and skips text/html entries, so a page the origin does not have stays the origin's answer and an HTML entry ahead of a text/plain one no longer makes the file as HTML. The HEAD test now checks the answer has no body. Comments and the guides say the rule once.
A missing file was made by the first non-HTML entry whose path covered the
request, so an entry for text/css on / written ahead of the text/plain entry
for /ads.txt made an empty stylesheet in place of the sellers file. The entry
with the longest covering path now makes it, the first written winning
between two of the same length.
A file was made for any path under an entry's path, so an entry at / for a
media type other than HTML answered every page the origin does not have
with an empty 200, and an entry at /ads.txt made /ads.txt.bak as well. An
entry now makes a file at its own path alone, never at a longer path it
covers and never for a folder written with its closing slash, so a page the
origin does not have keeps its 404. Where entries for two media types name
the path, the first written makes the file.
Core runs no middleware for a request that names no host, so a file made
for one went out as an empty 200. Core now makes no file for such a request
and the origin's answer stands.
The check accepted any text/plain entry that names the module, so an entry
with no path wrote the lines into every text/plain response and made no
ads.txt, and an entry for another path wrote them into that file. A selected
module is now refused unless a [[fetch]] entry for text/plain with path
/ads.txt names it, which is the entry that makes the file where the origin
has none.
A POST, PUT, PATCH, DELETE or OPTIONS keeps the origin's 404. A made file
carries its Content-Type alone and none of the headers the origin sent with
its 404. A made file goes through publisher_response_into_streaming_response,
which the Fastly adapter finishes the answer with, and carries what the
middleware writes.
Through the adapter's router with the stock modules and a stub origin that
answers 404 with Set-Cookie and Cache-Control, a GET for /ads.txt gets 200,
text/plain; charset=utf-8, neither header and the sellers lines alone. The
route tests' settings take a test's own sections through test_settings_with.
@jwrosewell jwrosewell mentioned this pull request Oct 17, 2026
13 of 14 tasks
@jwrosewell
jwrosewell removed this pull request from stack #1408 October 18, 2026 20:15
@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 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 marked this pull request as ready for review October 19, 2026 20:23
@jwrosewell
jwrosewell removed this pull request from stack #1422 October 28, 2026 11:47

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 well known file the origin does not have cannot be made, because the origin's 404 passes through whatever middleware is configured for the path

2 participants