Skip to content

Commit a3cc138

Browse files
cicoyleartursouza
andauthored
Add the dapr runtime returned error details to the Java DaprException (#998)
* properly add the dapr runtime returned error details to the Java DaprException Signed-off-by: Cassandra Coyle <[email protected]> * add error handling to sdk docs Signed-off-by: Cassandra Coyle <[email protected]> * add tests for the dapr exception changes Signed-off-by: Cassandra Coyle <[email protected]> * try verifyNoMoreInteractions w/ channel Signed-off-by: Cassandra Coyle <[email protected]> * verify channel close -> channel close explicitly Signed-off-by: Cassandra Coyle <[email protected]> * rm verifyNoMoreInteractions Signed-off-by: Cassandra Coyle <[email protected]> * rm test to see if that is the orphaned managed channel issue Signed-off-by: Cassandra Coyle <[email protected]> * re-add test since that doesnt seem to be the issue Signed-off-by: Cassandra Coyle <[email protected]> * channel.close(); -> verify(channel).close(); Signed-off-by: Cassandra Coyle <[email protected]> * Rewrite and redesign of the DaprErrorDetail in DaprException. Signed-off-by: Artur Souza <[email protected]> * Update daprdocs too for DaprErrorDetails. Signed-off-by: Artur Souza <[email protected]> * Fix README.md mm string. Signed-off-by: Artur Souza <[email protected]> * Fix exception example. Signed-off-by: Artur Souza <[email protected]> * Use runtime 1.13.0-rc.2 Signed-off-by: Artur Souza <[email protected]> * Fix exception example to match gRPC output. Signed-off-by: Artur Souza <[email protected]> * Update error message in IT as per new Dapr runtime version. Signed-off-by: Artur Souza <[email protected]> * Dapr 1.13 is less tolerant of app downtime to keep timers. Signed-off-by: Artur Souza <[email protected]> --------- Signed-off-by: Cassandra Coyle <[email protected]> Signed-off-by: Artur Souza <[email protected]> Co-authored-by: Artur Souza <[email protected]> Co-authored-by: Artur Souza <[email protected]>
1 parent cd81ee8 commit a3cc138

File tree

16 files changed

+605
-90
lines changed

16 files changed

+605
-90
lines changed

.github/workflows/build.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
GOPROXY: https://proxy.golang.org
4343
JDK_VER: ${{ matrix.java }}
4444
DAPR_CLI_VER: 1.12.0
45-
DAPR_RUNTIME_VER: 1.12.4
45+
DAPR_RUNTIME_VER: 1.13.0-rc.2
4646
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
4747
DAPR_CLI_REF:
4848
DAPR_REF:

.github/workflows/validate.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
GOPROXY: https://proxy.golang.org
3939
JDK_VER: ${{ matrix.java }}
4040
DAPR_CLI_VER: 1.12.0
41-
DAPR_RUNTIME_VER: 1.12.4
41+
DAPR_RUNTIME_VER: 1.13.0-rc.2
4242
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
4343
DAPR_CLI_REF:
4444
DAPR_REF:

daprdocs/content/en/java-sdk-docs/java-client/_index.md

+22
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,29 @@ If your Dapr instance is configured to require the `DAPR_API_TOKEN` environment
4040
set it in the environment and the client will use it automatically.
4141
You can read more about Dapr API token authentication [here](https://docs.dapr.io/operations/security/api-token/).
4242

43+
#### Error Handling
4344

45+
Initially, errors in Dapr followed the Standard gRPC error model. However, to provide more detailed and informative error
46+
messages, in version 1.13 an enhanced error model has been introduced which aligns with the gRPC Richer error model. In
47+
response, the Java SDK extended the DaprException to include the error details that were added in Dapr.
48+
49+
Example of handling the DaprException and consuming the error details when using the Dapr Java SDK:
50+
51+
```java
52+
...
53+
try {
54+
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
55+
} catch (DaprException exception) {
56+
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
57+
System.out.println("Dapr exception's message: " + exception.getMessage());
58+
// DaprException now contains `getStatusDetails()` to include more details about the error from Dapr runtime.
59+
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
60+
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
61+
"reason",
62+
TypeRef.STRING));
63+
}
64+
...
65+
```
4466

4567
## Building blocks
4668

examples/src/main/java/io/dapr/examples/exception/Client.java

+13-6
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,14 @@
1515

1616
import io.dapr.client.DaprClient;
1717
import io.dapr.client.DaprClientBuilder;
18+
import io.dapr.exceptions.DaprErrorDetails;
1819
import io.dapr.exceptions.DaprException;
20+
import io.dapr.utils.TypeRef;
21+
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.List;
25+
import java.util.Map;
1926

2027
/**
2128
* 1. Build and install jars:
@@ -33,17 +40,17 @@ public class Client {
3340
*/
3441
public static void main(String[] args) throws Exception {
3542
try (DaprClient client = new DaprClientBuilder().build()) {
36-
3743
try {
38-
client.getState("Unknown state store", "myKey", String.class).block();
44+
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
3945
} catch (DaprException exception) {
4046
System.out.println("Error code: " + exception.getErrorCode());
4147
System.out.println("Error message: " + exception.getMessage());
42-
43-
exception.printStackTrace();
48+
System.out.println("Reason: " + exception.getStatusDetails().get(
49+
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
50+
"reason",
51+
TypeRef.STRING));
4452
}
45-
46-
System.out.println("Done");
4753
}
54+
System.out.println("Done");
4855
}
4956
}

examples/src/main/java/io/dapr/examples/exception/README.md

+30-44
Original file line numberDiff line numberDiff line change
@@ -23,52 +23,49 @@ cd java-sdk
2323
Then build the Maven project:
2424

2525
```sh
26-
# make sure you are in the `java-sdk` directory.
27-
mvn install
26+
# make sure you are in the `java-sdk` (root) directory.
27+
./mvnw clean install
2828
```
2929

3030
Then get into the examples directory:
3131
```sh
3232
cd examples
3333
```
3434

35-
### Running the StateClient
36-
This example uses the Java SDK Dapr client in order to perform an invalid operation, causing Dapr runtime to return an error. See the code snippet below:
35+
### Understanding the code
36+
37+
This example uses the Java SDK Dapr client in order to perform an invalid operation, causing Dapr runtime to return an error. See the code snippet below, from `Client.java`:
3738

3839
```java
3940
public class Client {
4041

4142
public static void main(String[] args) throws Exception {
4243
try (DaprClient client = new DaprClientBuilder().build()) {
43-
4444
try {
45-
client.getState("Unknown state store", "myKey", String.class).block();
45+
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
4646
} catch (DaprException exception) {
47-
System.out.println("Error code: " + exception.getErrorCode());
48-
System.out.println("Error message: " + exception.getMessage());
49-
50-
exception.printStackTrace();
47+
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
48+
System.out.println("Dapr exception's message: " + exception.getMessage());
49+
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
50+
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
51+
"reason",
52+
TypeRef.STRING));
5153
}
52-
53-
System.out.println("Done");
5454
}
55+
System.out.println("Done");
5556
}
5657

5758
}
5859
```
59-
The code uses the `DaprClient` created by the `DaprClientBuilder`. It tries to get a state from state store, but provides an unknown state store. It causes the Dapr sidecar to return an error, which is converted to a `DaprException` to the application. To be compatible with Project Reactor, `DaprException` extends from `RuntimeException` - making it an unchecked exception. Applications might also get an `IllegalArgumentException` when invoking methods with invalid input parameters that are validated at the client side.
60-
61-
The Dapr client is also within a try-with-resource block to properly close the client at the end.
6260

6361
### Running the example
6462

65-
Run this example with the following command:
66-
6763
<!-- STEP
6864
name: Run exception example
6965
expected_stdout_lines:
7066
- '== APP == Error code: INVALID_ARGUMENT'
71-
- '== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found'
67+
- '== APP == Error message: INVALID_ARGUMENT: pubsub unknown_pubsub is not found'
68+
- '== APP == Reason: DAPR_PUBSUB_NOT_FOUND'
7269
background: true
7370
sleep: 5
7471
-->
@@ -79,41 +76,30 @@ dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-e
7976

8077
<!-- END_STEP -->
8178

82-
Once running, the OutputBindingExample should print the output as follows:
79+
Once running, the State Client Example should print the output as follows:
8380

8481
```txt
85-
== APP == Error code: INVALID_ARGUMENT
86-
87-
== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found
88-
89-
== APP == io.dapr.exceptions.DaprException: INVALID_ARGUMENT: state store Unknown state store is not found
90-
91-
== APP == at io.dapr.exceptions.DaprException.propagate(DaprException.java:168)
92-
93-
== APP == at io.dapr.client.DaprClientGrpc$2.onError(DaprClientGrpc.java:716)
82+
== APP == Error code: ERR_PUBSUB_NOT_FOUND
9483
95-
== APP == at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onClose(ClientCalls.java:478)
84+
== APP == Error message: ERR_PUBSUB_NOT_FOUND: pubsub unknown_pubsub is not found
9685
97-
== APP == at io.grpc.internal.DelayedClientCall$DelayedListener$3.run(DelayedClientCall.java:464)
98-
99-
== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.delayOrExecute(DelayedClientCall.java:428)
100-
101-
== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.onClose(DelayedClientCall.java:461)
102-
103-
== APP == at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:617)
104-
105-
== APP == at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:70)
106-
107-
== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:803)
86+
== APP == Reason: DAPR_PUBSUB_NOT_FOUND
87+
...
10888
109-
== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:782)
89+
```
11090

111-
== APP == at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
91+
### Debug
11292

113-
== APP == at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
114-
...
93+
You can further explore all the error details returned in the `DaprException` class.
94+
Before running it in your favorite IDE (like IntelliJ), compile and run the Dapr sidecar first.
11595

96+
1. Pre-req:
97+
```sh
98+
# make sure you are in the `java-sdk` (root) directory.
99+
./mvnw clean install
116100
```
101+
2. From the examples directory, run: `dapr run --app-id exception-example --dapr-grpc-port=50001 --dapr-http-port=3500`
102+
3. From your IDE click the play button on the client code and put break points where desired.
117103

118104
### Cleanup
119105

sdk-tests/src/test/java/io/dapr/it/TestUtils.java

+20
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313

1414
package io.dapr.it;
1515

16+
import io.dapr.exceptions.DaprErrorDetails;
1617
import io.dapr.exceptions.DaprException;
18+
import io.dapr.utils.TypeRef;
1719
import org.junit.jupiter.api.Assertions;
1820
import org.junit.jupiter.api.function.Executable;
1921

@@ -42,6 +44,24 @@ public static <T extends Throwable> void assertThrowsDaprException(
4244
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
4345
}
4446

47+
public static <T extends Throwable> void assertThrowsDaprExceptionWithReason(
48+
String expectedErrorCode,
49+
String expectedErrorMessage,
50+
String expectedReason,
51+
Executable executable) {
52+
DaprException daprException = Assertions.assertThrows(DaprException.class, executable);
53+
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
54+
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
55+
Assertions.assertNotNull(daprException.getStatusDetails());
56+
Assertions.assertEquals(
57+
expectedReason,
58+
daprException.getStatusDetails().get(
59+
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
60+
"reason",
61+
TypeRef.STRING
62+
));
63+
}
64+
4565
public static <T extends Throwable> void assertThrowsDaprExceptionSubstring(
4666
String expectedErrorCode,
4767
String expectedErrorMessageSubstring,

sdk-tests/src/test/java/io/dapr/it/actors/ActorTimerRecoveryIT.java

+5-10
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,14 @@
2525
import org.slf4j.Logger;
2626
import org.slf4j.LoggerFactory;
2727

28-
import java.time.Duration;
2928
import java.util.ArrayList;
3029
import java.util.List;
3130
import java.util.UUID;
3231

3332
import static io.dapr.it.Retry.callWithRetry;
3433
import static io.dapr.it.actors.MyActorTestUtils.fetchMethodCallLogs;
35-
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
3634
import static io.dapr.it.actors.MyActorTestUtils.validateMessageContent;
35+
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
3736
import static org.junit.jupiter.api.Assertions.assertNotEquals;
3837

3938
public class ActorTimerRecoveryIT extends BaseIT {
@@ -82,21 +81,17 @@ public void timerRecoveryTest() throws Exception {
8281

8382
// Restarts app only.
8483
runs.left.stop();
85-
86-
// Pause a bit to let placements settle.
87-
logger.info("Pausing 12 seconds to let placements settle.");
88-
Thread.sleep(Duration.ofSeconds(12).toMillis());
89-
84+
// Cannot sleep between app's stop and start since it can trigger unhealthy actor in runtime and lose timers.
85+
// Timers will survive only if the restart is "quick" and survives the runtime's actor health check.
86+
// Starting in 1.13, sidecar is more sensitive to an app restart and will not keep actors active for "too long".
9087
runs.left.start();
9188

92-
logger.debug("Pausing 10 seconds to allow timer to fire");
93-
Thread.sleep(10000);
9489
final List<MethodEntryTracker> newLogs = new ArrayList<>();
9590
callWithRetry(() -> {
9691
newLogs.clear();
9792
newLogs.addAll(fetchMethodCallLogs(proxy));
9893
validateMethodCalls(newLogs, METHOD_NAME, 3);
99-
}, 5000);
94+
}, 10000);
10095

10196
// Check that the restart actually happened by confirming the old logs are not in the new logs.
10297
for (MethodEntryTracker oldLog: logs) {

sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java

+8-11
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,13 @@
3232
import io.dapr.it.DaprRun;
3333
import io.dapr.serializer.DaprObjectSerializer;
3434
import io.dapr.utils.TypeRef;
35-
import org.junit.After;
3635
import org.junit.jupiter.api.AfterEach;
3736
import org.junit.jupiter.api.Assertions;
38-
import org.junit.jupiter.api.Test;
3937
import org.junit.jupiter.params.ParameterizedTest;
4038
import org.junit.jupiter.params.provider.ValueSource;
41-
import org.junit.runner.RunWith;
42-
import org.junit.runners.Parameterized;
4339

4440
import java.io.IOException;
4541
import java.util.ArrayList;
46-
import java.util.Arrays;
47-
import java.util.Collection;
4842
import java.util.Collections;
4943
import java.util.HashSet;
5044
import java.util.Iterator;
@@ -56,6 +50,7 @@
5650

5751
import static io.dapr.it.Retry.callWithRetry;
5852
import static io.dapr.it.TestUtils.assertThrowsDaprException;
53+
import static io.dapr.it.TestUtils.assertThrowsDaprExceptionWithReason;
5954
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
6055
import static org.junit.jupiter.api.Assertions.assertEquals;
6156
import static org.junit.jupiter.api.Assertions.assertNotNull;
@@ -119,14 +114,16 @@ public void publishPubSubNotFound(boolean useGrpc) throws Exception {
119114
try (DaprClient client = new DaprClientBuilder().build()) {
120115

121116
if (useGrpc) {
122-
assertThrowsDaprException(
117+
assertThrowsDaprExceptionWithReason(
123118
"INVALID_ARGUMENT",
124-
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
119+
"INVALID_ARGUMENT: pubsub unknown pubsub is not found",
120+
"DAPR_PUBSUB_NOT_FOUND",
125121
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
126122
} else {
127-
assertThrowsDaprException(
123+
assertThrowsDaprExceptionWithReason(
128124
"ERR_PUBSUB_NOT_FOUND",
129-
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub not found",
125+
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub is not found",
126+
"DAPR_PUBSUB_NOT_FOUND",
130127
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
131128
}
132129
}
@@ -149,7 +146,7 @@ public void testBulkPublishPubSubNotFound(boolean useGrpc) throws Exception {
149146
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
150147
assertThrowsDaprException(
151148
"INVALID_ARGUMENT",
152-
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
149+
"INVALID_ARGUMENT: pubsub unknown pubsub is not found",
153150
() -> client.publishEvents("unknown pubsub", "mytopic","text/plain", "message").block());
154151
}
155152
}

sdk/src/main/java/io/dapr/client/DaprHttp.java

+8-6
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.dapr.client.domain.Metadata;
1818
import io.dapr.config.Properties;
1919
import io.dapr.exceptions.DaprError;
20+
import io.dapr.exceptions.DaprErrorDetails;
2021
import io.dapr.exceptions.DaprException;
2122
import io.dapr.utils.Version;
2223
import okhttp3.Call;
@@ -73,6 +74,11 @@ public class DaprHttp implements AutoCloseable {
7374
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
7475
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("grpc-trace-bin", "traceparent", "tracestate")));
7576

77+
/**
78+
* Object mapper to parse DaprError with or without details.
79+
*/
80+
private static final ObjectMapper DAPR_ERROR_DETAILS_OBJECT_MAPPER = new ObjectMapper();
81+
7682
/**
7783
* HTTP Methods supported.
7884
*/
@@ -136,11 +142,6 @@ public int getStatusCode() {
136142
*/
137143
private static final byte[] EMPTY_BYTES = new byte[0];
138144

139-
/**
140-
* JSON Object Mapper.
141-
*/
142-
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
143-
144145
/**
145146
* Endpoint used to communicate to Dapr's HTTP endpoint.
146147
*/
@@ -347,12 +348,13 @@ private static DaprError parseDaprError(byte[] json) {
347348
}
348349

349350
try {
350-
return OBJECT_MAPPER.readValue(json, DaprError.class);
351+
return DAPR_ERROR_DETAILS_OBJECT_MAPPER.readValue(json, DaprError.class);
351352
} catch (IOException e) {
352353
throw new DaprException("UNKNOWN", new String(json, StandardCharsets.UTF_8));
353354
}
354355
}
355356

357+
356358
private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
357359
ResponseBody body = response.body();
358360
if (body != null) {

0 commit comments

Comments
 (0)