diff --git a/api/opentrons/server/rpc.py b/api/opentrons/server/rpc.py index ba749df277f..065463d1ea6 100755 --- a/api/opentrons/server/rpc.py +++ b/api/opentrons/server/rpc.py @@ -276,8 +276,11 @@ async def make_call(self, func, token): line_no = 'unknown' finally: response['$']['status'] = 'error' - call_result = '{0} [line {1}]: {2}'.format( - e.__class__.__name__, line_no, str(e)) + call_result = { + 'message': '{0} [line {1}]: {2}'.format( + e.__class__.__name__, line_no, str(e)), + 'traceback': traceback.format_exc() + } finally: response['data'] = call_result return response diff --git a/api/tests/opentrons/server/test_server.py b/api/tests/opentrons/server/test_server.py index b374b21b9ec..8726c37798a 100755 --- a/api/tests/opentrons/server/test_server.py +++ b/api/tests/opentrons/server/test_server.py @@ -199,13 +199,16 @@ async def test_exception_on_call(session, root): await session.socket.receive_json() # Skip ack res = await session.socket.receive_json() # Get call result - expected = {'$': { - 'token': session.token, - 'status': 'error', - 'type': rpc.CALL_RESULT_MESSAGE}, - 'data': 'Exception [line unknown]: Kaboom!'} + expectedMeta = { + 'token': session.token, + 'status': 'error', + 'type': rpc.CALL_RESULT_MESSAGE + } + expectedMessage = 'Exception [line unknown]: Kaboom!' - assert res == expected + assert res['$'] == expectedMeta + assert res['data']['message'] == expectedMessage + assert isinstance(res['data']['traceback'], str) @pytest.mark.parametrize('root', [Foo(0)]) diff --git a/app/src/robot/api-client/index.js b/app/src/robot/api-client/index.js index 41ec368d761..edd31f07055 100644 --- a/app/src/robot/api-client/index.js +++ b/app/src/robot/api-client/index.js @@ -1,8 +1,11 @@ // robot api client redux middleware // wraps the api client worker to handle API side effects in a different thread +import createLogger from '../../logger' import Worker from './worker' +const log = createLogger(__filename) + export default function apiClientMiddleware () { const worker = new Worker() @@ -10,7 +13,21 @@ export default function apiClientMiddleware () { const {getState, dispatch} = store worker.onmessage = function handleWorkerMessage (event) { - dispatch(event.data) + const action = event.data + + // log error actions + if (action && action.payload) { + const error = action.error === true + ? action.payload + : action.payload.error + + if (error) { + log.warn('Error response from robot', {action}) + if (error.traceback) log.warn(error.traceback) + } + } + + dispatch(action) } // initialize worker diff --git a/app/src/rpc/client.js b/app/src/rpc/client.js index f10e504b5c1..2c6fe075682 100644 --- a/app/src/rpc/client.js +++ b/app/src/rpc/client.js @@ -65,12 +65,16 @@ class RpcContext extends EventEmitter { return new Promise((resolve, reject) => { let timeout - const handleError = (reason) => { + const handleError = (reason, traceback) => { cleanup() - reject(new RemoteError(reason, name, args)) + reject(new RemoteError(reason, name, args, traceback)) + } + + const handleFailure = (res) => { + if (typeof res === 'string') return handleError(res) + handleError(res.message, res.traceback) } - const handleFailure = (result) => handleError(result) const handleNack = (reason) => handleError(`Received NACK with ${reason}`) const handleAck = () => { diff --git a/app/src/rpc/remote-error.js b/app/src/rpc/remote-error.js index 56dd41062ff..6e89d9ae4bd 100644 --- a/app/src/rpc/remote-error.js +++ b/app/src/rpc/remote-error.js @@ -1,9 +1,10 @@ // remote call error object export default class RemoteError extends Error { - constructor (message, methodName, args) { + constructor (message, methodName, args, traceback) { super(message) this.name = this.constructor.name this.methodName = methodName this.args = args + this.traceback = traceback } } diff --git a/app/src/rpc/test/client.test.js b/app/src/rpc/test/client.test.js index 6afa031daa4..a2a4f322a28 100644 --- a/app/src/rpc/test/client.test.js +++ b/app/src/rpc/test/client.test.js @@ -206,7 +206,7 @@ describe('rpc client', () => { }) }) - test('rejects if call is unsuccessful', () => { + test('rejects if call is unsuccessful (string response)', () => { sendControlAndResolveRemote((message) => { const token = message.$.token const ack = makeAckResponse(token) @@ -222,6 +222,23 @@ describe('rpc client', () => { message: expect.stringMatching(/ahhh/) }) }) + + test('rejects if call is unsuccessful (object response)', () => { + sendControlAndResolveRemote((message) => { + const token = message.$.token + const ack = makeAckResponse(token) + const result = makeCallResponse(token, FAILURE, {message: 'ahhh'}) + setTimeout(() => ws.send(ack), 1) + setTimeout(() => ws.send(result), 5) + }) + + const call = Client(url) + .then((client) => client.callRemote(id, name, args)) + + return expect(call).rejects.toMatchObject({ + message: expect.stringMatching(/ahhh/) + }) + }) }) describe('resolveTypeValues', () => {