Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions crates/common/src/cookies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,21 @@ pub fn create_synthetic_cookie(settings: &Settings, synthetic_id: &str) -> Strin
)
}

/// Sets the synthetic ID cookie on the given response.
///
/// This helper abstracts the logic of creating the cookie string and appending
/// the Set-Cookie header to the response.
pub fn set_synthetic_cookie(
settings: &Settings,
response: &mut fastly::Response,
synthetic_id: &str,
) {
response.append_header(
header::SET_COOKIE,
create_synthetic_cookie(settings, synthetic_id),
);
}

#[cfg(test)]
mod tests {
use crate::test_support::tests::create_test_settings;
Expand Down
195 changes: 194 additions & 1 deletion crates/common/src/integrations/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ use fastly::http::Method;
use fastly::{Request, Response};
use matchit::Router;

use crate::constants::HEADER_X_SYNTHETIC_ID;
use crate::error::TrustedServerError;
use crate::settings::Settings;
use crate::synthetic::get_or_generate_synthetic_id;

/// Action returned by attribute rewriters to describe how the runtime should mutate the element.
#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -602,6 +604,9 @@ impl IntegrationRegistry {
}

/// Dispatch a proxy request when an integration handles the path.
///
/// This method automatically sets the `x-synthetic-id` header and
/// `synthetic_id` cookie on successful responses.
#[must_use]
pub async fn handle_proxy(
&self,
Expand All @@ -611,7 +616,19 @@ impl IntegrationRegistry {
req: Request,
) -> Option<Result<Response, Report<TrustedServerError>>> {
if let Some((proxy, _)) = self.find_route(method, path) {
Some(proxy.handle(settings, req).await)
// Generate synthetic ID before consuming request
let synthetic_id_result = get_or_generate_synthetic_id(settings, &req);

let mut result = proxy.handle(settings, req).await;

// Set synthetic ID header on successful responses
if let Ok(ref mut response) = result {
if let Ok(ref synthetic_id) = synthetic_id_result {
response.set_header(HEADER_X_SYNTHETIC_ID, synthetic_id.as_str());
crate::cookies::set_synthetic_cookie(settings, response, synthetic_id.as_str());
}
}
Some(result)
} else {
None
}
Expand Down Expand Up @@ -1042,4 +1059,180 @@ mod tests {
assert!(!registry.has_route(&Method::GET, "/integrations/test/users"));
assert!(!registry.has_route(&Method::POST, "/integrations/test/users"));
}

Copy link
Collaborator

Choose a reason for hiding this comment

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

🔧 Duplicate test

These three cookie_jar_* tests are testing parse_cookies_to_jar from cookies.rs — they should live there, not in the registry module. cookies.rs already has equivalent coverage (test_parse_cookies_to_jar, test_parse_cookies_to_jar_emtpy, etc.), so these are duplicates and can be removed.

// Tests for synthetic ID header on proxy responses
use crate::constants::COOKIE_SYNTHETIC_ID;
use crate::test_support::tests::create_test_settings;
use fastly::http::header;

/// Mock proxy that returns a simple 200 OK response
struct SyntheticIdTestProxy;

#[async_trait(?Send)]
impl IntegrationProxy for SyntheticIdTestProxy {
fn integration_name(&self) -> &'static str {
"synthetic_id_test"
}

fn routes(&self) -> Vec<IntegrationEndpoint> {
vec![
IntegrationEndpoint {
method: Method::GET,
path: "/integrations/test/synthetic".to_string(),
},
IntegrationEndpoint {
method: Method::POST,
path: "/integrations/test/synthetic".to_string(),
},
]
}

async fn handle(
&self,
_settings: &Settings,
_req: Request,
) -> Result<Response, Report<TrustedServerError>> {
// Return a simple response without the synthetic ID header.
// The registry's handle_proxy should add it.
Ok(Response::from_status(fastly::http::StatusCode::OK).with_body("test response"))
}
}

#[test]
fn handle_proxy_sets_synthetic_id_header_on_response() {
let settings = create_test_settings();
let routes = vec![(
Method::GET,
"/integrations/test/synthetic",
(
Arc::new(SyntheticIdTestProxy) as Arc<dyn IntegrationProxy>,
"synthetic_id_test",
),
)];
let registry = IntegrationRegistry::from_routes(routes);

// Create a request without a synthetic ID cookie
let req = Request::get("https://test-publisher.com/integrations/test/synthetic");

// Call handle_proxy (uses futures executor in test environment)
let result = futures::executor::block_on(registry.handle_proxy(
&Method::GET,
"/integrations/test/synthetic",
&settings,
req,
));

// Should have matched and returned a response
assert!(result.is_some(), "Should find route and handle request");
let response = result.unwrap();
assert!(response.is_ok(), "Handler should succeed");

let response = response.unwrap();

// Verify x-synthetic-id header is present
assert!(
response.get_header(HEADER_X_SYNTHETIC_ID).is_some(),
"Response should have x-synthetic-id header"
);

// Verify Set-Cookie header is present (since no cookie was in request)
let set_cookie = response.get_header(header::SET_COOKIE);
assert!(
set_cookie.is_some(),
"Response should have Set-Cookie header for synthetic_id"
);

let cookie_value = set_cookie.unwrap().to_str().unwrap();
assert!(
cookie_value.contains(COOKIE_SYNTHETIC_ID),
"Set-Cookie should contain synthetic_id cookie, got: {}",
cookie_value
);
}

#[test]
fn handle_proxy_always_sets_cookie() {
let settings = create_test_settings();
let routes = vec![(
Method::GET,
"/integrations/test/synthetic",
(
Arc::new(SyntheticIdTestProxy) as Arc<dyn IntegrationProxy>,
"test",
),
)];

let registry = IntegrationRegistry::from_routes(routes);

let mut req = Request::get("https://test.example.com/integrations/test/synthetic");
// Pre-existing cookie
req.set_header(header::COOKIE, "synthetic_id=existing_id_12345");

let result = futures::executor::block_on(registry.handle_proxy(
&Method::GET,
"/integrations/test/synthetic",
&settings,
req,
))
.expect("should handle proxy request");

let response = result.expect("proxy handle should succeed");

// Should still have x-synthetic-id header
assert!(
response.get_header(HEADER_X_SYNTHETIC_ID).is_some(),
"Response should still have x-synthetic-id header"
);

// Should ALWAYS set the cookie again (per new requirements)
let set_cookie = response.get_header(header::SET_COOKIE);

assert!(
set_cookie.is_some(),
"Should set Set-Cookie header even if cookie is present"
);

if let Some(cookie) = set_cookie {
let cookie_str = cookie.to_str().unwrap_or("");
assert!(
cookie_str.contains(COOKIE_SYNTHETIC_ID),
"Should contain synthetic_id cookie, got: {}",
cookie_str
);
}
}

#[test]
fn handle_proxy_works_with_post_method() {
let settings = create_test_settings();
let routes = vec![(
Method::POST,
"/integrations/test/synthetic",
(
Arc::new(SyntheticIdTestProxy) as Arc<dyn IntegrationProxy>,
"synthetic_id_test",
),
)];
let registry = IntegrationRegistry::from_routes(routes);

let req = Request::post("https://test-publisher.com/integrations/test/synthetic")
.with_body("test body");

let result = futures::executor::block_on(registry.handle_proxy(
&Method::POST,
"/integrations/test/synthetic",
&settings,
req,
));

assert!(result.is_some(), "Should find POST route");
let response = result.unwrap();
assert!(response.is_ok(), "Handler should succeed");

let response = response.unwrap();
assert!(
response.get_header(HEADER_X_SYNTHETIC_ID).is_some(),
"POST response should have x-synthetic-id header"
);
}
}
2 changes: 0 additions & 2 deletions crates/common/src/integrations/testlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use validator::Validate;

