@@ -12,6 +12,44 @@ use super::config::AuctionConfig;
1212use super :: provider:: AuctionProvider ;
1313use super :: types:: { AuctionContext , AuctionRequest , AuctionResponse , Bid , BidStatus } ;
1414
15+ const PROVIDER_ERROR_MESSAGE_CHARS : usize = 500 ;
16+
17+ pub ( crate ) const ERROR_TYPE_HTTP_STATUS : & str = "http_status" ;
18+ const ERROR_TYPE_PARSE_RESPONSE : & str = "parse_response" ;
19+ const ERROR_TYPE_LAUNCH_FAILED : & str = "launch_failed" ;
20+
21+ // SECURITY: the returned string is included verbatim (truncated to
22+ // PROVIDER_ERROR_MESSAGE_CHARS) in the public /auction response via
23+ // ProviderSummary.metadata["message"]. Providers MUST NOT interpolate
24+ // upstream-controlled content (response bodies, parse errors, headers) into
25+ // their TrustedServerError::*.message fields. Use static text and log details
26+ // server-side with `log::warn!` instead.
27+ fn provider_error_message ( error : & Report < TrustedServerError > ) -> String {
28+ error
29+ . current_context ( )
30+ . to_string ( )
31+ . chars ( )
32+ . take ( PROVIDER_ERROR_MESSAGE_CHARS )
33+ . collect ( )
34+ }
35+
36+ fn provider_error_response (
37+ provider_name : & str ,
38+ response_time_ms : u64 ,
39+ error_type : & str ,
40+ error : & Report < TrustedServerError > ,
41+ ) -> AuctionResponse {
42+ AuctionResponse :: error ( provider_name, response_time_ms)
43+ . with_metadata ( "error_type" , serde_json:: json!( error_type) )
44+ . with_metadata ( "message" , serde_json:: json!( provider_error_message( error) ) )
45+ }
46+
47+ fn provider_launch_failed_response ( provider_name : & str , response_time_ms : u64 ) -> AuctionResponse {
48+ AuctionResponse :: error ( provider_name, response_time_ms)
49+ . with_metadata ( "error_type" , serde_json:: json!( ERROR_TYPE_LAUNCH_FAILED ) )
50+ . with_metadata ( "message" , serde_json:: json!( "Provider launch failed" ) )
51+ }
52+
1553/// Compute the remaining time budget from a deadline.
1654///
1755/// Returns the number of milliseconds left before `timeout_ms` is exceeded,
@@ -252,6 +290,7 @@ impl AuctionOrchestrator {
252290 let mut backend_to_provider: HashMap < String , ( & str , Instant , & dyn AuctionProvider ) > =
253291 HashMap :: new ( ) ;
254292 let mut pending_requests: Vec < PendingRequest > = Vec :: new ( ) ;
293+ let mut responses = Vec :: new ( ) ;
255294
256295 for provider_name in provider_names {
257296 let provider = match self . providers . get ( provider_name) {
@@ -331,11 +370,16 @@ impl AuctionOrchestrator {
331370 ) ;
332371 }
333372 Err ( e) => {
373+ let response_time_ms = start_time. elapsed ( ) . as_millis ( ) as u64 ;
334374 log:: warn!(
335375 "Provider '{}' failed to launch request: {:?}" ,
336376 provider. provider_name( ) ,
337377 e
338378 ) ;
379+ responses. push ( provider_launch_failed_response (
380+ provider. provider_name ( ) ,
381+ response_time_ms,
382+ ) ) ;
339383 }
340384 }
341385 }
@@ -357,7 +401,6 @@ impl AuctionOrchestrator {
357401 // transport timeout fires). Hard deadline enforcement therefore depends
358402 // on every backend's `first_byte_timeout` being set to at most the
359403 // remaining auction budget — which Phase 1 above guarantees.
360- let mut responses = Vec :: new ( ) ;
361404 let mut remaining = pending_requests;
362405
363406 while !remaining. is_empty ( ) {
@@ -397,8 +440,12 @@ impl AuctionOrchestrator {
397440 provider_name,
398441 e
399442 ) ;
400- responses
401- . push ( AuctionResponse :: error ( provider_name, response_time_ms) ) ;
443+ responses. push ( provider_error_response (
444+ provider_name,
445+ response_time_ms,
446+ ERROR_TYPE_PARSE_RESPONSE ,
447+ & e,
448+ ) ) ;
402449 }
403450 }
404451 } else {
@@ -602,9 +649,11 @@ mod tests {
602649 use crate :: auction:: config:: AuctionConfig ;
603650 use crate :: auction:: test_support:: create_test_auction_context;
604651 use crate :: auction:: types:: {
605- AdFormat , AdSlot , AuctionRequest , Bid , MediaType , PublisherInfo , UserInfo ,
652+ AdFormat , AdSlot , AuctionRequest , Bid , BidStatus , MediaType , PublisherInfo , UserInfo ,
606653 } ;
654+ use crate :: error:: TrustedServerError ;
607655 use crate :: test_support:: tests:: crate_test_settings_str;
656+ use error_stack:: Report ;
608657 use fastly:: Request ;
609658 use std:: collections:: { HashMap , HashSet } ;
610659
@@ -657,6 +706,81 @@ mod tests {
657706 crate :: settings:: Settings :: from_toml ( & settings_str) . expect ( "should parse test settings" )
658707 }
659708
709+ #[ test]
710+ fn provider_error_response_includes_diagnostic_metadata ( ) {
711+ let error = Report :: new ( TrustedServerError :: Auction {
712+ message : "parse failed" . to_string ( ) ,
713+ } )
714+ . attach ( "internal/source.rs:12:34" ) ;
715+
716+ let response =
717+ super :: provider_error_response ( "prebid" , 37 , super :: ERROR_TYPE_PARSE_RESPONSE , & error) ;
718+
719+ assert_eq ! (
720+ response. status,
721+ BidStatus :: Error ,
722+ "should mark diagnostic provider responses as errors"
723+ ) ;
724+ assert_eq ! (
725+ response. metadata[ "error_type" ] ,
726+ serde_json:: json!( "parse_response" ) ,
727+ "should include the provider error classification"
728+ ) ;
729+
730+ let message = response. metadata [ "message" ]
731+ . as_str ( )
732+ . expect ( "should include provider error message" ) ;
733+ assert ! (
734+ message. contains( "parse failed" ) ,
735+ "should include user-safe diagnostic detail"
736+ ) ;
737+ assert ! (
738+ !message. contains( "internal/source.rs" ) ,
739+ "should not include attached internal details"
740+ ) ;
741+ }
742+
743+ #[ test]
744+ fn launch_failed_response_has_safe_static_message ( ) {
745+ let response = super :: provider_launch_failed_response ( "prebid" , 58 ) ;
746+
747+ assert_eq ! (
748+ response. status,
749+ BidStatus :: Error ,
750+ "should mark launch failures as errors"
751+ ) ;
752+ assert_eq ! (
753+ response. metadata[ "error_type" ] ,
754+ serde_json:: json!( "launch_failed" ) ,
755+ "should include launch_failed classification"
756+ ) ;
757+ assert_eq ! (
758+ response. metadata[ "message" ] ,
759+ serde_json:: json!( "Provider launch failed" ) ,
760+ "should use a safe, stable public launch failure message"
761+ ) ;
762+ }
763+
764+ #[ test]
765+ fn provider_error_message_truncates_user_safe_context ( ) {
766+ let long_message = "x" . repeat ( super :: PROVIDER_ERROR_MESSAGE_CHARS + 100 ) ;
767+ let error = Report :: new ( TrustedServerError :: Auction {
768+ message : long_message,
769+ } ) ;
770+
771+ let message = super :: provider_error_message ( & error) ;
772+
773+ assert_eq ! (
774+ message. chars( ) . count( ) ,
775+ super :: PROVIDER_ERROR_MESSAGE_CHARS ,
776+ "should cap provider error messages"
777+ ) ;
778+ assert ! (
779+ message. starts_with( "Auction error: " ) ,
780+ "should preserve the current context display text"
781+ ) ;
782+ }
783+
660784 #[ test]
661785 fn filters_winning_bids_below_floor ( ) {
662786 let orchestrator = AuctionOrchestrator :: new ( AuctionConfig :: default ( ) ) ;
0 commit comments