diff --git a/python/coinbase-agentkit/coinbase_agentkit/action_providers/x402/x402_action_provider.py b/python/coinbase-agentkit/coinbase_agentkit/action_providers/x402/x402_action_provider.py index 731c7cbfd..a682cdd20 100644 --- a/python/coinbase-agentkit/coinbase_agentkit/action_providers/x402/x402_action_provider.py +++ b/python/coinbase-agentkit/coinbase_agentkit/action_providers/x402/x402_action_provider.py @@ -241,16 +241,25 @@ def make_http_request(self, wallet_provider: WalletProvider, args: dict[str, Any timeout=30, ) - # Retry with other http method for 404 status code + # Never silently retry with a different HTTP method: flipping GET to + # POST turns an intended read into a possible write on services that + # map both methods to the same path. Surface the 404 with a hint and + # let the agent choose explicitly. (Parity with TypeScript #1405.) if response.status_code == 404: - method = "POST" if method == "GET" else "GET" - can_have_body = method in ["POST", "PUT", "PATCH"] - response = requests.request( - url=final_url, - method=method, - headers=args.get("headers"), - json=args.get("body") if can_have_body else None, - timeout=30, + data = self._parse_response_data(response) + return json.dumps( + { + "success": False, + "url": final_url, + "method": method, + "status": 404, + "data": data, + "hint": ( + f"The service returned 404 for {method}. If it expects a " + "different method, call this action again with that method explicitly." + ), + }, + indent=2, ) if response.status_code != 402: diff --git a/python/coinbase-agentkit/tests/action_providers/x402/test_x402_action_provider.py b/python/coinbase-agentkit/tests/action_providers/x402/test_x402_action_provider.py index 318da7033..bee56e517 100644 --- a/python/coinbase-agentkit/tests/action_providers/x402/test_x402_action_provider.py +++ b/python/coinbase-agentkit/tests/action_providers/x402/test_x402_action_provider.py @@ -121,6 +121,34 @@ def test_make_http_request_success(mock_wallet, mock_requests): assert response["data"] == {"data": "success"} +def test_make_http_request_404_does_not_flip_method(mock_wallet, mock_requests): + """404 must not silently retry with the opposite HTTP method (TS #1405 parity).""" + from coinbase_agentkit.action_providers.x402.schemas import X402Config + + not_found = Mock(spec=requests.Response) + not_found.status_code = 404 + not_found.headers = {"content-type": "text/plain"} + not_found.text = "missing" + # Fixture installs a side_effect; replace it so GET is not forced to 200. + mock_requests.side_effect = None + mock_requests.return_value = not_found + + config = X402Config(registered_services=[MOCK_URL]) + provider = x402_action_provider(config) + + response = json.loads( + provider.make_http_request(mock_wallet, {"url": MOCK_URL, "method": "GET"}) + ) + + assert response["success"] is False + assert response["status"] == 404 + assert response["method"] == "GET" + assert "hint" in response + # Single request: no silent GET->POST flip + assert mock_requests.call_count == 1 + assert mock_requests.call_args.kwargs["method"] == "GET" + + def test_make_http_request_402(mock_wallet, mock_requests): """Test HTTP request that returns 402 Payment Required.""" from coinbase_agentkit.action_providers.x402.schemas import X402Config