Skip to content

Add dual-path EdgeZero entry point with feature flag (PR 14)#628

Open
prk-Jr wants to merge 21 commits intofeature/edgezero-pr13-integration-provider-type-migrationfrom
feature/edgezero-pr14-entry-point-dual-path
Open

Add dual-path EdgeZero entry point with feature flag (PR 14)#628
prk-Jr wants to merge 21 commits intofeature/edgezero-pr13-integration-provider-type-migrationfrom
feature/edgezero-pr14-entry-point-dual-path

Conversation

@prk-Jr
Copy link
Copy Markdown
Collaborator

@prk-Jr prk-Jr commented Apr 13, 2026

Summary

  • Adds a feature-flagged dual-path entry point: requests are dispatched through the new EdgeZero TrustedServerApp or the preserved legacy_main based on edgezero_enabled in the trusted_server_config Fastly ConfigStore, with false as the safe default on any read failure
  • Implements TrustedServerApp via edgezero_core::app::Hooks with all routes, plus FinalizeResponseMiddleware and AuthMiddleware — matching legacy routing semantics without the compat conversion layer
  • Upgrades edgezero workspace dependencies from a pinned revision to branch=main and uses dispatch_with_config (non-deprecated) to avoid the double set_logger panic that run_app/run_app_with_logging would cause

Changes

File Change
Cargo.toml Pin all four edgezero deps to branch=main; bump toml to "1.1"
Cargo.lock Updated for edgezero and toml version changes
crates/trusted-server-adapter-fastly/src/main.rs Add is_edgezero_enabled(), feature-flag dispatch block, extract legacy_main(), use dispatch_with_config
crates/trusted-server-adapter-fastly/src/app.rs TrustedServerApp implementing Hooks with all 14 routes; explicit GET / and POST / to cover matchit root path gap
crates/trusted-server-adapter-fastly/src/middleware.rs New file: FinalizeResponseMiddleware and AuthMiddleware with golden header-precedence test
fastly.toml Add trusted_server_config config store for local dev with edgezero_enabled = "true"
crates/trusted-server-core/src/auction/orchestrator.rs rustfmt: remove blank line
crates/trusted-server-core/src/integrations/google_tag_manager.rs rustfmt: reorder imports, reformat long use lists
crates/trusted-server-core/src/integrations/lockr.rs rustfmt: inline trailing-brace onto condition
crates/trusted-server-core/src/integrations/permutive.rs rustfmt: inline trailing-brace onto condition
crates/trusted-server-core/src/integrations/prebid.rs rustfmt: reorder imports
crates/trusted-server-core/src/integrations/testlight.rs rustfmt: reformat long use list
.claude/settings.json Remove trailing comma in allowed-tools array

Closes

Closes #495

Test plan

  • cargo test --workspace
  • cargo clippy --workspace --all-targets --all-features -- -D warnings
  • cargo fmt --all -- --check
  • JS tests: cd crates/js/lib && npx vitest run
  • JS format: cd crates/js/lib && npm run format
  • Docs format: cd docs && npm run format
  • Manual testing via fastly compute serve — EdgeZero path routes all named routes and GET / correctly; legacy path reachable by setting edgezero_enabled = "false"

Checklist

  • Changes follow CLAUDE.md conventions
  • No unwrap() in production code — use expect("should ...")
  • Uses log macros (not println!)
  • New code has tests
  • No secrets or credentials committed

prk-Jr added 14 commits April 13, 2026 11:15
Replace the app.rs stub with the full EdgeZero application wiring:
- AppState struct holding Settings, AuctionOrchestrator,
  IntegrationRegistry, and PlatformKvStore
- build_per_request_services() builds RuntimeServices per request using
  FastlyRequestContext for client IP extraction
- http_error() mirrors legacy http_error_response() from main.rs
- All 12 routes from legacy route_request() registered on RouterService
- Catch-all GET/POST handlers using matchit {*rest} wildcard dispatch
  to integration proxy or publisher origin fallback
