88from astro_airflow_mcp .adapters .airflow_v3 import AirflowV3Adapter
99from astro_airflow_mcp .adapters .base import AirflowAdapter , NotFoundError
1010from astro_airflow_mcp .astro_pat import AstroPATError
11+ from astro_airflow_mcp .utils import normalize_airflow_url
1112
1213
1314def detect_version (
@@ -33,6 +34,8 @@ def detect_version(
3334 Raises:
3435 RuntimeError: If version detection fails
3536 """
37+ airflow_url = normalize_airflow_url (airflow_url )
38+
3639 headers : dict [str , str ] = {}
3740 auth : tuple [str , str ] | httpx .Auth | None = None
3841
@@ -46,48 +49,52 @@ def detect_version(
4649 if basic_auth_getter :
4750 auth = basic_auth_getter ()
4851
49- # Try Airflow 3 API first (/api/v2/version)
50- try :
51- with httpx .Client (timeout = 10.0 , verify = verify ) as client :
52- response = client .get (
53- f"{ airflow_url } /api/v2/version" ,
54- headers = headers ,
55- auth = auth ,
56- )
57- if response .status_code == 200 :
58- data = response .json ()
59- version = data .get ("version" , "3.0.0" )
60- major = int (version .split ("." )[0 ])
61- return (major , version )
62- except AstroPATError :
63- # PAT misconfiguration (no astro login, refresh failed, etc) —
64- # surface to the caller rather than masking as a version detection
65- # failure.
66- raise
67- except Exception : # nosec B110 - try v1 API next
68- pass
69-
70- # Try Airflow 2 API (/api/v1/version)
71- try :
72- with httpx .Client (timeout = 10.0 , verify = verify ) as client :
73- response = client .get (
74- f"{ airflow_url } /api/v1/version" ,
75- headers = headers ,
76- auth = auth ,
77- )
78- if response .status_code == 200 :
52+ probe_failures : list [str ] = []
53+
54+ def _probe (api_path : str , default_version : str ) -> tuple [int , str ] | None :
55+ try :
56+ with httpx .Client (timeout = 10.0 , verify = verify ) as client :
57+ response = client .get (
58+ f"{ airflow_url } { api_path } /version" ,
59+ headers = headers ,
60+ auth = auth ,
61+ )
62+ except AstroPATError :
63+ # PAT misconfiguration (no astro login, refresh failed, etc) —
64+ # surface to the caller rather than masking as a version
65+ # detection failure.
66+ raise
67+ except Exception as e :
68+ probe_failures .append (f"{ api_path } : { type (e ).__name__ } : { e } " )
69+ return None
70+
71+ if response .status_code == 200 :
72+ try :
7973 data = response .json ()
80- version = data .get ("version" , "2.0.0" )
81- major = int (version .split ("." )[0 ])
82- return (major , version )
83- except AstroPATError :
84- raise
85- except Exception : # nosec B110 - raise RuntimeError below
86- pass
87-
74+ except ValueError as e :
75+ probe_failures .append (
76+ f"{ api_path } : 200 but non-JSON body ({ type (e ).__name__ } ); "
77+ f"got Content-Type={ response .headers .get ('content-type' , '?' )} "
78+ )
79+ return None
80+ version = data .get ("version" , default_version )
81+ major = int (version .split ("." )[0 ])
82+ return (major , version )
83+
84+ probe_failures .append (f"{ api_path } : HTTP { response .status_code } " )
85+ return None
86+
87+ result = _probe ("/api/v2" , "3.0.0" )
88+ if result is not None :
89+ return result
90+ result = _probe ("/api/v1" , "2.0.0" )
91+ if result is not None :
92+ return result
93+
94+ detail = "; " .join (probe_failures ) if probe_failures else "no response"
8895 raise RuntimeError (
8996 f"Failed to detect Airflow version at { airflow_url } . "
90- " Ensure Airflow is running and accessible."
97+ f"Probes: { detail } . Ensure Airflow is running and accessible."
9198 )
9299
93100
0 commit comments