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
|
||||
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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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`):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
* 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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in New Issue