- FinalizeResponseMiddleware (outermost) and AuthMiddleware registered
…x handler pattern

- Remove Arc::new() wrapper around build_state() which already returns Arc<AppState>
- Remove dedicated GET /static/{*rest} route and its tsjs_handler closure
- Move tsjs handling into GET /{*rest} catch-all: check path.starts_with("/static/tsjs=") first
- Extract path/method from ctx.request() before ctx.into_request() to keep &req valid
- Replace .map_err(|e| EdgeError::internal(...)) with .unwrap_or_else(|e| http_error(&e)) in all named-route handlers
- Remove configure() method from TrustedServerApp (not part of spec)
- Remove unused App import
…, unused turbofish, and overly-broad field visibility

- Drop `.into_bytes()` in `http_error`; `Body` implements `From<String>` directly
- Remove `Box::pin` wrapper from `get_fallback` closure; plain `async move` matches all other handlers
- Remove `Ok::<Response, EdgeError>` turbofish in `post_fallback`; type is now inferred
- Drop now-unused `EdgeError` import that was only needed for the turbofish
- Narrow `AppState` field visibility from `pub` to `pub(crate)`; struct is internal to this crate
Switches all four edgezero workspace dependencies from rev=170b74b to
branch=main so the adapter can use dispatch_with_config, the non-deprecated
public dispatch path. The main branch requires toml ^1.1, so the workspace
pin is bumped from "1.0" to "1.1" to resolve the version conflict.
Replaces the deprecated dispatch() call with dispatch_with_config(), which
injects the named config store into request extensions without initialising
the logger a second time (a second set_logger call would panic because the
custom fern logger is already initialised above). Adds log::info lines for
both the EdgeZero and legacy routing paths.
matchit's /{*rest} catch-all does not match the bare root path /. Add
explicit .get("/", ...) and .post("/", ...) routes that clone the fallback
closures so requests to / reach the publisher origin fallback rather than
returning a 404.
Registers the trusted_server_config config store in fastly.toml with
edgezero_enabled = "true" so that fastly compute serve routes requests
through the EdgeZero path without needing a deployed service.
@prk-Jr prk-Jr marked this pull request as draft April 13, 2026 14:20
@prk-Jr prk-Jr self-assigned this Apr 13, 2026
@prk-Jr prk-Jr changed the title Add feature-flagged dual-path entry point for EdgeZero migration (PR 14) Add dual-path EdgeZero entry point with feature flag (PR 14) Apr 13, 2026
@prk-Jr prk-Jr linked an issue Apr 13, 2026 that may be closed by this pull request
@aram356 aram356 linked an issue Apr 13, 2026 that may be closed by this pull request
- Normalise get_fallback to extract path/method from req after consuming
  the context, consistent with post_fallback and avoiding a double borrow
  on ctx
- Add comment to http_error documenting the intentional duplication with
  http_error_response in main.rs (different HTTP type systems; removable
  in PR 15)
- Add comment above route handlers explaining why the explicit per-handler
  pattern is kept over a macro abstraction
@prk-Jr prk-Jr marked this pull request as ready for review April 13, 2026 16:43
@prk-Jr prk-Jr requested review from ChristianPavilonis and aram356 and removed request for aram356 April 13, 2026 16:43
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Reviewed the 13 files that PR 628 actually changes vs its base (feature/edgezero-pr13-...). The dual-path entry point is a sensible migration shape, and the explicit GET/POST "/" routes + the header-precedence middleware test are the kind of load-bearing details that prevent future outages. However, the EdgeZero path currently diverges from the legacy path in ways that are security- and reliability-relevant, and the branch = "main" pin on upstream deps makes builds non-deterministic. Blocking on those.

Blocking

