diff --git a/src/python/grpcio/grpc/_channel.py b/src/python/grpcio/grpc/_channel.py index 8cc0e981ef1..2017d471308 100644 --- a/src/python/grpcio/grpc/_channel.py +++ b/src/python/grpcio/grpc/_channel.py @@ -58,6 +58,17 @@ _STREAM_STREAM_INITIAL_DUE = ( _CHANNEL_SUBSCRIPTION_CALLBACK_ERROR_LOG_MESSAGE = ( 'Exception calling channel subscription callback!') +_OK_RENDEZVOUS_REPR_FORMAT = ('<_Rendezvous of RPC that terminated with:\n' + '\tstatus = {}\n' + '\tdetails = "{}"\n' + '>') + +_NON_OK_RENDEZVOUS_REPR_FORMAT = ('<_Rendezvous of RPC that terminated with:\n' + '\tstatus = {}\n' + '\tdetails = "{}"\n' + '\tdebug_error_string = "{}"\n' + '>') + def _deadline(timeout): return None if timeout is None else time.time() + timeout @@ -91,6 +102,7 @@ class _RPCState(object): self.trailing_metadata = trailing_metadata self.code = code self.details = details + self.debug_error_string = None # The semantics of grpc.Future.cancel and grpc.Future.cancelled are # slightly wonky, so they have to be tracked separately from the rest of the # result of the RPC. This field tracks whether cancellation was requested @@ -137,6 +149,7 @@ def _handle_event(event, state, response_deserializer): else: state.code = code state.details = batch_operation.details() + state.debug_error_string = batch_operation.error_string() callbacks.extend(state.callbacks) state.callbacks = None return callbacks @@ -374,13 +387,23 @@ class _Rendezvous(grpc.RpcError, grpc.Future, grpc.Call): self._state.condition.wait() return _common.decode(self._state.details) + def debug_error_string(self): + with self._state.condition: + while self._state.debug_error_string is None: + self._state.condition.wait() + return _common.decode(self._state.debug_error_string) + def _repr(self): with self._state.condition: if self._state.code is None: return '<_Rendezvous object of in-flight RPC>' + elif self._state.code is grpc.StatusCode.OK: + return _OK_RENDEZVOUS_REPR_FORMAT.format( + self._state.code, self._state.details) else: - return '<_Rendezvous of RPC that terminated with ({}, {})>'.format( - self._state.code, _common.decode(self._state.details)) + return _NON_OK_RENDEZVOUS_REPR_FORMAT.format( + self._state.code, self._state.details, + self._state.debug_error_string) def __repr__(self): return self._repr() diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi index a4c0319553d..2d6c900c544 100644 --- a/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi +++ b/src/python/grpcio/grpc/_cython/_cygrpc/grpc.pxi @@ -291,6 +291,7 @@ cdef extern from "grpc/grpc.h": grpc_metadata_array *trailing_metadata grpc_status_code *status grpc_slice *status_details + char** error_string ctypedef struct grpc_op_data_recv_close_on_server: int *cancelled diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/operation.pxd.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/operation.pxd.pxi index bfbe27785b7..69a2a4989ed 100644 --- a/src/python/grpcio/grpc/_cython/_cygrpc/operation.pxd.pxi +++ b/src/python/grpcio/grpc/_cython/_cygrpc/operation.pxd.pxi @@ -91,9 +91,11 @@ cdef class ReceiveStatusOnClientOperation(Operation): cdef grpc_metadata_array _c_trailing_metadata cdef grpc_status_code _c_code cdef grpc_slice _c_details + cdef const char* _c_error_string cdef tuple _trailing_metadata cdef object _code cdef str _details + cdef str _error_string cdef void c(self) cdef void un_c(self) diff --git a/src/python/grpcio/grpc/_cython/_cygrpc/operation.pyx.pxi b/src/python/grpcio/grpc/_cython/_cygrpc/operation.pyx.pxi index 239d0f3f952..454627f570a 100644 --- a/src/python/grpcio/grpc/_cython/_cygrpc/operation.pyx.pxi +++ b/src/python/grpcio/grpc/_cython/_cygrpc/operation.pyx.pxi @@ -199,6 +199,8 @@ cdef class ReceiveStatusOnClientOperation(Operation): &self._c_code) self.c_op.data.receive_status_on_client.status_details = ( &self._c_details) + self.c_op.data.receive_status_on_client.error_string = ( + &self._c_error_string) cdef void un_c(self): self._trailing_metadata = _metadata(&self._c_trailing_metadata) @@ -206,6 +208,11 @@ cdef class ReceiveStatusOnClientOperation(Operation): self._code = self._c_code self._details = _decode(_slice_bytes(self._c_details)) grpc_slice_unref(self._c_details) + if self._c_error_string != NULL: + self._error_string = _decode(self._c_error_string) + gpr_free(self._c_error_string) + else: + self._error_string = "" def trailing_metadata(self): return self._trailing_metadata @@ -216,6 +223,9 @@ cdef class ReceiveStatusOnClientOperation(Operation): def details(self): return self._details + def error_string(self): + return self._error_string + cdef class ReceiveCloseOnServerOperation(Operation): diff --git a/src/python/grpcio_tests/tests/unit/_rpc_test.py b/src/python/grpcio_tests/tests/unit/_rpc_test.py index 54f01d9f8dd..34e7831a983 100644 --- a/src/python/grpcio_tests/tests/unit/_rpc_test.py +++ b/src/python/grpcio_tests/tests/unit/_rpc_test.py @@ -225,6 +225,7 @@ class RPCTest(unittest.TestCase): self.assertEqual(expected_response, response) self.assertIs(grpc.StatusCode.OK, call.code()) + self.assertEqual("", call.debug_error_string()) def testSuccessfulUnaryRequestFutureUnaryResponse(self): request = b'\x07\x08' @@ -706,6 +707,13 @@ class RPCTest(unittest.TestCase): self.assertIs(grpc.StatusCode.UNKNOWN, exception_context.exception.code()) + # sanity checks on to make sure returned string contains default members + # of the error + debug_error_string = exception_context.exception.debug_error_string() + self.assertIn("created", debug_error_string) + self.assertIn("description", debug_error_string) + self.assertIn("file", debug_error_string) + self.assertIn("file_line", debug_error_string) def testFailedUnaryRequestFutureUnaryResponse(self): request = b'\x37\x17'