3333//! let mut adapter = Adapter::new(&options)?;
3434//!
3535//! adapter.register_default_extension();
36- //! adapter.check_init_health().await;
36+ //! adapter.check_init_health().await? ;
3737//! adapter.run().await
3838//! })
3939//! }
@@ -714,7 +714,7 @@ fn matches_hook_path(configured: &Option<String>, request_path: &str) -> bool {
714714/// let mut adapter = Adapter::new(&options)?;
715715///
716716/// adapter.register_default_extension();
717- /// adapter.check_init_health().await;
717+ /// adapter.check_init_health().await? ;
718718/// adapter.run().await
719719/// # }
720720/// ```
@@ -969,7 +969,17 @@ impl Adapter<HttpConnector, Body> {
969969 /// - Allow the application to continue booting in the background
970970 ///
971971 /// The first request will re-check readiness if the application wasn't ready
972- /// during initialization.
972+ /// during initialization. The async path always returns `Ok`.
973+ ///
974+ /// # Synchronous Initialization
975+ ///
976+ /// Without `async_init`, this waits for the app to report ready before the
977+ /// Lambda runtime starts serving. If `readiness_check_timeout`
978+ /// (`AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS`) is set and the app does not
979+ /// become ready within it, this returns `Err`: init fails and no traffic is
980+ /// served against an app that never came up. When the timeout is unset the
981+ /// wait is unbounded and this returns `Ok` once the check completes
982+ /// (historical behavior).
973983 ///
974984 /// # Examples
975985 ///
@@ -979,25 +989,38 @@ impl Adapter<HttpConnector, Body> {
979989 /// # async fn example() -> Result<(), lambda_web_adapter::Error> {
980990 /// let options = AdapterOptions::default();
981991 /// let mut adapter = Adapter::new(&options)?;
982- /// adapter.check_init_health().await;
992+ /// adapter.check_init_health().await? ;
983993 /// # Ok(())
984994 /// # }
985995 /// ```
986- pub async fn check_init_health ( & mut self ) {
996+ pub async fn check_init_health ( & mut self ) -> Result < ( ) , Error > {
987997 let ready_at_init = if self . async_init {
988998 // async_init keeps its own fixed bound, independent of
989999 // readiness_check_timeout (see AdapterOptions::readiness_check_timeout).
1000+ // A timeout here is non-fatal: the app keeps booting and the first
1001+ // request re-checks readiness.
9901002 timeout ( Duration :: from_secs_f32 ( 9.8 ) , self . check_readiness ( ) )
9911003 . await
9921004 . unwrap_or_default ( )
9931005 } else if let Some ( t) = self . readiness_check_timeout {
994- // Bound the sync-init readiness wait when configured; unset = unbounded
995- // (historical behavior), so an unset value never fails a slow cold start.
996- timeout ( t, self . check_readiness ( ) ) . await . unwrap_or_default ( )
1006+ // Bound the sync-init readiness wait when configured. On expiry, refuse
1007+ // to serve: fail init rather than admit traffic to an app that never
1008+ // reported ready — this is the point of configuring the bound.
1009+ match timeout ( t, self . check_readiness ( ) ) . await {
1010+ Ok ( ready) => ready,
1011+ Err ( _) => {
1012+ return Err ( Error :: from ( format ! (
1013+ "web application did not become ready within {t:?} \
1014+ (AWS_LWA_READINESS_CHECK_TIMEOUT_SECONDS); failing initialization"
1015+ ) ) ) ;
1016+ }
1017+ }
9971018 } else {
1019+ // Unset: unbounded wait, non-fatal result (historical behavior).
9981020 self . check_readiness ( ) . await
9991021 } ;
10001022 self . ready_at_init . store ( ready_at_init, Ordering :: SeqCst ) ;
1023+ Ok ( ( ) )
10011024 }
10021025
10031026 /// Performs a single readiness check against the configured endpoint.
@@ -1512,6 +1535,36 @@ mod tests {
15121535 healthcheck. assert ( ) ;
15131536 }
15141537
1538+ #[ tokio:: test]
1539+ async fn test_check_init_health_fails_when_sync_init_readiness_timeout_expires ( ) {
1540+ // App server that never reports ready (always 500) so the readiness
1541+ // retry loop runs until the configured timeout fires.
1542+ let app_server = MockServer :: start ( ) ;
1543+ app_server. mock ( |when, then| {
1544+ when. method ( GET ) . path ( "/healthcheck" ) ;
1545+ then. status ( 500 ) . body ( "nope" ) ;
1546+ } ) ;
1547+
1548+ // Sync init (async_init defaults to false) with a short configured bound.
1549+ let options = AdapterOptions {
1550+ host : app_server. host ( ) ,
1551+ port : app_server. port ( ) . to_string ( ) ,
1552+ readiness_check_port : app_server. port ( ) . to_string ( ) ,
1553+ readiness_check_path : "/healthcheck" . to_string ( ) ,
1554+ readiness_check_timeout : Some ( Duration :: from_millis ( 100 ) ) ,
1555+ ..Default :: default ( )
1556+ } ;
1557+
1558+ let mut adapter = Adapter :: new ( & options) . expect ( "Failed to create adapter" ) ;
1559+
1560+ // Refuse to serve: a configured timeout that expires fails initialization.
1561+ let result = adapter. check_init_health ( ) . await ;
1562+ assert ! (
1563+ result. is_err( ) ,
1564+ "sync-init readiness timeout should fail init, got {result:?}"
1565+ ) ;
1566+ }
1567+
15151568 #[ tokio:: test]
15161569 async fn test_status_403_is_bad_when_configured ( ) {
15171570 // Start app server
0 commit comments