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 <cassie@diagrid.io>

* add error handling to sdk docs

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* add tests for the dapr exception changes

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* try verifyNoMoreInteractions w/ channel

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* verify channel close -> channel close explicitly

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* rm verifyNoMoreInteractions

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* rm test to see if that is the orphaned managed channel issue

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* re-add test since that doesnt seem to be the issue

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* channel.close(); -> verify(channel).close();

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>

* Rewrite and redesign of the DaprErrorDetail in DaprException.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Update daprdocs too for DaprErrorDetails.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix README.md mm string.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix exception example.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Use runtime 1.13.0-rc.2

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Fix exception example to match gRPC output.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Update error message in IT as per new Dapr runtime version.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

* Dapr 1.13 is less tolerant of app downtime to keep timers.

Signed-off-by: Artur Souza <asouza.pro@gmail.com>

---------

Signed-off-by: Cassandra Coyle <cassie@diagrid.io>
Signed-off-by: Artur Souza <asouza.pro@gmail.com>
Co-authored-by: Artur Souza <artursouza.ms@outlook.com>
Co-authored-by: Artur Souza <asouza.pro@gmail.com>
This commit is contained in:
Cassie Coyle 2024-02-13 15:34:50 -06:00 committed by GitHub
parent cd81ee8cd4
commit a3cc1384b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 607 additions and 92 deletions

View File

@ -42,7 +42,7 @@ jobs:
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.12.0
DAPR_RUNTIME_VER: 1.12.4
DAPR_RUNTIME_VER: 1.13.0-rc.2
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:

View File

@ -38,7 +38,7 @@ jobs:
GOPROXY: https://proxy.golang.org
JDK_VER: ${{ matrix.java }}
DAPR_CLI_VER: 1.12.0
DAPR_RUNTIME_VER: 1.12.4
DAPR_RUNTIME_VER: 1.13.0-rc.2
DAPR_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
DAPR_CLI_REF:
DAPR_REF:

View File

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

View File

@ -15,7 +15,14 @@ package io.dapr.examples.exception;
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:
@ -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();
} catch (DaprException exception) {
System.out.println("Error code: " + exception.getErrorCode());
System.out.println("Error message: " + exception.getMessage());
exception.printStackTrace();
System.out.println("Reason: " + exception.getStatusDetails().get(
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
"reason",
TypeRef.STRING));
}
System.out.println("Done");
}
System.out.println("Done");
}
}

View File

