Skip to content

Commit 8a83c5e

Browse files
committed
Merge feature/edgezero-pr13-integration-provider-type-migration into PR14
Conflicts resolved: - .claude/settings.json: take PR13's additional MCP allowlist entry - main.rs (doc comment): merge AuthChallenge variant doc from PR13 into PR14's "legacy routes" wording - main.rs (match arm): take PR13's Buffered | AuthChallenge combined arm; keep PR14's state.settings reference (legacy_main uses AppState) - sourcepoint.rs: take PR13's collect_response_bounded fix; PR14 HEAD still had the old empty-body-on-stream bug that PR13 addressed
2 parents 1973b5d + 2b17d4c commit 8a83c5e

18 files changed

Lines changed: 183 additions & 120 deletions

File tree

.claude/settings.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,31 @@
2222
"Bash(git branch:*)",
2323
"Bash(git diff:*)",
2424
"Bash(git log:*)",
25+
"Bash(git show:*)",
2526
"Bash(git status:*)",
27+
"Bash(gh pr view:*)",
28+
"Bash(gh pr list:*)",
29+
"Bash(gh pr diff:*)",
30+
"Bash(gh pr checks:*)",
31+
"Bash(gh issue view:*)",
32+
"Bash(gh issue list:*)",
33+
"Bash(gh run view:*)",
34+
"Bash(gh run list:*)",
35+
"Bash(gh repo view:*)",
2636
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__new_page",
37+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_pages",
38+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__select_page",
39+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__navigate_page",
40+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__take_screenshot",
41+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__take_snapshot",
42+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_console_messages",
43+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__get_console_message",
44+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__list_network_requests",
45+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__get_network_request",
46+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__wait_for",
47+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_start_trace",
2748
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_stop_trace",
49+
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__performance_analyze_insight",
2850
"mcp__plugin_chrome-devtools-mcp_chrome-devtools__evaluate_script"
2951
]
3052
},

CLAUDE.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,14 @@ impl core::error::Error for MyError {}
209209
- Format messages with present-tense verbs.
210210
- Use `log-fastly` as the backend for Fastly Compute.
211211

212+
## Other guidelines
213+
214+
- Use only example or fictional information in comments, tests, docs, examples,
215+
and similar non-runtime materials. (eg. for urls use: example.com domains only)
216+
- Do not write or commit real domains, customer names, credentials,
217+
configuration values, or other potentially sensitive real-world information in
218+
comments, tests, docs, or examples.
219+
212220
---
213221

