Skip to content

Map folder origin URLs in headers, add a serve headers step - #1378

Open
jwrosewell wants to merge 43 commits into
feature/36-object-storagefrom
feature/37-origin-redirect
Open

jwrosewell wants to merge 43 commits into
feature/36-object-storagefrom
feature/37-origin-redirect

Conversation

@jwrosewell

@jwrosewell jwrosewell commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Changes

Area Change
crates/trusted-server-core/src 5 files, 1012 lines added, 15 removed
docs/superpowers 2 files, 172 lines added, 1 removed
docs/guide 1 file, 30 lines added, 0 removed
CHANGELOG.md 1 file, 2 lines added, 0 removed

Closes

Closes #1330

Spec

Spec: origin-redirect

Test plan

CI on this pull request at dbbc8b3 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 dbbc8b3

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 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 in Location, Content-Location, Refresh, Link or 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 in origin_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 for https://publisher.example/news:

The origin answers The reader receives
301 with Location: https://storage.example.com/bucket/site/news/ Location: https://publisher.example/news/
302 with Location: https://storage.example.com/bucket/site/news/?page=2#latest Location: https://publisher.example/news/?page=2#latest
301 with Location: https://storage.example.com/bucket/site Location: https://publisher.example/
301 with Location: https://storage.example.com/bucket/other/news/ The header as the origin wrote it
200 with Link: <https://storage.example.com/bucket/site/style.css>; rel=preload Link: <https://publisher.example/style.css>; rel=preload
200 with Content-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.

  • The folder. OriginHeaderRewrite gains origin_path, which handle_publisher_request fills from the path of origin_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.
  • Content Security Policy. A source with no path still gains the whole serving host. A source in the folder gains a sibling without the folder, and one outside it gains none.
  • The loop exceptions. The two exceptions the mapping in main makes compare the reader's path with the path of the target without the folder, so both apply under a folder.
  • What names the origin. The mapping in main decides. The origin is the host of origin_url with its port, under a Host override as well, and a Location is mapped on a response of any status.
  • A serve middleware may set headers on every response. Middleware::response_headers is 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, the 502 for an unexpected 304 and 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's Location with it.

Why

With a folder in origin_url, a request for /news is sent to the origin as /bucket/site/news. The mapping in main keeps the folder in the path, so a redirect to https://storage.example.com/bucket/site/news/ reaches the reader as https://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 a Location, and the headers step gives it one.

What breaks

Nothing for a deployment whose origin_url has 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

Commit What it does
0e9172e The rule, its place in the publisher path, the guide and the design page
0affe09 The design page names what a redirect to another name needs
3dd7955 A serve middleware sets headers on every response, a bodiless redirect included
cfe5229 The merge of #1377, after the merges of #1374 as its head moved
43a55dd A Host override written with the scheme's own port still names the origin
61f1b6f for_path shares covers_path and has tests of its own
fd0ba29 The design page names Middleware::response_headers
d014822 Serve headers are written on the response that is sent
002b96e The origin-redirect design names its pull request and issue
9dafb08 The origin redirect design names the headers step it adds and the responses that skip it
2e12310 The origin redirect design takes its round 2 corrections
ef10ee3 The origin redirect design names the specs pull request that holds it
873bb66 The header rewrite maps a folder origin's URLs and replaces the redirect rule
6ec3e07 The changelog, the guide and the design follow the header rewrite

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

What is not in this pull request

  • Another name for the same site. An origin at https://publisher.example that redirects to https://www.publisher.example names 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 a Location goes with a middleware of its own, which Middleware::response_headers lets it do.
  • A Location that names no host, from an origin in a folder, which is relayed as written.
  • Following a redirect. The reader's browser makes the next request.
  • The redirects of asset routes and the first-party proxy, which are handled in proxy.rs and unchanged.

The design page is docs/superpowers/specs/2026-10-13-origin-redirect-design.md.

jwrosewell and others added 13 commits October 13, 2026 23:44
…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.
The related specs line named #1084 as the pull request that holds every design document of the stack. That pull request was closed on 2026-10-15 and the specs pull request is now #1342, so the line names the new one. Only the number changes.
@jwrosewell
jwrosewell added this pull request to stack #1401 October 15, 2026 17:56
@jwrosewell
jwrosewell removed this pull request from stack #1401 October 15, 2026 20:51
@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
…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
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 An origin's redirect to itself keeps the reader on the address they asked for Map folder origin URLs in headers, add a serve headers step 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.

An origin's redirect to itself keeps the reader on the address they asked for

2 participants