use crate::constants::HEADER_X_SYNTHETIC_ID;
use crate::error::TrustedServerError;
use crate::integrations::{
AttributeRewriteAction, IntegrationAttributeContext, IntegrationAttributeRewriter,
Expand Down Expand Up @@ -175,7 +174,6 @@ impl IntegrationProxy for TestlightIntegration {
}
}

response.set_header(HEADER_X_SYNTHETIC_ID, &synthetic_id);
Ok(response)
}
}
Expand Down
28 changes: 4 additions & 24 deletions crates/common/src/publisher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ use fastly::{Body, Request, Response};
use crate::backend::ensure_backend_from_url;
use crate::http_util::{serve_static_with_etag, RequestInfo};

use crate::constants::{COOKIE_SYNTHETIC_ID, HEADER_X_COMPRESS_HINT, HEADER_X_SYNTHETIC_ID};
use crate::cookies::create_synthetic_cookie;
use crate::constants::{HEADER_X_COMPRESS_HINT, HEADER_X_SYNTHETIC_ID};
use crate::cookies::set_synthetic_cookie;
use crate::error::TrustedServerError;
use crate::integrations::IntegrationRegistry;
use crate::rsc_flight::RscFlightUrlRewriter;
Expand Down Expand Up @@ -198,23 +198,8 @@ pub fn handle_publisher_request(

// Generate synthetic identifiers before the request body is consumed.
let synthetic_id = get_or_generate_synthetic_id(settings, &req)?;
let has_synthetic_cookie = req
.get_header(header::COOKIE)
.and_then(|h| h.to_str().ok())
.map(|cookies| {
cookies.split(';').any(|cookie| {
cookie
.trim_start()
.starts_with(&format!("{}=", COOKIE_SYNTHETIC_ID))
})
})
.unwrap_or(false);

log::debug!(
"Proxy synthetic IDs - trusted: {}, has_cookie: {}",
synthetic_id,
has_synthetic_cookie
);
log::debug!("Proxy synthetic IDs - trusted: {}", synthetic_id,);

let backend_name = ensure_backend_from_url(&settings.publisher.origin_url)?;
let origin_host = settings.publisher.origin_host();
Expand Down Expand Up @@ -308,12 +293,7 @@ pub fn handle_publisher_request(
}

response.set_header(HEADER_X_SYNTHETIC_ID, synthetic_id.as_str());
if !has_synthetic_cookie {
response.set_header(
header::SET_COOKIE,
create_synthetic_cookie(settings, synthetic_id.as_str()),
);
}
set_synthetic_cookie(settings, &mut response, synthetic_id.as_str());

Ok(response)
}
Expand Down