214222
## Git Commit Conventions
@@ -382,6 +390,7 @@ both runtime behavior and build/tooling changes.
382390
- Do not use `unwrap()` in production code — use `expect("should ...")`.
383391
- Do not use thiserror — use `derive_more::Display` + `impl Error`.
384392
- Do not use wildcard imports (except `use super::*` in test modules).
385-
- Do not commit `.env` files or secrets.
393+
- Do not commit `.env` files, secrets, or potentially sensitive real-world
394+
information in comments, tests, docs, examples, or configuration files.
386395
- Do not make large refactors without approval.
387396
- Always run tests and linting before committing.

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

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,13 @@ const EDGEZERO_ENABLED_KEY: &str = "edgezero_enabled";
5858
///
5959
/// The streaming arm keeps the publisher body out of WASM heap until it is written directly
6060
/// to the client via [`fastly::Response::stream_to_client`]. All other legacy routes are buffered.
61+
///
62+
/// [`AuthChallenge`](HandlerOutcome::AuthChallenge) marks responses produced by this server's
63+
/// own `enforce_basic_auth` so the geo-lookup gate can distinguish them from origin-forwarded
64+
/// 401s, which should still carry geo headers.
6165
enum HandlerOutcome {
6266
Buffered(HttpResponse),
67+
AuthChallenge(HttpResponse),
6368
Streaming {
6469
response: HttpResponse,
6570
body: EdgeBody,
@@ -68,9 +73,10 @@ enum HandlerOutcome {
6873
}
6974

7075
impl HandlerOutcome {
76+
#[cfg(test)]
7177
fn status(&self) -> edgezero_core::http::StatusCode {
7278
match self {
73-
HandlerOutcome::Buffered(resp) => resp.status(),
79+
HandlerOutcome::Buffered(resp) | HandlerOutcome::AuthChallenge(resp) => resp.status(),
7480
HandlerOutcome::Streaming { response, .. } => response.status(),
7581
}
7682
}
@@ -296,8 +302,10 @@ fn legacy_main(mut req: FastlyRequest) {
296302
))
297303
.unwrap_or_else(|e| HandlerOutcome::Buffered(http_error_response(&e)));
298304

299-
// Skip geo lookup for 401s: avoids exposing geo headers to unauthenticated callers.
300-
let geo_info = if outcome.status() == edgezero_core::http::StatusCode::UNAUTHORIZED {
305+
// Skip geo lookup for our own auth challenges: avoids exposing geo headers to
306+
// unauthenticated callers. Origin-forwarded 401s are not AuthChallenge and
307+
// do receive geo headers — the client already reached the origin anyway.
308+
let geo_info = if matches!(outcome, HandlerOutcome::AuthChallenge(_)) {
301309
None
302310
} else {
303311
runtime_services
@@ -310,7 +318,7 @@ fn legacy_main(mut req: FastlyRequest) {
310318
};
311319

312320
match outcome {
313-
HandlerOutcome::Buffered(mut response) => {
321+
HandlerOutcome::Buffered(mut response) | HandlerOutcome::AuthChallenge(mut response) => {
314322
finalize_response(&state.settings, geo_info.as_ref(), &mut response);
315323
compat::to_fastly_response(response).send_to_client();
316324
}
@@ -336,9 +344,9 @@ fn legacy_main(mut req: FastlyRequest) {
336344
}
337345
Err(e) => {
338346
log::error!("streaming processing failed: {e:?}");
339-
if let Err(finish_err) = streaming_body.finish() {
340-
log::error!("failed to finish streaming body after error: {finish_err}");
341-
}
347+
// Headers already committed. Drop the body so the client sees a
348+
// truncated response (EOF mid-stream) — standard proxy behavior.
349+
drop(streaming_body);
342350
}
343351
}
344352
}
@@ -398,7 +406,7 @@ async fn route_request(
398406
// Keep this fallback so manually-constructed or otherwise unprepared
399407
// settings still become an error response instead of panicking.
400408
match enforce_basic_auth(settings, &req) {
401-
Ok(Some(response)) => return Ok(HandlerOutcome::Buffered(response)),
409+
Ok(Some(response)) => return Ok(HandlerOutcome::AuthChallenge(response)),
402410
Ok(None) => {}
403411
Err(e) => return Err(e),
404412
}

crates/trusted-server-core/src/auction/orchestrator.rs

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,9 +434,30 @@ impl AuctionOrchestrator {
434434
}
435435
}
436436
Err(e) => {
437-
// When select() returns an error, we can't easily identify which
438-
// provider failed since the PendingRequest is consumed
439-
log::warn!("A provider request failed: {:?}", e);
437+
// Identify the failed provider by finding the backend_to_provider
438+
// entry whose backend name is no longer present in `remaining`
439+
// (select() consumed exactly one pending request — the failed one).
440+
let remaining_backends: std::collections::HashSet<&str> =
441+
remaining.iter().filter_map(|r| r.backend_name()).collect();
442+
let failed_backend = backend_to_provider
443+
.keys()
444+
.find(|name| !remaining_backends.contains(name.as_str()))
445+
.cloned();
446+
447+
if let Some(ref backend_name) = failed_backend {
448+
if let Some((provider_name, start_time, _)) =
449+
backend_to_provider.remove(backend_name)
450+
{
451+
let response_time_ms = start_time.elapsed().as_millis() as u64;
452+
log::warn!("Provider '{}' request failed: {:?}", provider_name, e);
453+
responses.push(AuctionResponse::error(provider_name, response_time_ms));
454+
}
455+
} else {
456+
log::warn!(
457+
"A provider request failed (backend not identified): {:?}",
458+
e
459+
);
460+
}
440461
}
441462
}
442463

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

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,10 @@ pub fn to_fastly_request(req: http::Request<EdgeBody>) -> fastly::Request {
9393

9494
/// Convert a borrowed `http::Request<EdgeBody>` into a `fastly::Request`.
9595
///
96-
/// Headers, method, and URI are copied; the body is empty.
96+
/// Headers, method, and URI are copied; the body is always empty. This function
97+
/// requires that the caller has already consumed or discarded the body — passing
98+
/// a request with a non-empty body is a caller error: the body bytes will be
99+
/// silently lost with no warning or panic.
97100
///
98101
/// # PR 15 removal target
99102
pub fn to_fastly_request_ref(req: &http::Request<EdgeBody>) -> fastly::Request {
@@ -640,6 +643,38 @@ mod tests {
640643
);
641644
}
642645

646+
#[test]
647+
fn to_fastly_response_skeleton_copies_status_and_headers_discards_body() {
648+
let http_resp = http::Response::builder()
649+
.status(206)
650+
.header("content-type", "text/html; charset=utf-8")
651+
.header("x-custom", "value")
652+
.body(EdgeBody::from(b"some body bytes".as_ref()))
653+
.expect("should build response");
654+
655+
let fastly_resp = to_fastly_response_skeleton(http_resp);
656+
657+
assert_eq!(
658+
fastly_resp.get_status().as_u16(),
659+
206,
660+
"should copy status code"
661+
);
662+
assert_eq!(
663+
fastly_resp
664+
.get_header("content-type")
665+
.and_then(|v| v.to_str().ok()),
666+
Some("text/html; charset=utf-8"),
667+
"should copy content-type header"
668+
);
669+
assert_eq!(
670+
fastly_resp
671+
.get_header("x-custom")
672+
.and_then(|v| v.to_str().ok()),
673+
Some("value"),
674+
"should copy custom header"
675+
);
676+
}
677+
643678
#[test]
644679
fn to_fastly_request_with_streaming_body_produces_empty_body() {
645680
// Stream bodies cannot cross the compat boundary: the Fastly SDK has no

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
22
use chacha20poly1305::{aead::Aead, aead::KeyInit, XChaCha20Poly1305, XNonce};
33
use edgezero_core::body::Body as EdgeBody;
4+
use error_stack::Report;
45
use http::{header, Request, Response, StatusCode};
56
use sha2::{Digest, Sha256};
67
use subtle::ConstantTimeEq as _;
78

89
use crate::constants::INTERNAL_HEADERS;
10+
use crate::error::TrustedServerError;
911
use crate::platform::ClientInfo;
1012
use crate::settings::Settings;
1113

@@ -401,6 +403,27 @@ pub fn compute_encrypted_sha256_token(settings: &Settings, full_url: &str) -> St
401403
URL_SAFE_NO_PAD.encode(digest)
402404
}
403405

406+
/// Return an error if `bytes` exceeds `limit`.
407+
///
408+
/// # Errors
409+
///
410+
/// Returns [`TrustedServerError::RequestTooLarge`] when `bytes.len() > limit`.
411+
pub fn enforce_max_body_size(
412+
bytes: &[u8],
413+
limit: usize,
414+
what: &str,
415+
) -> Result<(), Report<TrustedServerError>> {
416+
if bytes.len() > limit {
417+
return Err(Report::new(TrustedServerError::RequestTooLarge {
418+
message: format!(
419+
"{what} payload {} exceeds limit of {limit} bytes",
420+
bytes.len()
421+
),
422+
}));
423+
}
424+
Ok(())
425+
}
426+
404427
#[cfg(test)]
405428
mod tests {
406429
use super::*;

crates/trusted-server-core/src/integrations/adserver_mock.rs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ use crate::auction::types::{
2222
};
2323
use crate::backend::BackendConfig;
2424
use crate::error::TrustedServerError;
25-
use crate::integrations::{collect_response_bounded, UPSTREAM_RTB_MAX_RESPONSE_BYTES};
25+
use crate::integrations::{
26+
collect_response_bounded, ensure_integration_backend, UPSTREAM_RTB_MAX_RESPONSE_BYTES,
27+
};
2628
use crate::platform::{PlatformHttpRequest, PlatformPendingRequest, PlatformResponse};
2729
use crate::settings::{IntegrationConfig, Settings};
2830

@@ -334,20 +336,12 @@ impl AuctionProvider for AdServerMockProvider {
334336
}
335337
}
336338

337-
// Uses context.timeout_ms (auction-scoped) rather than the 15 s fixed
338-
// timeout in ensure_integration_backend, which is for proxy endpoints.
339-
// Send async with auction-scoped timeout
340-
let backend_name = BackendConfig::from_url_with_first_byte_timeout(
339+
let backend_name = ensure_integration_backend(
340+
context.services,
341341
&self.config.endpoint,
342-
true,
343-
Duration::from_millis(u64::from(context.timeout_ms)),
344-
)
345-
.change_context(TrustedServerError::Auction {
346-
message: format!(
347-
"Failed to resolve backend for mediation endpoint: {}",
348-
self.config.endpoint
349-
),
350-
})?;
342+
"adserver_mock",
343+
Some(Duration::from_millis(u64::from(context.timeout_ms))),
344+
)?;
351345

352346
let pending = context
353347
.services

crates/trusted-server-core/src/integrations/aps.rs

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ use crate::auction::provider::AuctionProvider;
1616
use crate::auction::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, MediaType};
1717
use crate::backend::BackendConfig;
1818
use crate::error::TrustedServerError;
19-
use crate::integrations::{collect_response_bounded, UPSTREAM_RTB_MAX_RESPONSE_BYTES};
19+
use crate::integrations::{
20+
collect_response_bounded, ensure_integration_backend, UPSTREAM_RTB_MAX_RESPONSE_BYTES,
21+
};
2022
use crate::platform::{PlatformHttpRequest, PlatformPendingRequest, PlatformResponse};
2123
use crate::settings::IntegrationConfig;
2224

@@ -513,20 +515,12 @@ impl AuctionProvider for ApsAuctionProvider {
513515
message: "Failed to build APS request".to_string(),
514516
})?;
515517

516-
// Uses context.timeout_ms (auction-scoped) rather than the 15 s fixed
517-
// timeout in ensure_integration_backend, which is for proxy endpoints.
518-
// Send request asynchronously with auction-scoped timeout
519-
let backend_name = BackendConfig::from_url_with_first_byte_timeout(
518+
let backend_name = ensure_integration_backend(
519+
context.services,
520520
&self.config.endpoint,
521-
true,
522-
Duration::from_millis(u64::from(context.timeout_ms)),
523-
)
524-
.change_context(TrustedServerError::Auction {
525-
message: format!(
526-
"Failed to resolve backend for APS endpoint: {}",
527-
self.config.endpoint
528-
),
529-
})?;
521+
"aps",
522+
Some(Duration::from_millis(u64::from(context.timeout_ms))),
523+
)?;
530524

531525
let pending = context
532526
.services

crates/trusted-server-core/src/integrations/datadome.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,7 +428,7 @@ impl DataDomeIntegration {
428428
services: &RuntimeServices,
429429
target_url: &str,
430430
) -> Result<String, Report<TrustedServerError>> {
431-
ensure_integration_backend(services, target_url, DATADOME_INTEGRATION_ID)
431+
ensure_integration_backend(services, target_url, DATADOME_INTEGRATION_ID, None)
432432
}
433433
}
434434

crates/trusted-server-core/src/integrations/didomi.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,7 @@ impl DidomiIntegration {
172172
services: &RuntimeServices,
173173
origin: &str,
174174
) -> Result<String, Report<TrustedServerError>> {
175-
ensure_integration_backend(services, origin, DIDOMI_INTEGRATION_ID)
175+
ensure_integration_backend(services, origin, DIDOMI_INTEGRATION_ID, None)
176176
}
177177
}
178178

0 commit comments

Comments
 (0)