Repository navigation
Map folder origin URLs in headers, add a serve headers step - #1378
Open
jwrosewell wants to merge 43 commits into
Open
jwrosewell wants to merge 43 commits into
jwrosewell wants to merge 43 commits into
Conversation
…sked for The publisher proxy relayed an origin's redirect as the origin wrote it. An origin redirects to itself for ordinary reasons, a request path without its trailing slash being enough, and it names itself in the Location, so the reader's browser left the deployment for the origin. Where the Location of a 3xx response names the origin, the reader is now sent to the same path, query and fragment on the scheme and host they asked for. The origin is named by the Host it was sent, port included, which is origin_host_header_override where one is set and the host of origin_url otherwise. A Location naming any other host, one that names no host, and the Location of a response that is not a redirect are left as the origin wrote them. With a path in origin_url, a Location at or beneath that path comes back without it, and one outside it is left alone, so no response carries a path the reader has no address for. The header is rewritten in the one place handle_publisher_request receives the origin's response, so the streamed page, the buffered page, a HEAD and every other method carry it. The guide's origin_url entry, the changelog and a design page describe the behavior, and the object storage design page's sentence on redirects now points at that page.
…t included A serve middleware could set a response header only through the action it returns for a page, so it ran on the responses the phase processes and on no other. An origin's redirect to a root-relative address, which the redirect rule leaves as written, reached the reader untouched on a deployment that names the publisher in the first part of the address, and the reader left the publisher, because the middleware that would put the prefix on the Location never saw the response: a 301 or 302 from such an origin has no body and no media type an entry covers. The Middleware trait gains response_headers, given the response's status and headers through a ResponseView and answering the headers to set, nothing by default. The publisher path asks it of every serve middleware an entry whose path covers the request names, whatever the entries' media types, on every response the origin path sends, after core's own redirect rewrite and before the response goes on, and a header set replaces one of the same name through write_response_headers. PhaseEntries::for_path and IntegrationRegistry::middleware_for_path give that list. A fixture middleware puts a prefix on a redirect's root-relative Location through the step alone, and a test through handle_publisher_request shows a bodiless 302 reaching it and the reader receiving the header it set, a Location it does not set left alone, and a middleware whose entry covers another path not asked. The guide's page changes section says what the step is for, and the changelog says what it adds.
…igin The origin's name is read on the scheme of the Location, so a port that is the usual one for it counts as no port, as url writes the Location.
…r wins PhaseEntries::for_path and PhaseEntry::covers call one covers_path check. The rustdoc of Middleware::response_headers is cut to what an implementer needs and says a header the action from create sets replaces one set here. for_path is tested with entries of several media types and a repeated middleware.
The headers step ran before a made file or the 502 for an unexpected 304 replaced the response, so its headers were lost. It now runs on the made file, the 502 and a middleware refusal. The locating fixture names the status it was shown, so tests show a made file and the 502 carry it. middleware_for_path is tested directly, the TEXT_HEADER rustdoc is back on TEXT_HEADER, and the redirect tests share one response_status helper.
# Conflicts: # crates/trusted-server-core/src/publisher.rs
…ponses that skip it Review round 1 found that the design called the serve middleware headers step outside the change although the series adds it, said the step is asked about every response the publisher path sends when a refusal before the origin is asked, a page from the template cache and an answer made from an error skip it, and said nothing of a publisher served under a path prefix, whose reader the rewritten Location sends to an address without the prefix. The design now has a section for the step with its tests, says a serve middleware and not core puts the prefix back, and covers the adapters, the setting, consent, failure and the loop with an index file. The same sentence is corrected in the changelog and the configuration guide.
Review round 2 approved the design with no blocking issues and found two
gaps in the changelog and the configuration guide. Neither named the two
origins that make the reader's browser loop, being one reached over plain
HTTP that redirects to its own https address and one that redirects its
index file back to its folder, or that a reader under a removed path prefix
is sent to an address without the prefix, which a Fastly service whose
selector carries {host} answers 421. Both named only two of the three kinds
of response the headers step is not called for. The sentences now name an
answer made from an error and the refusal of a template cache page as well.
The design records the round.
This was referenced Oct 17, 2026
…ect rule The header rewrite merged in main (#1238) maps an origin URL in Location, Content-Location, Refresh, Link and CSP to the serving host, on every route of handle_publisher_request. Its loop exceptions, and its rules for a Location on any status and under a Host override, differ from the Location rule this change added. With both in place the two loop tests from #1238 failed, and so did three of this change's own tests. This change now follows the header rewrite. Its own rule, rewritten_origin_redirect, the call to it and its unit tests are removed. The header rewrite gains origin_path, so with a folder in origin_url a URL on the origin host is mapped only when it is in the folder, and it loses the folder. Dot segments are resolved first, the folder counts only where it ends at a slash, and the loop exceptions compare the reader's path with the URL's path without the folder. A CSP source with no path still gains the whole serving host. The folder cases of the removed unit tests move to the header rewrite's tests. The serve middleware headers step now runs after the header rewrite, so a middleware sees the headers the reader receives and a header it sets is the one sent. Two handler tests take the header rewrite's behavior. A Location on an answer that is not a redirect is named for the reader, and under a Host override the host of origin_url is the origin's name. cargo test -p trusted-server-core --lib -- redirect response_header_rewrite passes 58 tests. The same run on this branch before the change, with #1238 merged, failed five.
The changelog entry, the configuration guide and the origin redirect design describe the header rewrite merged in main (#1238) with the origin folder this change gives it, in place of the Location rule the change no longer has. The guide's section on origin URLs in response headers says how a folder is mapped, and the override is no longer named as the name a redirect is compared with. The design records five deliberate faults, each caught by a test. The object storage design's sentence on redirects names the mapping it now follows.
jwrosewell
added a commit
to jwrosewell/trusted-server
that referenced
this pull request
Oct 19, 2026
The origin redirect design describes the header rewrite merged in main (IABTechLab#1238) with the origin folder that IABTechLab#1378 gives it, in place of the Location rule IABTechLab#1378 no longer has, and records the five deliberate faults its tests catch. The object storage design's sentence on redirects names the mapping it now follows. The same text is on the branch of IABTechLab#1378.
jwrosewell
marked this pull request as ready for review
October 19, 2026 20:23
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/superpowersdocs/guideCHANGELOG.mdCloses
Closes #1330
Spec
Spec: origin-redirect
Test plan
CI on this pull request at dbbc8b3 passed all 29 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!)On top of the publisher origin in a folder of object storage (#1377). Issue: #1330.
James Rogers approved an earlier version of this change, in which an origin's redirect to itself kept the reader on the address they asked for. The change was reworked after that approval to follow the mapping of origin URLs in response headers that main merged in #1238, and James Rogers has not reviewed the reworked version.
What this adds
What changes.
The mapping of origin URLs in response headers merged in main (#1238) learns the folder of an origin in object storage (#1377). With a path in
origin_url, a URL on the origin inLocation,Content-Location,Refresh,Linkor a Content Security Policy header is mapped to the serving host only when it is in that folder, and it loses the folder. A URL on the origin's host outside the folder is left as the origin wrote it, because no address on the serving host leads there. With no path inorigin_url, the mapping is the one in main. There is no setting.With
origin_url = "https://storage.example.com/bucket/site"and a reader who asked forhttps://publisher.example/news:301withLocation: https://storage.example.com/bucket/site/news/Location: https://publisher.example/news/302withLocation: https://storage.example.com/bucket/site/news/?page=2#latestLocation: https://publisher.example/news/?page=2#latest301withLocation: https://storage.example.com/bucket/siteLocation: https://publisher.example/301withLocation: https://storage.example.com/bucket/other/news/200withLink: <https://storage.example.com/bucket/site/style.css>; rel=preloadLink: <https://publisher.example/style.css>; rel=preload200withContent-Security-Policy: img-src https://storage.example.com storage.example.com/bucket/site/img/Content-Security-Policy: img-src https://storage.example.com https://publisher.example storage.example.com/bucket/site/img/ publisher.example/img/How it works.
OriginHeaderRewritegainsorigin_path, whichhandle_publisher_requestfills from the path oforigin_url. Dot segments are resolved first, the folder counts only where it ends at a/, paths are compared exactly, and the folder itself is the serving host's root.origin_urlwith its port, under aHostoverride as well, and aLocationis mapped on a response of any status.Middleware::response_headersis asked, with the response's status and headers, of every serve middleware named by an entry whose path covers the request, on the response that is sent, a redirect with no body, a made file, the502for an unexpected304and a middleware refusal included, and a header it returns replaces one of the same name. It is asked after the mapping, so it sees the headers the reader receives. A deployment that names the publisher in the first part of the address puts the prefix back on a redirect'sLocationwith it.Why
With a folder in
origin_url, a request for/newsis sent to the origin as/bucket/site/news. The mapping in main keeps the folder in the path, so a redirect tohttps://storage.example.com/bucket/site/news/reaches the reader ashttps://publisher.example/bucket/site/news/, which the publisher proxy sends to the origin as/bucket/site/bucket/site/news/, an object that does not exist. The folder is part of the origin[publisher]names and the mapping is core's, so the folder belongs in core's mapping. A response with no page, such as most redirects, reached no serve middleware, so a deployment had nowhere to put a policy of its own for aLocation, and the headers step gives it one.What breaks
Nothing for a deployment whose
origin_urlhas no path. One with a path has its readers sent to addresses in the folder, where the mapping in main sent them to addresses that do not exist.The commits
for_pathsharescovers_pathand has tests of its ownMiddleware::response_headersThe other commits merge the branch below into this one, and through it Tech Lab main at 6ef07ae.
What is not in this pull request
https://publisher.examplethat redirects tohttps://www.publisher.examplenames a host that is not the one it was asked for, so the header is relayed. A deployment that serves both names and wants the reader kept across that redirect decides where such aLocationgoes with a middleware of its own, whichMiddleware::response_headerslets it do.Locationthat names no host, from an origin in a folder, which is relayed as written.proxy.rsand unchanged.The design page is
docs/superpowers/specs/2026-10-13-origin-redirect-design.md.