diff --git a/CHANGELOG.md b/CHANGELOG.md index 1007b960e..c266cf9c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.15.0](https://github.com/googleapis/gax-java/compare/v2.14.0...v2.15.0) (2022-04-06) + + +### Features + +* Error Details Improvements - GRPC ([#1634](https://github.com/googleapis/gax-java/issues/1634)) ([00c3b9d](https://github.com/googleapis/gax-java/commit/00c3b9ddae11736e1231e4aa45965deff7689f2a)) +* relocate native image properties from java-core to gax ([#1648](https://github.com/googleapis/gax-java/issues/1648)) ([609c2aa](https://github.com/googleapis/gax-java/commit/609c2aab94ff215c879932e60ae041309f20c2c7)) + + +### Dependencies + +* upgrade grpc to 1.45.1 and auth to 1.6.0 ([#1652](https://github.com/googleapis/gax-java/issues/1652)) ([8f8f625](https://github.com/googleapis/gax-java/commit/8f8f62572795c83b4459f6d3a0f33f31a1ecfb71)) + ## [2.14.0](https://github.com/googleapis/gax-java/compare/v2.13.0...v2.14.0) (2022-04-01) diff --git a/build.gradle b/build.gradle index 02dde1018..cfd2d9e36 100644 --- a/build.gradle +++ b/build.gradle @@ -13,7 +13,7 @@ plugins { } // TODO: Populate this from dependencies.properties version property (for proper Gradle-Bazel sync) -project.version = "2.14.0" // {x-version-update:gax:current} +project.version = "2.15.0" // {x-version-update:gax:current} allprojects { group = 'com.google.api' diff --git a/dependencies.properties b/dependencies.properties index 7e432daa3..d901df5c3 100644 --- a/dependencies.properties +++ b/dependencies.properties @@ -8,16 +8,16 @@ # Versions of oneself # {x-version-update-start:gax:current} -version.gax=2.14.0 +version.gax=2.15.0 # {x-version-update-end} # {x-version-update-start:gax:current} -version.gax_grpc=2.14.0 +version.gax_grpc=2.15.0 # {x-version-update-end} # {x-version-update-start:gax:current} -version.gax_bom=2.14.0 +version.gax_bom=2.15.0 # {x-version-update-end} # {x-version-update-start:gax-httpjson:current} -version.gax_httpjson=0.99.0 +version.gax_httpjson=0.100.0 # {x-version-update-end} # Versions for dependencies which actual artifacts differ between Bazel and Gradle. @@ -25,7 +25,7 @@ version.gax_httpjson=0.99.0 # with the sources. version.com_google_protobuf=3.19.4 version.google_java_format=1.1 -version.io_grpc=1.45.0 +version.io_grpc=1.45.1 # Maven artifacts. # Note, the actual name of each property matters (bazel build scripts depend on it). @@ -34,8 +34,8 @@ version.io_grpc=1.45.0 # 2) Replace all characters which are neither alphabetic nor digits with the underscore ('_') character maven.com_google_api_grpc_proto_google_common_protos=com.google.api.grpc:proto-google-common-protos:2.8.0 maven.com_google_api_grpc_grpc_google_common_protos=com.google.api.grpc:grpc-google-common-protos:2.8.0 -maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.2.1 -maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.2.1 +maven.com_google_auth_google_auth_library_oauth2_http=com.google.auth:google-auth-library-oauth2-http:1.6.0 +maven.com_google_auth_google_auth_library_credentials=com.google.auth:google-auth-library-credentials:1.6.0 maven.io_opencensus_opencensus_api=io.opencensus:opencensus-api:0.28.0 maven.io_opencensus_opencensus_contrib_grpc_metrics=io.opencensus:opencensus-contrib-grpc-metrics:0.28.0 maven.io_opencensus_opencensus_contrib_http_util=io.opencensus:opencensus-contrib-http-util:0.28.0 diff --git a/gax-bom/build.gradle b/gax-bom/build.gradle index 1dbcded2d..fcdf470e6 100644 --- a/gax-bom/build.gradle +++ b/gax-bom/build.gradle @@ -5,7 +5,7 @@ plugins { archivesBaseName = 'gax-bom' -project.version = "2.14.0" // {x-version-update:gax-bom:current} +project.version = "2.15.0" // {x-version-update:gax-bom:current} def mavenJavaDir = "$buildDir/publications/mavenJava" def mavenJavaBomOutputFile = file(mavenJavaDir + '/pom-default.xml') diff --git a/gax-bom/pom.xml b/gax-bom/pom.xml index e861f615c..c00f88341 100644 --- a/gax-bom/pom.xml +++ b/gax-bom/pom.xml @@ -3,7 +3,7 @@ 4.0.0 com.google.api gax-bom - 2.14.0 + 2.15.0 pom GAX (Google Api eXtensions) for Java Google Api eXtensions for Java @@ -33,34 +33,34 @@ com.google.api gax - 2.14.0 + 2.15.0 com.google.api gax - 2.14.0 + 2.15.0 testlib com.google.api gax-grpc - 2.14.0 + 2.15.0 com.google.api gax-grpc - 2.14.0 + 2.15.0 testlib com.google.api gax-httpjson - 0.99.0 + 0.100.0 com.google.api gax-httpjson - 0.99.0 + 0.100.0 testlib diff --git a/gax-grpc/build.gradle b/gax-grpc/build.gradle index a8d3df04b..6bdbe6f7c 100644 --- a/gax-grpc/build.gradle +++ b/gax-grpc/build.gradle @@ -1,7 +1,7 @@ archivesBaseName = 'gax-grpc' // TODO: Populate this from dependencies.properties version property (for proper Gradle-Bazel sync) -project.version = "2.14.0" // {x-version-update:gax-grpc:current} +project.version = "2.15.0" // {x-version-update:gax-grpc:current} dependencies { api(project(':gax'), diff --git a/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcApiExceptionFactory.java b/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcApiExceptionFactory.java index 7e33946df..9c3dd0a84 100644 --- a/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcApiExceptionFactory.java +++ b/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcApiExceptionFactory.java @@ -31,8 +31,12 @@ import com.google.api.gax.rpc.ApiException; import com.google.api.gax.rpc.ApiExceptionFactory; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode.Code; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; +import com.google.protobuf.InvalidProtocolBufferException; +import io.grpc.Metadata; import io.grpc.Status; import io.grpc.StatusException; import io.grpc.StatusRuntimeException; @@ -40,11 +44,13 @@ /** * Core logic for transforming GRPC exceptions into {@link ApiException}s. This logic is shared - * amongst all of the call types. + * amongst all the call types. * *

Package-private for internal use. */ class GrpcApiExceptionFactory { + + @VisibleForTesting static final String ERROR_DETAIL_KEY = "grpc-status-details-bin"; private final ImmutableSet retryableCodes; GrpcApiExceptionFactory(Set retryCodes) { @@ -54,10 +60,10 @@ class GrpcApiExceptionFactory { ApiException create(Throwable throwable) { if (throwable instanceof StatusException) { StatusException e = (StatusException) throwable; - return create(throwable, e.getStatus().getCode()); + return create(throwable, e.getStatus().getCode(), e.getTrailers()); } else if (throwable instanceof StatusRuntimeException) { StatusRuntimeException e = (StatusRuntimeException) throwable; - return create(throwable, e.getStatus().getCode()); + return create(throwable, e.getStatus().getCode(), e.getTrailers()); } else if (throwable instanceof ApiException) { return (ApiException) throwable; } else { @@ -67,8 +73,29 @@ ApiException create(Throwable throwable) { } } - private ApiException create(Throwable throwable, Status.Code statusCode) { + private ApiException create(Throwable throwable, Status.Code statusCode, Metadata metadata) { boolean canRetry = retryableCodes.contains(GrpcStatusCode.grpcCodeToStatusCode(statusCode)); - return ApiExceptionFactory.createException(throwable, GrpcStatusCode.of(statusCode), canRetry); + GrpcStatusCode grpcStatusCode = GrpcStatusCode.of(statusCode); + + if (metadata == null) { + return ApiExceptionFactory.createException(throwable, grpcStatusCode, canRetry); + } + + byte[] bytes = metadata.get(Metadata.Key.of(ERROR_DETAIL_KEY, Metadata.BINARY_BYTE_MARSHALLER)); + if (bytes == null) { + return ApiExceptionFactory.createException(throwable, grpcStatusCode, canRetry); + } + + com.google.rpc.Status status; + try { + status = com.google.rpc.Status.parseFrom(bytes); + } catch (InvalidProtocolBufferException e) { + return ApiExceptionFactory.createException(throwable, grpcStatusCode, canRetry); + } + + ErrorDetails.Builder errorDetailsBuilder = ErrorDetails.builder(); + errorDetailsBuilder.setRawErrorMessages(status.getDetailsList()); + return ApiExceptionFactory.createException( + throwable, grpcStatusCode, canRetry, errorDetailsBuilder.build()); } } diff --git a/gax-grpc/src/main/resources/META-INF/native-image/com.google.api/gax-grpc/native-image.properties b/gax-grpc/src/main/resources/META-INF/native-image/com.google.api/gax-grpc/native-image.properties new file mode 100644 index 000000000..ad3745380 --- /dev/null +++ b/gax-grpc/src/main/resources/META-INF/native-image/com.google.api/gax-grpc/native-image.properties @@ -0,0 +1,10 @@ +Args = --initialize-at-run-time=io.grpc.netty.shaded.io.netty.handler.ssl.OpenSsl,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.SSL,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.CertificateVerifier,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.SSLPrivateKeyMethod,\ + io.grpc.netty.shaded.io.netty.internal.tcnative.AsyncSSLPrivateKeyMethod,\ + io.grpc.netty.shaded.io.grpc.netty,\ + io.grpc.netty.shaded.io.netty.channel.epoll,\ + io.grpc.netty.shaded.io.netty.channel.unix,\ + io.grpc.netty.shaded.io.netty.handler.ssl,\ + io.grpc.internal.RetriableStream \ No newline at end of file diff --git a/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcApiExceptionFactoryTest.java b/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcApiExceptionFactoryTest.java new file mode 100644 index 000000000..bb8febef5 --- /dev/null +++ b/gax-grpc/src/test/java/com/google/api/gax/grpc/GrpcApiExceptionFactoryTest.java @@ -0,0 +1,139 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.grpc; + +import static com.google.api.gax.grpc.GrpcApiExceptionFactory.ERROR_DETAIL_KEY; + +import com.google.api.gax.rpc.ApiException; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.common.collect.ImmutableList; +import com.google.common.truth.Truth; +import com.google.protobuf.Any; +import com.google.protobuf.Duration; +import com.google.rpc.ErrorInfo; +import com.google.rpc.RetryInfo; +import com.google.rpc.Status; +import io.grpc.Metadata; +import io.grpc.StatusException; +import io.grpc.StatusRuntimeException; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcApiExceptionFactoryTest { + + private static final ErrorInfo ERROR_INFO = + ErrorInfo.newBuilder() + .setDomain("googleapis.com") + .setReason("SERVICE_DISABLED") + .putAllMetadata(Collections.emptyMap()) + .build(); + + private static final RetryInfo RETRY_INFO = + RetryInfo.newBuilder().setRetryDelay(Duration.newBuilder().setSeconds(213).build()).build(); + + private static final ImmutableList RAW_ERROR_MESSAGES = + ImmutableList.of(Any.pack(ERROR_INFO), Any.pack(RETRY_INFO)); + + private static final ErrorDetails ERROR_DETAILS = + ErrorDetails.builder().setRawErrorMessages(RAW_ERROR_MESSAGES).build(); + + private static final io.grpc.Status GRPC_STATUS = io.grpc.Status.CANCELLED; + + private GrpcApiExceptionFactory factory; + + @Before + public void setUp() throws Exception { + factory = new GrpcApiExceptionFactory(Collections.emptySet()); + } + + @Test + public void create_shouldCreateApiExceptionWithErrorDetailsForStatusException() { + Metadata trailers = new Metadata(); + Status status = Status.newBuilder().addAllDetails(RAW_ERROR_MESSAGES).build(); + trailers.put( + Metadata.Key.of(ERROR_DETAIL_KEY, Metadata.BINARY_BYTE_MARSHALLER), status.toByteArray()); + StatusException statusException = new StatusException(GRPC_STATUS, trailers); + + ApiException actual = factory.create(statusException); + + Truth.assertThat(actual.getErrorDetails()).isEqualTo(ERROR_DETAILS); + } + + @Test + public void create_shouldCreateApiExceptionWithErrorDetailsForStatusRuntimeException() { + Metadata trailers = new Metadata(); + Status status = Status.newBuilder().addAllDetails(RAW_ERROR_MESSAGES).build(); + trailers.put( + Metadata.Key.of(ERROR_DETAIL_KEY, Metadata.BINARY_BYTE_MARSHALLER), status.toByteArray()); + StatusRuntimeException statusException = new StatusRuntimeException(GRPC_STATUS, trailers); + + ApiException actual = factory.create(statusException); + + Truth.assertThat(actual.getErrorDetails()).isEqualTo(ERROR_DETAILS); + } + + @Test + public void create_shouldCreateApiExceptionWithNoErrorDetailsIfMetadataIsNull() { + StatusRuntimeException statusException = new StatusRuntimeException(GRPC_STATUS, null); + + ApiException actual = factory.create(statusException); + + Truth.assertThat(actual.getErrorDetails()).isNull(); + } + + @Test + public void create_shouldCreateApiExceptionWithNoErrorDetailsIfMetadataDoesNotHaveErrorDetails() { + StatusRuntimeException statusException = + new StatusRuntimeException(GRPC_STATUS, new Metadata()); + + ApiException actual = factory.create(statusException); + + Truth.assertThat(actual.getErrorDetails()).isNull(); + } + + @Test + public void create_shouldCreateApiExceptionWithNoErrorDetailsIfStatusIsMalformed() { + Metadata trailers = new Metadata(); + Status status = Status.newBuilder().addDetails(Any.pack(ERROR_INFO)).build(); + byte[] bytes = status.toByteArray(); + // manually manipulate status bytes array + bytes[0] = 123; + trailers.put(Metadata.Key.of(ERROR_DETAIL_KEY, Metadata.BINARY_BYTE_MARSHALLER), bytes); + StatusRuntimeException statusException = new StatusRuntimeException(GRPC_STATUS, trailers); + + ApiException actual = factory.create(statusException); + + Truth.assertThat(actual.getErrorDetails()).isNull(); + } +} diff --git a/gax-httpjson/build.gradle b/gax-httpjson/build.gradle index b52dee58f..d885140c5 100644 --- a/gax-httpjson/build.gradle +++ b/gax-httpjson/build.gradle @@ -1,7 +1,7 @@ archivesBaseName = 'gax-httpjson' // TODO: Populate this from dependencies.properties version property (for proper Gradle-Bazel sync) -project.version = "0.99.0" // {x-version-update:gax-httpjson:current} +project.version = "0.100.0" // {x-version-update:gax-httpjson:current} dependencies { api(project(':gax'), diff --git a/gax/BUILD.bazel b/gax/BUILD.bazel index f35f3fc99..ede88dee4 100644 --- a/gax/BUILD.bazel +++ b/gax/BUILD.bazel @@ -9,6 +9,8 @@ _JAVA_COPTS = [ _COMPILE_DEPS = [ "@com_google_api_api_common//jar", + "@com_google_api_grpc_proto_google_common_protos//jar", + "@com_google_protobuf_java//jar", "@com_google_auth_google_auth_library_credentials//jar", "@com_google_auth_google_auth_library_oauth2_http//jar", "@com_google_auto_value_auto_value//jar", diff --git a/gax/build.gradle b/gax/build.gradle index cdd5d63bd..1e2b4cd23 100644 --- a/gax/build.gradle +++ b/gax/build.gradle @@ -1,11 +1,12 @@ archivesBaseName = "gax" // TODO: Populate this from dependencies.properties version property (for proper Gradle-Bazel sync) -project.version = "2.14.0" // {x-version-update:gax:current} +project.version = "2.15.0" // {x-version-update:gax:current} dependencies { api(libraries['maven.com_google_api_api_common'], - libraries['maven.com_google_auth_google_auth_library_credentials'], + libraries['maven.com_google_api_grpc_proto_google_common_protos'], + libraries['maven.com_google_auth_google_auth_library_credentials'], libraries['maven.org_threeten_threetenbp']) implementation(libraries['maven.com_google_auth_google_auth_library_oauth2_http'], diff --git a/gax/src/main/java/com/google/api/gax/rpc/AbortedException.java b/gax/src/main/java/com/google/api/gax/rpc/AbortedException.java index 7549b8b6d..4cd2e31b6 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/AbortedException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/AbortedException.java @@ -42,4 +42,9 @@ public AbortedException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public AbortedException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/AlreadyExistsException.java b/gax/src/main/java/com/google/api/gax/rpc/AlreadyExistsException.java index 5a0c81c49..6ee118898 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/AlreadyExistsException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/AlreadyExistsException.java @@ -42,4 +42,9 @@ public AlreadyExistsException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public AlreadyExistsException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/ApiException.java b/gax/src/main/java/com/google/api/gax/rpc/ApiException.java index a7d1f784a..3722b894e 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/ApiException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/ApiException.java @@ -30,24 +30,34 @@ package com.google.api.gax.rpc; import com.google.common.base.Preconditions; +import java.util.Map; /** Represents an exception thrown during an RPC call. */ public class ApiException extends RuntimeException { + private static final long serialVersionUID = -4375114339928877996L; + private final ErrorDetails errorDetails; private final StatusCode statusCode; private final boolean retryable; public ApiException(Throwable cause, StatusCode statusCode, boolean retryable) { - super(cause); - this.statusCode = Preconditions.checkNotNull(statusCode); - this.retryable = retryable; + this(cause, statusCode, retryable, null); } public ApiException(String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause); this.statusCode = Preconditions.checkNotNull(statusCode); this.retryable = retryable; + this.errorDetails = null; + } + + public ApiException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause); + this.statusCode = Preconditions.checkNotNull(statusCode); + this.retryable = retryable; + this.errorDetails = errorDetails; } /** Returns whether the failed request can be retried. */ @@ -59,4 +69,43 @@ public boolean isRetryable() { public StatusCode getStatusCode() { return statusCode; } + + /** + * Returns the reason of the exception. This is a constant value that identifies the proximate + * cause of the error. e.g. SERVICE_DISABLED + */ + public String getReason() { + if (isErrorInfoEmpty()) { + return null; + } + return errorDetails.getErrorInfo().getReason(); + } + + /** + * Returns the logical grouping to which the "reason" belongs. The error domain is typically the + * registered service name of the tool or product that generates the error. e.g. googleapis.com + */ + public String getDomain() { + if (isErrorInfoEmpty()) { + return null; + } + return errorDetails.getErrorInfo().getDomain(); + } + + /** Returns additional structured details about this exception. */ + public Map getMetadata() { + if (isErrorInfoEmpty()) { + return null; + } + return errorDetails.getErrorInfo().getMetadataMap(); + } + + /** Returns all standard error messages that server sends. */ + public ErrorDetails getErrorDetails() { + return errorDetails; + } + + private boolean isErrorInfoEmpty() { + return errorDetails == null || errorDetails.getErrorInfo() == null; + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/ApiExceptionFactory.java b/gax/src/main/java/com/google/api/gax/rpc/ApiExceptionFactory.java index 9820b751e..4ca5f41c7 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/ApiExceptionFactory.java +++ b/gax/src/main/java/com/google/api/gax/rpc/ApiExceptionFactory.java @@ -38,81 +38,85 @@ private ApiExceptionFactory() {} public static ApiException createException( Throwable cause, StatusCode statusCode, boolean retryable) { + return createException(cause, statusCode, retryable, null); + } + + public static ApiException createException( + String message, Throwable cause, StatusCode statusCode, boolean retryable) { switch (statusCode.getCode()) { case CANCELLED: - return new CancelledException(cause, statusCode, retryable); + return new CancelledException(message, cause, statusCode, retryable); case NOT_FOUND: - return new NotFoundException(cause, statusCode, retryable); + return new NotFoundException(message, cause, statusCode, retryable); case INVALID_ARGUMENT: - return new InvalidArgumentException(cause, statusCode, retryable); + return new InvalidArgumentException(message, cause, statusCode, retryable); case DEADLINE_EXCEEDED: - return new DeadlineExceededException(cause, statusCode, retryable); + return new DeadlineExceededException(message, cause, statusCode, retryable); case ALREADY_EXISTS: - return new AlreadyExistsException(cause, statusCode, retryable); + return new AlreadyExistsException(message, cause, statusCode, retryable); case PERMISSION_DENIED: - return new PermissionDeniedException(cause, statusCode, retryable); + return new PermissionDeniedException(message, cause, statusCode, retryable); case RESOURCE_EXHAUSTED: - return new ResourceExhaustedException(cause, statusCode, retryable); + return new ResourceExhaustedException(message, cause, statusCode, retryable); case FAILED_PRECONDITION: - return new FailedPreconditionException(cause, statusCode, retryable); + return new FailedPreconditionException(message, cause, statusCode, retryable); case ABORTED: - return new AbortedException(cause, statusCode, retryable); + return new AbortedException(message, cause, statusCode, retryable); case OUT_OF_RANGE: - return new OutOfRangeException(cause, statusCode, retryable); + return new OutOfRangeException(message, cause, statusCode, retryable); case UNIMPLEMENTED: - return new UnimplementedException(cause, statusCode, retryable); + return new UnimplementedException(message, cause, statusCode, retryable); case INTERNAL: - return new InternalException(cause, statusCode, retryable); + return new InternalException(message, cause, statusCode, retryable); case UNAVAILABLE: - return new UnavailableException(cause, statusCode, retryable); + return new UnavailableException(message, cause, statusCode, retryable); case DATA_LOSS: - return new DataLossException(cause, statusCode, retryable); + return new DataLossException(message, cause, statusCode, retryable); case UNAUTHENTICATED: - return new UnauthenticatedException(cause, statusCode, retryable); + return new UnauthenticatedException(message, cause, statusCode, retryable); case UNKNOWN: // Fall through. default: - return new UnknownException(cause, statusCode, retryable); + return new UnknownException(message, cause, statusCode, retryable); } } public static ApiException createException( - String message, Throwable cause, StatusCode statusCode, boolean retryable) { + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { switch (statusCode.getCode()) { case CANCELLED: - return new CancelledException(message, cause, statusCode, retryable); + return new CancelledException(cause, statusCode, retryable, errorDetails); case NOT_FOUND: - return new NotFoundException(message, cause, statusCode, retryable); + return new NotFoundException(cause, statusCode, retryable, errorDetails); case INVALID_ARGUMENT: - return new InvalidArgumentException(message, cause, statusCode, retryable); + return new InvalidArgumentException(cause, statusCode, retryable, errorDetails); case DEADLINE_EXCEEDED: - return new DeadlineExceededException(message, cause, statusCode, retryable); + return new DeadlineExceededException(cause, statusCode, retryable, errorDetails); case ALREADY_EXISTS: - return new AlreadyExistsException(message, cause, statusCode, retryable); + return new AlreadyExistsException(cause, statusCode, retryable, errorDetails); case PERMISSION_DENIED: - return new PermissionDeniedException(message, cause, statusCode, retryable); + return new PermissionDeniedException(cause, statusCode, retryable, errorDetails); case RESOURCE_EXHAUSTED: - return new ResourceExhaustedException(message, cause, statusCode, retryable); + return new ResourceExhaustedException(cause, statusCode, retryable, errorDetails); case FAILED_PRECONDITION: - return new FailedPreconditionException(message, cause, statusCode, retryable); + return new FailedPreconditionException(cause, statusCode, retryable, errorDetails); case ABORTED: - return new AbortedException(message, cause, statusCode, retryable); + return new AbortedException(cause, statusCode, retryable, errorDetails); case OUT_OF_RANGE: - return new OutOfRangeException(message, cause, statusCode, retryable); + return new OutOfRangeException(cause, statusCode, retryable, errorDetails); case UNIMPLEMENTED: - return new UnimplementedException(message, cause, statusCode, retryable); + return new UnimplementedException(cause, statusCode, retryable, errorDetails); case INTERNAL: - return new InternalException(message, cause, statusCode, retryable); + return new InternalException(cause, statusCode, retryable, errorDetails); case UNAVAILABLE: - return new UnavailableException(message, cause, statusCode, retryable); + return new UnavailableException(cause, statusCode, retryable, errorDetails); case DATA_LOSS: - return new DataLossException(message, cause, statusCode, retryable); + return new DataLossException(cause, statusCode, retryable, errorDetails); case UNAUTHENTICATED: - return new UnauthenticatedException(message, cause, statusCode, retryable); - + return new UnauthenticatedException(cause, statusCode, retryable, errorDetails); case UNKNOWN: // Fall through. default: - return new UnknownException(message, cause, statusCode, retryable); + return new UnknownException(cause, statusCode, retryable, errorDetails); } } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/CancelledException.java b/gax/src/main/java/com/google/api/gax/rpc/CancelledException.java index f0ea4c38a..7bc15f04c 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/CancelledException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/CancelledException.java @@ -39,4 +39,9 @@ public CancelledException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public CancelledException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/DataLossException.java b/gax/src/main/java/com/google/api/gax/rpc/DataLossException.java index 24abb294d..e3d9a8107 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/DataLossException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/DataLossException.java @@ -39,4 +39,9 @@ public DataLossException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public DataLossException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/DeadlineExceededException.java b/gax/src/main/java/com/google/api/gax/rpc/DeadlineExceededException.java index dfccaaaa4..37b5d8db0 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/DeadlineExceededException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/DeadlineExceededException.java @@ -44,4 +44,9 @@ public DeadlineExceededException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public DeadlineExceededException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/ErrorDetails.java b/gax/src/main/java/com/google/api/gax/rpc/ErrorDetails.java new file mode 100644 index 000000000..09c924b59 --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/rpc/ErrorDetails.java @@ -0,0 +1,166 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.auto.value.AutoValue; +import com.google.common.annotations.VisibleForTesting; +import com.google.protobuf.Any; +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.Message; +import com.google.rpc.BadRequest; +import com.google.rpc.DebugInfo; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Help; +import com.google.rpc.LocalizedMessage; +import com.google.rpc.PreconditionFailure; +import com.google.rpc.QuotaFailure; +import com.google.rpc.RequestInfo; +import com.google.rpc.ResourceInfo; +import com.google.rpc.RetryInfo; +import java.util.List; +import javax.annotation.Nullable; + +/** This class contains a list of standard error messages that returns from server. */ +@AutoValue +public abstract class ErrorDetails { + + /** + * This is the most important and special error message. It describes the cause of the error with + * structured details that both humans and applications can depend on. + */ + @Nullable + public ErrorInfo getErrorInfo() { + return unpack(ErrorInfo.class); + } + + /** + * Describes when the clients can retry a failed request. Clients could ignore the recommendation + * here or retry when this information is missing from error responses. + */ + @Nullable + public RetryInfo getRetryInfo() { + return unpack(RetryInfo.class); + } + + /** Describes additional debugging info. */ + @Nullable + public DebugInfo getDebugInfo() { + return unpack(DebugInfo.class); + } + + /** Describes how a quota check failed. */ + @Nullable + public QuotaFailure getQuotaFailure() { + return unpack(QuotaFailure.class); + } + + /** Describes what preconditions have failed. */ + @Nullable + public PreconditionFailure getPreconditionFailure() { + return unpack(PreconditionFailure.class); + } + + /** + * Describes violations in a client request. This error type focuses on the syntactic aspects of + * the request. + */ + @Nullable + public BadRequest getBadRequest() { + return unpack(BadRequest.class); + } + + /** + * Contains metadata about the request that clients can attach when filing a bug or providing + * other forms of feedback. + */ + @Nullable + public RequestInfo getRequestInfo() { + return unpack(RequestInfo.class); + } + + /** Describes the resource that is being accessed. */ + @Nullable + public ResourceInfo getResourceInfo() { + return unpack(ResourceInfo.class); + } + + /** Provides links to documentation or for performing an out-of-band action. */ + @Nullable + public Help getHelp() { + return unpack(Help.class); + } + + /** + * Provides a localized error message that is safe to return to the user which can be attached to + * an RPC error + */ + @Nullable + public LocalizedMessage getLocalizedMessage() { + return unpack(LocalizedMessage.class); + } + + /** This is a list of raw/unparsed error messages that returns from server. */ + @Nullable + abstract List getRawErrorMessages(); + + public static Builder builder() { + return new AutoValue_ErrorDetails.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + + public abstract Builder setRawErrorMessages(List rawErrorMessages); + + public abstract ErrorDetails build(); + } + + @VisibleForTesting + T unpack(Class errorTypeClazz) { + List rawErrorMessages = getRawErrorMessages(); + if (rawErrorMessages == null) { + return null; + } + for (Any detail : rawErrorMessages) { + if (!detail.is(errorTypeClazz)) { + continue; + } + try { + return detail.unpack(errorTypeClazz); + } catch (InvalidProtocolBufferException e) { + throw new ProtocolBufferParsingException( + String.format( + "Failed to unpack %s from raw error messages", errorTypeClazz.getSimpleName()), + e); + } + } + return null; + } +} diff --git a/gax/src/main/java/com/google/api/gax/rpc/FailedPreconditionException.java b/gax/src/main/java/com/google/api/gax/rpc/FailedPreconditionException.java index 3cb05d8ff..e1b3ec75e 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/FailedPreconditionException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/FailedPreconditionException.java @@ -43,4 +43,9 @@ public FailedPreconditionException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public FailedPreconditionException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/InternalException.java b/gax/src/main/java/com/google/api/gax/rpc/InternalException.java index cbe81aeec..3d5763f06 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/InternalException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/InternalException.java @@ -42,4 +42,9 @@ public InternalException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public InternalException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/InvalidArgumentException.java b/gax/src/main/java/com/google/api/gax/rpc/InvalidArgumentException.java index d21d4b0ed..6b0ccc6f6 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/InvalidArgumentException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/InvalidArgumentException.java @@ -43,4 +43,9 @@ public InvalidArgumentException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public InvalidArgumentException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/NotFoundException.java b/gax/src/main/java/com/google/api/gax/rpc/NotFoundException.java index 3a705ce72..ce8b0b1d7 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/NotFoundException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/NotFoundException.java @@ -39,4 +39,9 @@ public NotFoundException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public NotFoundException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/OutOfRangeException.java b/gax/src/main/java/com/google/api/gax/rpc/OutOfRangeException.java index 9a44f78ef..33ecb38ca 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/OutOfRangeException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/OutOfRangeException.java @@ -42,4 +42,9 @@ public OutOfRangeException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public OutOfRangeException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/PermissionDeniedException.java b/gax/src/main/java/com/google/api/gax/rpc/PermissionDeniedException.java index 8600c1599..c9e751597 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/PermissionDeniedException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/PermissionDeniedException.java @@ -39,4 +39,9 @@ public PermissionDeniedException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public PermissionDeniedException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/ProtocolBufferParsingException.java b/gax/src/main/java/com/google/api/gax/rpc/ProtocolBufferParsingException.java new file mode 100644 index 000000000..ce8f306a2 --- /dev/null +++ b/gax/src/main/java/com/google/api/gax/rpc/ProtocolBufferParsingException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +/** Exception thrown when parsing protocol buffer message failed */ +public class ProtocolBufferParsingException extends RuntimeException { + + public ProtocolBufferParsingException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/gax/src/main/java/com/google/api/gax/rpc/ResourceExhaustedException.java b/gax/src/main/java/com/google/api/gax/rpc/ResourceExhaustedException.java index 2de1516f9..2561c54ce 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/ResourceExhaustedException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/ResourceExhaustedException.java @@ -42,4 +42,9 @@ public ResourceExhaustedException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public ResourceExhaustedException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/UnauthenticatedException.java b/gax/src/main/java/com/google/api/gax/rpc/UnauthenticatedException.java index 77f1f4780..6daeece5a 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/UnauthenticatedException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/UnauthenticatedException.java @@ -42,4 +42,9 @@ public UnauthenticatedException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public UnauthenticatedException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/UnavailableException.java b/gax/src/main/java/com/google/api/gax/rpc/UnavailableException.java index 6ca890906..96fbc8d28 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/UnavailableException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/UnavailableException.java @@ -42,4 +42,9 @@ public UnavailableException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public UnavailableException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/UnimplementedException.java b/gax/src/main/java/com/google/api/gax/rpc/UnimplementedException.java index 13769c940..8e042f441 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/UnimplementedException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/UnimplementedException.java @@ -41,4 +41,9 @@ public UnimplementedException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public UnimplementedException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/java/com/google/api/gax/rpc/UnknownException.java b/gax/src/main/java/com/google/api/gax/rpc/UnknownException.java index 29d0502ed..a3ebe3599 100644 --- a/gax/src/main/java/com/google/api/gax/rpc/UnknownException.java +++ b/gax/src/main/java/com/google/api/gax/rpc/UnknownException.java @@ -44,4 +44,9 @@ public UnknownException( String message, Throwable cause, StatusCode statusCode, boolean retryable) { super(message, cause, statusCode, retryable); } + + public UnknownException( + Throwable cause, StatusCode statusCode, boolean retryable, ErrorDetails errorDetails) { + super(cause, statusCode, retryable, errorDetails); + } } diff --git a/gax/src/main/resources/META-INF/native-image/com.google.api/gax/native-image.properties b/gax/src/main/resources/META-INF/native-image/com.google.api/gax/native-image.properties new file mode 100644 index 000000000..f5fbadc5c --- /dev/null +++ b/gax/src/main/resources/META-INF/native-image/com.google.api/gax/native-image.properties @@ -0,0 +1,5 @@ +Args = --allow-incomplete-classpath \ +--enable-url-protocols=https,http \ +--initialize-at-build-time=org.conscrypt,\ + org.slf4j.LoggerFactory,\ + org.junit.platform.engine.TestTag \ No newline at end of file diff --git a/gax/src/test/java/com/google/api/gax/rpc/ApiExceptionTest.java b/gax/src/test/java/com/google/api/gax/rpc/ApiExceptionTest.java new file mode 100644 index 000000000..85bf2d9ec --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/rpc/ApiExceptionTest.java @@ -0,0 +1,115 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import com.google.api.gax.rpc.StatusCode.Code; +import com.google.api.gax.rpc.testing.FakeStatusCode; +import com.google.common.collect.ImmutableList; +import com.google.common.truth.Truth; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import java.util.Collections; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ApiExceptionTest { + + private static final ErrorDetails ERROR_DETAILS_WITH_NO_ERROR_INFO = + ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build(); + private static final String DOMAIN = "googleapis.com"; + private static final String REASON = "SERVICE_DISABLED"; + private static final String METADATA_KEY = "service"; + private static final String METADATA_VALUE = "language.googleapis.com"; + private static final ErrorDetails ERROR_DETAILS_WITH_ERROR_INFO = + ErrorDetails.builder() + .setRawErrorMessages( + ImmutableList.of( + Any.pack( + ErrorInfo.newBuilder() + .setDomain(DOMAIN) + .setReason(REASON) + .putMetadata(METADATA_KEY, METADATA_VALUE) + .build()))) + .build(); + private static final FakeStatusCode STATUS_CODE = FakeStatusCode.of(Code.UNAVAILABLE); + + private ApiException apiException; + + @Test + public void getReason_shouldReturnNullIfErrorDetailsIsNull() { + apiException = new ApiException(null, STATUS_CODE, false, null); + + Truth.assertThat(apiException.getReason()).isNull(); + } + + @Test + public void getReason_shouldReturnNullIfErrorInfoIsNull() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_NO_ERROR_INFO); + + Truth.assertThat(apiException.getReason()).isNull(); + } + + @Test + public void getReason_shouldReturnReasonIfAvailable() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_ERROR_INFO); + + Truth.assertThat(apiException.getReason()).isEqualTo(REASON); + } + + @Test + public void getDomain_shouldReturnNullIfErrorInfoIsNull() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_NO_ERROR_INFO); + + Truth.assertThat(apiException.getDomain()).isNull(); + } + + @Test + public void getDomain_shouldReturnDomainIfAvailable() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_ERROR_INFO); + + Truth.assertThat(apiException.getDomain()).isEqualTo(DOMAIN); + } + + @Test + public void getMetadata_shouldReturnNullIfErrorInfoIsNull() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_NO_ERROR_INFO); + + Truth.assertThat(apiException.getMetadata()).isNull(); + } + + @Test + public void getMetadata_shouldReturnMetadataIfAvailable() { + apiException = new ApiException(null, STATUS_CODE, false, ERROR_DETAILS_WITH_ERROR_INFO); + + Truth.assertThat(apiException.getMetadata()).containsExactly(METADATA_KEY, METADATA_VALUE); + } +} diff --git a/gax/src/test/java/com/google/api/gax/rpc/ErrorDetailsTest.java b/gax/src/test/java/com/google/api/gax/rpc/ErrorDetailsTest.java new file mode 100644 index 000000000..8edef41bf --- /dev/null +++ b/gax/src/test/java/com/google/api/gax/rpc/ErrorDetailsTest.java @@ -0,0 +1,234 @@ +/* + * Copyright 2022 Google LLC + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google LLC nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.google.api.gax.rpc; + +import static org.junit.Assert.assertThrows; + +import com.google.common.collect.ImmutableList; +import com.google.common.truth.Truth; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.Duration; +import com.google.rpc.BadRequest; +import com.google.rpc.BadRequest.FieldViolation; +import com.google.rpc.DebugInfo; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Help; +import com.google.rpc.Help.Link; +import com.google.rpc.LocalizedMessage; +import com.google.rpc.PreconditionFailure; +import com.google.rpc.QuotaFailure; +import com.google.rpc.QuotaFailure.Violation; +import com.google.rpc.RequestInfo; +import com.google.rpc.ResourceInfo; +import com.google.rpc.RetryInfo; +import java.util.Collections; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class ErrorDetailsTest { + + private static final ErrorInfo ERROR_INFO = + ErrorInfo.newBuilder() + .setDomain("googleapis.com") + .setReason("SERVICE_DISABLED") + .putAllMetadata(Collections.emptyMap()) + .build(); + + private static final RetryInfo RETRY_INFO = + RetryInfo.newBuilder().setRetryDelay(Duration.newBuilder().setSeconds(213).build()).build(); + + private static final DebugInfo DEBUG_INFO = + DebugInfo.newBuilder() + .setDetail("No more details available") + .addStackEntries("Does not matter") + .build(); + + private static final QuotaFailure QUOTA_FAILURE = + QuotaFailure.newBuilder() + .addViolations( + Violation.newBuilder() + .setDescription("new violation") + .setSubject("This is a breaking news") + .build()) + .build(); + + private static final PreconditionFailure PRECONDITION_FAILURE = + PreconditionFailure.newBuilder() + .addViolations( + PreconditionFailure.Violation.newBuilder() + .setDescription("new violation") + .setSubject("This is a breaking news") + .setType("Unknown") + .build()) + .build(); + + private static final BadRequest BAD_REQUEST = + BadRequest.newBuilder() + .addFieldViolations( + FieldViolation.newBuilder() + .setDescription("new field violation") + .setField("unknown field") + .build()) + .build(); + + private static final RequestInfo REQUEST_INFO = + RequestInfo.newBuilder() + .setRequestId("ukajsdkansdk123") + .setServingData("no data available") + .build(); + + private static final ResourceInfo RESOURCE_INFO = + ResourceInfo.newBuilder() + .setDescription("not available") + .setResourceName("my resource") + .setResourceType("mystery") + .setOwner("myself") + .build(); + + private static final Help HELP = + Help.newBuilder() + .addLinks(Link.newBuilder().setDescription("new link").setUrl("https://abc.com").build()) + .build(); + + private static final LocalizedMessage LOCALIZED_MESSAGE = + LocalizedMessage.newBuilder().setLocale("en").setMessage("nothing").build(); + + ErrorDetails errorDetails; + + @Before + public void setUp() throws Exception { + ImmutableList rawErrorMessages = + ImmutableList.of( + Any.pack(ERROR_INFO), + Any.pack(RETRY_INFO), + Any.pack(DEBUG_INFO), + Any.pack(QUOTA_FAILURE), + Any.pack(PRECONDITION_FAILURE), + Any.pack(BAD_REQUEST), + Any.pack(REQUEST_INFO), + Any.pack(RESOURCE_INFO), + Any.pack(HELP), + Any.pack(LOCALIZED_MESSAGE)); + + errorDetails = ErrorDetails.builder().setRawErrorMessages(rawErrorMessages).build(); + } + + @Test + public void unpack_shouldReturnNullIfRawErrorMessagesIsNull() { + errorDetails = ErrorDetails.builder().setRawErrorMessages(null).build(); + + Truth.assertThat(errorDetails.unpack(ErrorInfo.class)).isNull(); + } + + @Test + public void unpack_shouldReturnNullIfErrorMessageTypeDoesNotExist() { + errorDetails = + ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(Any.pack(ERROR_INFO))).build(); + + Truth.assertThat(errorDetails.unpack(DebugInfo.class)).isNull(); + } + + @Test + public void unpack_shouldThrowExceptionIfUnpackingErrorMassageFailed() { + Any malformedErrorType = + Any.newBuilder() + .setTypeUrl("type.googleapis.com/google.rpc.ErrorInfo") + .setValue(ByteString.copyFromUtf8("This is an invalid message!")) + .build(); + errorDetails = + ErrorDetails.builder().setRawErrorMessages(ImmutableList.of(malformedErrorType)).build(); + ProtocolBufferParsingException exception = + assertThrows( + ProtocolBufferParsingException.class, () -> errorDetails.unpack(ErrorInfo.class)); + Truth.assertThat(exception.getMessage()) + .isEqualTo( + String.format( + "Failed to unpack %s from raw error messages", ErrorInfo.class.getSimpleName())); + } + + @Test + public void unpack_shouldReturnDesiredErrorMessageTypeIfItExist() { + Truth.assertThat(errorDetails.unpack(ErrorInfo.class)).isEqualTo(ERROR_INFO); + } + + @Test + public void errorInfo_shouldUnpackErrorInfoProtoMessage() { + Truth.assertThat(errorDetails.getErrorInfo()).isEqualTo(ERROR_INFO); + } + + @Test + public void retryInfo_shouldUnpackRetryInfoProtoMessage() { + Truth.assertThat(errorDetails.getRetryInfo()).isEqualTo(RETRY_INFO); + } + + @Test + public void debugInfo_shouldUnpackDebugInfoProtoMessage() { + Truth.assertThat(errorDetails.getDebugInfo()).isEqualTo(DEBUG_INFO); + } + + @Test + public void quotaFailure_shouldUnpackQuotaFailureProtoMessage() { + Truth.assertThat(errorDetails.getQuotaFailure()).isEqualTo(QUOTA_FAILURE); + } + + @Test + public void preconditionFailure_shouldUnpackPreconditionFailureProtoMessage() { + Truth.assertThat(errorDetails.getPreconditionFailure()).isEqualTo(PRECONDITION_FAILURE); + } + + @Test + public void badRequest_shouldUnpackBadRequestProtoMessage() { + Truth.assertThat(errorDetails.getBadRequest()).isEqualTo(BAD_REQUEST); + } + + @Test + public void requestInfo_shouldUnpackRequestInfoProtoMessage() { + Truth.assertThat(errorDetails.getRequestInfo()).isEqualTo(REQUEST_INFO); + } + + @Test + public void resourceInfo_shouldUnpackResourceInfoProtoMessage() { + Truth.assertThat(errorDetails.getResourceInfo()).isEqualTo(RESOURCE_INFO); + } + + @Test + public void help_shouldUnpackHelpProtoMessage() { + Truth.assertThat(errorDetails.getHelp()).isEqualTo(HELP); + } + + @Test + public void localizedMessage_shouldUnpackLocalizedMessageProtoMessage() { + Truth.assertThat(errorDetails.getHelp()).isEqualTo(HELP); + } +} diff --git a/repositories.bzl b/repositories.bzl index 2896b375c..4231c5790 100644 --- a/repositories.bzl +++ b/repositories.bzl @@ -80,6 +80,14 @@ def com_google_api_gax_java_repositories(): licenses = ["notice", "reciprocal"], ) + _maybe( + jvm_maven_import_external, + name = "com_google_protobuf_java", + artifact = "com.google.protobuf:protobuf-java:%s" % PROPERTIES["version.com_google_protobuf"], + server_urls = ["https://repo.maven.apache.org/maven2/", "http://repo1.maven.org/maven2/"], + licenses = ["notice", "reciprocal"], + ) + _maybe( jvm_maven_import_external, name = "io_grpc_grpc_netty_shaded", diff --git a/versions.txt b/versions.txt index 41301da08..01180a8d5 100644 --- a/versions.txt +++ b/versions.txt @@ -1,7 +1,7 @@ # Format: # module:released-version:current-version -gax:2.14.0:2.14.0 -gax-bom:2.14.0:2.14.0 -gax-grpc:2.14.0:2.14.0 -gax-httpjson:0.99.0:0.99.0 +gax:2.15.0:2.15.0 +gax-bom:2.15.0:2.15.0 +gax-grpc:2.15.0:2.15.0 +gax-httpjson:0.100.0:0.100.0