@ -23,8 +23,8 @@ 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:
@ -32,43 +32,40 @@ Then get into the examples directory:
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 == Error message: INVALID_ARGUMENT: pubsub unknown_pubsub is not found'
- '== APP == Reason: DAPR_PUBSUB_NOT_FOUND'
background: true
sleep: 5
-->
@ -79,42 +76,31 @@ 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 code: ERR_PUBSUB_NOT_FOUND
== APP == Error message: INVALID_ARGUMENT: state store Unknown state store is not found
== APP == Error message: ERR_PUBSUB_NOT_FOUND: pubsub unknown_pubsub 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 == at io.grpc.stub.ClientCalls$StreamObserverToCallListenerAdapter.onClose(ClientCalls.java:478)
== 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 == at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1StreamClosed.runInContext(ClientCallImpl.java:782)
== APP == at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
== APP == at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
== APP == Reason: DAPR_PUBSUB_NOT_FOUND
...
```
### Debug
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
To stop the app run (or press `CTRL+C`):

View File

@ -13,7 +13,9 @@ limitations under the License.
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;
@ -42,6 +44,24 @@ public final class TestUtils {
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,

View File

@ -25,15 +25,14 @@ import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import static io.dapr.it.Retry.callWithRetry;
import static io.dapr.it.actors.MyActorTestUtils.fetchMethodCallLogs;
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
import static io.dapr.it.actors.MyActorTestUtils.validateMessageContent;
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
public class ActorTimerRecoveryIT extends BaseIT {
@ -82,21 +81,17 @@ public class ActorTimerRecoveryIT extends BaseIT {
// Restarts app only.
runs.left.stop();
// Pause a bit to let placements settle.
logger.info("Pausing 12 seconds to let placements settle.");
Thread.sleep(Duration.ofSeconds(12).toMillis());
// Cannot sleep between app's stop and start since it can trigger unhealthy actor in runtime and lose timers.
// Timers will survive only if the restart is "quick" and survives the runtime's actor health check.
// Starting in 1.13, sidecar is more sensitive to an app restart and will not keep actors active for "too long".
runs.left.start();
logger.debug("Pausing 10 seconds to allow timer to fire");
Thread.sleep(10000);
final List<MethodEntryTracker> newLogs = new ArrayList<>();
callWithRetry(() -> {
newLogs.clear();
newLogs.addAll(fetchMethodCallLogs(proxy));
validateMethodCalls(newLogs, METHOD_NAME, 3);
}, 5000);
}, 10000);
// Check that the restart actually happened by confirming the old logs are not in the new logs.
for (MethodEntryTracker oldLog: logs) {

View File

@ -32,19 +32,13 @@ import io.dapr.it.BaseIT;
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;
@ -56,6 +50,7 @@ import java.util.Set;
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;
@ -119,14 +114,16 @@ public class PubSubIT extends BaseIT {
try (DaprClient client = new DaprClientBuilder().build()) {
if (useGrpc) {
assertThrowsDaprException(
assertThrowsDaprExceptionWithReason(
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
"INVALID_ARGUMENT: pubsub unknown pubsub is 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",
"ERR_PUBSUB_NOT_FOUND: pubsub unknown pubsub is not found",
"DAPR_PUBSUB_NOT_FOUND",
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
}
}
@ -149,7 +146,7 @@ public class PubSubIT extends BaseIT {
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
assertThrowsDaprException(
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: pubsub unknown pubsub not found",
"INVALID_ARGUMENT: pubsub unknown pubsub is not found",
() -> client.publishEvents("unknown pubsub", "mytopic","text/plain", "message").block());
}
}

View File

@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
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;
@ -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();
/**
* HTTP Methods supported.
*/
@ -136,11 +142,6 @@ public class DaprHttp implements AutoCloseable {
*/
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.
*/
@ -347,12 +348,13 @@ public class DaprHttp implements AutoCloseable {
}
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) {

View File

@ -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
@ -14,7 +14,10 @@ limitations under the License.
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.
@ -37,6 +40,11 @@ public class DaprError {
*/
private Integer code;
/**
* Details about the error.
*/
private List<Map<String, Object>> details;
/**
* Gets the error code.
*
@ -44,7 +52,7 @@ public class DaprError {
*/
public String getErrorCode() {
if ((errorCode == null) && (code != null)) {
return Status.fromCodeValue(code).getCode().name();
return io.grpc.Status.fromCodeValue(code).getCode().name();
}
return errorCode;
}
@ -80,4 +88,24 @@ public class DaprError {
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;
}
}

View File

@ -0,0 +1,223 @@
/*
* 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
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.exceptions;
import com.google.protobuf.Any;
import com.google.protobuf.InvalidProtocolBufferException;
import com.google.protobuf.Message;
import com.google.rpc.Status;
import io.dapr.utils.TypeRef;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
public class DaprErrorDetails {
static final DaprErrorDetails EMPTY_INSTANCE = new DaprErrorDetails((Status) null);
private static final Map<Class<? extends Message>, ErrorDetailType> SUPPORTED_ERROR_TYPES =
Collections.unmodifiableMap(new HashMap<>() {
{
put(com.google.rpc.ErrorInfo.class, ErrorDetailType.ERROR_INFO);
put(com.google.rpc.RetryInfo.class, ErrorDetailType.RETRY_INFO);
put(com.google.rpc.DebugInfo.class, ErrorDetailType.DEBUG_INFO);
put(com.google.rpc.QuotaFailure.class, ErrorDetailType.QUOTA_FAILURE);
put(com.google.rpc.PreconditionFailure.class, ErrorDetailType.PRECONDITION_FAILURE);
put(com.google.rpc.BadRequest.class, ErrorDetailType.BAD_REQUEST);
put(com.google.rpc.RequestInfo.class, ErrorDetailType.REQUEST_INFO);
put(com.google.rpc.ResourceInfo.class, ErrorDetailType.RESOURCE_INFO);
put(com.google.rpc.Help.class, ErrorDetailType.HELP);
put(com.google.rpc.LocalizedMessage.class, ErrorDetailType.LOCALIZED_MESSAGE);
}
});
private static final Map<String, Class<? extends Message>> ERROR_TYPES_FQN_REVERSE_LOOKUP =
SUPPORTED_ERROR_TYPES.keySet().stream().collect(Collectors.toMap(
item -> generateErrorTypeFqn(item),
item -> item
));
/**
* Error status details.
*/
private final Map<ErrorDetailType, Map<String, Object>> map;
public DaprErrorDetails(Status grpcStatus) {
this.map = parse(grpcStatus);
}
public DaprErrorDetails(List<Map<String, Object>> entries) {
this.map = parse(entries);
}
/**
* Gets an attribute of an error detail.
* @param errorDetailType Type of the error detail.
* @param errAttribute Attribute of the error detail.
* @param typeRef Type of the value expected to be returned.
* @param <T> Type of the value to be returned.
* @return Value of the attribute or null if not found.
*/
public <T> T get(ErrorDetailType errorDetailType, String errAttribute, TypeRef<T> typeRef) {
Map<String, Object> dictionary = map.get(errorDetailType);
if (dictionary == null) {
return null;
}
return (T) dictionary.get(errAttribute);
}
/**
* Parses status details from a gRPC Status.
*
* @param status The gRPC Status to parse details from.
* @return List containing parsed status details.
*/
private static Map<ErrorDetailType, Map<String, Object>> parse(Status status) {
if (status == null || status.getDetailsList() == null) {
return Collections.emptyMap();
}
Map<ErrorDetailType, Map<String, Object>> detailsList = new HashMap<>();
List<Any> grpcDetailsList = status.getDetailsList();
for (Any detail : grpcDetailsList) {
for (Map.Entry<Class<? extends Message>, ErrorDetailType>
supportedClazzAndType : SUPPORTED_ERROR_TYPES.entrySet()) {
Class<? extends Message> clazz = supportedClazzAndType.getKey();
ErrorDetailType errorDetailType = supportedClazzAndType.getValue();
if (detail.is(clazz)) {
detailsList.put(errorDetailType, parseProtoMessage(detail, clazz));
}
}
}
return Collections.unmodifiableMap(detailsList);
}
private static Map<ErrorDetailType, Map<String, Object>> parse(List<Map<String, Object>> entries) {
if ((entries == null) || entries.isEmpty()) {
return Collections.emptyMap();
}
Map<ErrorDetailType, Map<String, Object>> detailsList = new HashMap<>();
for (Map<String, Object> entry : entries) {
Object type = entry.getOrDefault("@type", "");
if (type == null) {
continue;
}
Class<? extends Message> clazz = ERROR_TYPES_FQN_REVERSE_LOOKUP.get(type.toString());
if (clazz == null) {
continue;
}
ErrorDetailType errorDetailType = SUPPORTED_ERROR_TYPES.get(clazz);
if (errorDetailType == null) {
continue;
}
detailsList.put(errorDetailType, entry);
}
return Collections.unmodifiableMap(detailsList);
}
private static <T extends com.google.protobuf.Message> Map<String, Object> parseProtoMessage(
Any detail, Class<T> clazz) {
try {
T message = detail.unpack(clazz);
return messageToMap(message);
} catch (InvalidProtocolBufferException e) {
return Collections.singletonMap(e.getClass().getSimpleName(), e.getMessage());
}
}
/**
* Converts a Protocol Buffer (proto) message to a Map.
*
* @param message The proto message to be converted.
* @return A Map representing the fields of the proto message.
*/
private static Map<String, Object> messageToMap(Message message) {
Map<String, Object> result = new HashMap<>();
Field[] fields = message.getClass().getDeclaredFields();
result.put("@type", generateErrorTypeFqn(message.getClass()));
for (Field field : fields) {
if (field.isSynthetic() || Modifier.isStatic(field.getModifiers())) {
continue;
}
String normalizedFieldName = field.getName().replaceAll("_$", "");
try {
field.setAccessible(true);
Object value = field.get(message);
result.put(normalizedFieldName, value);
} catch (IllegalAccessException e) {
// no-op, just ignore this attribute.
}
}
return Collections.unmodifiableMap(result);
}
private static <T extends com.google.protobuf.Message> String generateErrorTypeFqn(Class<T> clazz) {
String className = clazz.getName();
// trim the 'com.' to match the kit error details returned to users
return "type.googleapis.com/" + (className.startsWith("com.") ? className.substring(4) : className);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
DaprErrorDetails that = (DaprErrorDetails) o;
return Objects.equals(map, that.map);
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
return Objects.hash(map);
}
public enum ErrorDetailType {
ERROR_INFO,
RETRY_INFO,
DEBUG_INFO,
QUOTA_FAILURE,
PRECONDITION_FAILURE,
BAD_REQUEST,
REQUEST_INFO,
RESOURCE_INFO,
HELP,
LOCALIZED_MESSAGE,
}
}

View File

@ -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
@ -18,6 +18,8 @@ import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
/**
@ -30,13 +32,18 @@ public class DaprException extends RuntimeException {
*/
private String errorCode;
/**
* The status details for the error.
*/
private DaprErrorDetails errorDetails;
/**
* New exception from a server-side generated error code and message.
*
* @param daprError Server-side error.
*/
public DaprException(DaprError daprError) {
this(daprError.getErrorCode(), daprError.getMessage());
this(daprError.getErrorCode(), daprError.getMessage(), daprError.getDetails());
}
/**
@ -66,8 +73,31 @@ public class DaprException extends RuntimeException {
* @param message Client-side error message.
*/
public DaprException(String errorCode, String message) {
this(errorCode, message, DaprErrorDetails.EMPTY_INSTANCE);
}
/**
* New Exception from a client-side generated error code and message.
*
* @param errorCode Client-side error code.
* @param message Client-side error message.
* @param errorDetails Details of the error from runtime.
*/
public DaprException(String errorCode, String message, List<Map<String, Object>> errorDetails) {
this(errorCode, message, new DaprErrorDetails(errorDetails));
}
/**
* New Exception from a client-side generated error code and message.
*
* @param errorCode Client-side error code.
* @param message Client-side error message.
* @param errorDetails Details of the error from runtime.
*/
public DaprException(String errorCode, String message, DaprErrorDetails errorDetails) {
super(String.format("%s: %s", errorCode, message));
this.errorCode = errorCode;
this.errorDetails = errorDetails;
}
/**
@ -84,6 +114,22 @@ public class DaprException extends RuntimeException {
this.errorCode = errorCode;
}
/**
* New exception from a server-side generated error code and message.
* @param errorCode Client-side error code.
* @param message Client-side error message.
* @param cause the cause (which is saved for later retrieval by the
* {@link #getCause()} method). (A {@code null} value is
* permitted, and indicates that the cause is nonexistent or
* unknown.)
* @param errorDetails the status details for the error.
*/
public DaprException(String errorCode, String message, Throwable cause, DaprErrorDetails errorDetails) {
super(String.format("%s: %s", errorCode, emptyIfNull(message)), cause);
this.errorCode = errorCode;
this.errorDetails = errorDetails == null ? DaprErrorDetails.EMPTY_INSTANCE : errorDetails;
}
/**
* Returns the exception's error code.
*
@ -93,6 +139,10 @@ public class DaprException extends RuntimeException {
return this.errorCode;
}
public DaprErrorDetails getStatusDetails() {
return this.errorDetails;
}
/**
* Wraps an exception into DaprException (if not already DaprException).
*
@ -189,10 +239,15 @@ public class DaprException extends RuntimeException {
while (e != null) {
if (e instanceof StatusRuntimeException) {
StatusRuntimeException statusRuntimeException = (StatusRuntimeException) e;
com.google.rpc.Status status = io.grpc.protobuf.StatusProto.fromThrowable(statusRuntimeException);
DaprErrorDetails errorDetails = new DaprErrorDetails(status);
return new DaprException(
statusRuntimeException.getStatus().getCode().toString(),
statusRuntimeException.getStatus().getDescription(),
exception);
exception,
errorDetails);
}
e = e.getCause();

View File

@ -37,6 +37,7 @@ import io.dapr.v1.DaprGrpc;
import io.dapr.v1.DaprProtos;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.protobuf.StatusProto;
import io.grpc.stub.StreamObserver;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@ -2507,7 +2508,17 @@ public class DaprClientGrpcTest {
}
}
private static StatusRuntimeException newStatusRuntimeException(String status, String message) {
return new StatusRuntimeException(Status.fromCode(Status.Code.valueOf(status)).withDescription(message));
public static StatusRuntimeException newStatusRuntimeException(String statusCode, String message) {
return new StatusRuntimeException(Status.fromCode(Status.Code.valueOf(statusCode)).withDescription(message));
}
public static StatusRuntimeException newStatusRuntimeException(String statusCode, String message, com.google.rpc.Status statusDetails) {
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(Status.Code.valueOf(statusCode).value())
.setMessage(message)
.addAllDetails(statusDetails.getDetailsList())
.build();
return StatusProto.toStatusRuntimeException(status);
}
}

View File

@ -0,0 +1,122 @@
/*
* 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
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
limitations under the License.
*/
package io.dapr.client;
import com.google.protobuf.Any;
import com.google.rpc.ErrorInfo;
import com.google.rpc.ResourceInfo;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.serializer.DefaultObjectSerializer;
import io.dapr.v1.DaprGrpc;
import io.dapr.v1.DaprProtos;
import io.grpc.StatusRuntimeException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.stubbing.Answer;
import java.io.IOException;
import static io.dapr.client.DaprClientGrpcTest.newStatusRuntimeException;
import static io.dapr.utils.TestUtils.assertThrowsDaprException;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
public class DaprExceptionTest {
private GrpcChannelFacade channel;
private DaprGrpc.DaprStub daprStub;
private DaprClient client;
@BeforeEach
public void setup() throws IOException {
channel = mock(GrpcChannelFacade.class);
daprStub = mock(DaprGrpc.DaprStub.class);
when(daprStub.withInterceptors(any())).thenReturn(daprStub);
DaprClient grpcClient = new DaprClientGrpc(
channel, daprStub, new DefaultObjectSerializer(), new DefaultObjectSerializer());
client = new DaprClientProxy(grpcClient);
doNothing().when(channel).close();
}
@AfterEach
public void tearDown() throws Exception {
client.close();
verify(channel).close();
}
@Test
public void daprExceptionWithMultipleDetailsThrownTest() {
ErrorInfo errorInfo = ErrorInfo.newBuilder()
.setDomain("dapr.io")
.setReason("fake")
.build();
ResourceInfo resourceInfo = ResourceInfo.newBuilder()
.setResourceName("")
.setResourceType("pubsub")
.setDescription("pubsub name is empty")
.build();
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(io.grpc.Status.Code.INVALID_ARGUMENT.value())
.setMessage("bad bad argument")
.addDetails(Any.pack(errorInfo))
.addDetails(Any.pack(resourceInfo))
.build();
doAnswer((Answer<Void>) invocation -> {
throw newStatusRuntimeException("INVALID_ARGUMENT", "bad bad argument", status);
}).when(daprStub).publishEvent(any(DaprProtos.PublishEventRequest.class), any());
DaprErrorDetails expectedStatusDetails = new DaprErrorDetails(status);
assertThrowsDaprException(
StatusRuntimeException.class,
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: bad bad argument",
expectedStatusDetails,
() -> client.publishEvent("pubsubname","topic", "object").block());
}
@Test
public void daprExceptionWithOneDetailThrownTest() {
ErrorInfo errorInfo = ErrorInfo.newBuilder()
.setDomain("dapr.io")
.setReason("DAPR_STATE_NOT_FOUND")
.build();
com.google.rpc.Status status = com.google.rpc.Status.newBuilder()
.setCode(io.grpc.Status.Code.INVALID_ARGUMENT.value())
.setMessage("bad bad argument")
.addDetails(Any.pack(errorInfo))
.build();
doAnswer((Answer<Void>) invocation -> {
throw newStatusRuntimeException("INVALID_ARGUMENT", "bad bad argument", status);
}).when(daprStub).getState(any(DaprProtos.GetStateRequest.class), any());
DaprErrorDetails expectedStatusDetails = new DaprErrorDetails(status);
assertThrowsDaprException(
StatusRuntimeException.class,
"INVALID_ARGUMENT",
"INVALID_ARGUMENT: bad bad argument",
expectedStatusDetails,
() -> client.getState("Unknown state store", "myKey", String.class).block());
}
}

View File

@ -13,7 +13,9 @@ limitations under the License.
package io.dapr.client;
import io.dapr.config.Properties;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import io.dapr.utils.TypeRef;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import reactor.test.StepVerifier;
@ -186,7 +188,7 @@ public class DaprHttpTest {
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
Mono<DaprHttp.Response> mono =
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
StepVerifier.create(mono).expectError(RuntimeException.class);
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
@ -197,7 +199,7 @@ public class DaprHttpTest {
"{\"errorCode\":null,\"message\":null}"));
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
StepVerifier.create(mono).expectError(RuntimeException.class);
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
@ -208,7 +210,36 @@ public class DaprHttpTest {
"{\"errorCode\":\"null\",\"message\":\"null\"}"));
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
StepVerifier.create(mono).expectError(RuntimeException.class);
StepVerifier.create(mono).expectError(RuntimeException.class).verify();
}
@Test
public void validateExceptionParsing() {
final String payload = "{" +
"\"errorCode\":\"ERR_PUBSUB_NOT_FOUND\"," +
"\"message\":\"pubsub abc is not found\"," +
"\"details\":[" +
"{" +
"\"@type\":\"type.googleapis.com/google.rpc.ErrorInfo\"," +
"\"domain\":\"dapr.io\"," +
"\"metadata\":{}," +
"\"reason\":\"DAPR_PUBSUB_NOT_FOUND\"" +
"}]}";
mockInterceptor.addRule()
.post("http://127.0.0.1:3500/v1.0/pubsub/publish")
.respond(500, ResponseBody.create(MediaType.parse("application/json"),
payload));
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
Mono<DaprHttp.Response> mono = daprHttp.invokeApi("POST", "v1.0/pubsub/publish".split("/"), null, null, Context.empty());
StepVerifier.create(mono).expectErrorMatches(e -> {
assertEquals(DaprException.class, e.getClass());
DaprException daprException = (DaprException)e;
assertEquals("ERR_PUBSUB_NOT_FOUND", daprException.getErrorCode());
assertEquals("DAPR_PUBSUB_NOT_FOUND",
daprException.getStatusDetails()
.get(DaprErrorDetails.ErrorDetailType.ERROR_INFO, "reason", TypeRef.STRING));
return true;
}).verify();
}
/**

View File

@ -13,12 +13,14 @@ limitations under the License.
package io.dapr.utils;
import io.dapr.exceptions.DaprErrorDetails;
import io.dapr.exceptions.DaprException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.function.Executable;
import java.io.IOException;
import java.net.ServerSocket;
import java.util.Map;
public final class TestUtils {
@ -58,6 +60,20 @@ public final class TestUtils {
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
}
public static <T extends Throwable> void assertThrowsDaprException(
Class<T> expectedType,
String expectedErrorCode,
String expectedErrorMessage,
DaprErrorDetails expectedStatusDetails,
Executable executable) {
DaprException daprException = Assertions.assertThrows(DaprException.class, executable);
Assertions.assertNotNull(daprException.getCause());
Assertions.assertEquals(expectedType, daprException.getCause().getClass());
Assertions.assertEquals(expectedErrorCode, daprException.getErrorCode());
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
Assertions.assertEquals(expectedStatusDetails, daprException.getStatusDetails());
}
public static int findFreePort() throws IOException {
try (ServerSocket socket = new ServerSocket(0)) {
socket.setReuseAddress(true);