🔧 wrench

  • Forwarded-header sanitization missing on EdgeZero path — legacy strips Forwarded / X-Forwarded-* / Fastly-SSL before routing; EdgeZero hands the raw req to dispatch_with_config. With edgezero_enabled = "true" as the local-dev default, this is the default path. (main.rs:95)
  • build_state() panics on misconfigexpect("should …") on settings / orchestrator / registry. Legacy returns a structured error response; EdgeZero now 5xx's with no detail. (app.rs:75)
  • Docstring "built once at startup" is misleading — every request spins up a fresh Wasm instance, so build_state() runs per-request. Invites future false caching. (app.rs:61)
  • Stale #[allow(dead_code)] on now-live middleware — five suppressions with "until Task 4 wires app.rs" comments. Task 4 is this PR. (middleware.rs:50,57,96,103,146)
  • AuthMiddleware flattens Report<TrustedServerError> into EdgeError::internal(io::Error::other(...)) — loses per-variant status code and user message; generic 500 instead of the specific error. (middleware.rs:122)
  • edgezero-* deps pinned to branch = "main" — non-deterministic builds; supply-chain path into prod via a moving upstream branch. Pin to a specific rev or fork tag. (Cargo.toml:59-62)

Non-blocking

🤔 thinking

  • TLS metadata dropped on EdgeZero pathtls_protocol / tls_cipher hardcoded to None; legacy populates both. Low impact today (debug logging only), but a silent regression if any future signing/audit path reads them. (app.rs:123-124)

♻️ refactor

  • 11 near-identical handler closures in routes() — a pair of file-local make_sync_handler / make_async_handler helpers would cut ~120 lines without harming auditability. (app.rs:175-301)
  • FinalizeResponseMiddleware hardcodes FastlyPlatformGeo — take Arc<dyn PlatformGeo> instead so Middleware::handle can be unit-tested end-to-end. (middleware.rs:68)
  • build_per_request_services duplicates platform::build_runtime_services — extract a shared helper that takes ClientInfo. (app.rs:111-127)

🌱 seedling

  • fastly.toml flips local dev default to EdgeZero — combined with the blockers above, every fastly compute serve now exposes them. Consider defaulting to "false" until the blockers land. (fastly.toml:52)

⛏ nitpick

  • AppState fields can be private (not pub(crate)). (app.rs:62-66)
  • Root-route pairs clone closures four times — upstream RouterService::get_many would help. (app.rs:374-377)

📝 note

  • The dispatch_with_config comment explaining the set_logger panic is excellent "why, not what" documentation. (main.rs:91-94)

👍 praise

  • operator_response_headers_override_earlier_headers codifies a brittle precedence contract. (middleware.rs:217-233)
  • Explicit GET "/" / POST "/" routes with the in-code explanation of matchit's wildcard gap prevent a future 404 outage. (app.rs:374-377)

CI Status

  • browser integration tests: PASS
  • integration tests: PASS
  • prepare integration artifacts: PASS
  • fmt / clippy / unit tests: not surfaced by gh pr checks — please confirm these ran.

Comment thread crates/trusted-server-adapter-fastly/src/main.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs Outdated
Comment thread fastly.toml
Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/main.rs
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Supplementary review — see aram356's review for the primary findings. This review covers additional items not raised there.

Non-blocking

🔧 wrench (cross-cutting, from earlier PRs in this stack)

  • set_header drops multi-valued headers: edge_request_to_fastly in platform.rs:187 uses set_header instead of append_header, silently dropping duplicate headers. Pre-existing pattern (also in compat::to_fastly_request), but the EdgeZero path creates a new copy of the same bug.

🌱 seedling

  • parse_edgezero_flag is case-sensitive: "TRUE" and "True" silently fall through to legacy path. Consider eq_ignore_ascii_case or logging unrecognized values.

📝 note (cross-cutting, from earlier PRs)

  • Stale doc comment in platform/mod.rs:31: References fastly::Body in publisher.rs, but PR 11 already migrated to EdgeBody.

