11import os
22import re
33import json
4+ import time
45from pathlib import Path
56from typing import Literal , Optional
67from urllib .parse import urlparse
@@ -39,6 +40,8 @@ def __init__(self, gateway_origin: Optional[str] = None):
3940 ).rstrip ("/" )
4041 self .npm_credential_path = Path (os .getenv ("WEBSOFT9_NPM_CREDENTIAL_PATH" , str (data_root / "credential.json" )))
4142 self .npm_database_path = Path (os .getenv ("WEBSOFT9_NPM_DATABASE_PATH" , str (data_root / "database.sqlite" )))
43+ self .npm_bootstrap_retry_attempts = max (1 , int (os .getenv ("WEBSOFT9_NPM_BOOTSTRAP_RETRY_ATTEMPTS" , "6" )))
44+ self .npm_bootstrap_retry_interval_seconds = max (1 , int (os .getenv ("WEBSOFT9_NPM_BOOTSTRAP_RETRY_INTERVAL_SECONDS" , "2" )))
4245
4346 def _resolve_platform_origin (self ) -> str :
4447 public_origin = (os .getenv ("WEBSOFT9_PLATFORM_PUBLIC_ORIGIN" ) or "" ).strip ().rstrip ("/" )
@@ -273,7 +276,7 @@ def bootstrap_npm(self) -> list[dict[str, object]]:
273276 active_credentials = self .credential_provider .get_npm_credentials ()
274277
275278 fallback_credentials = self .credential_provider .get_npm_config_credentials ()
276- response = self ._request_npm_token (session , active_credentials .username , active_credentials .password )
279+ response = self ._request_npm_token_with_retry (session , active_credentials .username , active_credentials .password )
277280
278281 should_try_fallback = (
279282 response .status_code in {401 , 403 }
@@ -287,7 +290,7 @@ def bootstrap_npm(self) -> list[dict[str, object]]:
287290 )
288291
289292 if should_try_fallback :
290- fallback_response = self ._request_npm_token (session , fallback_credentials .username , fallback_credentials .password )
293+ fallback_response = self ._request_npm_token_with_retry (session , fallback_credentials .username , fallback_credentials .password )
291294 if fallback_response .status_code == 200 :
292295 self .credential_provider .write_npm_credentials (fallback_credentials )
293296 active_credentials = fallback_credentials
@@ -298,7 +301,7 @@ def bootstrap_npm(self) -> list[dict[str, object]]:
298301
299302 if response .status_code in {401 , 403 } and self .credential_provider .sync_npm_credentials (active_credentials ):
300303 logger .warning ("Nginx Proxy Manager authentication recovered after synchronizing stored credentials into the runtime database" )
301- response = self ._request_npm_token (session , active_credentials .username , active_credentials .password )
304+ response = self ._request_npm_token_with_retry (session , active_credentials .username , active_credentials .password )
302305
303306 if response .status_code in {401 , 403 }:
304307 raise CustomException (
@@ -348,6 +351,45 @@ def _request_npm_token(self, session: requests.Session, username: str, password:
348351 timeout = 20 ,
349352 )
350353
354+ def _request_npm_token_with_retry (self , session : requests .Session , username : str , password : str ) -> requests .Response :
355+ last_exception : Exception | None = None
356+ response : requests .Response | None = None
357+
358+ for attempt in range (1 , self .npm_bootstrap_retry_attempts + 1 ):
359+ try :
360+ response = self ._request_npm_token (session , username , password )
361+ if response .status_code not in {502 , 503 , 504 }:
362+ return response
363+
364+ if attempt < self .npm_bootstrap_retry_attempts :
365+ logger .warning (
366+ "Nginx Proxy Manager token endpoint not ready "
367+ f"(status={ response .status_code } , attempt={ attempt } /{ self .npm_bootstrap_retry_attempts } ); retrying"
368+ )
369+ time .sleep (self .npm_bootstrap_retry_interval_seconds )
370+ except requests .RequestException as exc :
371+ last_exception = exc
372+ if attempt < self .npm_bootstrap_retry_attempts :
373+ logger .warning (
374+ "Nginx Proxy Manager token request failed "
375+ f"(attempt={ attempt } /{ self .npm_bootstrap_retry_attempts } ): { exc } ; retrying"
376+ )
377+ time .sleep (self .npm_bootstrap_retry_interval_seconds )
378+ else :
379+ break
380+
381+ if response is not None :
382+ return response
383+
384+ if last_exception is not None :
385+ raise last_exception
386+
387+ raise CustomException (
388+ status_code = 502 ,
389+ message = "Integration Session Bootstrap Failed" ,
390+ details = "Unable to restore the embedded gateway session automatically" ,
391+ )
392+
351393 def _request_portainer_token (self , session : requests .Session , username : str , password : str ) -> requests .Response :
352394 return session .post (
353395 f"{ self .portainer_direct_origin } /api/auth" ,
0 commit comments