diff --git a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java index 264bd0ab65..5c31f0f8b8 100644 --- a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java +++ b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/ComplianceTest.java @@ -333,9 +333,12 @@ public void exceptions() { assertEquals(23, calc3.getValue()); boolean thrown = false; try { - calc3.add(10); + calc3.add(10); + } catch (JsiiException e) { + // We expect a RuntimeException that is NOT a JsiiException. + throw e; } catch (RuntimeException e) { - thrown = true; + thrown = true; } assertTrue(thrown); calc3.setMaxValue(40); @@ -450,6 +453,8 @@ public java.lang.Number overrideMe(java.lang.Number mult) { boolean thrown = false; try { obj.callMe(); + } catch (JsiiException e) { + throw e; } catch (RuntimeException e) { assertTrue(e.getMessage().contains( "Thrown by native code")); thrown = true; @@ -519,6 +524,8 @@ public String getTheProperty() { boolean thrown = false; try { so.retrieveValueOfTheProperty(); + } catch (JsiiException e) { + throw e; } catch (RuntimeException e) { assertTrue(e.getMessage().contains("Oh no, this is bad")); thrown = true; @@ -537,6 +544,8 @@ public void setTheProperty(String value) { boolean thrown = false; try { so.modifyValueOfTheProperty("Hii"); + } catch (JsiiException e) { + throw e; } catch (RuntimeException e) { assertTrue(e.getMessage().contains("Exception from overloaded setter")); thrown = true; diff --git a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java index cecf0af514..0715378209 100644 --- a/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java +++ b/packages/@jsii/java-runtime-test/project/src/test/java/software/amazon/jsii/testing/JsiiClientTest.java @@ -117,7 +117,7 @@ public void asyncMethodOverrides() { assertEquals(obj.getObjId(), JsiiObjectRef.parse(first.getInvoke().getObjref()).getObjId()); // now complete the callback with some override value - client.completeCallback(first, null, toSandbox(999)); + client.completeCallback(first, null, null, toSandbox(999)); // end the async invocation, but now we expect the value to be different since we override the method. JsonNode result = client.endAsyncMethod(promise); @@ -146,19 +146,55 @@ public void asyncMethodOverridesThrow() { assertEquals(obj.getObjId(), JsiiObjectRef.parse(first.getInvoke().getObjref()).getObjId()); // now complete the callback with an error - client.completeCallback(first, "Hello, Error", null); + client.completeCallback(first, "Hello, Error", null, null); // end the async invocation, but now we expect the value to be different since we override the method. boolean thrown = false; try { client.endAsyncMethod(promise); - } catch (JsiiException e) { + } catch (RuntimeException e) { + assertEquals(RuntimeException.class, e.getClass()); assertTrue(e.getMessage().contains("Hello, Error")); thrown = true; } assertTrue(thrown); } + @Test + public void asyncMethodOverridesThrowWithFault() { + JsiiObjectRef obj = client.createObject("jsii-calc.AsyncVirtualMethods", Arrays.asList(), methodOverride("overrideMe", "myCookie"), Arrays.asList()); + + // begin will return a promise + JsiiPromise promise = client.beginAsyncMethod(obj, "callMe", toSandboxArray()); + assertFalse(promise.getPromiseId().isEmpty()); + + // now we expect to see a callback to "overrideMe" in the pending callbacks queue + + List callbacks = client.pendingCallbacks(); + + assertEquals(1, callbacks.size()); + + Callback first = callbacks.get(0); + assertEquals("overrideMe", first.getInvoke().getMethod()); + assertEquals("myCookie", first.getCookie()); + assertEquals(1, first.getInvoke().getArgs().size()); + assertEquals(JsiiObjectMapper.valueToTree(10), first.getInvoke().getArgs().get(0)); + assertEquals(obj.getObjId(), JsiiObjectRef.parse(first.getInvoke().getObjref()).getObjId()); + + // now complete the callback with an error + client.completeCallback(first, "Hello, Fault", "@jsii/kernel.Fault", null); + + // end the async invocation, but now we expect the value to be different since we override the method. + boolean thrown = false; + try { + client.endAsyncMethod(promise); + } catch (JsiiException e) { + assertTrue(e.getMessage().contains("Hello, Fault")); + thrown = true; + } + assertTrue(thrown); + } + @Test public void syncVirtualMethods() { JsiiObjectRef obj = client.createObject("jsii-calc.SyncVirtualMethods", Arrays.asList(), methodOverride("virtualMethod","myCookie"), Arrays.asList()); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java index 9dbbad425d..0f5981d6a1 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiClient.java @@ -243,12 +243,14 @@ public List pendingCallbacks() { * Completes a callback. * @param callback The callback to complete. * @param error Error information (or null). + * @param name Error type (or null). * @param result Result (or null). */ - public void completeCallback(final Callback callback, final String error, final JsonNode result) { + public void completeCallback(final Callback callback, final String error, final String name, final JsonNode result) { ObjectNode req = makeRequest("complete"); req.put("cbid", callback.getCbid()); req.put("err", error); + req.put("name", name); req.set("result", result); this.runtime.requestResponse(req); diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java index c4222013b4..8be4fc2a46 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiEngine.java @@ -486,7 +486,15 @@ private Object invokeMethod(final Object obj, final Method method, final Object. throw e; } } catch (InvocationTargetException e) { - throw new JsiiError(e.getTargetException()); + if (e.getTargetException() instanceof JsiiError){ + throw (JsiiError)(e.getTargetException()); + } else if (e.getTargetException() instanceof RuntimeException) { + // can rethrow without wrapping here + throw (RuntimeException)(e.getTargetException()); + } else { + // Can't just throw a checked error without wrapping it :( + throw new RuntimeException(e.getTargetException()); + } } catch (IllegalAccessException e) { throw new JsiiError(e); } finally { @@ -502,9 +510,12 @@ private Object invokeMethod(final Object obj, final Method method, final Object. private void processCallback(final Callback callback) { try { JsonNode result = handleCallback(callback); - this.getClient().completeCallback(callback, null, result); - } catch (JsiiError e) { - this.getClient().completeCallback(callback, e.getMessage(), null); + this.getClient().completeCallback(callback, null, null, result); + } catch (Exception e) { + String name = e instanceof JsiiException + ? JsiiException.Type.JSII_FAULT.toString() + : JsiiException.Type.RUNTIME_ERROR.toString(); + this.getClient().completeCallback(callback, e.getMessage(), name, null); } } diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java index 92e5d5a305..011e7f94d6 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiException.java @@ -8,7 +8,7 @@ public abstract class JsiiException extends RuntimeException { static enum Type { JSII_FAULT("@jsii/kernel.Fault"), - RUNTIME_EXCEPTION("@jsii/kernel.RuntimeException"); + RUNTIME_ERROR("@jsii/kernel.RuntimeError"); private final String errorType; diff --git a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java index 94520bbcbf..57f97fcc35 100644 --- a/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java +++ b/packages/@jsii/java-runtime/project/src/main/java/software/amazon/jsii/JsiiRuntime.java @@ -123,7 +123,7 @@ private JsonNode processErrorResponse(final JsonNode resp) { errorMessage += "\n" + resp.get("stack").asText(); } - if (errorName.equals(JsiiException.Type.RUNTIME_EXCEPTION.toString())) { + if (errorName.equals(JsiiException.Type.RUNTIME_ERROR.toString())) { throw new RuntimeException(errorMessage); } @@ -146,6 +146,7 @@ private JsonNode processCallbackResponse(final JsonNode resp) { JsonNode result = null; String error = null; + String name = null; try { result = this.callbackHandler.handleCallback(callback); } catch (Exception e) { @@ -154,12 +155,17 @@ private JsonNode processCallbackResponse(final JsonNode resp) { } else { error = e.getMessage(); } + + name = e instanceof JsiiError + ? JsiiException.Type.JSII_FAULT.toString() + : JsiiException.Type.RUNTIME_ERROR.toString(); } ObjectNode completeResponse = JsonNodeFactory.instance.objectNode(); completeResponse.put("cbid", callback.getCbid()); if (error != null) { completeResponse.put("err", error); + completeResponse.put("name", name); } if (result != null) { completeResponse.set("result", result); diff --git a/packages/@jsii/kernel/src/api.ts b/packages/@jsii/kernel/src/api.ts index b339a4716b..453b8c639e 100644 --- a/packages/@jsii/kernel/src/api.ts +++ b/packages/@jsii/kernel/src/api.ts @@ -217,6 +217,7 @@ export interface CallbacksResponse { export interface CompleteRequest { readonly cbid: string; readonly err?: string; + readonly name?: string; readonly result?: any; } diff --git a/packages/@jsii/kernel/src/kernel.ts b/packages/@jsii/kernel/src/kernel.ts index 8277146bae..f9681ab7b1 100644 --- a/packages/@jsii/kernel/src/kernel.ts +++ b/packages/@jsii/kernel/src/kernel.ts @@ -470,9 +470,15 @@ export class Kernel { try { result = await promise; this._debug('promise result:', result); - } catch (e) { + } catch (e: any) { this._debug('promise error:', e); - throw new JsiiFault((e as any).message); + if (e.name === JsiiErrorType.JSII_FAULT) { + throw new JsiiFault(e.message); + } + + // default to RuntimeError, since non-kernel errors may not + // have their `name` field defined + throw new RuntimeError(e); } return { @@ -505,7 +511,7 @@ export class Kernel { } public complete(req: api.CompleteRequest): api.CompleteResponse { - const { cbid, err, result } = req; + const { cbid, err, result, name } = req; this._debug('complete', cbid, err, result); @@ -516,7 +522,11 @@ export class Kernel { if (err) { this._debug('completed with error:', err); - cb.fail(new Error(err)); + cb.fail( + name === JsiiErrorType.JSII_FAULT + ? new JsiiFault(err) + : new RuntimeError(err), + ); } else { const sandoxResult = this._toSandbox( result, diff --git a/packages/@jsii/kernel/src/recording.ts b/packages/@jsii/kernel/src/recording.ts index bb77e9c05c..4af9604ab2 100644 --- a/packages/@jsii/kernel/src/recording.ts +++ b/packages/@jsii/kernel/src/recording.ts @@ -60,7 +60,7 @@ export function recordInteraction(kernel: Kernel, inputOutputLogPath: string) { return ret; } catch (e: any) { logOutput({ error: e.message, name: e.name }); - if (e.type === JsiiErrorType.RUNTIME_ERROR) { + if (e.name === JsiiErrorType.RUNTIME_ERROR) { throw new RuntimeError(e.message); } throw new JsiiFault(e.message); diff --git a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py index 14466d8dfa..429f9a70fe 100644 --- a/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py +++ b/packages/@jsii/python-runtime/src/jsii/_kernel/providers/process.py @@ -62,7 +62,7 @@ CompleteRequest, CompleteResponse, ) -from ...errors import JSIIError, JavaScriptError +from ...errors import ErrorType, JSIIError, JavaScriptError @attr.s(auto_attribs=True, frozen=True, slots=True) @@ -83,10 +83,11 @@ class _OkayResponse: @attr.s(auto_attribs=True, frozen=True, slots=True) -class _ErrorRespose: +class _ErrorResponse: error: str stack: str + name: str @attr.s(auto_attribs=True, frozen=True, slots=True) @@ -101,7 +102,7 @@ class _CompleteRequest: complete: CompleteRequest -_ProcessResponse = Union[_OkayResponse, _ErrorRespose, _CallbackResponse] +_ProcessResponse = Union[_OkayResponse, _ErrorResponse, _CallbackResponse] def _with_api_key(api_name, asdict): @@ -326,6 +327,8 @@ def send( elif isinstance(resp, _CallbackResponse): return resp.callback else: + if resp.name == ErrorType.RUNTIME_ERROR.value: + raise RuntimeError(resp.error) from JavaScriptError(resp.stack) raise JSIIError(resp.error) from JavaScriptError(resp.stack) diff --git a/packages/@jsii/python-runtime/src/jsii/errors.py b/packages/@jsii/python-runtime/src/jsii/errors.py index 3e85012c88..abd659ebbc 100644 --- a/packages/@jsii/python-runtime/src/jsii/errors.py +++ b/packages/@jsii/python-runtime/src/jsii/errors.py @@ -1,4 +1,5 @@ import textwrap +from enum import Enum class JSIIError(Exception): @@ -11,3 +12,8 @@ def __init__(self, stack): def __str__(self): return "\n" + textwrap.indent(self.stack, " ", lambda line: bool(line)) + + +class ErrorType(Enum): + JSII_FAULT = "@jsii/kernel.Fault" + RUNTIME_ERROR = "@jsii/kernel.RuntimeError" diff --git a/packages/@jsii/python-runtime/tests/test_compliance.py b/packages/@jsii/python-runtime/tests/test_compliance.py index 329aabfca6..7ae4b89545 100644 --- a/packages/@jsii/python-runtime/tests/test_compliance.py +++ b/packages/@jsii/python-runtime/tests/test_compliance.py @@ -438,7 +438,7 @@ def test_exceptions(): assert calc3.value == 23 - with pytest.raises(Exception): + with pytest.raises(RuntimeError): calc3.add(10) calc3.max_value = 40 @@ -544,7 +544,7 @@ def override_me(self, mult): obj = ThrowingAsyncVirtualMethods() - with pytest.raises(Exception, match="Thrown by native code"): + with pytest.raises(RuntimeError, match="Thrown by native code"): obj.call_me() @@ -623,7 +623,7 @@ def the_property(self, value): so = ThrowingSyncVirtualMethods() - with pytest.raises(Exception, match="Oh no, this is bad"): + with pytest.raises(RuntimeError, match="Oh no, this is bad"): so.retrieve_value_of_the_property() @@ -639,7 +639,7 @@ def the_property(self, value): so = ThrowingSyncVirtualMethods() - with pytest.raises(Exception, match="Exception from overloaded setter"): + with pytest.raises(RuntimeError, match="Exception from overloaded setter"): so.modify_value_of_the_property("Hii") @@ -704,7 +704,7 @@ def test_fail_syncOverrides_callsDoubleAsync_method(): obj.call_async = True # TODO: Error Handling - with pytest.raises(Exception): + with pytest.raises(RuntimeError): obj.caller_is_method() @@ -713,7 +713,7 @@ def test_fail_syncOverrides_callsDoubleAsync_propertyGetter(): obj.call_async = True # TODO: Error Handling - with pytest.raises(Exception): + with pytest.raises(RuntimeError): obj.caller_is_property @@ -723,7 +723,7 @@ def test_fail_syncOverrides_callsDoubleAsync_propertySetter(): obj.call_async = True # TODO: Error Handling - with pytest.raises(Exception): + with pytest.raises(RuntimeError): obj.caller_is_property = 12 diff --git a/packages/@jsii/runtime/lib/host.ts b/packages/@jsii/runtime/lib/host.ts index e12d33f418..aa0b1dff2c 100644 --- a/packages/@jsii/runtime/lib/host.ts +++ b/packages/@jsii/runtime/lib/host.ts @@ -1,4 +1,11 @@ -import { api, Kernel, JsiiFault, JsiiError } from '@jsii/kernel'; +import { + api, + Kernel, + JsiiFault, + JsiiError, + RuntimeError, + JsiiErrorType, +} from '@jsii/kernel'; import { EventEmitter } from 'events'; import { Input, IInputOutput } from './in-out'; @@ -59,7 +66,10 @@ export class KernelHost { completeReq.complete.cbid === callback.cbid ) { if (completeReq.complete.err) { - throw new JsiiFault(completeReq.complete.err); + if (completeReq.complete.name === JsiiErrorType.JSII_FAULT) { + throw new JsiiFault(completeReq.complete.err); + } + throw new RuntimeError(completeReq.complete.err); } return completeReq.complete.result;