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
100 changes: 95 additions & 5 deletions crates/trusted-server-core/src/auction/orchestrator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,28 @@ use super::config::AuctionConfig;
use super::provider::AuctionProvider;
use super::types::{AuctionContext, AuctionRequest, AuctionResponse, Bid, BidStatus};

const PROVIDER_ERROR_MESSAGE_CHARS: usize = 500;

fn provider_error_message(error: &Report<TrustedServerError>) -> String {
error
.current_context()
.to_string()
.chars()
.take(PROVIDER_ERROR_MESSAGE_CHARS)
.collect()
}
Comment thread
ChristianPavilonis marked this conversation as resolved.

fn provider_error_response(
provider_name: &str,
response_time_ms: u64,
error_type: &str,
error: &Report<TrustedServerError>,
) -> AuctionResponse {
AuctionResponse::error(provider_name, response_time_ms)
.with_metadata("error_type", serde_json::json!(error_type))
.with_metadata("message", serde_json::json!(provider_error_message(error)))
}

/// Compute the remaining time budget from a deadline.
///
/// Returns the number of milliseconds left before `timeout_ms` is exceeded,
Expand Down Expand Up @@ -274,6 +296,7 @@ impl AuctionOrchestrator {
let mut backend_to_provider: HashMap<String, (&str, Instant, &dyn AuctionProvider)> =
HashMap::new();
let mut pending_requests: Vec<PlatformPendingRequest> = Vec::new();
let mut responses = Vec::new();
Comment thread
ChristianPavilonis marked this conversation as resolved.

for provider_name in provider_names {
let provider = match self.providers.get(provider_name) {
Expand Down Expand Up @@ -353,11 +376,18 @@ impl AuctionOrchestrator {
);
}
Err(e) => {
let response_time_ms = start_time.elapsed().as_millis() as u64;
log::warn!(
"Provider '{}' failed to launch request: {:?}",
provider.provider_name(),
e
);
responses.push(provider_error_response(
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🔧 wrench — Launch-failure messages can surface internal identifiers to /auction clients.

provider_error_message returns error.current_context().to_string() (capped at 500 chars). That's a clean fix for the prior format!("{:?}") issue — but it still exposes whatever string each provider chose to put into its message field, and those weren't designed as user-facing.

Concrete example: when request_bids fails because RequestSigner::from_services() couldn't load the active key, the error message is "Configuration error: failed to get signing key for kid: <kid>" (request_signing/signing.rs:140-148). With this change, that kid (an internal Secret Store key identifier) lands in the public provider_details[].metadata.message. Kids aren't strictly secret, but they're internal state that didn't previously surface — and the same pattern means anyone adding a new change_context(...) site upstream of request_bids is now shipping that detail to /auction callers by default.

Suggested fix — give error_type=launch_failed a static public message, and keep the rich context in the existing warn! log on line 380:

Err(e) => {
    let response_time_ms = start_time.elapsed().as_millis() as u64;
    log::warn!(
        "Provider '{}' failed to launch request: {:?}",
        provider.provider_name(),
        e
    );
    let mut error_response = AuctionResponse::error(provider.provider_name(), response_time_ms)
        .with_metadata("error_type", serde_json::json!("launch_failed"));
    // Optionally surface error_type only here; full detail stays in logs.
    responses.push(error_response);
}

Or — if you want a per-error-type policy — split provider_error_response into a public path (static description per error_type) and an operator path (rich Report formatting), so future call sites can't accidentally widen the public surface.

provider.provider_name(),
response_time_ms,
"launch_failed",
&e,
));
}
}
}
Expand All @@ -377,7 +407,6 @@ impl AuctionOrchestrator {
// transport timeout fires). Hard deadline enforcement therefore depends
// on every backend's `first_byte_timeout` being set to at most the
// remaining auction budget — which Phase 1 above guarantees.
let mut responses = Vec::new();
let mut remaining = pending_requests;

while !remaining.is_empty() {
Expand Down Expand Up @@ -419,9 +448,11 @@ impl AuctionOrchestrator {
provider_name,
e
);
responses.push(AuctionResponse::error(
responses.push(provider_error_response(
provider_name,
response_time_ms,
"parse_response",
&e,
));
}
}
Expand All @@ -432,8 +463,12 @@ impl AuctionOrchestrator {
provider_name,
e
);
responses
.push(AuctionResponse::error(provider_name, response_time_ms));
responses.push(provider_error_response(
provider_name,
response_time_ms,
"unsupported_response_body",
&e,
));
}
}
} else {
Expand Down Expand Up @@ -635,8 +670,9 @@ mod tests {
use crate::auction::config::AuctionConfig;
use crate::auction::test_support::create_test_auction_context;
use crate::auction::types::{
AdFormat, AdSlot, AuctionRequest, Bid, MediaType, PublisherInfo, UserInfo,
AdFormat, AdSlot, AuctionRequest, Bid, BidStatus, MediaType, PublisherInfo, UserInfo,
};
use crate::error::TrustedServerError;

// All-None ClientInfo used across tests that don't need real IP/TLS data.
// Defined as a const so &EMPTY_CLIENT_INFO has 'static lifetime, avoiding
Expand All @@ -648,6 +684,7 @@ mod tests {
};
use crate::platform::test_support::noop_services;
use crate::test_support::tests::crate_test_settings_str;
use error_stack::Report;
use fastly::Request;
use std::collections::{HashMap, HashSet};

Expand Down Expand Up @@ -700,6 +737,59 @@ mod tests {
crate::settings::Settings::from_toml(&settings_str).expect("should parse test settings")
}

#[test]
fn provider_error_response_includes_diagnostic_metadata() {
let error = Report::new(TrustedServerError::Auction {
message: "parse failed".to_string(),
})
.attach("internal/source.rs:12:34");

let response = super::provider_error_response("prebid", 37, "parse_response", &error);

assert_eq!(
response.status,
BidStatus::Error,
"should mark diagnostic provider responses as errors"
);
assert_eq!(
response.metadata["error_type"],
serde_json::json!("parse_response"),
"should include the provider error classification"
);

let message = response.metadata["message"]
.as_str()
.expect("should include provider error message");
assert!(
message.contains("parse failed"),
"should include user-safe diagnostic detail"
);
assert!(
!message.contains("internal/source.rs"),
"should not include attached internal details"
);
}

#[test]
fn provider_error_message_truncates_user_safe_context() {
let long_message = "x".repeat(super::PROVIDER_ERROR_MESSAGE_CHARS + 100);
let error = Report::new(TrustedServerError::Auction {
message: long_message,
});

let message = super::provider_error_message(&error);

assert_eq!(
message.chars().count(),
super::PROVIDER_ERROR_MESSAGE_CHARS,
"should cap provider error messages"
);
assert!(
message.starts_with("Auction error: "),
"should preserve the current context display text"
);
}

#[test]
fn filters_winning_bids_below_floor() {
let orchestrator = AuctionOrchestrator::new(AuctionConfig::default());
Expand Down
Loading
Loading