♻️ refactor (cross-cutting, from earlier PRs)

  • Duplicated body_as_reader helper: Identical function in proxy.rs:24 and publisher.rs:23. Extract to shared utility.

⛏ nitpick (cross-cutting)

  • Management API client re-created per write: Each put/delete in platform.rs constructs a new FastlyManagementApiClient. Fine for current usage, noted for future batch writes.

📌 out of scope

  • compat.rs in core depends on fastly types: Already tracked as PR 15 removal target.

CI Status

  • browser integration tests: PASS
  • integration tests: PASS
  • prepare integration artifacts: PASS

Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/main.rs
prk-Jr added 2 commits April 16, 2026 13:31
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

I found 5 issues to address before this dual-path entry-point rollout is ready. Three are functional regressions that can change request handling or response metadata, and two are documentation/configuration gaps that should be cleaned up before operators rely on the new path.

Findings folded into the review body

🤔 API/docs comments lag new proxy-rebuild and settings behavior: The latest reviewer findings also called out stale API/docs comments in crates/trusted-server-core/src/proxy.rs:989-997 and crates/trusted-server-core/src/settings.rs:476-480. Those files are not part of this PR's current diff, so I could not place them as inline comments on this review, but they should still be reconciled before the new proxy-rebuild and runtime-settings behavior is considered fully documented.

Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs
Comment thread fastly.toml
Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

The previously blocking findings from the earlier review round are resolved: forwarded-header sanitization is now applied on the EdgeZero path, build_state() returns a Result with a startup_error_router fallback instead of panicking, AuthMiddleware preserves per-variant status codes via http_error, the "built once at startup" docstring is clarified, and edgezero deps are pinned to the full 40-char commit SHA 38198f9839b70aef03ab971ae5876982773fc2a1. parse_edgezero_flag is now case-insensitive with full test coverage.

Deep analysis surfaces two new blockers that share a single root cause: RouterService middleware in edgezero-core only runs on matched routes, and only GET/POST handlers are registered on this app. The combination produces two independent regressions on the EdgeZero path — non-GET/POST requests return 405 instead of being proxied to origin (legacy proxies every method), and router-level errors bypass FinalizeResponseMiddleware so responses miss X-Geo-*, X-TS-Version, X-TS-ENV, and operator-configured response_headers. Both can plausibly be fixed in one pass.

Inline comments cover the two blockers and the remaining non-blocking findings. CI locally: cargo fmt --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, and cargo test --workspace all pass (821 tests).

Blocking

🔧 wrench

  • Non-GET/POST methods regress to 405 — legacy route_request falls through to handle_publisher_request for every method; EdgeZero registers only GET and POST. RouterInner::dispatch returns Err(EdgeError::method_not_allowed) for HEAD / OPTIONS / PUT / DELETE / PATCH, which oneshot turns into a bare 405. See inline on crates/trusted-server-adapter-fastly/src/app.rs:422.
  • TS response headers dropped on router-level errors and handler EdgeError pathsRouterService::oneshot handles Err(EdgeError) via err.into_response() outside the middleware chain, so 405/404/handler-error responses never reach FinalizeResponseMiddleware. Separately, next.run(ctx).await? in the middleware short-circuits on any inner Err. Both need changing so finalize always runs. See inline on crates/trusted-server-adapter-fastly/src/middleware.rs:69.

Non-blocking

🤔 thinking

  • Geo lookup runs on auth-rejected responses — legacy skips geo on 401 and sets X-Geo-Info-Available: false; EdgeZero unconditionally attaches full geo headers to the 401. Inline on middleware.rs:71.
  • Per-request double open of trusted_server_configis_edgezero_enabled() and dispatch_with_config each open the store. Use dispatch_with_config_handle and reuse a single handle. Inline on main.rs:98.
  • TLS metadata hardcoded to None on EdgeZero pathtls_protocol / tls_cipher are populated by legacy; the EdgeZero FastlyRequestContext only carries client_ip. Silent regression for any future signing/audit path. Previously flagged; still present. Inline on app.rs:126.

