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 13 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
22 changes: 22 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,29 @@ 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("unknown_pubsub", "mytopic", "mydata").block();
} catch (DaprException exception) {
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
// DaprException now contains `getStatusDetails()` to include more details about the error from Dapr runtime.
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}
...
```

## Building blocks

Expand Down
23 changes: 15 additions & 8 deletions examples/src/main/java/io/dapr/examples/exception/Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@

import io.dapr.client.DaprClient;
import io.dapr.client.DaprClientBuilder;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;

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

/**
* 1. Build and install jars:
Expand All @@ -33,17 +40,17 @@ public class Client {
*/
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("unknown_pubsub", "mytopic", "mydata").block();
Copy link
Member

Choose a reason for hiding this comment

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

Clarifying my change: keeping this example simple.

} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

exception.printStackTrace();
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
Copy link
Member

Choose a reason for hiding this comment

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

Clarifying my change: this is the opinionated layer in the SDK and determines how users can retrieve error details. We don't enumerate all the attributes on purpose. We can decide to change this later but I think this is a good first design to give a semi-structured way to fetch error details.

DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}

System.out.println("Done");
}
System.out.println("Done");
}
}
76 changes: 31 additions & 45 deletions examples/src/main/java/io/dapr/examples/exception/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,52 +23,49 @@ 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
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:
### Understanding the code

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`:

```java
public class Client {

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("unknown_pubsub", "mytopic", "mydata").block();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());

exception.printStackTrace();
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
System.out.println("Dapr exception's message: " + exception.getMessage());
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}

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

}
```
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.

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

### Running the example

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: state store Unknown state store is not found'
- '== APP == Dapr exception's error code: INVALID_ARGUMENT'
- '== APP == Dapr exception's error message: INVALID_ARGUMENT: state store Unknown state store is not found'
- '== APP == Dapr exception's error reason: DAPR_PUBSUB_NOT_FOUND'
background: true
sleep: 5
-->
Expand All @@ -79,41 +76,30 @@ dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-e

<!-- 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

== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found

== APP == io.dapr.exceptions.DaprException: INVALID_ARGUMENT: state store Unknown state store is not found

== APP == at io.dapr.exceptions.DaprException.propagate(DaprException.java:168)

== APP == at io.dapr.client.DaprClientGrpc$2.onError(DaprClientGrpc.java:716)
== APP == Dapr exception's error code: INVALID_ARGUMENT

== APP == at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onClose(ClientCalls.java:478)
== APP == Dapr exception's error message: INVALID_ARGUMENT: state store Unknown state store is not found

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener$3.run(DelayedClientCall.java:464)

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.delayOrExecute(DelayedClientCall.java:428)

== APP == at io.grpc.internal.DelayedClientCall$DelayedListener.onClose(DelayedClientCall.java:461)

== APP == at io.grpc.internal.ClientCallImpl.closeObserver(ClientCallImpl.java:617)

== APP == at io.grpc.internal.ClientCallImpl.access$300(ClientCallImpl.java:70)

== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInternal(ClientCallImpl.java:803)
== APP == Dapr exception's error reason: DAPR_PUBSUB_NOT_FOUND
...

== APP == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:782)
```

== APP == at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
### Debug

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

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 --app-id exception-example --dapr-grpc-port=50001 --dapr-http-port=3500`
3. From your IDE click the play button on the client code and put break points where desired.

### Cleanup

Expand Down
20 changes: 20 additions & 0 deletions sdk-tests/src/test/java/io/dapr/it/TestUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@

package io.dapr.it;

import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;

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

public static <T extends Throwable> void assertThrowsDaprExceptionWithReason(
String expectedErrorCode,
String expectedErrorMessage,
String expectedReason,
Executable executable) {
DaprException daprException = Assertions.assertThrows(DaprException.class, executable);
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
Assertions.assertNotNull(daprException.getStatusDetails());
Assertions.assertEquals(
expectedReason,
daprException.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING
));
}

public static <T extends Throwable> void assertThrowsDaprExceptionSubstring(
String expectedErrorCode,
String expectedErrorMessageSubstring,
Expand Down
13 changes: 5 additions & 8 deletions sdk-tests/src/test/java/io/dapr/it/pubsub/http/PubSubIT.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,13 @@
import io.dapr.it.DaprRun;
import io.dapr.serializer.DaprObjectSerializer;
import io.dapr.utils.TypeRef;
import org.junit.After;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
Expand All @@ -56,6 +50,7 @@

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

if (useGrpc) {
assertThrowsDaprException(
assertThrowsDaprExceptionWithReason(
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
"DAPR_PUBSUB_NOT_FOUND",
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
} else {
assertThrowsDaprException(
assertThrowsDaprExceptionWithReason(
"ERR_PUBSUB_NOT_FOUND",
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub not found",
"DAPR_PUBSUB_NOT_FOUND",
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
}
}
Expand Down
14 changes: 8 additions & 6 deletions sdk/src/main/java/io/dapr/client/DaprHttp.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import io.dapr.client.domain.Metadata;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprError;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.Version;
import okhttp3.Call;
Expand Down Expand Up @@ -73,6 +74,11 @@ public class DaprHttp implements AutoCloseable {
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("grpc-trace-bin", "traceparent", "tracestate")));

/**
* Object mapper to parse DaprError with or without details.
*/
private static final ObjectMapper DAPR_ERROR_DETAILS_OBJECT_MAPPER = new ObjectMapper();
Copy link
Member

Choose a reason for hiding this comment

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

Clarifying my change: no need for a custom ObjectMapper. The stock one is enough now.


/**
* HTTP Methods supported.
*/
Expand Down Expand Up @@ -136,11 +142,6 @@ public int getStatusCode() {
*/
private static final byte[] EMPTY_BYTES = new byte[0];

/**
* JSON Object Mapper.
*/
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

/**
* Endpoint used to communicate to Dapr's HTTP endpoint.
*/
Expand Down Expand Up @@ -347,12 +348,13 @@ private static DaprError parseDaprError(byte[] json) {
}

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


private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
ResponseBody body = response.body();
if (body != null) {
Expand Down
34 changes: 31 additions & 3 deletions sdk/src/main/java/io/dapr/exceptions/DaprError.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2021 The Dapr Authors
* Copyright 2024 The Dapr Authors
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
Expand All @@ -14,7 +14,10 @@
package io.dapr.exceptions;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import io.grpc.Status;

import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
* Represents an error message from Dapr.
Expand All @@ -37,14 +40,19 @@ public class DaprError {
*/
private Integer code;

/**
* Details about the error.
*/
private List<Map<String, Object>> details;

/**
* Gets the error code.
*
* @return Error code.
*/
public String getErrorCode() {
if ((errorCode == null) && (code != null)) {
return Status.fromCodeValue(code).getCode().name();
return io.grpc.Status.fromCodeValue(code).getCode().name();
}
return errorCode;
}
Expand Down Expand Up @@ -80,4 +88,24 @@ public DaprError setMessage(String message) {
return this;
}

/**
* Gets the error details.
*
* @return Error details.
*/
public List<Map<String, Object>> getDetails() {
return details;
}

/**
* Sets the error details.
*
* @param details Error details.
* @return This instance.
*/
public DaprError setDetails(List<Map<String, Object>> details) {
this.details = Collections.unmodifiableList(details);
return this;
}

}