Commits

Olemis Lang  committed b955297

TracRpc: API v2: Fixed JSON-RPC (multicall & IDs on error). Exception handling.

  • Participants
  • Parent commits 6939d0f

Comments (0)

Files changed (1)

File t5437/t5437-protocol_api_v2-r7194.diff

 RPC Protocol API version 2
 
-diff -r b5e897b63dc2 trunk/setup.py
---- a/trunk/setup.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/setup.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/setup.py
+--- a/trunk/setup.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/setup.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -28,7 +28,7 @@
      url='http://trac-hacks.org/wiki/XmlRpcPlugin',
      description='RPC interface to Trac',
      tests_require = test_deps,
      packages=find_packages(exclude=['*.tests']),
      package_data={
-diff -r b5e897b63dc2 trunk/tracrpc/api.py
---- a/trunk/tracrpc/api.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/api.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/api.py
+--- a/trunk/tracrpc/api.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/api.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -21,13 +21,34 @@
      """ RPC Binary type. Currently == xmlrpclib.Binary. """
      pass
  
  class IXMLRPCHandler(Interface):
  
-diff -r b5e897b63dc2 trunk/tracrpc/json_rpc.py
---- a/trunk/tracrpc/json_rpc.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/json_rpc.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/json_rpc.py
+--- a/trunk/tracrpc/json_rpc.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/json_rpc.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -5,8 +5,9 @@
  (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
  """
  from types import GeneratorType
  
  from trac.core import *
-@@ -16,7 +17,7 @@
+@@ -14,9 +15,10 @@
+ from trac.resource import ResourceNotFound
+ from trac.util.datefmt import utc
  from trac.util.text import to_unicode
++from trac.web.api import RequestDone
  
  from tracrpc.api import IRPCProtocol, XMLRPCSystem, Binary, \
 -        RPCError, MethodNotFound
  from tracrpc.util import exception_to_unicode, empty, prepare_docs
  
  __all__ = ['JsonRpcProtocol']
-@@ -92,6 +93,12 @@
+@@ -92,6 +94,12 @@
          obj = json.JSONDecoder.decode(self, obj, *args, **kwargs)
          return self._normalize(obj)
  
  class JsonRpcProtocol(Component):
      r"""
      Example `POST` request using `curl` with `Content-Type` header
-@@ -126,34 +133,41 @@
+@@ -126,34 +134,47 @@
          # Legacy path - provided for backwards compatibility:
          yield ('jsonrpc', 'application/json')
  
 +            raise JsonProtocolException("Error: JSON-RPC not available.\n")
          try:
              data = json.load(req, cls=TracRpcJSONDecoder)
++            self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id'))
 +            if data.get('method') == 'system.multicall':
 +                # Prepare for multicall
-+                for signature in data.itervalues() :
++                self.log.debug("RPC(json) Multicall request %s", data)
++                params = data.get('params', [])
++                for signature in params :
 +                    signature['methodName'] = signature.get('method', '')
-+            self.log.info("RPC(json) JSON-RPC request ID : %s.", data.get('id'))
++                data['params'] = [params]
 +            return data
          except Exception, e:
              # Abort with exception - no data can be read
 -                response = {'result': results, 'error': None, 'id': r_id}
 +            if rpcreq.get('method') == 'system.multicall': 
 +                # Custom multicall
-+                args = rpcreq.get('params') or []
-+                mcresults = [self._json_result(value, sig.get('id') or r_id) \
++                args = (rpcreq.get('params') or [[]])[0]
++                mcresults = [self._json_result(
++                                        isinstance(value, Exception) and \
++                                                    value or value[0], \
++                                        sig.get('id') or r_id) \
 +                              for sig, value in izip(args, result)]
 +                
 +                # TODO: Which one is better ?
              try: # JSON encoding
                  self.log.debug("RPC(json) result: %s" % repr(response))
                  response = json.dumps(response, cls=TracRpcJSONEncoder)
-@@ -166,7 +180,13 @@
+@@ -165,28 +186,34 @@
+                                                     traceback=True))
              response = json.dumps(self._json_error(e, r_id=r_id),
                              cls=TracRpcJSONEncoder)
-         self.log.debug("RPC(json) encoded result: %s" % response)
+-        self.log.debug("RPC(json) encoded result: %s" % response)
 -        self._send_response(req, response + '\n', content_type)
 +        self._send_response(req, response + '\n', rpcreq['mimetype'])
 +
 +    def send_rpc_error(self, req, rpcreq, e):
 +        """Send a JSON-RPC fault message back to the caller. """
 +        # TODO : Check error codes and RPC exceptions
-+        response = json.dumps(self._json_error(e), cls=TracRpcJSONEncoder)
++        r_id = rpcreq.get('id')
++        response = json.dumps(self._json_error(e, r_id=r_id), \
++                                  cls=TracRpcJSONEncoder)
 +        self._send_response(req, response + '\n', rpcreq['mimetype'])
  
      # Internal methods
  
-@@ -178,14 +198,11 @@
+     def _send_response(self, req, response, content_type='application/json'):
++        self.log.debug("RPC(json) encoded response: %s" % response)
+         response = to_unicode(response).encode("utf-8")
+         req.send_response(200)
+         req.send_header('Content-Type', content_type)
+         req.send_header('Content-Length', len(response))
          req.end_headers()
          req.write(response)
++        raise RequestDone()
  
 -    def _json_call(self, req, method, args, r_id=None):
 -        """ Call method and create response dictionary. """
 +        if not isinstance(result, Exception):
              return {'result': result, 'error': None, 'id': r_id}
 -        except Exception, e:
+-            return self._json_error(e, r_id=r_id)
 +        else :
-             return self._json_error(e, r_id=r_id)
++            return self._json_error(result, r_id=r_id)
  
      def _json_error(self, e, c=None, r_id=None):
-diff -r b5e897b63dc2 trunk/tracrpc/tests/__init__.py
---- a/trunk/tracrpc/tests/__init__.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/__init__.py	Thu Mar 18 14:13:28 2010 -0500
+         """ Makes a response dictionary that is an error. """
+diff -r 369fe39e568b trunk/tracrpc/tests/__init__.py
+--- a/trunk/tracrpc/tests/__init__.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/__init__.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -35,6 +35,7 @@
              print "Enabling RPC plugin and permissions..."
              env.config.set('components', 'tracrpc.*', 'enabled')
 +                raise self.failureException, "Expected %s\n\nNothing raised" % excName
 +
 +        assertRaises = failUnlessRaises
-diff -r b5e897b63dc2 trunk/tracrpc/tests/api.py
---- a/trunk/tracrpc/tests/api.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/api.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/tests/api.py
+--- a/trunk/tracrpc/tests/api.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/api.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -9,14 +9,14 @@
  import unittest
  import urllib2
  if __name__ == '__main__':
 -    unittest.main(defaultTest='suite')
 +    unittest.main(defaultTest='test_suite')
-diff -r b5e897b63dc2 trunk/tracrpc/tests/json_rpc.py
---- a/trunk/tracrpc/tests/json_rpc.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/json_rpc.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/tests/json_rpc.py
+--- a/trunk/tracrpc/tests/json_rpc.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/json_rpc.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -14,10 +14,10 @@
  from tracrpc.util import StringIO
  
          def _anon_req(self, data):
              req = urllib2.Request(rpc_testenv.url_anon,
                          headers={'Content-Type': 'application/json'})
-@@ -167,11 +167,11 @@
+@@ -59,6 +59,7 @@
+                     {'method': 'nonexisting', 'params': []}
+                 ], 'id': 233}
+             result = self._anon_req(data)
++            self.assertEquals(None, result['error'])
+             self.assertEquals(4, len(result['result']))
+             items = result['result']
+             self.assertEquals(1, items[0]['id'])
+@@ -167,11 +168,11 @@
              self.assertEquals(result['error']['message'],
                       'Wiki page "Test" does not exist at version 10')
  
  if __name__ == '__main__':
 -    unittest.main(defaultTest='suite')
 +    unittest.main(defaultTest='test_suite')
