diff --git a/pom.xml b/pom.xml index 7bb556983..e2ec3bea2 100644 --- a/pom.xml +++ b/pom.xml @@ -88,7 +88,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.10 diff --git a/sdk-actors/pom.xml b/sdk-actors/pom.xml index 3e42c7900..48295d015 100644 --- a/sdk-actors/pom.xml +++ b/sdk-actors/pom.xml @@ -101,7 +101,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.10 default-prepare-agent diff --git a/sdk-springboot/pom.xml b/sdk-springboot/pom.xml index 924f7c26b..d1e7088d2 100644 --- a/sdk-springboot/pom.xml +++ b/sdk-springboot/pom.xml @@ -125,7 +125,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.10 default-prepare-agent diff --git a/sdk/pom.xml b/sdk/pom.xml index 3bb2b599a..13b96c7c4 100644 --- a/sdk/pom.xml +++ b/sdk/pom.xml @@ -158,7 +158,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.10 default-prepare-agent diff --git a/sdk/src/main/java/io/dapr/client/DaprClientBuilder.java b/sdk/src/main/java/io/dapr/client/DaprClientBuilder.java index 717e3309a..772af71f2 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientBuilder.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientBuilder.java @@ -14,6 +14,7 @@ package io.dapr.client; import io.dapr.config.Properties; +import io.dapr.exceptions.DaprError; import io.dapr.serializer.DaprObjectSerializer; import io.dapr.serializer.DefaultObjectSerializer; import io.dapr.utils.Version; @@ -53,6 +54,11 @@ public class DaprClientBuilder { */ private DaprObjectSerializer objectSerializer; + /** + * Response parser used for custom handling of error responses from Dapr. + */ + private DaprErrorResponseParser errorParser; + /** * Serializer used for state objects in DaprClient. */ @@ -61,7 +67,7 @@ public class DaprClientBuilder { /** * Creates a constructor for DaprClient. * - * {@link DefaultObjectSerializer} is used for object and state serializers by defaul but is not recommended + *

{@link DefaultObjectSerializer} is used for object and state serializers by defaul but is not recommended * for production scenarios. */ public DaprClientBuilder() { @@ -92,6 +98,22 @@ public DaprClientBuilder withObjectSerializer(DaprObjectSerializer objectSeriali return this; } + /** + * Sets the error parser for objects to received from Dapr. + * See {@link DefaultDaprHttpErrorResponseParser} as a default parser for {@link DaprError}. + * + * @param errorParser Parser for objects received from Dapr. + * @return This instance. + */ + public DaprClientBuilder withErrorParser(DaprErrorResponseParser errorParser) { + if (errorParser == null) { + throw new IllegalArgumentException("Response parser is required"); + } + + this.errorParser = errorParser; + return this; + } + /** * Sets the serializer for objects to be persisted. * See {@link DefaultObjectSerializer} as possible serializer for non-production scenarios. @@ -149,9 +171,12 @@ private DaprClient buildDaprClient(DaprApiProtocol protocol) { } switch (protocol) { - case GRPC: return buildDaprClientGrpc(); - case HTTP: return buildDaprClientHttp(); - default: throw new IllegalStateException("Unsupported protocol: " + protocol.name()); + case GRPC: + return buildDaprClientGrpc(); + case HTTP: + return buildDaprClientHttp(); + default: + throw new IllegalStateException("Unsupported protocol: " + protocol.name()); } } @@ -174,7 +199,9 @@ private DaprClient buildDaprClientGrpc() { } }; DaprGrpc.DaprStub asyncStub = DaprGrpc.newStub(channel); - return new DaprClientGrpc(closeableChannel, asyncStub, this.objectSerializer, this.stateSerializer); + + return new DaprClientGrpc( + closeableChannel, asyncStub, this.objectSerializer, this.stateSerializer, this.errorParser); } /** @@ -183,7 +210,7 @@ private DaprClient buildDaprClientGrpc() { * @return DaprClient over HTTP. */ private DaprClient buildDaprClientHttp() { - return new DaprClientHttp(this.daprHttpBuilder.build(), this.objectSerializer, this.stateSerializer); + return new DaprClientHttp(this.daprHttpBuilder.build(errorParser), this.objectSerializer, this.stateSerializer); } } diff --git a/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java b/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java index 9b795c7d7..e98807e69 100644 --- a/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java +++ b/sdk/src/main/java/io/dapr/client/DaprClientGrpc.java @@ -62,6 +62,7 @@ import io.grpc.ForwardingClientCall; import io.grpc.Metadata; import io.grpc.MethodDescriptor; +import io.grpc.StatusRuntimeException; import io.grpc.stub.StreamObserver; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; @@ -71,6 +72,7 @@ import java.io.Closeable; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -81,6 +83,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; +import static io.dapr.config.Properties.STRING_CHARSET; + /** * An adapter for the GRPC Client. * @@ -89,6 +93,11 @@ */ public class DaprClientGrpc extends AbstractDaprClient { + /** + * Default error parser for gRPC. + */ + private static final DaprErrorResponseParser DEFAULT_ERROR_PARSER = new DefaultDaprGrpcErrorResponseParser(); + /** * The GRPC managed channel to be used. */ @@ -99,6 +108,11 @@ public class DaprClientGrpc extends AbstractDaprClient { */ private DaprGrpc.DaprStub asyncStub; + /** + * Error parser. + */ + private DaprErrorResponseParser errorResponseParser; + /** * Default access level constructor, in order to create an instance of this class use io.dapr.client.DaprClientBuilder * @@ -112,10 +126,13 @@ public class DaprClientGrpc extends AbstractDaprClient { Closeable closeableChannel, DaprGrpc.DaprStub asyncStub, DaprObjectSerializer objectSerializer, - DaprObjectSerializer stateSerializer) { + DaprObjectSerializer stateSerializer, + DaprErrorResponseParser errorResponseParser + ) { super(objectSerializer, stateSerializer); this.channel = closeableChannel; this.asyncStub = intercept(asyncStub); + this.errorResponseParser = errorResponseParser == null ? DEFAULT_ERROR_PARSER : errorResponseParser; } private CommonProtos.StateOptions.StateConsistency getGrpcStateConsistency(StateOptions options) { @@ -299,15 +316,16 @@ public Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef // gRPC to HTTP does not map correctly in Dapr runtime as per https://github.com/dapr/dapr/issues/2342 return Mono.deferContextual( - context -> this.createMono( - it -> intercept(context, asyncStub).invokeService(envelope, it) + context -> this.createMonoWithErrorHandling( + it -> intercept(context, asyncStub).invokeService(envelope, it), + errorResponseParser ) - ).flatMap( + ).flatMap( it -> { try { return Mono.justOrEmpty(objectSerializer.deserialize(it.getData().getValue().toByteArray(), type)); - } catch (IOException e) { - throw DaprException.propagate(e); + } catch (IOException e) { + return Mono.error(DaprException.propagate(e)); } } ); @@ -316,6 +334,45 @@ public Mono invokeMethod(InvokeMethodRequest invokeMethodRequest, TypeRef } } + private Mono createMonoWithErrorHandling( + Consumer> consumer, + DaprErrorResponseParser errorResponseParser) { + return Mono.create(sink -> { + StreamObserver streamObserver = new StreamObserver() { + @Override + public void onNext(T value) { + sink.success(value); + } + + @Override + public void onError(Throwable t) { + if (t instanceof StatusRuntimeException) { + StatusRuntimeException statusException = (StatusRuntimeException) t; + int statusCode = statusException.getStatus().getCode().value(); + byte[] errorDetails = statusException.getStatus().getDescription() != null + ? statusException.getStatus().getDescription().getBytes(STRING_CHARSET.get()) + : new byte[0]; + try { + DaprException exception = errorResponseParser.parse(statusCode, errorDetails); + sink.error(new ExecutionException(exception)); + } catch (IOException e) { + sink.error(new ExecutionException(new DaprException("Error parsing error response", e.toString()))); + } + } else { + sink.error(DaprException.propagate(new ExecutionException(t))); + } + } + + @Override + public void onCompleted() { + sink.success(); + } + }; + + DaprException.wrap(() -> consumer.accept(streamObserver)).run(); + }); + } + /** * {@inheritDoc} */ @@ -900,7 +957,7 @@ private Mono> getConfiguration(DaprProtos.GetConf Iterator> itr = it.getItems().entrySet().iterator(); while (itr.hasNext()) { Map.Entry entry = itr.next(); - configMap.put(entry.getKey(), buildConfigurationItem(entry.getValue(), entry.getKey())); + configMap.put(entry.getKey(), buildConfigurationItem(entry.getValue(), entry.getKey())); } return Collections.unmodifiableMap(configMap); } @@ -939,7 +996,7 @@ public Flux subscribeConfiguration(SubscribeConf Iterator> itr = it.getItemsMap().entrySet().iterator(); while (itr.hasNext()) { Map.Entry entry = itr.next(); - configMap.put(entry.getKey(), buildConfigurationItem(entry.getValue(), entry.getKey())); + configMap.put(entry.getKey(), buildConfigurationItem(entry.getValue(), entry.getKey())); } return new SubscribeConfigurationResponse(it.getId(), Collections.unmodifiableMap(configMap)); } diff --git a/sdk/src/main/java/io/dapr/client/DaprErrorResponseParser.java b/sdk/src/main/java/io/dapr/client/DaprErrorResponseParser.java new file mode 100644 index 000000000..06195496b --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DaprErrorResponseParser.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 io.dapr.exceptions.DaprException; + +import java.io.IOException; + +/** + * Parses an error response from Dapr APIs. + */ +public interface DaprErrorResponseParser { + + /** + * Parses an error code and throws an Exception. + * + * @param statusCode HTTP or gRPC status code. + * @param response response payload from Dapr API. + * @return Exception parsed from payload + * @throws IOException if cannot parse error. + */ + DaprException parse(int statusCode, byte[] response) throws IOException; +} diff --git a/sdk/src/main/java/io/dapr/client/DaprHttp.java b/sdk/src/main/java/io/dapr/client/DaprHttp.java index 830bfad61..7f7507fb9 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttp.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttp.java @@ -13,10 +13,8 @@ package io.dapr.client; -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.DaprException; import io.dapr.utils.Version; import okhttp3.Call; @@ -27,11 +25,13 @@ import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.ResponseBody; +import org.apache.commons.io.IOUtils; import org.jetbrains.annotations.NotNull; import reactor.core.publisher.Mono; import reactor.util.context.ContextView; import java.io.IOException; +import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Collections; @@ -70,7 +70,13 @@ public class DaprHttp implements AutoCloseable { * Context entries allowed to be in HTTP Headers. */ private static final Set 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"))); + + private static final DaprErrorResponseParser DEFAULT_ERROR_PARSER = new DefaultDaprHttpErrorResponseParser(); + /** + * Error response parser. + */ + private DaprErrorResponseParser errorParser; /** * HTTP Methods supported. @@ -135,11 +141,6 @@ public int getStatusCode() { */ private static final byte[] EMPTY_BYTES = new byte[0]; - /** - * JSON Object Mapper. - */ - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - /** * Hostname used to communicate to Dapr's HTTP endpoint. */ @@ -162,10 +163,15 @@ public int getStatusCode() { * @param port Port for calling Dapr. (e.g. 3500) * @param httpClient RestClient used for all API calls in this new instance. */ - DaprHttp(String hostname, int port, OkHttpClient httpClient) { + DaprHttp(String hostname, int port, OkHttpClient httpClient, DaprErrorResponseParser errorParser) { this.hostname = hostname; this.port = port; this.httpClient = httpClient; + this.errorParser = errorParser == null ? DEFAULT_ERROR_PARSER : errorParser; + } + + DaprHttp(String hostname, int port, OkHttpClient httpClient) { + this(hostname, port, httpClient, null); } /** @@ -290,8 +296,8 @@ private CompletableFuture doInvokeApi(String method, .addHeader(HEADER_DAPR_REQUEST_ID, requestId); if (context != null) { context.stream() - .filter(entry -> ALLOWED_CONTEXT_IN_HEADERS.contains(entry.getKey().toString().toLowerCase())) - .forEach(entry -> requestBuilder.addHeader(entry.getKey().toString(), entry.getValue().toString())); + .filter(entry -> ALLOWED_CONTEXT_IN_HEADERS.contains(entry.getKey().toString().toLowerCase())) + .forEach(entry -> requestBuilder.addHeader(entry.getKey().toString(), entry.getValue().toString())); } if (HttpMethods.GET.name().equals(method)) { requestBuilder.get(); @@ -318,32 +324,16 @@ private CompletableFuture doInvokeApi(String method, CompletableFuture future = new CompletableFuture<>(); - this.httpClient.newCall(request).enqueue(new ResponseFutureCallback(future)); + this.httpClient.newCall(request).enqueue(new ResponseFutureCallback(this.errorParser, future)); return future; } - /** - * Tries to parse an error from Dapr response body. - * - * @param json Response body from Dapr. - * @return DaprError or null if could not parse. - */ - private static DaprError parseDaprError(byte[] json) { - if ((json == null) || (json.length == 0)) { - return null; - } - - try { - return 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) { - return body.bytes(); + try (InputStream inputStream = body.byteStream()) { + return IOUtils.toByteArray(inputStream); + } } return EMPTY_BYTES; @@ -353,9 +343,12 @@ private static byte[] getBodyBytesOrEmptyArray(okhttp3.Response response) throws * Converts the okhttp3 response into the response object expected internally by the SDK. */ private static class ResponseFutureCallback implements Callback { + + private final DaprErrorResponseParser errorParser; private final CompletableFuture future; - public ResponseFutureCallback(CompletableFuture future) { + public ResponseFutureCallback(DaprErrorResponseParser errorParser, CompletableFuture future) { + this.errorParser = errorParser; this.future = future; } @@ -367,24 +360,10 @@ public void onFailure(Call call, IOException e) { @Override public void onResponse(@NotNull Call call, @NotNull okhttp3.Response response) throws IOException { if (!response.isSuccessful()) { - try { - DaprError error = parseDaprError(getBodyBytesOrEmptyArray(response)); - if ((error != null) && (error.getErrorCode() != null)) { - if (error.getMessage() != null) { - future.completeExceptionally(new DaprException(error)); - } else { - future.completeExceptionally( - new DaprException(error.getErrorCode(), "HTTP status code: " + response.code())); - } - return; - } - - future.completeExceptionally(new DaprException("UNKNOWN", "HTTP status code: " + response.code())); - return; - } catch (DaprException e) { - future.completeExceptionally(e); - return; - } + byte[] errorDetails = getBodyBytesOrEmptyArray(response); + DaprException customException = this.errorParser.parse(response.code(), errorDetails); + + future.completeExceptionally(customException); } Map mapHeaders = new HashMap<>(); diff --git a/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java b/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java index 3c4c34820..8f960c0de 100644 --- a/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java +++ b/sdk/src/main/java/io/dapr/client/DaprHttpBuilder.java @@ -45,6 +45,12 @@ public class DaprHttpBuilder { */ private static final int KEEP_ALIVE_DURATION = 30; + /** + * A default parser for Dapr error responses. + */ + private static final DaprErrorResponseParser DEFAULT_PARSER = new DefaultDaprHttpErrorResponseParser(); + + /** * Build an instance of the Http client based on the provided setup. * @@ -52,7 +58,11 @@ public class DaprHttpBuilder { * @throws IllegalStateException if any required field is missing */ public DaprHttp build() { - return buildDaprHttp(); + return buildDaprHttp(null); + } + + public DaprHttp build(DaprErrorResponseParser parser) { + return buildDaprHttp(parser); } /** @@ -60,7 +70,7 @@ public DaprHttp build() { * * @return Instance of {@link DaprHttp} */ - private DaprHttp buildDaprHttp() { + private DaprHttp buildDaprHttp(DaprErrorResponseParser parser) { if (OK_HTTP_CLIENT == null) { synchronized (LOCK) { if (OK_HTTP_CLIENT == null) { @@ -85,6 +95,7 @@ private DaprHttp buildDaprHttp() { } } - return new DaprHttp(Properties.SIDECAR_IP.get(), Properties.HTTP_PORT.get(), OK_HTTP_CLIENT); + DaprErrorResponseParser parserToUse = parser == null ? DEFAULT_PARSER : parser; + return new DaprHttp(Properties.SIDECAR_IP.get(), Properties.HTTP_PORT.get(), OK_HTTP_CLIENT, parserToUse); } } diff --git a/sdk/src/main/java/io/dapr/client/DefaultDaprGrpcErrorResponseParser.java b/sdk/src/main/java/io/dapr/client/DefaultDaprGrpcErrorResponseParser.java new file mode 100644 index 000000000..fcb71a5cd --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DefaultDaprGrpcErrorResponseParser.java @@ -0,0 +1,34 @@ +/* + * Copyright 2023 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 io.dapr.exceptions.DaprException; + + +import static io.dapr.config.Properties.STRING_CHARSET; + +/** + * Default error parser for Dapr Grpc API. + */ +public class DefaultDaprGrpcErrorResponseParser implements DaprErrorResponseParser { + + /** + * {@inheritDoc} + */ + @Override + public DaprException parse(int statusCode, byte[] errorDetails) { + String errorMessage = new String(errorDetails, STRING_CHARSET.get()); + return new DaprException("UNKNOWN: ", errorMessage); + } +} diff --git a/sdk/src/main/java/io/dapr/client/DefaultDaprHttpErrorResponseParser.java b/sdk/src/main/java/io/dapr/client/DefaultDaprHttpErrorResponseParser.java new file mode 100644 index 000000000..5fb3e8ca5 --- /dev/null +++ b/sdk/src/main/java/io/dapr/client/DefaultDaprHttpErrorResponseParser.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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 io.dapr.exceptions.DaprError; +import io.dapr.exceptions.DaprException; + +import java.io.IOException; + +import static io.dapr.client.ObjectSerializer.OBJECT_MAPPER; +import static io.dapr.config.Properties.STRING_CHARSET; + +/** + * Default implementation for DaprHTTPErrorResponse. + */ +public class DefaultDaprHttpErrorResponseParser implements DaprErrorResponseParser { + + /** + * {@inheritDoc} + */ + @Override + public DaprException parse(int statusCode, byte[] response) { + String errorMessage = + (response == null || (response.length == 0)) + ? "HTTP status code: " + statusCode + : new String(response, STRING_CHARSET.get()); + DaprError error = null; + String errorCode = "UNKNOWN"; + DaprException unknownException = new DaprException(errorCode, errorMessage); + + if ((response != null) && (response.length != 0)) { + try { + error = OBJECT_MAPPER.readValue(response, DaprError.class); + } catch (IOException e) { + return new DaprException("UNKNOWN", new String(response, STRING_CHARSET.get())); + } + } + + if (error != null) { + errorMessage = error.getMessage() == null ? errorMessage : error.getMessage(); + errorCode = error.getErrorCode() == null ? errorCode : error.getErrorCode(); + return new DaprException(errorCode, errorMessage); + } + return unknownException; + } +} diff --git a/sdk/src/main/java/io/dapr/exceptions/DaprHttpException.java b/sdk/src/main/java/io/dapr/exceptions/DaprHttpException.java new file mode 100644 index 000000000..9f3cf01c6 --- /dev/null +++ b/sdk/src/main/java/io/dapr/exceptions/DaprHttpException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2023 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 okhttp3.Response; + +/** + * HTTP Exception from Dapr API. + */ +public class DaprHttpException extends RuntimeException { + + /** + * Creates a new instance of DaprHTTPException. + * + * @param response HTTP response from OkHTTP + */ + public DaprHttpException(Response response) { + super("Dapr HTTP exception: " + response.code() + " " + response.message()); + } +} diff --git a/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java b/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java index c543c1b93..8a83f3261 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientBuilderTest.java @@ -25,11 +25,13 @@ public class DaprClientBuilderTest { @Test public void build() { DaprObjectSerializer objectSerializer = mock(DaprObjectSerializer.class); + DaprErrorResponseParser errorResponseParser = mock(DaprErrorResponseParser.class); when(objectSerializer.getContentType()).thenReturn("application/json"); DaprObjectSerializer stateSerializer = mock(DaprObjectSerializer.class); DaprClientBuilder daprClientBuilder = new DaprClientBuilder(); daprClientBuilder.withObjectSerializer(objectSerializer); daprClientBuilder.withStateSerializer(stateSerializer); + daprClientBuilder.withErrorParser(errorResponseParser); DaprClient daprClient = daprClientBuilder.build(); assertNotNull(daprClient); } diff --git a/sdk/src/test/java/io/dapr/client/DaprClientGrpcTelemetryTest.java b/sdk/src/test/java/io/dapr/client/DaprClientGrpcTelemetryTest.java index 2b747a3bf..8181ee248 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientGrpcTelemetryTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientGrpcTelemetryTest.java @@ -170,7 +170,8 @@ public ServerCall.Listener interceptCall(ServerCall) invocation -> { StreamObserver observer = (StreamObserver) invocation.getArguments()[1]; observer.onNext(Empty.getDefaultInstance()); @@ -290,7 +291,7 @@ public void invokeBindingIllegalArgumentExceptionTest() { @Test public void invokeBindingSerializeException() throws IOException { DaprObjectSerializer mockSerializer = mock(DaprObjectSerializer.class); - client = new DaprClientGrpc(closeable, daprStub, mockSerializer, new DefaultObjectSerializer()); + client = new DaprClientGrpc(closeable, daprStub, mockSerializer, new DefaultObjectSerializer(), null); doAnswer((Answer) invocation -> { StreamObserver observer = (StreamObserver) invocation.getArguments()[1]; observer.onNext(Empty.getDefaultInstance()); @@ -1451,7 +1452,7 @@ public void executeTransactionIllegalArgumentExceptionTest() { @Test public void executeTransactionSerializerExceptionTest() throws IOException { DaprObjectSerializer mockSerializer = mock(DaprObjectSerializer.class); - client = new DaprClientGrpc(closeable, daprStub, mockSerializer, mockSerializer); + client = new DaprClientGrpc(closeable, daprStub, mockSerializer, mockSerializer, null); String etag = "ETag1"; String key = "key1"; String data = "my data"; diff --git a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java index 9988d6462..f573d3353 100644 --- a/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprClientHttpTest.java @@ -72,6 +72,8 @@ public class DaprClientHttpTest { + private static final DaprErrorResponseParser DEFAULT_PARSER = new DefaultDaprHttpErrorResponseParser(); + private static final String STATE_STORE_NAME = "MyStateStore"; private static final String CONFIG_STORE_NAME = "MyConfigStore"; @@ -275,7 +277,7 @@ public void invokeServiceDaprErrorFromGRPC() { }); assertEquals("PERMISSION_DENIED", exception.getErrorCode()); - assertEquals("PERMISSION_DENIED: HTTP status code: 500", exception.getMessage()); + assertEquals("PERMISSION_DENIED: { \"code\": 7 }", exception.getMessage()); } @Test diff --git a/sdk/src/test/java/io/dapr/client/DaprHttpStub.java b/sdk/src/test/java/io/dapr/client/DaprHttpStub.java index f6a52b8c4..6df6cce0b 100644 --- a/sdk/src/test/java/io/dapr/client/DaprHttpStub.java +++ b/sdk/src/test/java/io/dapr/client/DaprHttpStub.java @@ -34,7 +34,7 @@ public ResponseStub(byte[] body, Map headers, int statusCode) { * Instantiates a stub for DaprHttp */ public DaprHttpStub() { - super(null, 3000, null); + super(null, 3000, null, null); } /** diff --git a/sdk/src/test/java/io/dapr/client/DaprHttpTest.java b/sdk/src/test/java/io/dapr/client/DaprHttpTest.java index 4d475560c..3e156cb07 100644 --- a/sdk/src/test/java/io/dapr/client/DaprHttpTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprHttpTest.java @@ -14,6 +14,8 @@ import io.dapr.config.Properties; import io.dapr.exceptions.DaprException; +import io.dapr.utils.TestCustomError; +import io.dapr.utils.TestCustomErrorResponseParser; import reactor.util.context.Context; import okhttp3.MediaType; import okhttp3.OkHttpClient; @@ -35,6 +37,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.assertThrows; public class DaprHttpTest { @@ -177,9 +180,8 @@ public void invokePostMethodRuntime() throws IOException { assertEquals(EXPECTED_RESULT, body); } - @Test(expected = RuntimeException.class) + @Test(expected = DaprException.class) public void invokePostDaprError() throws IOException { - mockInterceptor.addRule() .post("http://127.0.0.1:3500/v1.0/state") .respond(500, ResponseBody.create(MediaType.parse("text"), @@ -191,17 +193,25 @@ public void invokePostDaprError() throws IOException { assertEquals(EXPECTED_RESULT, body); } - @Test(expected = RuntimeException.class) - public void invokePostMethodUnknownError() throws IOException { + @Test + public void invokePostDaprErrorWithCustomParser() { mockInterceptor.addRule() .post("http://127.0.0.1:3500/v1.0/state") - .respond(500, ResponseBody.create(MediaType.parse("application/json"), - "{\"errorCode\":\"null\",\"message\":\"null\"}")); - DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient); + .respond(404, ResponseBody.create(MediaType.parse("text"), + "{\"message\":\"someMessage\",\"body\":\"property\"}")); + DaprHttp daprHttp = new DaprHttp(Properties.SIDECAR_IP.get(), 3500, okHttpClient, new TestCustomErrorResponseParser()); Mono mono = daprHttp.invokeApi("POST", "v1.0/state".split("/"), null, null, Context.empty()); - DaprHttp.Response response = mono.block(); - String body = serializer.deserialize(response.getBody(), String.class); - assertEquals(EXPECTED_RESULT, body); + + TestCustomError exception = assertThrows(TestCustomError.class, () -> { + DaprHttp.Response response = mono.block(); + String body = serializer.deserialize(response.getBody(), String.class); + assertEquals(EXPECTED_RESULT, body); + }); + + assertEquals(String.valueOf(404), exception.getCustomField()); + assertEquals("customStatus", exception.getStatus()); + assertEquals("customStatus", exception.getErrorCode()); + assertEquals("customMessage", exception.getMessage()); } /** diff --git a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java index dcf6051e4..e68d2e308 100644 --- a/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java +++ b/sdk/src/test/java/io/dapr/client/DaprPreviewClientGrpcTest.java @@ -79,7 +79,7 @@ public void setup() throws IOException { daprStub = mock(DaprGrpc.DaprStub.class); when(daprStub.withInterceptors(any())).thenReturn(daprStub); previewClient = new DaprClientGrpc( - closeable, daprStub, new DefaultObjectSerializer(), new DefaultObjectSerializer()); + closeable, daprStub, new DefaultObjectSerializer(), new DefaultObjectSerializer(), null); doNothing().when(closeable).close(); } @@ -143,7 +143,7 @@ public void publishEventsContentTypeMismatchException() throws IOException { @Test public void publishEventsSerializeException() throws IOException { DaprObjectSerializer mockSerializer = mock(DaprObjectSerializer.class); - previewClient = new DaprClientGrpc(closeable, daprStub, mockSerializer, new DefaultObjectSerializer()); + previewClient = new DaprClientGrpc(closeable, daprStub, mockSerializer, new DefaultObjectSerializer(), null); doAnswer((Answer) invocation -> { StreamObserver observer = (StreamObserver) invocation.getArguments()[1]; diff --git a/sdk/src/test/java/io/dapr/utils/TestCustomError.java b/sdk/src/test/java/io/dapr/utils/TestCustomError.java new file mode 100644 index 000000000..64468bd7e --- /dev/null +++ b/sdk/src/test/java/io/dapr/utils/TestCustomError.java @@ -0,0 +1,30 @@ +package io.dapr.utils; + +import io.dapr.exceptions.DaprException; + +public class TestCustomError extends DaprException { + private String status; + + private String message; + + private String customField; + + public TestCustomError(String status, String message, String customField) { + super(status, message); + this.status = status; + this.message = message; + this.customField = customField; + } + + public String getStatus() { + return status; + } + + public String getMessage() { + return message; + } + + public String getCustomField() { + return customField; + } +} diff --git a/sdk/src/test/java/io/dapr/utils/TestCustomErrorResponseParser.java b/sdk/src/test/java/io/dapr/utils/TestCustomErrorResponseParser.java new file mode 100644 index 000000000..7967ee458 --- /dev/null +++ b/sdk/src/test/java/io/dapr/utils/TestCustomErrorResponseParser.java @@ -0,0 +1,12 @@ +package io.dapr.utils; + +import io.dapr.client.DaprErrorResponseParser; +import io.dapr.exceptions.DaprException; + +public class TestCustomErrorResponseParser implements DaprErrorResponseParser { + + @Override + public DaprException parse(int statusCode, byte[] response) { + return new TestCustomError("customStatus", "customMessage", String.valueOf(statusCode)); + } +}