11import json
2+
23from unittest .mock import AsyncMock , MagicMock , patch
34
45import httpx
@@ -232,14 +233,48 @@ async def mock_send_stream_request(*args, **kwargs):
232233 assert events [1 ] == StreamResponse (message = Message (message_id = 'msg-123' ))
233234
234235
236+ def create_405_error ():
237+ mock_response = MagicMock (spec = httpx .Response )
238+ mock_response .status_code = 405
239+ mock_response .json .return_value = {
240+ 'type' : 'MethodNotAllowed' ,
241+ 'message' : 'Method Not Allowed' ,
242+ }
243+ mock_request = MagicMock (spec = httpx .Request )
244+ mock_request .url = 'http://example.com/v1/tasks/task-123:subscribe'
245+
246+ status_error = httpx .HTTPStatusError (
247+ '405 Method Not Allowed' , request = mock_request , response = mock_response
248+ )
249+ raise A2AClientError ('HTTP Error 405' ) from status_error
250+
251+
252+ def create_500_error ():
253+ mock_response = MagicMock (spec = httpx .Response )
254+ mock_response .status_code = 500
255+ mock_response .json .return_value = {
256+ 'type' : 'InternalError' ,
257+ 'message' : 'Internal Error' ,
258+ }
259+ mock_request = MagicMock (spec = httpx .Request )
260+
261+ status_error = httpx .HTTPStatusError (
262+ '500 Internal Error' , request = mock_request , response = mock_response
263+ )
264+ raise A2AClientError ('HTTP Error 500' ) from status_error
265+
266+
235267@pytest .mark .asyncio
236- async def test_compat_rest_transport_subscribe (transport ):
237- async def mock_send_stream_request (* args , ** kwargs ):
268+ async def test_compat_rest_transport_subscribe_post_works_no_retry (transport ):
269+ """Scenario: POST works, no retry."""
270+
271+ async def mock_stream (method , path , context = None ):
272+ assert method == 'POST'
238273 task = Task (id = 'task-123' )
239274 task .status .message .role = Role .ROLE_AGENT
240275 yield StreamResponse (task = task )
241276
242- transport ._send_stream_request = mock_send_stream_request
277+ transport ._send_stream_request = mock_stream
243278
244279 req = SubscribeToTaskRequest (id = 'task-123' )
245280 events = [event async for event in transport .subscribe (req )]
@@ -248,6 +283,101 @@ async def mock_send_stream_request(*args, **kwargs):
248283 expected_task = Task (id = 'task-123' )
249284 expected_task .status .message .role = Role .ROLE_AGENT
250285 assert events [0 ] == StreamResponse (task = expected_task )
286+ assert transport ._subscribe_method == 'POST'
287+ assert transport ._subscribe_retry_attempted is False
288+
289+
290+ @pytest .mark .asyncio
291+ async def test_compat_rest_transport_subscribe_post_405_retry_get_success (
292+ transport ,
293+ ):
294+ """Scenario: POST returns 405, automatic retry GET. Second call uses GET directly."""
295+ call_count = 0
296+
297+ async def mock_stream (method , path , context = None ):
298+ nonlocal call_count
299+ call_count += 1
300+ if method == 'POST' :
301+ create_405_error ()
302+ if method == 'GET' :
303+ task = Task (id = 'task-123' )
304+ task .status .message .role = Role .ROLE_AGENT
305+ yield StreamResponse (task = task )
306+
307+ transport ._send_stream_request = mock_stream
308+
309+ req = SubscribeToTaskRequest (id = 'task-123' )
310+ events = [event async for event in transport .subscribe (req )]
311+
312+ assert len (events ) == 1
313+ assert call_count == 2
314+ assert transport ._subscribe_method == 'GET'
315+ assert transport ._subscribe_retry_attempted is True
316+
317+ # Second call should use GET directly
318+ call_count = 0
319+ events = [event async for event in transport .subscribe (req )]
320+ assert len (events ) == 1
321+ assert call_count == 1 # Only GET called
322+ assert transport ._subscribe_method == 'GET'
323+
324+
325+ @pytest .mark .asyncio
326+ async def test_compat_rest_transport_subscribe_post_405_get_405_fails (
327+ transport ,
328+ ):
329+ """Scenario: POST return 405, retry GET, return 405 - error. Second call is just POST."""
330+ call_count = 0
331+
332+ async def mock_stream (method , path , context = None ):
333+ nonlocal call_count
334+ call_count += 1
335+ # To make it an async generator even when it raises
336+ if False :
337+ yield
338+ create_405_error ()
339+
340+ transport ._send_stream_request = mock_stream
341+
342+ req = SubscribeToTaskRequest (id = 'task-123' )
343+ with pytest .raises (A2AClientError ) as exc_info :
344+ [event async for event in transport .subscribe (req )]
345+
346+ assert '405' in str (exc_info .value )
347+ assert call_count == 2 # Tried POST then GET
348+ assert transport ._subscribe_method == 'POST'
349+ assert transport ._subscribe_retry_attempted is True
350+
351+ # Second call should try POST directly and fail without retry
352+ call_count = 0
353+ with pytest .raises (A2AClientError ):
354+ [event async for event in transport .subscribe (req )]
355+ assert call_count == 1
356+ assert transport ._subscribe_method == 'POST'
357+
358+
359+ @pytest .mark .asyncio
360+ async def test_compat_rest_transport_subscribe_post_500_no_retry (transport ):
361+ """Scenario: POST return 500, no automatic retry."""
362+ call_count = 0
363+
364+ async def mock_stream (method , path , context = None ):
365+ nonlocal call_count
366+ call_count += 1
367+ if False :
368+ yield
369+ create_500_error ()
370+
371+ transport ._send_stream_request = mock_stream
372+
373+ req = SubscribeToTaskRequest (id = 'task-123' )
374+ with pytest .raises (A2AClientError ) as exc_info :
375+ [event async for event in transport .subscribe (req )]
376+
377+ assert '500' in str (exc_info .value )
378+ assert call_count == 1 # No retry on 500
379+ assert transport ._subscribe_method == 'POST'
380+ assert transport ._subscribe_retry_attempted is False
251381
252382
253383def test_compat_rest_transport_handle_http_error (transport ):
0 commit comments