-diff -r b5e897b63dc2 trunk/tracrpc/tests/ticket.py
---- a/trunk/tracrpc/tests/ticket.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/ticket.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/tests/ticket.py
+--- a/trunk/tracrpc/tests/ticket.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/ticket.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -12,9 +12,9 @@
  import shutil
  import time
  if __name__ == '__main__':
 -    unittest.main(defaultTest='suite')
 +    unittest.main(defaultTest='test_suite')
-diff -r b5e897b63dc2 trunk/tracrpc/tests/wiki.py
---- a/trunk/tracrpc/tests/wiki.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/wiki.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/tests/wiki.py
+--- a/trunk/tracrpc/tests/wiki.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/wiki.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -13,10 +13,10 @@
  
  from trac.util.compat import sorted
  if __name__ == '__main__':
 -    unittest.main(defaultTest='suite')
 +    unittest.main(defaultTest='test_suite')
-diff -r b5e897b63dc2 trunk/tracrpc/tests/xml_rpc.py
---- a/trunk/tracrpc/tests/xml_rpc.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/tests/xml_rpc.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/tests/xml_rpc.py
+--- a/trunk/tracrpc/tests/xml_rpc.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/tests/xml_rpc.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -9,9 +9,9 @@
  
  import xmlrpclib
  if __name__ == '__main__':
 -    unittest.main(defaultTest='suite')
 +    unittest.main(defaultTest='test_suite')
