15
15
import copy
16
16
import json
17
17
import logging
18
- import ssl
19
18
from json import JSONDecodeError
20
19
from typing import Any , AsyncGenerator
21
20
57
56
HealthCheckSummary ,
58
57
HealthCheckSummaryException ,
59
58
)
59
+ from ansible_ai_connect .main .ssl_manager import ssl_manager
60
60
61
61
logger = logging .getLogger (__name__ )
62
62
@@ -66,7 +66,9 @@ class HttpMetaData(MetaData[HttpConfiguration]):
66
66
67
67
def __init__ (self , config : HttpConfiguration ):
68
68
super ().__init__ (config = config )
69
- self .session = requests .Session ()
69
+ # Use centralized SSL manager for all HTTP requests
70
+ self .session = ssl_manager .get_requests_session ()
71
+
70
72
self .headers = {"Content-Type" : "application/json" }
71
73
i = self .config .timeout
72
74
self ._timeout = int (i ) if i is not None else None
@@ -97,9 +99,6 @@ def invoke(self, params: CompletionsParameters) -> CompletionsResponse:
97
99
headers = self .headers ,
98
100
json = model_input ,
99
101
timeout = self .task_gen_timeout (task_count ),
100
- verify = (
101
- self .config .ca_cert_file if self .config .ca_cert_file else self .config .verify_ssl
102
- ),
103
102
)
104
103
result .raise_for_status ()
105
104
response = json .loads (result .text )
@@ -117,11 +116,8 @@ def self_test(self) -> HealthCheckSummary:
117
116
}
118
117
)
119
118
try :
120
- res = requests .get (
119
+ res = self . session .get (
121
120
url ,
122
- verify = (
123
- self .config .ca_cert_file if self .config .ca_cert_file else self .config .verify_ssl
124
- ),
125
121
timeout = 1 ,
126
122
)
127
123
res .raise_for_status ()
@@ -151,13 +147,10 @@ def self_test(self) -> HealthCheckSummary:
151
147
)
152
148
try :
153
149
headers = {"Content-Type" : "application/json" }
154
- r = requests .get (
150
+ r = self . session .get (
155
151
self .config .inference_url + "/readiness" ,
156
152
headers = headers ,
157
153
timeout = 1 ,
158
- verify = (
159
- self .config .ca_cert_file if self .config .ca_cert_file else self .config .verify_ssl
160
- ),
161
154
)
162
155
r .raise_for_status ()
163
156
@@ -178,6 +171,20 @@ def self_test(self) -> HealthCheckSummary:
178
171
)
179
172
return summary
180
173
174
+ def _safe_parse_error_detail (self , response_text : str ) -> str :
175
+ """
176
+ Safely parse error detail from response text.
177
+ If JSON parsing fails, return the raw text or a default message.
178
+ """
179
+ if not response_text :
180
+ return "No error details available"
181
+ try :
182
+ parsed = json .loads (response_text )
183
+ return parsed .get ("detail" , response_text )
184
+ except (json .JSONDecodeError , TypeError ):
185
+ # Return raw text if JSON parsing fails, but limit length for safety
186
+ return response_text [:500 ] if len (response_text ) <= 500 else response_text [:500 ] + "..."
187
+
181
188
182
189
@Register (api_type = "http" )
183
190
class HttpChatBotPipeline (HttpChatBotMetaData , ModelPipelineChatBot [HttpConfiguration ]):
@@ -209,12 +216,11 @@ def invoke(self, params: ChatBotParameters) -> ChatBotResponse:
209
216
if params .mcp_headers :
210
217
headers ["MCP-HEADERS" ] = json .dumps (params .mcp_headers )
211
218
212
- response = requests .post (
219
+ response = self . session .post (
213
220
self .config .inference_url + "/v1/query" ,
214
- headers = self . headers ,
221
+ headers = headers ,
215
222
json = data ,
216
223
timeout = self .task_gen_timeout (1 ),
217
- verify = self .config .ca_cert_file if self .config .ca_cert_file else self .config .verify_ssl ,
218
224
)
219
225
220
226
if response .status_code == 200 :
@@ -228,19 +234,19 @@ def invoke(self, params: ChatBotParameters) -> ChatBotResponse:
228
234
return data
229
235
230
236
elif response .status_code == 401 :
231
- detail = json . loads (response .text ). get ( "detail" , "" )
237
+ detail = self . _safe_parse_error_detail (response .text )
232
238
raise ChatbotUnauthorizedException (detail = detail )
233
239
elif response .status_code == 403 :
234
- detail = json . loads (response .text ). get ( "detail" , "" )
240
+ detail = self . _safe_parse_error_detail (response .text )
235
241
raise ChatbotForbiddenException (detail = detail )
236
242
elif response .status_code == 413 :
237
- detail = json . loads (response .text ). get ( "detail" , "" )
243
+ detail = self . _safe_parse_error_detail (response .text )
238
244
raise ChatbotPromptTooLongException (detail = detail )
239
245
elif response .status_code == 422 :
240
- detail = json . loads (response .text ). get ( "detail" , "" )
246
+ detail = self . _safe_parse_error_detail (response .text )
241
247
raise ChatbotValidationException (detail = detail )
242
248
else :
243
- detail = json . loads (response .text ). get ( "detail" , "" )
249
+ detail = self . _safe_parse_error_detail (response .text )
244
250
raise ChatbotInternalServerException (detail = detail )
245
251
246
252
@@ -275,14 +281,42 @@ def send_schema1_event(self, ev):
275
281
276
282
send_schema1_event (ev )
277
283
284
+ def _get_aiohttp_connector (self , verify_ssl : bool = True ) -> aiohttp .TCPConnector :
285
+ """Create aiohttp connector with proper SSL configuration.
286
+ - aiohttp.TCPConnector does NOT accept ssl=None
287
+ - ssl=True: Uses system default SSL verification (SECURE)
288
+ - ssl=False: Disables SSL verification completely (INSECURE needed for dev/test)
289
+ - ssl=SSLContext: Uses custom SSL context (SECURE with custom CAs)
290
+ Args:
291
+ verify_ssl: Whether SSL verification should be enabled
292
+ Returns:
293
+ TCPConnector with consistent SSL behavior:
294
+ - verify_ssl=True + custom context: ssl=SSLContext (infrastructure CA bundle)
295
+ - verify_ssl=True + no context: ssl=True (system default CAs)
296
+ - verify_ssl=False: ssl=False (DISABLED - consistent with requests.Session)
297
+ Raises:
298
+ ssl.SSLError: If SSL context creation fails when verify_ssl=True
299
+ """
300
+ if not verify_ssl :
301
+ # SECURITY NOTE: This disables SSL verification
302
+ # should only be used in development/testing
303
+ # This matches requests.Session.verify=False behavior for consistency
304
+ return aiohttp .TCPConnector (ssl = False )
305
+
306
+ # Get SSL context from centralized SSL manager
307
+ ssl_context = ssl_manager .get_ssl_context ()
308
+
309
+ if ssl_context is not None :
310
+ # Use custom SSL context from SSL manager (infrastructure CA bundle)
311
+ return aiohttp .TCPConnector (ssl = ssl_context )
312
+ else :
313
+ # Use system default SSL verification (fallback when no custom CA bundle)
314
+ return aiohttp .TCPConnector (ssl = True )
315
+
278
316
async def async_invoke (self , params : StreamingChatBotParameters ) -> AsyncGenerator :
279
317
280
- # Configure SSL context based on verify_ssl setting
281
- if self .config .ca_cert_file :
282
- ssl_context = ssl .create_default_context (cafile = self .config .ca_cert_file )
283
- connector = aiohttp .TCPConnector (ssl = ssl_context )
284
- else :
285
- connector = aiohttp .TCPConnector (ssl = self .config .verify_ssl )
318
+ # Create connector with proper SSL handling
319
+ connector = self ._get_aiohttp_connector (verify_ssl = self .config .verify_ssl )
286
320
287
321
async with aiohttp .ClientSession (raise_for_status = True , connector = connector ) as session :
288
322
headers = {
0 commit comments