mirror of https://github.com/dapr/java-sdk.git
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:
parent
cd81ee8cd4
commit
a3cc1384b5
|
|
@ -42,7 +42,7 @@ jobs:
|
||||||
GOPROXY: https://proxy.golang.org
|
GOPROXY: https://proxy.golang.org
|
||||||
JDK_VER: ${{ matrix.java }}
|
JDK_VER: ${{ matrix.java }}
|
||||||
DAPR_CLI_VER: 1.12.0
|
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_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
|
||||||
DAPR_CLI_REF:
|
DAPR_CLI_REF:
|
||||||
DAPR_REF:
|
DAPR_REF:
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ jobs:
|
||||||
GOPROXY: https://proxy.golang.org
|
GOPROXY: https://proxy.golang.org
|
||||||
JDK_VER: ${{ matrix.java }}
|
JDK_VER: ${{ matrix.java }}
|
||||||
DAPR_CLI_VER: 1.12.0
|
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_INSTALL_URL: https://raw.githubusercontent.com/dapr/cli/v1.12.0/install/install.sh
|
||||||
DAPR_CLI_REF:
|
DAPR_CLI_REF:
|
||||||
DAPR_REF:
|
DAPR_REF:
|
||||||
|
|
|
||||||
|
|
@ -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.
|
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/).
|
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
|
## Building blocks
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,14 @@ package io.dapr.examples.exception;
|
||||||
|
|
||||||
import io.dapr.client.DaprClient;
|
import io.dapr.client.DaprClient;
|
||||||
import io.dapr.client.DaprClientBuilder;
|
import io.dapr.client.DaprClientBuilder;
|
||||||
|
import io.dapr.exceptions.DaprErrorDetails;
|
||||||
import io.dapr.exceptions.DaprException;
|
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:
|
* 1. Build and install jars:
|
||||||
|
|
@ -33,17 +40,17 @@ public class Client {
|
||||||
*/
|
*/
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
try (DaprClient client = new DaprClientBuilder().build()) {
|
try (DaprClient client = new DaprClientBuilder().build()) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.getState("Unknown state store", "myKey", String.class).block();
|
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
|
||||||
} catch (DaprException exception) {
|
} catch (DaprException exception) {
|
||||||
System.out.println("Error code: " + exception.getErrorCode());
|
System.out.println("Error code: " + exception.getErrorCode());
|
||||||
System.out.println("Error message: " + exception.getMessage());
|
System.out.println("Error message: " + exception.getMessage());
|
||||||
|
System.out.println("Reason: " + exception.getStatusDetails().get(
|
||||||
exception.printStackTrace();
|
DaprErrorDetails.ErrorDetailType.ERROR_INFO,
|
||||||
|
"reason",
|
||||||
|
TypeRef.STRING));
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("Done");
|
|
||||||
}
|
}
|
||||||
|
System.out.println("Done");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,8 @@ cd java-sdk
|
||||||
Then build the Maven project:
|
Then build the Maven project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# make sure you are in the `java-sdk` directory.
|
# make sure you are in the `java-sdk` (root) directory.
|
||||||
mvn install
|
./mvnw clean install
|
||||||
```
|
```
|
||||||
|
|
||||||
Then get into the examples directory:
|
Then get into the examples directory:
|
||||||
|
|
@ -32,43 +32,40 @@ Then get into the examples directory:
|
||||||
cd examples
|
cd examples
|
||||||
```
|
```
|
||||||
|
|
||||||
### Running the StateClient
|
### 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:
|
|
||||||
|
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
|
```java
|
||||||
public class Client {
|
public class Client {
|
||||||
|
|
||||||
public static void main(String[] args) throws Exception {
|
public static void main(String[] args) throws Exception {
|
||||||
try (DaprClient client = new DaprClientBuilder().build()) {
|
try (DaprClient client = new DaprClientBuilder().build()) {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
client.getState("Unknown state store", "myKey", String.class).block();
|
client.publishEvent("unknown_pubsub", "mytopic", "mydata").block();
|
||||||
} catch (DaprException exception) {
|
} catch (DaprException exception) {
|
||||||
System.out.println("Error code: " + exception.getErrorCode());
|
System.out.println("Dapr exception's error code: " + exception.getErrorCode());
|
||||||
System.out.println("Error message: " + exception.getMessage());
|
System.out.println("Dapr exception's message: " + exception.getMessage());
|
||||||
|
System.out.println("Dapr exception's reason: " + exception.getStatusDetails().get(
|
||||||
exception.printStackTrace();
|
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
|
### Running the example
|
||||||
|
|
||||||
Run this example with the following command:
|
|
||||||
|
|
||||||
<!-- STEP
|
<!-- STEP
|
||||||
name: Run exception example
|
name: Run exception example
|
||||||
expected_stdout_lines:
|
expected_stdout_lines:
|
||||||
- '== APP == Error code: INVALID_ARGUMENT'
|
- '== 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
|
background: true
|
||||||
sleep: 5
|
sleep: 5
|
||||||
-->
|
-->
|
||||||
|
|
@ -79,42 +76,31 @@ dapr run --app-id exception-example -- java -jar target/dapr-java-sdk-examples-e
|
||||||
|
|
||||||
<!-- END_STEP -->
|
<!-- 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
|
```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 == Reason: DAPR_PUBSUB_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)
|
|
||||||
...
|
...
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 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
|
### Cleanup
|
||||||
|
|
||||||
To stop the app run (or press `CTRL+C`):
|
To stop the app run (or press `CTRL+C`):
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,9 @@ limitations under the License.
|
||||||
|
|
||||||
package io.dapr.it;
|
package io.dapr.it;
|
||||||
|
|
||||||
|
import io.dapr.exceptions.DaprErrorDetails;
|
||||||
import io.dapr.exceptions.DaprException;
|
import io.dapr.exceptions.DaprException;
|
||||||
|
import io.dapr.utils.TypeRef;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.function.Executable;
|
import org.junit.jupiter.api.function.Executable;
|
||||||
|
|
||||||
|
|
@ -42,6 +44,24 @@ public final class TestUtils {
|
||||||
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
|
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(
|
public static <T extends Throwable> void assertThrowsDaprExceptionSubstring(
|
||||||
String expectedErrorCode,
|
String expectedErrorCode,
|
||||||
String expectedErrorMessageSubstring,
|
String expectedErrorMessageSubstring,
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,14 @@ import org.junit.jupiter.api.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import java.time.Duration;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
|
||||||
import static io.dapr.it.Retry.callWithRetry;
|
import static io.dapr.it.Retry.callWithRetry;
|
||||||
import static io.dapr.it.actors.MyActorTestUtils.fetchMethodCallLogs;
|
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.validateMessageContent;
|
||||||
|
import static io.dapr.it.actors.MyActorTestUtils.validateMethodCalls;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||||
|
|
||||||
public class ActorTimerRecoveryIT extends BaseIT {
|
public class ActorTimerRecoveryIT extends BaseIT {
|
||||||
|
|
@ -82,21 +81,17 @@ public class ActorTimerRecoveryIT extends BaseIT {
|
||||||
|
|
||||||
// Restarts app only.
|
// Restarts app only.
|
||||||
runs.left.stop();
|
runs.left.stop();
|
||||||
|
// Cannot sleep between app's stop and start since it can trigger unhealthy actor in runtime and lose timers.
|
||||||
// Pause a bit to let placements settle.
|
// Timers will survive only if the restart is "quick" and survives the runtime's actor health check.
|
||||||
logger.info("Pausing 12 seconds to let placements settle.");
|
// Starting in 1.13, sidecar is more sensitive to an app restart and will not keep actors active for "too long".
|
||||||
Thread.sleep(Duration.ofSeconds(12).toMillis());
|
|
||||||
|
|
||||||
runs.left.start();
|
runs.left.start();
|
||||||
|
|
||||||
logger.debug("Pausing 10 seconds to allow timer to fire");
|
|
||||||
Thread.sleep(10000);
|
|
||||||
final List<MethodEntryTracker> newLogs = new ArrayList<>();
|
final List<MethodEntryTracker> newLogs = new ArrayList<>();
|
||||||
callWithRetry(() -> {
|
callWithRetry(() -> {
|
||||||
newLogs.clear();
|
newLogs.clear();
|
||||||
newLogs.addAll(fetchMethodCallLogs(proxy));
|
newLogs.addAll(fetchMethodCallLogs(proxy));
|
||||||
validateMethodCalls(newLogs, METHOD_NAME, 3);
|
validateMethodCalls(newLogs, METHOD_NAME, 3);
|
||||||
}, 5000);
|
}, 10000);
|
||||||
|
|
||||||
// Check that the restart actually happened by confirming the old logs are not in the new logs.
|
// Check that the restart actually happened by confirming the old logs are not in the new logs.
|
||||||
for (MethodEntryTracker oldLog: logs) {
|
for (MethodEntryTracker oldLog: logs) {
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,13 @@ import io.dapr.it.BaseIT;
|
||||||
import io.dapr.it.DaprRun;
|
import io.dapr.it.DaprRun;
|
||||||
import io.dapr.serializer.DaprObjectSerializer;
|
import io.dapr.serializer.DaprObjectSerializer;
|
||||||
import io.dapr.utils.TypeRef;
|
import io.dapr.utils.TypeRef;
|
||||||
import org.junit.After;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.params.ParameterizedTest;
|
import org.junit.jupiter.params.ParameterizedTest;
|
||||||
import org.junit.jupiter.params.provider.ValueSource;
|
import org.junit.jupiter.params.provider.ValueSource;
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
import org.junit.runners.Parameterized;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
|
|
@ -56,6 +50,7 @@ import java.util.Set;
|
||||||
|
|
||||||
import static io.dapr.it.Retry.callWithRetry;
|
import static io.dapr.it.Retry.callWithRetry;
|
||||||
import static io.dapr.it.TestUtils.assertThrowsDaprException;
|
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.assertArrayEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
@ -119,14 +114,16 @@ public class PubSubIT extends BaseIT {
|
||||||
try (DaprClient client = new DaprClientBuilder().build()) {
|
try (DaprClient client = new DaprClientBuilder().build()) {
|
||||||
|
|
||||||
if (useGrpc) {
|
if (useGrpc) {
|
||||||
assertThrowsDaprException(
|
assertThrowsDaprExceptionWithReason(
|
||||||
"INVALID_ARGUMENT",
|
"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());
|
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
|
||||||
} else {
|
} else {
|
||||||
assertThrowsDaprException(
|
assertThrowsDaprExceptionWithReason(
|
||||||
"ERR_PUBSUB_NOT_FOUND",
|
"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());
|
() -> client.publishEvent("unknown pubsub", "mytopic", "payload").block());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,7 +146,7 @@ public class PubSubIT extends BaseIT {
|
||||||
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
|
try (DaprPreviewClient client = new DaprClientBuilder().buildPreviewClient()) {
|
||||||
assertThrowsDaprException(
|
assertThrowsDaprException(
|
||||||
"INVALID_ARGUMENT",
|
"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());
|
() -> client.publishEvents("unknown pubsub", "mytopic","text/plain", "message").block());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import io.dapr.client.domain.Metadata;
|
import io.dapr.client.domain.Metadata;
|
||||||
import io.dapr.config.Properties;
|
import io.dapr.config.Properties;
|
||||||
import io.dapr.exceptions.DaprError;
|
import io.dapr.exceptions.DaprError;
|
||||||
|
import io.dapr.exceptions.DaprErrorDetails;
|
||||||
import io.dapr.exceptions.DaprException;
|
import io.dapr.exceptions.DaprException;
|
||||||
import io.dapr.utils.Version;
|
import io.dapr.utils.Version;
|
||||||
import okhttp3.Call;
|
import okhttp3.Call;
|
||||||
|
|
@ -73,6 +74,11 @@ public class DaprHttp implements AutoCloseable {
|
||||||
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
|
private static final Set<String> ALLOWED_CONTEXT_IN_HEADERS =
|
||||||
Collections.unmodifiableSet(new HashSet<>(Arrays.asList("grpc-trace-bin", "traceparent", "tracestate")));
|
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.
|
* HTTP Methods supported.
|
||||||
*/
|
*/
|
||||||
|
|
@ -136,11 +142,6 @@ public class DaprHttp implements AutoCloseable {
|
||||||
*/
|
*/
|
||||||
private static final byte[] EMPTY_BYTES = new byte[0];
|
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.
|
* Endpoint used to communicate to Dapr's HTTP endpoint.
|
||||||
*/
|
*/
|
||||||
|
|
@ -347,12 +348,13 @@ public class DaprHttp implements AutoCloseable {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return OBJECT_MAPPER.readValue(json, DaprError.class);
|
return DAPR_ERROR_DETAILS_OBJECT_MAPPER.readValue(json, DaprError.class);
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new DaprException("UNKNOWN", new String(json, StandardCharsets.UTF_8));
|
throw new DaprException("UNKNOWN", new String(json, StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
|
private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws IOException {
|
||||||
ResponseBody body = response.body();
|
ResponseBody body = response.body();
|
||||||
if (body != null) {
|
if (body != null) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2021 The Dapr Authors
|
* Copyright 2024 The Dapr Authors
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* You may obtain a copy of the License at
|
||||||
|
|
@ -14,7 +14,10 @@ limitations under the License.
|
||||||
package io.dapr.exceptions;
|
package io.dapr.exceptions;
|
||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
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.
|
* Represents an error message from Dapr.
|
||||||
|
|
@ -37,6 +40,11 @@ public class DaprError {
|
||||||
*/
|
*/
|
||||||
private Integer code;
|
private Integer code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Details about the error.
|
||||||
|
*/
|
||||||
|
private List<Map<String, Object>> details;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the error code.
|
* Gets the error code.
|
||||||
*
|
*
|
||||||
|
|
@ -44,7 +52,7 @@ public class DaprError {
|
||||||
*/
|
*/
|
||||||
public String getErrorCode() {
|
public String getErrorCode() {
|
||||||
if ((errorCode == null) && (code != null)) {
|
if ((errorCode == null) && (code != null)) {
|
||||||
return Status.fromCodeValue(code).getCode().name();
|
return io.grpc.Status.fromCodeValue(code).getCode().name();
|
||||||
}
|
}
|
||||||
return errorCode;
|
return errorCode;
|
||||||
}
|
}
|
||||||
|
|
@ -80,4 +88,24 @@ public class DaprError {
|
||||||
return this;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
* Copyright 2021 The Dapr Authors
|
* Copyright 2024 The Dapr Authors
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
* You may obtain a copy of the License at
|
* 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.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.concurrent.Callable;
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -30,13 +32,18 @@ public class DaprException extends RuntimeException {
|
||||||
*/
|
*/
|
||||||
private String errorCode;
|
private String errorCode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The status details for the error.
|
||||||
|
*/
|
||||||
|
private DaprErrorDetails errorDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* New exception from a server-side generated error code and message.
|
* New exception from a server-side generated error code and message.
|
||||||
*
|
*
|
||||||
* @param daprError Server-side error.
|
* @param daprError Server-side error.
|
||||||
*/
|
*/
|
||||||
public DaprException(DaprError daprError) {
|
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.
|
* @param message Client-side error message.
|
||||||
*/
|
*/
|
||||||
public DaprException(String errorCode, String 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));
|
super(String.format("%s: %s", errorCode, message));
|
||||||
this.errorCode = errorCode;
|
this.errorCode = errorCode;
|
||||||
|
this.errorDetails = errorDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -84,6 +114,22 @@ public class DaprException extends RuntimeException {
|
||||||
this.errorCode = errorCode;
|
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.
|
* Returns the exception's error code.
|
||||||
*
|
*
|
||||||
|
|
@ -93,6 +139,10 @@ public class DaprException extends RuntimeException {
|
||||||
return this.errorCode;
|
return this.errorCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DaprErrorDetails getStatusDetails() {
|
||||||
|
return this.errorDetails;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wraps an exception into DaprException (if not already DaprException).
|
* Wraps an exception into DaprException (if not already DaprException).
|
||||||
*
|
*
|
||||||
|
|
@ -189,10 +239,15 @@ public class DaprException extends RuntimeException {
|
||||||
while (e != null) {
|
while (e != null) {
|
||||||
if (e instanceof StatusRuntimeException) {
|
if (e instanceof StatusRuntimeException) {
|
||||||
StatusRuntimeException statusRuntimeException = (StatusRuntimeException) e;
|
StatusRuntimeException statusRuntimeException = (StatusRuntimeException) e;
|
||||||
|
com.google.rpc.Status status = io.grpc.protobuf.StatusProto.fromThrowable(statusRuntimeException);
|
||||||
|
|
||||||
|
DaprErrorDetails errorDetails = new DaprErrorDetails(status);
|
||||||
|
|
||||||
return new DaprException(
|
return new DaprException(
|
||||||
statusRuntimeException.getStatus().getCode().toString(),
|
statusRuntimeException.getStatus().getCode().toString(),
|
||||||
statusRuntimeException.getStatus().getDescription(),
|
statusRuntimeException.getStatus().getDescription(),
|
||||||
exception);
|
exception,
|
||||||
|
errorDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
e = e.getCause();
|
e = e.getCause();
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ import io.dapr.v1.DaprGrpc;
|
||||||
import io.dapr.v1.DaprProtos;
|
import io.dapr.v1.DaprProtos;
|
||||||
import io.grpc.Status;
|
import io.grpc.Status;
|
||||||
import io.grpc.StatusRuntimeException;
|
import io.grpc.StatusRuntimeException;
|
||||||
|
import io.grpc.protobuf.StatusProto;
|
||||||
import io.grpc.stub.StreamObserver;
|
import io.grpc.stub.StreamObserver;
|
||||||
import org.junit.jupiter.api.AfterEach;
|
import org.junit.jupiter.api.AfterEach;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
@ -2507,7 +2508,17 @@ public class DaprClientGrpcTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static StatusRuntimeException newStatusRuntimeException(String status, String message) {
|
public static StatusRuntimeException newStatusRuntimeException(String statusCode, String message) {
|
||||||
return new StatusRuntimeException(Status.fromCode(Status.Code.valueOf(status)).withDescription(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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,7 +13,9 @@ limitations under the License.
|
||||||
package io.dapr.client;
|
package io.dapr.client;
|
||||||
|
|
||||||
import io.dapr.config.Properties;
|
import io.dapr.config.Properties;
|
||||||
|
import io.dapr.exceptions.DaprErrorDetails;
|
||||||
import io.dapr.exceptions.DaprException;
|
import io.dapr.exceptions.DaprException;
|
||||||
|
import io.dapr.utils.TypeRef;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import reactor.test.StepVerifier;
|
import reactor.test.StepVerifier;
|
||||||
|
|
@ -186,7 +188,7 @@ public class DaprHttpTest {
|
||||||
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
|
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
|
||||||
Mono<DaprHttp.Response> mono =
|
Mono<DaprHttp.Response> mono =
|
||||||
daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty());
|
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
|
@Test
|
||||||
|
|
@ -197,7 +199,7 @@ public class DaprHttpTest {
|
||||||
"{\"errorCode\":null,\"message\":null}"));
|
"{\"errorCode\":null,\"message\":null}"));
|
||||||
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
|
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());
|
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
|
@Test
|
||||||
|
|
@ -208,7 +210,36 @@ public class DaprHttpTest {
|
||||||
"{\"errorCode\":\"null\",\"message\":\"null\"}"));
|
"{\"errorCode\":\"null\",\"message\":\"null\"}"));
|
||||||
DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient);
|
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());
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,14 @@ limitations under the License.
|
||||||
|
|
||||||
package io.dapr.utils;
|
package io.dapr.utils;
|
||||||
|
|
||||||
|
import io.dapr.exceptions.DaprErrorDetails;
|
||||||
import io.dapr.exceptions.DaprException;
|
import io.dapr.exceptions.DaprException;
|
||||||
import org.junit.jupiter.api.Assertions;
|
import org.junit.jupiter.api.Assertions;
|
||||||
import org.junit.jupiter.api.function.Executable;
|
import org.junit.jupiter.api.function.Executable;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.ServerSocket;
|
import java.net.ServerSocket;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public final class TestUtils {
|
public final class TestUtils {
|
||||||
|
|
||||||
|
|
@ -58,6 +60,20 @@ public final class TestUtils {
|
||||||
Assertions.assertEquals(expectedErrorMessage, daprException.getMessage());
|
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 {
|
public static int findFreePort() throws IOException {
|
||||||
try (ServerSocket socket = new ServerSocket(0)) {
|
try (ServerSocket socket = new ServerSocket(0)) {
|
||||||
socket.setReuseAddress(true);
|
socket.setReuseAddress(true);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue