11use std:: sync:: {
2- Arc ,
2+ Arc , OnceLock ,
33 atomic:: { AtomicBool , Ordering } ,
44} ;
55
6- use tokio:: sync:: OnceCell ;
7-
86use crate :: { APIClient , Error } ;
97
108/// Shared reqwest client initialization for all run-time network consumers.
119///
12- /// Call `activate()` as soon as a command knows it will need networking, then
13- /// use `get_or_init()` at the actual point of use. This overlaps TLS/client
14- /// setup with other startup work without constructing a client for commands
15- /// that never touch the network.
16- #[ derive( Clone , Default ) ]
10+ /// Uses two-phase initialization to avoid blocking on macOS Keychain
11+ /// enumeration (~200ms). Phase 1 builds an instant client with bundled
12+ /// Mozilla CAs (webpki-roots). Phase 2 builds a full client with system
13+ /// CAs (native-roots) in the background. Consumers get whichever is
14+ /// best available at the time of use.
15+ #[ derive( Clone ) ]
1716pub struct SharedHttpClient {
18- cell : Arc < OnceCell < reqwest:: Client > > ,
17+ /// Instant client with bundled Mozilla CAs only (~0ms to build).
18+ fast_client : Arc < OnceLock < reqwest:: Client > > ,
19+ /// Full client with system Keychain CAs (~200ms on macOS).
20+ /// Built in the background; preferred once ready.
21+ native_client : Arc < OnceLock < reqwest:: Client > > ,
1922 warming : Arc < AtomicBool > ,
2023}
2124
25+ impl Default for SharedHttpClient {
26+ fn default ( ) -> Self {
27+ Self {
28+ fast_client : Arc :: new ( OnceLock :: new ( ) ) ,
29+ native_client : Arc :: new ( OnceLock :: new ( ) ) ,
30+ warming : Arc :: new ( AtomicBool :: new ( false ) ) ,
31+ }
32+ }
33+ }
34+
2235impl SharedHttpClient {
2336 pub fn new ( ) -> Self {
2437 Self :: default ( )
2538 }
2639
2740 pub fn activate ( & self ) {
28- if self . cell . get ( ) . is_some ( ) {
41+ if self . native_client . get ( ) . is_some ( ) {
2942 return ;
3043 }
3144
@@ -37,26 +50,54 @@ impl SharedHttpClient {
3750 return ;
3851 }
3952
40- let this = self . clone ( ) ;
41- tokio:: spawn ( async move {
42- let _ = this. get_or_init ( ) . await ;
43- this. warming . store ( false , Ordering :: Release ) ;
53+ // Phase 1: build fast client in background (webpki-roots only, ~0ms).
54+ let fast = self . fast_client . clone ( ) ;
55+ tokio:: task:: spawn_blocking ( move || {
56+ let _span = tracing:: info_span!( "http_client_init_fast" ) . entered ( ) ;
57+ let _ = fast. get_or_init ( || {
58+ APIClient :: build_http_client_webpki_only ( None )
59+ . expect ( "failed to build webpki HTTP client" )
60+ } ) ;
61+ } ) ;
62+
63+ // Phase 2: build full client in background (native-roots, ~200ms on macOS).
64+ let native = self . native_client . clone ( ) ;
65+ let warming = self . warming . clone ( ) ;
66+ tokio:: task:: spawn_blocking ( move || {
67+ let _span = tracing:: info_span!( "http_client_init" ) . entered ( ) ;
68+ let _ = native. get_or_init ( || {
69+ let client =
70+ APIClient :: build_http_client ( None ) . expect ( "failed to build native HTTP client" ) ;
71+ warming. store ( false , Ordering :: Release ) ;
72+ client
73+ } ) ;
4474 } ) ;
4575 }
4676
4777 pub async fn get_or_init ( & self ) -> Result < reqwest:: Client , Error > {
48- let client = self
49- . cell
50- . get_or_try_init ( || async {
51- tokio:: task:: spawn_blocking ( || {
52- let _span = tracing:: info_span!( "http_client_init" ) . entered ( ) ;
53- APIClient :: build_http_client ( None )
54- } )
55- . await
56- . map_err ( |_| Error :: HttpClientCancelled ) ?
78+ // Prefer the full client (includes system CAs for corporate proxies)
79+ if let Some ( client) = self . native_client . get ( ) {
80+ return Ok ( client. clone ( ) ) ;
81+ }
82+
83+ // If the fast client is ready, use it while native is still building
84+ if let Some ( client) = self . fast_client . get ( ) {
85+ return Ok ( client. clone ( ) ) ;
86+ }
87+
88+ // Neither is ready — build the fast client synchronously as fallback
89+ let fast = self . fast_client . clone ( ) ;
90+ let client = tokio:: task:: spawn_blocking ( move || {
91+ let _span = tracing:: info_span!( "http_client_init_fast" ) . entered ( ) ;
92+ fast. get_or_init ( || {
93+ APIClient :: build_http_client_webpki_only ( None )
94+ . expect ( "failed to build webpki HTTP client" )
5795 } )
58- . await ?;
96+ . clone ( )
97+ } )
98+ . await
99+ . map_err ( |_| Error :: HttpClientCancelled ) ?;
59100
60- Ok ( client. clone ( ) )
101+ Ok ( client)
61102 }
62103}
0 commit comments