♻️ refactor

  • Eleven near-identical handler closures — two small helpers collapse all of them. Previously flagged. Inline on app.rs:214.
  • build_per_request_services duplicates platform::build_runtime_services — extract a shared helper parameterized by ClientInfo. Previously flagged. Inline on app.rs:129.
  • Module docstring claims middleware is always attached — but startup_error_router skips it. Inline on app.rs:5.

🌱 seedling

  • fastly.toml defaults local dev to EdgeZero — combined with the blockers above, every fastly compute serve reproduces them. Inline on fastly.toml:54.

📝 note

  • No tests for AuthMiddleware::handle, FinalizeResponseMiddleware::handle, or startup_error_routerapply_finalize_headers is tested standalone, but the middleware handle methods and the degraded startup-error path have no coverage. Inline on middleware.rs:238.

CI Status

  • browser integration tests: PASS
  • integration tests: PASS
  • prepare integration artifacts: PASS
  • fmt / clippy / unit tests (local): PASS — 821 tests passing

Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs
Comment thread crates/trusted-server-adapter-fastly/src/main.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/app.rs
Comment thread fastly.toml
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs
prk-Jr added 3 commits April 21, 2026 09:18
…thods

FinalizeResponseMiddleware now absorbs errors from inner middleware/handlers
by converting EdgeError to a Response via IntoResponse, so apply_finalize_headers
always runs regardless of handler outcome. Geo lookup is moved after next.run
and skipped for UNAUTHORIZED responses to avoid unnecessary backend calls.

Register HEAD and OPTIONS catch-all routes so cache-validation and CORS
preflight requests reach the publisher origin instead of returning 405.
Update module docstring to note startup_error_router skips middleware.
Copy link
Copy Markdown
Collaborator

@ChristianPavilonis ChristianPavilonis left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

superseded

Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/middleware.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread crates/trusted-server-adapter-fastly/src/main.rs
Comment thread crates/trusted-server-adapter-fastly/src/app.rs Outdated
Comment thread fastly.toml
@ChristianPavilonis
Copy link
Copy Markdown
Collaborator

Superseding note: I re-posted the line-specific findings as inline comments after switching to the single-comment review-comments API with the PR head commit SHA.

One additional high-priority finding is still not inline because the relevant code is outside this PR diff:

P1 — ProxyRequestConfig.allowed_domains is ignored, so integration proxies inherit the first-party allowlist unintentionally (crates/trusted-server-core/src/proxy.rs:432-458,573-579,694-700)

ProxyRequestConfig documents two modes: integrations should pass &[] for open mode, while the first-party proxy should pass &settings.proxy.allowed_domains. But proxy_request() currently destructures allowed_domains into _, and proxy_with_redirects() always checks settings.proxy.allowed_domains instead. That means enabling proxy.allowed_domains for /first-party/proxy can unexpectedly break unrelated integration proxy traffic.

Suggested fix: thread config.allowed_domains through to proxy_with_redirects() and use that slice for both the initial target check and redirect-hop checks. I’d also add a regression test where settings.proxy.allowed_domains is non-empty but proxy_request(... allowed_domains: &[]) still succeeds.

@prk-Jr
Copy link
Copy Markdown
Collaborator Author

prk-Jr commented Apr 23, 2026

@ChristianPavilonis Fixed the non-inline ProxyRequestConfig.allowed_domains item in eb6a9e9f. ProxyRequestConfig::allowed_domains is now threaded into ProxyRedirectPolicy and used for both the initial target check and redirect-hop checks. Added regressions for open mode with a non-empty settings.proxy.allowed_domains and open-mode redirect hops; cargo test -p trusted-server-core proxy_request_ passes.

Copy link
Copy Markdown
Collaborator

