Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
5bf4a9d
Organize spec and design documents into docs/superpowers
aram356 Mar 25, 2026
deffcec
Enable superpowers and chrome-devtools plugins
aram356 Mar 25, 2026
baefae8
Add streaming response optimization spec for non-Next.js paths
aram356 Mar 25, 2026
de8dbfd
Expand testing strategy with Chrome DevTools MCP performance measurement
aram356 Mar 25, 2026
fa74167
Clarify flush vs drop behavior in process_through_compression
aram356 Mar 25, 2026
221d971
Add implementation plan for streaming response optimization
aram356 Mar 25, 2026
42b7b07
Merge branch 'main' into specs/streaming-response-optimization
aram356 Mar 26, 2026
6968201
Fix encoder finalization: explicit finish instead of drop
aram356 Mar 26, 2026
a4fd5c6
Convert process_gzip_to_gzip to chunk-based processing
aram356 Mar 26, 2026
a4f4a7c
Convert decompress_and_process to chunk-based processing
aram356 Mar 26, 2026
105244c
Rewrite HtmlRewriterAdapter for incremental lol_html streaming
aram356 Mar 26, 2026
d72669c
Unify compression paths into single process_chunks method
aram356 Mar 26, 2026
80e51d4
Update plan with compression refactor implementation note
aram356 Mar 26, 2026
c505c00
Accumulate output for post-processors in HtmlWithPostProcessing
aram356 Mar 26, 2026
6cae7f9
Add streaming response optimization spec for non-Next.js paths
aram356 Mar 25, 2026
930a584
Address spec review: Content-Length, streaming gate, finalization ord…
aram356 Mar 25, 2026
a2b71bf
Address deep review: header timing, error phases, process_response_st…
aram356 Mar 25, 2026
b363e56
Address deep review: remove fastly::init, fix API assumptions, add mi…
aram356 Mar 25, 2026
13366f8
Merge branch 'main' into feature/streaming-pipeline-phase1
aram356 Mar 26, 2026
b83f61c
Apply rustfmt formatting to streaming_processor
aram356 Mar 26, 2026
aeca9f6
Add debug logging, brotli round-trip test, and post-processor accumul…
aram356 Mar 26, 2026
e1c6cb8
Address deep review: imports, stale comments, brotli finalization, tests
aram356 Mar 26, 2026
9753026
Address second deep review: correctness, docs, and test robustness
aram356 Mar 26, 2026
0a4ece7
Add active post-processor test and precise flush docs per codec
aram356 Mar 26, 2026
68d11e8
Fix text node fragmentation regression for script rewriters
aram356 Mar 26, 2026
6faeea0
Gate streaming adapter on script rewriter presence
aram356 Mar 26, 2026
73c992e
Document text node fragmentation workaround and Phase 3 plan
aram356 Mar 26, 2026
75f455a
Add buffered mode guard, anti-fragmentation test, and fix stale spec
aram356 Mar 26, 2026
9877276
Migrate entry point from #[fastly::main] to undecorated main()
aram356 Mar 26, 2026
d59f9bc
Refactor process_response_streaming to accept W: Write
aram356 Mar 26, 2026
986f92d
Add streaming path to publisher proxy via StreamingBody
aram356 Mar 26, 2026
3873e14
Address review: replace expect with log, restore stripped comments
aram356 Mar 26, 2026
c7edd82
Deduplicate content-encoding extraction and simplify flow
aram356 Mar 26, 2026
94f238a
Address PR review feedback on streaming response spec
aram356 Mar 31, 2026
1f2091d
Move EC coordination note to Phase 2 / Step 2 level
aram356 Mar 31, 2026
d049915
Merge branch 'main' into specs/streaming-response-optimization
aram356 Mar 31, 2026
d00fc5d
Formatting
aram356 Mar 31, 2026
76f8965
Merge branch 'specs/streaming-response-optimization' into feature/str…
aram356 Mar 31, 2026
3ede297
Merge branch 'specs/streaming-response-optimization' into feature/str…
aram356 Mar 31, 2026
bd01180
Address PR #585 review feedback
aram356 Mar 31, 2026
4be5a13
Merge remote-tracking branch 'origin/specs/streaming-response-optimiz…
aram356 Mar 31, 2026
6c930d7
Merge branch 'main' into specs/streaming-response-optimization
aram356 Apr 1, 2026
6bea17b
Merge branch 'main' into specs/streaming-response-optimization
aram356 Apr 3, 2026
29a297b
Merge branch 'main' into specs/streaming-response-optimization
aram356 Apr 7, 2026
eeaa0fa
Add streaming gate guards for status code and Content-Encoding
aram356 Apr 8, 2026
fd33b50
Merge branch 'main' into specs/streaming-response-optimization
aram356 Apr 8, 2026
21f630b
Merge main into feature/streaming-pipeline-phase2
aram356 Apr 8, 2026
c6e787a
Merge remote-tracking branch 'origin/specs/streaming-response-optimiz…
aram356 Apr 8, 2026
6e6ac7c
Make NextJsNextDataRewriter fragment-safe for streaming
aram356 Mar 26, 2026
2fb546f
Make GoogleTagManagerIntegration rewrite fragment-safe for streaming
aram356 Mar 26, 2026
41c6bb3
Remove buffered mode from HtmlRewriterAdapter
aram356 Mar 26, 2026
8f171e9
Fix NextJs Keep-after-accumulation dropping intermediate fragments
aram356 Mar 26, 2026
379ff2e
Add 2xx streaming gate, pipeline tests, and small-chunk regression tests
aram356 Mar 26, 2026
dd2f82e
Add Phase 3 results and Phase 4 plan to spec and plan documents
aram356 Mar 27, 2026
bb4c72f
Address PR #591 review feedback
aram356 Mar 31, 2026
ff05483
Clarify Mutex rationale and add multi-element accumulation test
aram356 Apr 9, 2026
48f9bca
Return origin response unchanged for unsupported Content-Encoding
aram356 Apr 16, 2026
1fe1bb4
Merge branch 'main' into specs/streaming-response-optimization
aram356 Apr 16, 2026
c32fe3e
Merge branch 'specs/streaming-response-optimization' into feature/str…
aram356 Apr 16, 2026
ca151e5
Phase 2: Stream responses to client via StreamingBody #585
aram356 Apr 16, 2026
f9c08fa
Phase 3: Make script rewriters fragment-safe for streaming #591
aram356 Apr 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 61 additions & 16 deletions crates/trusted-server-adapter-fastly/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use error_stack::Report;
use fastly::http::Method;
use fastly::{Error, Request, Response};
use fastly::{Request, Response};
use log_fastly::Logger;

