@@ -113,9 +113,14 @@ impl<'a> SearchBackendChain<'a> {
113113 context, selected,
114114 ) ) ) ;
115115 if !matches ! ( selected, SearchProvider :: Bing | SearchProvider :: DuckDuckGo ) {
116+ // The keyless tail must stay reachable from mainland-China
117+ // networks, where DuckDuckGo is DNS-poisoned and SNI-reset while
118+ // Bing serves both its global and China endpoints without a key.
119+ // The tail is picked by reachability, not by geo detection: the
120+ // engine never guesses the user's location.
116121 backends. push ( Box :: new ( ConfiguredSearchBackend :: from_provider (
117122 context,
118- SearchProvider :: DuckDuckGo ,
123+ SearchProvider :: Bing ,
119124 ) ) ) ;
120125 }
121126 Self { backends }
@@ -242,7 +247,8 @@ async fn run_backend_chain(
242247 . collect :: < Vec < _ > > ( )
243248 . join ( ", " ) ;
244249 Err ( ToolError :: not_available ( format ! (
245- "web search backends unavailable: {backend_ids}"
250+ "web search backends unavailable: {backend_ids}; \
251+ configure an API-backed [search] provider (tavily, bocha, metaso, baidu, volcengine) for dependable results"
246252 ) ) )
247253}
248254
@@ -464,6 +470,47 @@ mod tests {
464470 }
465471 }
466472
473+ /// API-backed providers must fall back to the keyless Bing tail, not
474+ /// DuckDuckGo: DDG is unreachable from mainland-China networks
475+ /// (DNS poisoning + SNI reset), so a DDG tail turns every API outage
476+ /// into a guaranteed total failure there, while Bing stays reachable
477+ /// globally without a key. Bing and DuckDuckGo themselves stay
478+ /// single-backend chains (their internal fallbacks own that job).
479+ #[ test]
480+ fn forkguard_api_provider_chain_tail_is_bing ( ) {
481+ for selected in [
482+ SearchProvider :: Tavily ,
483+ SearchProvider :: Bocha ,
484+ SearchProvider :: Metaso ,
485+ SearchProvider :: Baidu ,
486+ SearchProvider :: Searxng ,
487+ SearchProvider :: Volcengine ,
488+ SearchProvider :: Sofya ,
489+ ] {
490+ let mut context = ToolContext :: new ( std:: path:: PathBuf :: from ( "." ) ) ;
491+ context. search_provider = selected;
492+ let chain = SearchBackendChain :: from_context ( & context) ;
493+ let ids: Vec < BackendId > = chain. backends . iter ( ) . map ( |backend| backend. id ( ) ) . collect ( ) ;
494+ let expected_tail = BackendId :: Bing ;
495+ assert_eq ! (
496+ ids. last( ) ,
497+ Some ( & expected_tail) ,
498+ "{selected:?} chain must keep a reachable keyless Bing tail"
499+ ) ;
500+ }
501+
502+ for selected in [ SearchProvider :: Bing , SearchProvider :: DuckDuckGo ] {
503+ let mut context = ToolContext :: new ( std:: path:: PathBuf :: from ( "." ) ) ;
504+ context. search_provider = selected;
505+ let chain = SearchBackendChain :: from_context ( & context) ;
506+ assert_eq ! (
507+ chain. backends. len( ) ,
508+ 1 ,
509+ "{selected:?} must stay a single-backend chain"
510+ ) ;
511+ }
512+ }
513+
467514 #[ test]
468515 fn provider_native_is_fail_closed_without_both_fact_and_client ( ) {
469516 assert ! ( !provider_native_is_available( false , false ) ) ;
@@ -641,6 +688,7 @@ mod tests {
641688
642689 assert ! ( matches!( error, ToolError :: NotAvailable { .. } ) ) ;
643690 assert ! ( message. contains( "bocha, duckduckgo" ) ) ;
691+ assert ! ( message. contains( "configure an API-backed [search] provider" ) ) ;
644692 assert ! ( !message. contains( private_error) ) ;
645693 assert ! ( !message. contains( "different private response" ) ) ;
646694 }
0 commit comments