-diff -r b5e897b63dc2 trunk/tracrpc/web_ui.py
---- a/trunk/tracrpc/web_ui.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/web_ui.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/web_ui.py
+--- a/trunk/tracrpc/web_ui.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/web_ui.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -6,6 +6,10 @@
  (c) 2009      ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
  """
      def _dump_docs(self, req):
          # Dump RPC documentation
          req.perm.require('XML_RPC') # Need at least XML_RPC
-@@ -128,6 +132,57 @@
+@@ -128,6 +132,66 @@
              return "Error rendering protocol documentation. " \
                         "Contact your '''Trac''' administrator for details"
  
 +        rpcreq = {'mimetype': content_type}
 +        try :
 +            self.log.debug("RPC(%s) call by '%s'", proto_id, req.authname)
-+            req.perm.require('XML_RPC') # Need at least XML_RPC
 +            rpcreq = protocol.parse_rpc_request(req, content_type)
 +            rpcreq['mimetype'] = content_type
++            
++            # Important ! Check after parsing RPC request to add 
++            #             protocol-specific fields in response 
++            #             (e.g. JSON-RPC response `id`)
++            req.perm.require('XML_RPC') # Need at least XML_RPC
++            
 +            method_name = rpcreq.get('method')
 +            if method_name is None :
 +                raise ProtocolException('Missing method name')
 +                raise ServiceException(e), None, tb
 +            else :
 +                protocol.send_rpc_result(req, rpcreq, result)
++        except RequestDone :
++            raise
 +        except (RPCError, PermissionError, ResourceNotFound), e:
 +            self.log.exception("RPC(%s) Error", proto_id)
 +            try :
 +                protocol.send_rpc_error(req, rpcreq, e)
++            except RequestDone :
++                raise
 +            except Exception, e :
 +                self.log.exception("RPC(%s) Unhandled protocol error", proto_id)
 +                self._send_unknown_error(req, rpcreq, e)
      # ITemplateProvider methods
  
      def get_htdocs_dirs(self):
-diff -r b5e897b63dc2 trunk/tracrpc/xml_rpc.py
---- a/trunk/tracrpc/xml_rpc.py	Thu Mar 18 12:50:57 2010 -0500
-+++ b/trunk/tracrpc/xml_rpc.py	Thu Mar 18 14:13:28 2010 -0500
+diff -r 369fe39e568b trunk/tracrpc/xml_rpc.py
+--- a/trunk/tracrpc/xml_rpc.py	Sat Mar 27 18:21:41 2010 -0500
++++ b/trunk/tracrpc/xml_rpc.py	Sat Mar 27 23:40:18 2010 -0500
 @@ -19,7 +19,7 @@
  from trac.util.text import to_unicode