Skip to content

Commit c426a57

Browse files
authored
Expose Fastly geo headers on every response (#375)
Expose Fastly geo headers on every response from Trusted Server Add GeoInfo::set_response_headers() to set x-geo-* headers on responses and wire it into the centralized response middleware in main.rs. When geo data is unavailable, sets x-geo-info-available: false. Resolves #280
1 parent 9400fe2 commit c426a57

2 files changed

Lines changed: 182 additions & 67 deletions

File tree

crates/common/src/geo.rs

Lines changed: 154 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
//! information from incoming requests, particularly DMA (Designated Market Area) codes.
55
66
use fastly::geo::geo_lookup;
7-
use fastly::Request;
7+
use fastly::{Request, Response};
88

99
use crate::constants::{
1010
HEADER_X_GEO_CITY, HEADER_X_GEO_CONTINENT, HEADER_X_GEO_COORDINATES, HEADER_X_GEO_COUNTRY,
11-
HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE,
11+
HEADER_X_GEO_INFO_AVAILABLE, HEADER_X_GEO_METRO_CODE, HEADER_X_GEO_REGION,
1212
};
1313

1414
/// Geographic information extracted from a request.
@@ -81,72 +81,163 @@ impl GeoInfo {
8181
pub fn has_metro_code(&self) -> bool {
8282
self.metro_code > 0
8383
}
84+
85+
/// Sets geo information headers on the response.
86+
///
87+
/// Adds `x-geo-city`, `x-geo-country`, `x-geo-continent`, `x-geo-coordinates`,
88+
/// `x-geo-metro-code`, `x-geo-region` (when available), and
89+
/// `x-geo-info-available: true` to the given response.
90+
pub fn set_response_headers(&self, response: &mut Response) {
91+
response.set_header(HEADER_X_GEO_CITY, &self.city);
92+
response.set_header(HEADER_X_GEO_COUNTRY, &self.country);
93+
response.set_header(HEADER_X_GEO_CONTINENT, &self.continent);
94+
response.set_header(HEADER_X_GEO_COORDINATES, self.coordinates_string());
95+
if self.has_metro_code() {
96+
response.set_header(HEADER_X_GEO_METRO_CODE, self.metro_code.to_string());
97+
}
98+
if let Some(ref region) = self.region {
99+
response.set_header(HEADER_X_GEO_REGION, region);
100+
}
101+
response.set_header(HEADER_X_GEO_INFO_AVAILABLE, "true");
102+
}
84103
}
85104

86-
/// Extracts the DMA (Designated Market Area) code from the request's geolocation data.
87-
///
88-
/// This function:
89-
/// 1. Checks if running in Fastly environment
90-
/// 2. Performs geo lookup based on client IP
91-
/// 3. Sets various geo headers on the request
92-
/// 4. Returns the metro code (DMA) if available
93-
///
94-
/// # Arguments
95-
///
96-
/// * `req` - The request to extract DMA code from
97-
///
98-
/// # Returns
99-
///
100-
/// The DMA/metro code as a string if available, None otherwise
101-
pub fn get_dma_code(req: &mut Request) -> Option<String> {
102-
// Debug: Check if we're running in Fastly environment
103-
log::info!("Fastly Environment Check:");
104-
log::info!(
105-
" FASTLY_POP: {}",
106-
std::env::var("FASTLY_POP").unwrap_or_else(|_| "not in Fastly".to_string())
107-
);
108-
log::info!(
109-
" FASTLY_REGION: {}",
110-
std::env::var("FASTLY_REGION").unwrap_or_else(|_| "not in Fastly".to_string())
111-
);
112-
113-
// Get detailed geo information using geo_lookup
114-
if let Some(geo) = req.get_client_ip_addr().and_then(geo_lookup) {
115-
log::info!("Geo Information Found:");
116-
117-
// Set all available geo information in headers
118-
let city = geo.city();
119-
req.set_header(HEADER_X_GEO_CITY, city);
120-
log::info!(" City: {}", city);
121-
122-
let country = geo.country_code();
123-
req.set_header(HEADER_X_GEO_COUNTRY, country);
124-
log::info!(" Country: {}", country);
125-
126-
req.set_header(HEADER_X_GEO_CONTINENT, format!("{:?}", geo.continent()));
127-
log::info!(" Continent: {:?}", geo.continent());
128-
129-
req.set_header(
130-
HEADER_X_GEO_COORDINATES,
131-
format!("{},{}", geo.latitude(), geo.longitude()),
105+
#[cfg(test)]
106+
mod tests {
107+
use super::*;
108+
use fastly::Response;
109+
110+
fn sample_geo_info() -> GeoInfo {
111+
GeoInfo {
112+
city: "San Francisco".to_string(),
113+
country: "US".to_string(),
114+
continent: "NorthAmerica".to_string(),
115+
latitude: 37.7749,
116+
longitude: -122.4194,
117+
metro_code: 807,
118+
region: Some("CA".to_string()),
119+
}
120+
}
121+
122+
#[test]
123+
fn set_response_headers_sets_all_geo_headers() {
124+
let geo = sample_geo_info();
125+
let mut response = Response::new();
126+
127+
geo.set_response_headers(&mut response);
128+
129+
assert_eq!(
130+
response
131+
.get_header(HEADER_X_GEO_CITY)
132+
.expect("should have city header")
133+
.to_str()
134+
.expect("should be valid str"),
135+
"San Francisco",
136+
"should set city header"
137+
);
138+
assert_eq!(
139+
response
140+
.get_header(HEADER_X_GEO_COUNTRY)
141+
.expect("should have country header")
142+
.to_str()
143+
.expect("should be valid str"),
144+
"US",
145+
"should set country header"
146+
);
147+
assert_eq!(
148+
response
149+
.get_header(HEADER_X_GEO_CONTINENT)
150+
.expect("should have continent header")
151+
.to_str()
152+
.expect("should be valid str"),
153+
"NorthAmerica",
154+
"should set continent header"
155+
);
156+
assert_eq!(
157+
response
158+
.get_header(HEADER_X_GEO_COORDINATES)
159+
.expect("should have coordinates header")
160+
.to_str()
161+
.expect("should be valid str"),
162+
"37.7749,-122.4194",
163+
"should set coordinates header"
164+
);
165+
assert_eq!(
166+
response
167+
.get_header(HEADER_X_GEO_METRO_CODE)
168+
.expect("should have metro code header")
169+
.to_str()
170+
.expect("should be valid str"),
171+
"807",
172+
"should set metro code header"
173+
);
174+
assert_eq!(
175+
response
176+
.get_header(HEADER_X_GEO_REGION)
177+
.expect("should have region header")
178+
.to_str()
179+
.expect("should be valid str"),
180+
"CA",
181+
"should set region header"
182+
);
183+
assert_eq!(
184+
response
185+
.get_header(HEADER_X_GEO_INFO_AVAILABLE)
186+
.expect("should have info available header")
187+
.to_str()
188+
.expect("should be valid str"),
189+
"true",
190+
"should set geo info available to true"
132191
);
133-
log::info!(" Location: ({}, {})", geo.latitude(), geo.longitude());
134-
135-
// Get and set the metro code (DMA)
136-
let metro_code = geo.metro_code();
137-
req.set_header(HEADER_X_GEO_METRO_CODE, metro_code.to_string());
138-
log::info!("Found DMA/Metro code: {}", metro_code);
139-
return Some(metro_code.to_string());
140-
} else {
141-
log::info!("No geo information available for the request");
142-
req.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false");
143192
}
144193

145-
// If no metro code is found, log all request headers for debugging
146-
log::info!("No DMA/Metro code found. All request headers:");
147-
for (name, value) in req.get_headers() {
148-
log::info!(" {}: {:?}", name, value);
194+
#[test]
195+
fn set_response_headers_omits_metro_code_when_zero() {
196+
let geo = GeoInfo {
197+
metro_code: 0,
198+
..sample_geo_info()
199+
};
200+
let mut response = Response::new();
201+
202+
geo.set_response_headers(&mut response);
203+
204+
assert!(
205+
response.get_header(HEADER_X_GEO_METRO_CODE).is_none(),
206+
"should not set metro code header when metro_code is 0"
207+
);
208+
assert!(
209+
response.get_header(HEADER_X_GEO_CITY).is_some(),
210+
"should still set city header"
211+
);
149212
}
150213

151-
None
214+
#[test]
215+
fn set_response_headers_omits_region_when_none() {
216+
let geo = GeoInfo {
217+
region: None,
218+
..sample_geo_info()
219+
};
220+
let mut response = Response::new();
221+
222+
geo.set_response_headers(&mut response);
223+
224+
assert!(
225+
response.get_header(HEADER_X_GEO_REGION).is_none(),
226+
"should not set region header when region is None"
227+
);
228+
// Other headers should still be present
229+
assert!(
230+
response.get_header(HEADER_X_GEO_CITY).is_some(),
231+
"should still set city header"
232+
);
233+
assert_eq!(
234+
response
235+
.get_header(HEADER_X_GEO_INFO_AVAILABLE)
236+
.expect("should have info available header")
237+
.to_str()
238+
.expect("should be valid str"),
239+
"true",
240+
"should still set geo info available to true"
241+
);
242+
}
152243
}

crates/fastly/src/main.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use trusted_server_common::auction::endpoints::handle_auction;
77
use trusted_server_common::auction::{build_orchestrator, AuctionOrchestrator};
88
use trusted_server_common::auth::enforce_basic_auth;
99
use trusted_server_common::constants::{
10-
ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_TS_ENV, HEADER_X_TS_VERSION,
10+
ENV_FASTLY_IS_STAGING, ENV_FASTLY_SERVICE_VERSION, HEADER_X_GEO_INFO_AVAILABLE,
11+
HEADER_X_TS_ENV, HEADER_X_TS_VERSION,
1112
};
1213
use trusted_server_common::error::TrustedServerError;
14+
use trusted_server_common::geo::GeoInfo;
1315
use trusted_server_common::integrations::IntegrationRegistry;
1416
use trusted_server_common::proxy::{
1517
handle_first_party_click, handle_first_party_proxy, handle_first_party_proxy_rebuild,
@@ -64,7 +66,11 @@ async fn route_request(
6466
integration_registry: &IntegrationRegistry,
6567
req: Request,
6668
) -> Result<Response, Error> {
67-
if let Some(response) = enforce_basic_auth(settings, &req) {
69+
// Extract geo info before auth check or routing consumes the request
70+
let geo_info = GeoInfo::from_request(&req);
71+
72+
if let Some(mut response) = enforce_basic_auth(settings, &req) {
73+
finalize_response(settings, geo_info.as_ref(), &mut response);
6874
return Ok(response);
6975
}
7076

@@ -132,6 +138,26 @@ async fn route_request(
132138
// Convert any errors to HTTP error responses
133139
let mut response = result.unwrap_or_else(|e| to_error_response(&e));
134140

141+
finalize_response(settings, geo_info.as_ref(), &mut response);
142+
143+
Ok(response)
144+
}
145+
146+
/// Applies all standard response headers: geo, version, staging, and configured headers.
147+
///
148+
/// Called from every response path (including auth early-returns) so that all
149+
/// outgoing responses carry a consistent set of Trusted Server headers.
150+
///
151+
/// Header precedence (last write wins): geo headers are set first, then
152+
/// version/staging, then operator-configured `settings.response_headers`.
153+
/// This means operators can intentionally override any managed header.
154+
fn finalize_response(settings: &Settings, geo_info: Option<&GeoInfo>, response: &mut Response) {
155+
if let Some(geo) = geo_info {
156+
geo.set_response_headers(response);
157+
} else {
158+
response.set_header(HEADER_X_GEO_INFO_AVAILABLE, "false");
159+
}
160+
135161
if let Ok(v) = ::std::env::var(ENV_FASTLY_SERVICE_VERSION) {
136162
response.set_header(HEADER_X_TS_VERSION, v);
137163
}
@@ -142,8 +168,6 @@ async fn route_request(
142168
for (key, value) in &settings.response_headers {
143169
response.set_header(key, value);
144170
}
145-
146-
Ok(response)
147171
}
148172

149173
fn init_logger() {

0 commit comments

Comments
 (0)