Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the dapr runtime returned error details to the Java DaprException #998

Merged
merged 19 commits into from
Feb 13, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 45 additions & 0 deletions daprdocs/content/en/java-sdk-docs/java-client/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,52 @@ If your Dapr instance is configured to require the `DAPR_API_TOKEN` environment
set it in the environment and the client will use it automatically.
You can read more about Dapr API token authentication [here](https://docs.dapr.io/operations/security/api-token/).

#### Error Handling

Initially, errors in Dapr followed the Standard gRPC error model. However, to provide more detailed and informative error
messages, in version 1.13 an enhanced error model has been introduced which aligns with the gRPC Richer error model. In
response, the Java SDK extended the DaprException to include the error details that were added in Dapr.

Example of handling the DaprException and consuming the error details when using the Dapr Java SDK:

```java
...
try {
client.publishEvent("", "", "").block();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

try {
Map<String, Object> detailsMap = exception.getStatusDetails();
if (detailsMap != null && detailsMap.containsKey("details")) {
Object detailsObject = detailsMap.get("details");
if (detailsObject instanceof List) {
List<Map<String, Object>> innerDetailsList = (List<Map<String, Object>>) detailsObject;
System.out.println("Error Details: ");

for (Map<String, Object> innerDetails : innerDetailsList) {

if (innerDetails.containsKey("@type") && innerDetails.get("@type").equals("type.googleapis.com/google.rpc.ErrorInfo")) {
System.out.println("\tError Detail is of type: Error_Info");
// Implement specific logic based on specific error type
}

for (Map.Entry<String, Object> entry : innerDetails.entrySet()) {
System.out.println("\t" + entry.getKey() + ": " + entry.getValue());
}
System.out.println(); // separate error details with newline
}
}
}
System.out.println("Error Details: " + exception.getStatusDetails());
} catch (RuntimeException e) {
System.out.println("Error Details: NULL");
}
exception.printStackTrace();
}
...
```

## Building blocks

Expand Down
35 changes: 34 additions & 1 deletion examples/src/main/java/io/dapr/examples/exception/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
import io.dapr.client.DaprClientBuilder;
import io.dapr.exceptions.DaprException;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* 1. Build and install jars:
* mvn clean install
Expand All @@ -35,11 +40,39 @@ public static void main(String[] args) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {

try {
client.getState("Unknown state store", "myKey", String.class).block();
client.publishEvent("", "", "").block();
// client.publishEvent("fake", "fake", "someData").block();
// client.getState("Unknown state store", "myKey", String.class).block();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

try {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is too complex for users. Can we have an opinionated structure that encapsulates this into the DaprException object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the latest version with the "opinionated" layer I added.

Map<String, Object> detailsMap = exception.getStatusDetails();
if (detailsMap != null && detailsMap.containsKey("details")) {
Object detailsObject = detailsMap.get("details");
if (detailsObject instanceof List) {
List<Map<String, Object>> innerDetailsList = (List<Map<String, Object>>) detailsObject;
System.out.println("Error Details: ");

for (Map<String, Object> innerDetails : innerDetailsList) {

if (innerDetails.containsKey("@type") && innerDetails.get("@type").equals("type.googleapis.com/google.rpc.ErrorInfo")) {
System.out.println("\tError Detail is of type: Error_Info");
// Implement specific logic based on specific error type
}

for (Map.Entry<String, Object> entry : innerDetails.entrySet()) {
System.out.println("\t" + entry.getKey() + ": " + entry.getValue());
}
System.out.println(); // separate error details with newline
}
}
}
System.out.println("Error Details: " + exception.getStatusDetails());
} catch (RuntimeException e) {
System.out.println("Error Details: NULL");
}
exception.printStackTrace();
}

Expand Down
138 changes: 124 additions & 14 deletions examples/src/main/java/io/dapr/examples/exception/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@ cd java-sdk
Then build the Maven project:

```sh
# make sure you are in the `java-sdk` directory.
mvn install
# make sure you are in the `java-sdk` (root) directory.
./mvnw clean install
```

Then get into the examples directory:
```sh
cd examples
```

### Running the StateClient
### Examples
#### Running the State Client
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:

```java
Expand Down Expand Up @@ -60,26 +61,71 @@ The code uses the `DaprClient` created by the `DaprClientBuilder`. It tries to g

The Dapr client is also within a try-with-resource block to properly close the client at the end.

### Running the example
#### Running the PubSub Client

Run this example with the following command:
##### Parsing the Error Details

<!-- STEP
name: Run exception example
expected_stdout_lines:
- '== APP == Error code: INVALID_ARGUMENT'
- '== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found'
background: true
sleep: 5
-->
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:

```java
public class Client {
public static void main(String[] args) throws Exception {
try (DaprClient client = new DaprClientBuilder().build()) {
try {
client.publishEvent("", "", "").block();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

try {
Map<String, Object> detailsMap = exception.getStatusDetails();
if (detailsMap != null && detailsMap.containsKey("details")) {
Object detailsObject = detailsMap.get("details");
if (detailsObject instanceof List) {
List<Map<String, Object>> innerDetailsList = (List<Map<String, Object>>) detailsObject;
System.out.println("Error Details: ");

for (Map<String, Object> innerDetails : innerDetailsList) {

if (innerDetails.containsKey("@type") && innerDetails.get("@type").equals("type.googleapis.com/google.rpc.ErrorInfo")) {
System.out.println("\tError Detail is of type: Error_Info");
// Implement specific logic based on specific error type
}

for (Map.Entry<String, Object> entry : innerDetails.entrySet()) {
System.out.println("\t" + entry.getKey() + ": " + entry.getValue());
}
System.out.println(); // separate error details with newline
}
}
}
System.out.println("Error Details: " + exception.getStatusDetails());
} catch (RuntimeException e) {
System.out.println("Error Details: NULL");
}
exception.printStackTrace();
}

System.out.println("Done");
}
}
}
```

### Running the examples

#### Run the State Example
1. Uncomment out this line: `client.getState("Unknown state store", "myKey", String.class).block();`
2. Comment out the publishEvent line: `client.publishEvent("", "", "").block();`
3. Run this example with the following command

```bash
dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.exception.Client
```

<!-- END_STEP -->

Once running, the OutputBindingExample should print the output as follows:
Once running, the State Client Example should print the output as follows:

```txt
== APP == Error code: INVALID_ARGUMENT
Expand Down Expand Up @@ -115,6 +161,70 @@ Once running, the OutputBindingExample should print the output as follows:

```

#### Run the PubSub Example
1. Run this example with the following command

<!-- STEP
name: Run exception example
expected_stdout_lines:
- '== APP == Error code: INVALID_ARGUMENT'
- '== APP == Error message: INVALID_ARGUMENT: pubsub name is empty'
- '== APP == Error Detail is of type: Error_Info'
- '== APP == Error Details:'
- '== APP == reason: DAPR_PUBSUB_NAME_EMPTY'
- '== APP == metadata: null'
- '== APP == @type: type.googleapis.com/google.rpc.ErrorInfo'
- '== APP == domain: dapr.io'
- '== APP == Error Details:'
- '== APP == owner: '
- '== APP == @type: type.googleapis.com/google.rpc.ResourceInfo'
- '== APP == description: pubsub name is empty'
- '== APP == resourceName: '
- '== APP == resourceType: pubsub'
- '== APP == Error Details: {details=[{reason=DAPR_PUBSUB_NAME_EMPTY, metadata=null, @type=type.googleapis.com/google.rpc.ErrorInfo, domain=dapr.io}, {owner=, @type=type.googleapis.com/google.rpc.ResourceInfo, description=pubsub name is empty, resourceName=, resourceType=pubsub}]}'
background: true
sleep: 5
-->

```bash
dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-exec.jar io.dapr.examples.exception.Client
```

<!-- END_STEP -->

Once running, the PubSub Client Example should print the output as follows:

```txt
== APP == Error code: INVALID_ARGUMENT
== APP == Error message: INVALID_ARGUMENT: pubsub name is empty
== APP == Error Detail is of type: Error_Info
== APP == Error Details:
== APP == reason: DAPR_PUBSUB_NAME_EMPTY
== APP == metadata: null
== APP == @type: type.googleapis.com/google.rpc.ErrorInfo
== APP == domain: dapr.io
== APP == Error Details:
== APP == owner:
== APP == @type: type.googleapis.com/google.rpc.ResourceInfo
== APP == description: pubsub name is empty
== APP == resourceName:
== APP == resourceType: pubsub
== APP == Error Details: {details=[{reason=DAPR_PUBSUB_NAME_EMPTY, metadata=null, @type=type.googleapis.com/google.rpc.ErrorInfo, domain=dapr.io}, {owner=, @type=type.googleapis.com/google.rpc.ResourceInfo, description=pubsub name is empty, resourceName=, resourceType=pubsub}]}
== APP == io.dapr.exceptions.DaprException: INVALID_ARGUMENT: pubsub name is empty
...
```

### Debug

If you would like to debug the Client code. Simply follow these steps:
1. Pre-req:
```sh
# make sure you are in the `java-sdk` (root) directory.
./mvnw clean install
```
2. From the examples directory, run: `dapr run --dapr-grpc-port=50001`
3. From your IDE click the play button on the Client code and put break points where desired

### Cleanup

To stop the app run (or press `CTRL+C`):
Expand Down
5 changes: 4 additions & 1 deletion sdk/src/main/java/io/dapr/client/DaprHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprError;
import io.dapr.exceptions.DaprException;
import io.dapr.exceptions.DetailObjectMapper;
import io.dapr.utils.Version;
import okhttp3.Call;
import okhttp3.Callback;
Expand Down Expand Up @@ -141,6 +142,8 @@ public int getStatusCode() {
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();


private static final DetailObjectMapper DETAIL_OBJECT_MAPPER = new DetailObjectMapper();
/**
* Endpoint used to communicate to Dapr's HTTP endpoint.
*/
Expand Down Expand Up @@ -347,7 +350,7 @@ private static DaprError parseDaprError(byte[] json) {
}

try {
return OBJECT_MAPPER.readValue(json, DaprError.class);
return DetailObjectMapper.OBJECT_MAPPER.readValue(json, DaprError.class);
} catch (IOException e) {
throw new DaprException("UNKNOWN", new String(json, StandardCharsets.UTF_8));
}
Expand Down