@aram356 aram356 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Round-3 review. The previously blocking findings from round 2 are all resolved: forwarded-header sanitization runs on the EdgeZero path, build_state returns a Result with a startup_error_router fallback, AuthMiddleware preserves per-variant status codes via http_error, FinalizeResponseMiddleware absorbs handler errors via IntoResponse, the AppState docstring is corrected, workspace edgezero deps are pinned to the full 40-char commit SHA 38198f9839b70aef03ab971ae5876982773fc2a1, parse_edgezero_flag is case-insensitive with full test coverage, HEAD/OPTIONS/PUT/PATCH/DELETE catch-all routes are registered, and FinalizeResponseMiddleware now takes Arc<dyn PlatformGeo> for testability.

This round surfaces two new blockers and four non-blocking findings. Both blockers are scope/behavior issues, not implementation bugs:

  • Silent behavior change in proxy.rs — integration-proxy redirect chains are no longer bounded by settings.proxy.allowed_domains, and nothing in the PR body documents it.
  • Non-standard HTTP methods bypass the middleware chain — legacy route_request falls through to publisher origin for every method; this path 405/404s them without running FinalizeResponseMiddleware, dropping all TS/geo/operator headers. Contradicts the FinalizeResponseMiddleware doc contract.

Both are addressable in this PR without a large rework. Inline comments describe concrete fix options.

Local CI is green: cargo fmt --check, cargo clippy --workspace --all-targets --all-features -D warnings, and cargo test --workspace (867 tests).

Blocking

🔧 wrench

  • Scope creep: silent integration-proxy allowlist semantics changeproxy.rs now lets integration proxies opt out of settings.proxy.allowed_domains for initial target and redirect hops. Not in PR description. See inline on crates/trusted-server-core/src/proxy.rs:446.
  • Non-standard HTTP methods bypass middleware — TRACE/CONNECT/WebDAV verbs skip FinalizeResponseMiddleware; TS/geo/operator headers dropped. Contradicts the doc contract. See inline on crates/trusted-server-adapter-fastly/src/app.rs:149.

Non-blocking

🤔 thinking

  • Per-request INFO-level routing log — noisy at production traffic; consider log::debug! or once-per-cold-start. Inline on main.rs:96.

📝 note

  • No tests for FinalizeResponseMiddleware::handle / AuthMiddleware::handle — the code changed this round has no direct unit coverage. Inline on middleware.rs:130.
  • No end-to-end test for the EdgeZero dispatch path — would catch the method-bypass regression directly. Inline on app.rs:392.

CI Status

  • browser integration tests: PASS (GitHub)
  • integration tests: PASS (GitHub)
  • prepare integration artifacts: PASS (GitHub)
  • fmt: PASS (local)
  • clippy: PASS (local)
  • rust tests: PASS (local, 867 tests)

copy_request_headers,
stream_passthrough,
allowed_domains: _,
allowed_domains,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench — Scope creep: silent behavior change to integration-proxy allowlist enforcement, not in PR description.

Before this PR, proxy_with_redirects accepted allowed_domains: _ (unused) and always enforced &settings.proxy.allowed_domains on the initial target and every redirect hop. After this PR, integration callers (gpt, google_tag_manager, permutive, etc.) pass allowed_domains: &[] → open mode (proxy.rs:519–524: empty slice permits all) → integration proxy redirect chains are no longer bounded by operator-configured allowed_domains. Only handle_first_party_proxy at proxy.rs:790 still enforces the settings allowlist.

This is security-relevant. Integration redirect hops can now follow Location to arbitrary hosts regardless of settings.proxy.allowed_domains. Likely intentional (integrations are operator-configured; first-party proxy handles untrusted creative-referenced URLs), but nothing in the PR body or the ## Changes table mentions it, and a reviewer reading git blame on the "dual-path entry point" PR will not expect this semantic shift.