use trusted_server_core::auction::endpoints::handle_auction;
Expand All @@ -19,7 +19,9 @@ use trusted_server_core::proxy::{
handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild,
handle_first_party_proxy_sign,
};
use trusted_server_core::publisher::{handle_publisher_request, handle_tsjs_dynamic};
use trusted_server_core::publisher::{
handle_publisher_request, handle_tsjs_dynamic, stream_publisher_body, PublisherResponse,
};
use trusted_server_core::request_signing::{
handle_deactivate_key, handle_rotate_key, handle_trusted_server_discovery,
handle_verify_signature,
Expand All @@ -35,39 +37,53 @@ mod route_tests;
use crate::error::to_error_response;
use crate::platform::{build_runtime_services, open_kv_store, UnavailableKvStore};

#[fastly::main]
fn main(req: Request) -> Result<Response, Error> {
/// Entry point for the Fastly Compute program.
///
/// Uses an undecorated `main()` with `Request::from_client()` instead of
/// `#[fastly::main]` so we can call `stream_to_client()` or `send_to_client()`
/// explicitly. `#[fastly::main]` is syntactic sugar that auto-calls
/// `send_to_client()` on the returned `Response`, which is incompatible with
/// streaming.
fn main() {
init_logger();

let req = Request::from_client();

// Keep the health probe independent from settings loading and routing so
// readiness checks still get a cheap liveness response during startup.
if req.get_method() == Method::GET && req.get_path() == "/health" {
return Ok(Response::from_status(200).with_body_text_plain("ok"));
Response::from_status(200)
.with_body_text_plain("ok")
.send_to_client();
return;
}

let settings = match get_settings() {
Ok(s) => s,
Err(e) => {
log::error!("Failed to load settings: {:?}", e);
return Ok(to_error_response(&e));
to_error_response(&e).send_to_client();
return;
}
};
log::debug!("Settings {settings:?}");

// Build the auction orchestrator once at startup
let orchestrator = match build_orchestrator(&settings) {
Ok(orchestrator) => orchestrator,
Ok(o) => o,
Err(e) => {
log::error!("Failed to build auction orchestrator: {:?}", e);
return Ok(to_error_response(&e));
to_error_response(&e).send_to_client();
return;
}
};

let integration_registry = match IntegrationRegistry::new(&settings) {
Ok(r) => r,
Err(e) => {
log::error!("Failed to create integration registry: {:?}", e);
return Ok(to_error_response(&e));
to_error_response(&e).send_to_client();
return;
}
};

Expand All @@ -78,13 +94,17 @@ fn main(req: Request) -> Result<Response, Error> {
as std::sync::Arc<dyn trusted_server_core::platform::PlatformKvStore>;
let runtime_services = build_runtime_services(&req, kv_store);

futures::executor::block_on(route_request(
// route_request may send the response directly (streaming path) or
// return it for us to send (buffered path).
if let Some(response) = futures::executor::block_on(route_request(
&settings,
&orchestrator,
&integration_registry,
&runtime_services,
req,
))
)) {
response.send_to_client();
}
}

async fn route_request(
Expand All @@ -93,7 +113,7 @@ async fn route_request(
integration_registry: &IntegrationRegistry,
runtime_services: &RuntimeServices,
mut req: Request,
) -> Result<Response, Error> {
) -> Option<Response> {
// Strip client-spoofable forwarded headers at the edge.
// On Fastly this service IS the first proxy — these headers from
// clients are untrusted and can hijack URL rewriting (see #409).
Expand All @@ -115,14 +135,14 @@ async fn route_request(
match enforce_basic_auth(settings, &req) {
Ok(Some(mut response)) => {
finalize_response(settings, geo_info.as_ref(), &mut response);
return Ok(response);
return Some(response);
}
Ok(None) => {}
Err(e) => {
log::error!("Failed to evaluate basic auth: {:?}", e);
let mut response = to_error_response(&e);
finalize_response(settings, geo_info.as_ref(), &mut response);
return Ok(response);
return Some(response);
}
}

Expand Down Expand Up @@ -197,7 +217,32 @@ async fn route_request(
&publisher_services,
req,
) {
Ok(response) => Ok(response),
Ok(PublisherResponse::Stream {
mut response,
body,
params,
}) => {
// Streaming path: finalize headers, then stream body to client.
finalize_response(settings, geo_info.as_ref(), &mut response);
let mut streaming_body = response.stream_to_client();
if let Err(e) = stream_publisher_body(
body,
&mut streaming_body,
&params,
settings,
integration_registry,
) {
// Headers already committed. Log and abort — client
// sees a truncated response. Standard proxy behavior.
log::error!("Streaming processing failed: {e:?}");
drop(streaming_body);
} else if let Err(e) = streaming_body.finish() {
log::error!("Failed to finish streaming body: {e}");
}
// Response already sent via stream_to_client()
return None;
}
Ok(PublisherResponse::Buffered(response)) => Ok(response),
Err(e) => {
log::error!("Failed to proxy to publisher origin: {:?}", e);
Err(e)
Expand All @@ -214,7 +259,7 @@ async fn route_request(

finalize_response(settings, geo_info.as_ref(), &mut response);

Ok(response)
Some(response)
}

fn runtime_services_for_consent_route(
Expand Down
Loading
Loading