Skip to content

Commit 1973b5d

Browse files
committed
Resolve PR 628 round-1 review findings
Blocking: - Default max_buffered_body_bytes to Some(16 MiB) so EdgeZero flag-flip fails-safe; operators must opt out for pages larger than 16 MiB Non-blocking: - Rename response_was_finalized_by_middleware to take_finalize_sentinel so the side-effecting header removal is visible in the name - Inline apply_entry_point_finalize at its single call site; remove the thin wrapper function; update the test to call resolve_geo_for_response and apply_finalize_headers directly - Extract build_state_from_settings(settings) so build_state() and the test-only app_state_for_settings() share a single constructor path - BoundedWriter: use std::io::Error::other() for consistency - Convert named_routes() -> [NamedRoute; 9] to const NAMED_ROUTES: &[NamedRoute]; removes the size literal coupling and avoids a stack copy on every call
1 parent 189c57c commit 1973b5d

4 files changed

Lines changed: 78 additions & 99 deletions

File tree

crates/trusted-server-adapter-fastly/src/app.rs

Lines changed: 55 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,15 @@ pub(crate) struct AppState {
9898
/// Returns an error when settings, the auction orchestrator, or the integration
9999
/// registry fail to initialise.
100100
pub(crate) fn build_state() -> Result<Arc<AppState>, Report<TrustedServerError>> {
101-
let settings = get_settings()?;
101+
build_state_from_settings(get_settings()?)
102+
}
102103

104+
pub(crate) fn build_state_from_settings(
105+
settings: Settings,
106+
) -> Result<Arc<AppState>, Report<TrustedServerError>> {
103107
let orchestrator = build_orchestrator(&settings)?;
104-
105108
let registry = IntegrationRegistry::new(&settings)?;
106-
107109
let kv_store = Arc::new(UnavailableKvStore) as Arc<dyn PlatformKvStore>;
108-
109110
Ok(Arc::new(AppState {
110111
settings: Arc::new(settings),
111112
orchestrator: Arc::new(orchestrator),
@@ -318,55 +319,53 @@ struct NamedRoute {
318319
handler: NamedRouteHandler,
319320
}
320321

321-
fn named_routes() -> [NamedRoute; 9] {
322-
[
323-
NamedRoute {
324-
path: "/.well-known/trusted-server.json",
325-
primary_methods: &[Method::GET],
326-
handler: NamedRouteHandler::TrustedServerDiscovery,
327-
},
328-
NamedRoute {
329-
path: "/verify-signature",
330-
primary_methods: &[Method::POST],
331-
handler: NamedRouteHandler::VerifySignature,
332-
},
333-
NamedRoute {
334-
path: "/admin/keys/rotate",
335-
primary_methods: &[Method::POST],
336-
handler: NamedRouteHandler::RotateKey,
337-
},
338-
NamedRoute {
339-
path: "/admin/keys/deactivate",
340-
primary_methods: &[Method::POST],
341-
handler: NamedRouteHandler::DeactivateKey,
342-
},
343-
NamedRoute {
344-
path: "/auction",
345-
primary_methods: &[Method::POST],
346-
handler: NamedRouteHandler::Auction,
347-
},
348-
NamedRoute {
349-
path: "/first-party/proxy",
350-
primary_methods: &[Method::GET],
351-
handler: NamedRouteHandler::FirstPartyProxy,
352-
},
353-
NamedRoute {
354-
path: "/first-party/click",
355-
primary_methods: &[Method::GET],
356-
handler: NamedRouteHandler::FirstPartyClick,
357-
},
358-
NamedRoute {
359-
path: "/first-party/sign",
360-
primary_methods: &[Method::GET, Method::POST],
361-
handler: NamedRouteHandler::FirstPartySign,
362-
},
363-
NamedRoute {
364-
path: "/first-party/proxy-rebuild",
365-
primary_methods: &[Method::POST],
366-
handler: NamedRouteHandler::FirstPartyProxyRebuild,
367-
},
368-
]
369-
}
322+
const NAMED_ROUTES: &[NamedRoute] = &[
323+
NamedRoute {
324+
path: "/.well-known/trusted-server.json",
325+
primary_methods: &[Method::GET],
326+
handler: NamedRouteHandler::TrustedServerDiscovery,
327+
},
328+
NamedRoute {
329+
path: "/verify-signature",
330+
primary_methods: &[Method::POST],
331+
handler: NamedRouteHandler::VerifySignature,
332+
},
333+
NamedRoute {
334+
path: "/admin/keys/rotate",
335+
primary_methods: &[Method::POST],
336+
handler: NamedRouteHandler::RotateKey,
337+
},
338+
NamedRoute {
339+
path: "/admin/keys/deactivate",
340+
primary_methods: &[Method::POST],
341+
handler: NamedRouteHandler::DeactivateKey,
342+
},
343+
NamedRoute {
344+
path: "/auction",
345+
primary_methods: &[Method::POST],
346+
handler: NamedRouteHandler::Auction,
347+
},
348+
NamedRoute {
349+
path: "/first-party/proxy",
350+
primary_methods: &[Method::GET],
351+
handler: NamedRouteHandler::FirstPartyProxy,
352+
},
353+
NamedRoute {
354+
path: "/first-party/click",
355+
primary_methods: &[Method::GET],
356+
handler: NamedRouteHandler::FirstPartyClick,
357+
},
358+
NamedRoute {
359+
path: "/first-party/sign",
360+
primary_methods: &[Method::GET, Method::POST],
361+
handler: NamedRouteHandler::FirstPartySign,
362+
},
363+
NamedRoute {
364+
path: "/first-party/proxy-rebuild",
365+
primary_methods: &[Method::POST],
366+
handler: NamedRouteHandler::FirstPartyProxyRebuild,
367+
},
368+
];
370369

371370
fn named_route_handler(
372371
state: Arc<AppState>,
@@ -463,7 +462,7 @@ impl TrustedServerApp {
463462
// named route is registered from this single table, then every
464463
// non-primary publisher fallback method is registered from the same
465464
// row. Adding a named route now requires editing only this table.
466-
for route in named_routes() {
465+
for route in NAMED_ROUTES {
467466
for method in route.primary_methods {
468467
router = router.route(
469468
route.path,
@@ -510,7 +509,7 @@ impl Hooks for TrustedServerApp {
510509

511510
#[cfg(test)]
512511
mod tests {
513-
use super::{startup_error_router, AppState, TrustedServerApp};
512+
use super::{build_state_from_settings, startup_error_router, AppState, TrustedServerApp};
514513

515514
use std::sync::Arc;
516515

@@ -520,11 +519,8 @@ mod tests {
520519
use error_stack::Report;
521520
use futures::executor::block_on;
522521
use serde_json::json;
523-
use trusted_server_core::auction::build_orchestrator;
524522
use trusted_server_core::constants::HEADER_X_GEO_INFO_AVAILABLE;
525523
use trusted_server_core::error::TrustedServerError;
526-
use trusted_server_core::integrations::IntegrationRegistry;
527-
use trusted_server_core::platform::PlatformKvStore;
528524
use trusted_server_core::settings::Settings;
529525

530526
fn settings_with_missing_consent_store() -> Settings {
@@ -566,17 +562,7 @@ mod tests {
566562
}
567563

568564
fn app_state_for_settings(settings: Settings) -> Arc<AppState> {
569-
let orchestrator =
570-
build_orchestrator(&settings).expect("should build auction orchestrator");
571-
let registry = IntegrationRegistry::new(&settings).expect("should create registry");
572-
let kv_store = Arc::new(crate::platform::UnavailableKvStore) as Arc<dyn PlatformKvStore>;
573-
574-
Arc::new(AppState {
575-
settings: Arc::new(settings),
576-
orchestrator: Arc::new(orchestrator),
577-
registry: Arc::new(registry),
578-
kv_store,
579-
})
565+
build_state_from_settings(settings).expect("should build app state from settings")
580566
}
581567

582568
fn empty_request(method: Method, uri: &str) -> edgezero_core::http::Request {

crates/trusted-server-adapter-fastly/src/main.rs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use std::io::Write;
2-
use std::net::IpAddr;
2+
33
use std::sync::Arc;
44

55
use edgezero_adapter_fastly::FastlyConfigStore;
@@ -215,20 +215,21 @@ fn edgezero_main(mut req: FastlyRequest, config_store: ConfigStoreHandle) {
215215
}
216216
};
217217

218-
if !response_was_finalized_by_middleware(&mut response) {
218+
if !take_finalize_sentinel(&mut response) {
219219
// Apply finalize headers at the entry point so that router-level
220220
// 405/404 responses for unregistered HTTP methods (e.g. TRACE, WebDAV
221221
// verbs) carry TS/geo headers. Middleware-finalized responses are
222222
// skipped here to avoid a second settings read and geo lookup on the
223223
// normal registered-route path.
224224
match get_settings() {
225225
Ok(settings) => {
226-
apply_entry_point_finalize(&settings, client_ip, &mut response, |client_ip| {
226+
let geo_info = resolve_geo_for_response(&response, client_ip, |client_ip| {
227227
FastlyPlatformGeo.lookup(client_ip).unwrap_or_else(|e| {
228228
log::warn!("entry-point geo lookup failed: {e}");
229229
None
230230
})
231-
})
231+
});
232+
apply_finalize_headers(&settings, geo_info.as_ref(), &mut response);
232233
}
233234
Err(e) => {
234235
log::warn!("entry-point finalize skipped: failed to reload settings: {e:?}");
@@ -239,29 +240,17 @@ fn edgezero_main(mut req: FastlyRequest, config_store: ConfigStoreHandle) {
239240
compat::to_fastly_response(response).send_to_client();
240241
}
241242

242-
fn response_was_finalized_by_middleware(response: &mut HttpResponse) -> bool {
243+
fn take_finalize_sentinel(response: &mut HttpResponse) -> bool {
243244
response
244245
.headers_mut()
245246
.remove(HEADER_X_TS_FINALIZED)
246247
.is_some()
247248
}
248249

249-
fn apply_entry_point_finalize<F>(
250-
settings: &Settings,
251-
client_ip: Option<IpAddr>,
252-
response: &mut HttpResponse,
253-
lookup_geo: F,
254-
) where
255-
F: FnOnce(Option<IpAddr>) -> Option<GeoInfo>,
256-
{
257-
let geo_info = resolve_geo_for_response(response, client_ip, lookup_geo);
258-
apply_finalize_headers(settings, geo_info.as_ref(), response);
259-
}
260-
261250
/// Handles a request using the original Fastly-native entry point.
262251
///
263252
/// Preserves identical semantics to the pre-PR14 `main()`. Called whenever
264-
/// the EdgeZero flag is disabled or cannot be read/parsed as enabled — that
253+
/// the `EdgeZero` flag is disabled or cannot be read/parsed as enabled — that
265254
/// includes config-store open failures, key-read errors, missing keys, and
266255
/// any value other than the accepted `"true"` / `"1"` forms.
267256
///
@@ -552,8 +541,7 @@ impl BoundedWriter {
552541
impl Write for BoundedWriter {
553542
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
554543
if self.inner.len() + buf.len() > self.limit {
555-
return Err(std::io::Error::new(
556-
std::io::ErrorKind::Other,
544+
return Err(std::io::Error::other(
557545
"publisher body exceeded maximum buffered size",
558546
));
559547
}
@@ -704,14 +692,14 @@ mod tests {
704692
}
705693

706694
#[test]
707-
fn response_was_finalized_by_middleware_strips_sentinel() {
695+
fn take_finalize_sentinel_strips_sentinel() {
708696
let mut response = HttpResponse::new(EdgeBody::empty());
709697
response
710698
.headers_mut()
711699
.insert("x-ts-finalized", HeaderValue::from_static("1"));
712700

713701
assert!(
714-
response_was_finalized_by_middleware(&mut response),
702+
take_finalize_sentinel(&mut response),
715703
"should detect middleware-finalized responses"
716704
);
717705
assert!(
@@ -729,9 +717,10 @@ mod tests {
729717
.body(EdgeBody::empty())
730718
.expect("should build response");
731719

732-
apply_entry_point_finalize(&settings, None, &mut response, |_| {
720+
let geo_info = resolve_geo_for_response(&response, None, |_| {
733721
panic!("should skip entry-point geo lookup for 401 responses");
734722
});
723+
apply_finalize_headers(&settings, geo_info.as_ref(), &mut response);
735724

736725
assert_eq!(
737726
response

crates/trusted-server-core/src/http_util.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ fn normalize_scheme(value: &str) -> Option<String> {
169169
/// 1. `ClientInfo` TLS fields populated at the adapter entry point (most reliable)
170170
/// 2. Forwarded header (RFC 7239)
171171
/// 3. X-Forwarded-Proto header
172-
/// 4. Fastly-SSL header (trusted on EdgeZero path; can be spoofed on legacy path)
172+
/// 4. Fastly-SSL header (trusted on `EdgeZero` path; can be spoofed on legacy path)
173173
/// 5. Default to HTTP
174174
fn detect_request_scheme(
175175
req: &Request<EdgeBody>,
@@ -210,7 +210,7 @@ fn detect_request_scheme(
210210
}
211211
}
212212

213-
// 4. Check Fastly-SSL header. On the EdgeZero path this is injected from
213+
// 4. Check Fastly-SSL header. On the `EdgeZero` path this is injected from
214214
// authoritative Fastly TLS metadata after spoofable headers are stripped,
215215
// so it is reliable. On direct or legacy paths it can be spoofed by clients.
216216
if let Some(ssl) = req.headers().get("fastly-ssl") {

crates/trusted-server-core/src/settings.rs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ pub struct Publisher {
2828
/// Keep this secret stable to allow existing links to decode.
2929
#[validate(custom(function = validate_redacted_not_empty))]
3030
pub proxy_secret: Redacted<String>,
31-
/// Maximum number of bytes buffered when the EdgeZero publisher fallback processes
32-
/// a streaming response. When `None` (the default), buffering is unbounded and limited
33-
/// only by the Wasm heap. Set to a byte limit to cap per-request allocation and return
34-
/// a 500 when the processed body exceeds the cap.
35-
#[serde(default)]
31+
/// Maximum number of bytes buffered when the `EdgeZero` publisher fallback processes
32+
/// a streaming response. Defaults to 16 MiB — a conservative cap that prevents
33+
/// Wasm-heap OOM at flag-flip. Set explicitly to a larger value or `null` (unbounded)
34+
/// when the deployment serves publisher pages larger than 16 MiB.
35+
#[serde(default = "default_max_buffered_body_bytes")]
3636
pub max_buffered_body_bytes: Option<usize>,
3737
}
3838

39+
fn default_max_buffered_body_bytes() -> Option<usize> {
40+
Some(16 * 1024 * 1024)
41+
}
42+
3943
impl Publisher {
4044
/// Known placeholder values that must not be used in production.
4145
pub const PROXY_SECRET_PLACEHOLDERS: &[&str] = &["change-me-proxy-secret", "proxy-secret"];

0 commit comments

Comments
 (0)