Fix options (pick one, not all):

  1. Split into a separate PR with its own description, tests, and reviewer sign-off on the security trade-off.
  2. Update the PR body with an explicit "Proxy allowlist semantics" section explaining which callers opt into open mode and why.
  3. Add a doc comment on ProxyRequestConfig::allowed_domains explaining the open-mode contract and the integration/first-party split, and update the PR body.

Blocking until the scope is explicit — the change may be fine to land, but not silently.

Method::PATCH,
Method::DELETE,
]
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔧 wrench — Non-standard HTTP methods bypass the middleware chain; TS/geo headers dropped.

publisher_fallback_methods() covers 7 methods (GET/POST/HEAD/OPTIONS/PUT/PATCH/DELETE). For any method outside that set (TRACE, CONNECT, WebDAV PROPFIND/PROPPATCH, LINK, UNLINK, custom verbs), RouterInner::dispatch (edgezero-core router.rs:260-271) returns Err(EdgeError::method_not_allowed) or Err(EdgeError::not_found). RouterService::oneshot (router.rs:234-240) converts that Err to a response via err.into_response() outside the middleware chain.

Consequence: 405/404 responses on those methods skip FinalizeResponseMiddleware — no X-Geo-*, X-TS-Version, X-TS-ENV, and no operator-configured response_headers. Legacy route_request falls through to handle_publisher_request for every method, so this is a silent legacy→EdgeZero semantic regression on the "unusual method" envelope, and it contradicts the FinalizeResponseMiddleware doc contract ("every outgoing response — including auth-rejected ones — carries a consistent set of headers").

Fix options:

  1. Wrap the dispatch at the entry point so apply_finalize_headers runs on router-level errors too — call it on the FastlyResponse produced by dispatch_with_config before returning from fn main.
  2. Add a top-level "always-apply-finalize" outer service that wraps the RouterService.
  3. Expand the method list to cover everything the edge can deliver.

Option 1 or 2 is the robust fix — Option 3 is fragile (new methods added upstream would silently regress).

// skips logger initialisation and injects the config store directly.
edgezero_adapter_fastly::dispatch_with_config(&app, req, "trusted_server_config")
} else {
log::info!("routing request through legacy path");
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 thinking — Per-request INFO-level routing log is noisy.

log::info!("routing request through EdgeZero path") at main.rs:85 and log::info!("routing request through legacy path") at main.rs:96 fire on every request. At production traffic rates this produces one INFO line per request with no actionable information — operators already know the flag value from config. Consider log::debug! for per-request, or use a OnceLock to log once per cold start so the signal is retained without the volume.


next.run(ctx).await
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note — Missing test coverage for FinalizeResponseMiddleware::handle and AuthMiddleware::handle.

apply_finalize_headers is tested directly (good), and startup_error_router / uses_dynamic_tsjs_fallback have focused tests. But the middleware handle methods themselves — the code that was changed this round — are not exercised:

  • "Auth rejection bubbles through FinalizeResponseMiddleware and gets TS headers" contract.
  • match next.run(ctx).await { Err(e) => e.into_response() } absorption path added this round.
  • if response.status() != UNAUTHORIZED { geo.lookup(...) } branch.
  • FastlyRequestContext::get(ctx.request()).and_then(|c| c.client_ip) extraction.

A couple of handle unit tests with a stub Next and a mock PlatformGeo would pin down the round-2 fixes and prevent regression.

}

router.build()
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 note — No end-to-end test exercises the full EdgeZero dispatch path.

None of the new tests drive TrustedServerApp::routes()oneshot(request) end-to-end through the router + middleware + fallback chain. An E2E test would:

  1. Catch the method-bypass regression directly (assert X-TS-Version present on a TRACE/CONNECT response).
  2. Pin down the middleware ordering contract (FinalizeResponseMiddleware outermost).
  3. Verify auth-rejected 401s still receive TS headers but skip geo lookup.

Given the size of this PR, an E2E test is high leverage.

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.

Fastly entry point switch (dual-path with flag)

3 participants