diff --git a/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/ComputeLroErrorParser.java b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/ComputeLroErrorParser.java new file mode 100644 index 000000000000..bdec01d4f36b --- /dev/null +++ b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/ComputeLroErrorParser.java @@ -0,0 +1,73 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.cloud.compute.v1.stub; + +import com.google.api.core.BetaApi; +import com.google.api.gax.httpjson.HttpJsonLroErrorParser; +import com.google.api.gax.rpc.ErrorDetails; +import com.google.cloud.compute.v1.Errors; +import com.google.cloud.compute.v1.Operation; +import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; +import java.util.ArrayList; +import java.util.List; + +@BetaApi("The surface for custom LRO error parsing is not stable yet and may change.") +class ComputeLroErrorParser implements HttpJsonLroErrorParser { + + @Override + public ErrorDetails parse(Object response) { + if (!(response instanceof Operation)) { + return null; + } + Operation operation = ((Operation) response); + if (!operation.hasError()) { + return null; + } + List rawErrorMessages = new ArrayList<>(); + for (Errors error : operation.getError().getErrorsList()) { + ErrorInfo errorInfo = + ErrorInfo.newBuilder() + .setReason(error.getCode()) + .setDomain("googleapis.com") + .putMetadata("message", error.getMessage()) + .putMetadata("location", error.getLocation()) + .build(); + rawErrorMessages.add(Any.pack(errorInfo)); + } + return ErrorDetails.builder().setRawErrorMessages(rawErrorMessages).build(); + } + + @Override + public String parseErrorMessage(Object response) { + if (!(response instanceof Operation)) { + return null; + } + Operation operation = ((Operation) response); + if (!operation.hasError() || operation.getError().getErrorsCount() == 0) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (Errors error : operation.getError().getErrorsList()) { + if (sb.length() > 0) { + sb.append("; "); + } + sb.append(error.getCode()).append(": ").append(error.getMessage()); + } + return sb.toString(); + } +} diff --git a/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonAddressesStub.java b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonAddressesStub.java index b4f05c6f7b62..92570887b020 100644 --- a/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonAddressesStub.java +++ b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonAddressesStub.java @@ -178,6 +178,7 @@ public class HttpJsonAddressesStub extends AddressesStub { .setDone(Status.DONE.equals(response.getStatus())) .setResponse(response) .setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage()) + .setErrorParser(new ComputeLroErrorParser()) .build(); }) .build(); @@ -264,6 +265,7 @@ public class HttpJsonAddressesStub extends AddressesStub { .setDone(Status.DONE.equals(response.getStatus())) .setResponse(response) .setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage()) + .setErrorParser(new ComputeLroErrorParser()) .build(); }) .build(); @@ -369,6 +371,7 @@ public class HttpJsonAddressesStub extends AddressesStub { .setDone(Status.DONE.equals(response.getStatus())) .setResponse(response) .setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage()) + .setErrorParser(new ComputeLroErrorParser()) .build(); }) .build(); @@ -426,6 +429,7 @@ public class HttpJsonAddressesStub extends AddressesStub { .setDone(Status.DONE.equals(response.getStatus())) .setResponse(response) .setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage()) + .setErrorParser(new ComputeLroErrorParser()) .build(); }) .build(); diff --git a/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonRegionOperationsStub.java b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonRegionOperationsStub.java index 436449594c8a..ff6e69878bb6 100644 --- a/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonRegionOperationsStub.java +++ b/java-compute/google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonRegionOperationsStub.java @@ -143,6 +143,7 @@ public class HttpJsonRegionOperationsStub extends RegionOperationsStub { .setDone(Status.DONE.equals(response.getStatus())) .setResponse(response) .setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage()) + .setErrorParser(new ComputeLroErrorParser()) .build(); }) .setPollingRequestFactory( diff --git a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLongRunningOperation.java b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLongRunningOperation.java index 09208861babd..aeffcc8e5f0f 100644 --- a/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLongRunningOperation.java +++ b/java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITLongRunningOperation.java @@ -20,13 +20,19 @@ import com.google.api.gax.longrunning.OperationFuture; import com.google.api.gax.retrying.RetrySettings; +import com.google.api.gax.rpc.ApiException; +import com.google.protobuf.Any; import com.google.protobuf.Timestamp; +import com.google.rpc.Code; +import com.google.rpc.ErrorInfo; +import com.google.rpc.Status; import com.google.showcase.v1beta1.EchoClient; import com.google.showcase.v1beta1.WaitMetadata; import com.google.showcase.v1beta1.WaitRequest; import com.google.showcase.v1beta1.WaitResponse; import com.google.showcase.v1beta1.it.util.TestClientInitializer; import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.Test; import org.threeten.bp.Duration; @@ -193,4 +199,33 @@ void testHttpJson_LROUnsuccessfulResponse_exceedsTotalTimeout_throwsDeadlineExce TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); } } + + @Test + void testGRPC_LROErrorResponse_propagatesErrorDetails() throws Exception { + EchoClient grpcClient = TestClientInitializer.createGrpcEchoClient(); + try { + ErrorInfo errorInfo = + ErrorInfo.newBuilder().setReason("TEST_REASON").setDomain("googleapis.com").build(); + Status status = + Status.newBuilder() + .setCode(Code.ALREADY_EXISTS_VALUE) + .setMessage("The resource already exists") + .addDetails(Any.pack(errorInfo)) + .build(); + WaitRequest waitRequest = WaitRequest.newBuilder().setError(status).build(); + OperationFuture operationFuture = + grpcClient.waitOperationCallable().futureCall(waitRequest); + ExecutionException exception = assertThrows(ExecutionException.class, operationFuture::get); + assertThat(exception.getCause()).isInstanceOf(ApiException.class); + ApiException apiException = (ApiException) exception.getCause(); + + // Verify that error details are successfully propagated + assertThat(apiException.getErrorDetails()).isNotNull(); + assertThat(apiException.getErrorDetails().getErrorInfo()).isEqualTo(errorInfo); + } finally { + grpcClient.close(); + grpcClient.awaitTermination( + TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS); + } + } } diff --git a/librarian.yaml b/librarian.yaml index a1128e1d24c9..76cc99099bd9 100644 --- a/librarian.yaml +++ b/librarian.yaml @@ -1231,6 +1231,9 @@ libraries: java: omit_common_resources: true keep: + - google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/ComputeLroErrorParser.java + - google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonAddressesStub.java + - google-cloud-compute/src/main/java/com/google/cloud/compute/v1/stub/HttpJsonRegionOperationsStub.java - google-cloud-compute/src/test/java/com/google/cloud/compute/v1/integration/BaseTest.java - google-cloud-compute/src/test/java/com/google/cloud/compute/v1/integration/ITComputeGoldenSignals.java - google-cloud-compute/src/test/java/com/google/cloud/compute/v1/integration/ITPaginationTest.java diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java index f3ca00f9db0f..1d11457716de 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java @@ -126,7 +126,9 @@ private static TypeStore createStaticTypes() { @Override protected boolean generateOperationsStubLogic(Service service) { - return service.hasLroMethods(); + return service.hasLroMethods() + && (service.pakkage().startsWith("com.google.cloud.compute.v1") + && !service.pakkage().startsWith("com.google.cloud.compute.v1small")); } @Override @@ -170,7 +172,7 @@ protected Statement createMethodDescriptorVariableDecl( methodMaker .apply( "setOperationSnapshotFactory", - setOperationSnapshotFactoryExpr(protoMethod, messageTypes)) + setOperationSnapshotFactoryExpr(service, protoMethod, messageTypes)) .apply(expr); } @@ -453,7 +455,7 @@ private MethodInvocationExpr getExpr(VariableExpr var, String num) { } private List setOperationSnapshotFactoryExpr( - Method protoMethod, Map messageTypes) { + Service service, Method protoMethod, Map messageTypes) { // Generate input variables for create() VariableExpr requestVarExpr = @@ -598,6 +600,21 @@ private List setOperationSnapshotFactoryExpr( .apply("setError", Arrays.asList(getHttpErrorStatusCodeExpr, getHttpErrorMessageExpr)) .apply(newBuilderExpr); + if (service.pakkage().startsWith("com.google.cloud.compute.v1") + && !service.pakkage().startsWith("com.google.cloud.compute.v1small")) { + TypeNode parserType = + TypeNode.withReference( + VaporReference.builder() + .setName("ComputeLroErrorParser") + .setPakkage(service.pakkage() + ".stub") + .build()); + Expr newParserExpr = NewObjectExpr.builder().setType(parserType).build(); + newBuilderExpr = + methodMaker + .apply("setErrorParser", Collections.singletonList(newParserExpr)) + .apply(newBuilderExpr); + } + buildExpr = MethodInvocationExpr.builder() .setExprReferenceExpr(newBuilderExpr) diff --git a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcOperationSnapshot.java b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcOperationSnapshot.java index 9f271e57f0b8..7fce1d524af4 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcOperationSnapshot.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/GrpcOperationSnapshot.java @@ -30,6 +30,7 @@ package com.google.api.gax.grpc; import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode; import com.google.longrunning.Operation; import io.grpc.Status; @@ -79,6 +80,14 @@ public String getErrorMessage() { return operation.getError().getMessage(); } + /** {@inheritDoc} */ + @Override + public ErrorDetails getErrorDetails() { + return ErrorDetails.builder() + .setRawErrorMessages(operation.getError().getDetailsList()) + .build(); + } + public static GrpcOperationSnapshot create(Operation operation) { return new GrpcOperationSnapshot(operation); } diff --git a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/ProtoOperationTransformers.java b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/ProtoOperationTransformers.java index 425842b17499..dfb4105b4755 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/ProtoOperationTransformers.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/main/java/com/google/api/gax/grpc/ProtoOperationTransformers.java @@ -63,7 +63,8 @@ public ResponseT apply(OperationSnapshot operationSnapshot) { + operationSnapshot.getErrorMessage(), null, operationSnapshot.getErrorCode(), - false); + false, + operationSnapshot.getErrorDetails()); } if (!(operationSnapshot.getResponse() instanceof Any)) { diff --git a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/ProtoOperationTransformersTest.java b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/ProtoOperationTransformersTest.java index 19122ff653c0..543ab04640d5 100644 --- a/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/ProtoOperationTransformersTest.java +++ b/sdk-platform-java/gax-java/gax-grpc/src/test/java/com/google/api/gax/grpc/ProtoOperationTransformersTest.java @@ -34,15 +34,17 @@ import com.google.api.gax.grpc.ProtoOperationTransformers.MetadataTransformer; import com.google.api.gax.grpc.ProtoOperationTransformers.ResponseTransformer; import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.UnavailableException; import com.google.api.gax.rpc.UnknownException; import com.google.common.truth.Truth; import com.google.longrunning.Operation; import com.google.protobuf.Any; +import com.google.rpc.ErrorInfo; import com.google.rpc.Status; -import com.google.type.Color; import com.google.type.Money; import io.grpc.Status.Code; +import java.util.Collections; import org.junit.jupiter.api.Test; class ProtoOperationTransformersTest { @@ -64,11 +66,13 @@ void testAnyResponseTransformer_exception() { OperationSnapshot operationSnapshot = GrpcOperationSnapshot.create( Operation.newBuilder().setResponse(Any.pack(inputMoney)).setError(status).build()); - Exception exception = + UnavailableException exception = assertThrows(UnavailableException.class, () -> transformer.apply(operationSnapshot)); Truth.assertThat(exception) .hasMessageThat() .contains("failed with status = GrpcStatusCode{transportCode=UNAVAILABLE}"); + Truth.assertThat(exception.getErrorDetails()) + .isEqualTo(ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build()); } @Test @@ -78,7 +82,7 @@ void testAnyResponseTransformer_mismatchedTypes() { OperationSnapshot operationSnapshot = GrpcOperationSnapshot.create( Operation.newBuilder() - .setResponse(Any.pack(Color.getDefaultInstance())) + .setResponse(Any.pack(ErrorInfo.getDefaultInstance())) .setError(status) .build()); Exception exception = @@ -103,11 +107,32 @@ void testAnyMetadataTransformer_mismatchedTypes() { OperationSnapshot operationSnapshot = GrpcOperationSnapshot.create( Operation.newBuilder() - .setMetadata(Any.pack(Color.getDefaultInstance())) + .setMetadata(Any.pack(ErrorInfo.getDefaultInstance())) .setError(status) .build()); Exception exception = assertThrows(UnknownException.class, () -> transformer.apply(operationSnapshot)); Truth.assertThat(exception).hasMessageThat().contains("encountered a problem unpacking it"); } + + @Test + void testAnyResponseTransformer_exceptionWithErrorDetails() { + ResponseTransformer transformer = ResponseTransformer.create(Money.class); + Money inputMoney = Money.newBuilder().setCurrencyCode("USD").build(); + ErrorInfo errorInfo = + ErrorInfo.newBuilder().setReason("TEST_REASON").setDomain("googleapis.com").build(); + Status status = + Status.newBuilder() + .setCode(Code.UNAVAILABLE.value()) + .addDetails(Any.pack(errorInfo)) + .build(); + OperationSnapshot operationSnapshot = + GrpcOperationSnapshot.create( + Operation.newBuilder().setResponse(Any.pack(inputMoney)).setError(status).build()); + + UnavailableException exception = + assertThrows(UnavailableException.class, () -> transformer.apply(operationSnapshot)); + Truth.assertThat(exception.getErrorDetails()).isNotNull(); + Truth.assertThat(exception.getErrorDetails().getErrorInfo()).isEqualTo(errorInfo); + } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLroErrorParser.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLroErrorParser.java new file mode 100644 index 000000000000..1d84c8cb902a --- /dev/null +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonLroErrorParser.java @@ -0,0 +1,46 @@ +/* + * Copyright 2026 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.httpjson; + +import com.google.api.core.BetaApi; +import com.google.api.gax.rpc.ErrorDetails; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +@BetaApi("The surface for custom LRO error parsing is not stable yet and may change.") +public interface HttpJsonLroErrorParser { + /** Parses custom LRO response object into standard ErrorDetails. */ + @Nullable ErrorDetails parse(Object response); + + /** Concatenates custom LRO response errors into a single descriptive message. */ + @Nullable String parseErrorMessage(Object response); +} diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshot.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshot.java index d709b680a9b6..b1b9a7afd0b0 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshot.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshot.java @@ -31,10 +31,13 @@ import com.google.api.core.InternalApi; import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode; import com.google.api.gax.rpc.StatusCode.Code; import com.google.longrunning.Operation; +import java.util.Collections; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * Implementation of OperationSnapshot based on REST transport. @@ -50,6 +53,7 @@ public class HttpJsonOperationSnapshot implements OperationSnapshot { private final Object response; private final StatusCode errorCode; private final String errorMessage; + private final ErrorDetails errorDetails; private HttpJsonOperationSnapshot( String name, @@ -57,13 +61,15 @@ private HttpJsonOperationSnapshot( boolean done, Object response, StatusCode errorCode, - String errorMessage) { + String errorMessage, + ErrorDetails errorDetails) { this.name = name; this.metadata = metadata; this.done = done; this.response = response; this.errorCode = errorCode; this.errorMessage = errorMessage; + this.errorDetails = errorDetails; } /** {@inheritDoc} */ @@ -102,6 +108,12 @@ public String getErrorMessage() { return this.errorMessage; } + /** {@inheritDoc} */ + @Override + public ErrorDetails getErrorDetails() { + return this.errorDetails; + } + public static HttpJsonOperationSnapshot create(Operation operation) { return newBuilder().setOperation(operation).build(); } @@ -117,6 +129,31 @@ public static class Builder { private Object response; private StatusCode errorCode; private String errorMessage; + private ErrorDetails errorDetails = + ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build(); + private @Nullable HttpJsonLroErrorParser errorParser; + + /** + * Sets the LRO error details. + * + * @param errorDetails the LRO error details + * @return the builder instance + */ + Builder setErrorDetails(final ErrorDetails errorDetails) { + this.errorDetails = errorDetails; + return this; + } + + /** + * Sets the LRO error parser. + * + * @param errorParser the LRO error parser + * @return the builder instance + */ + public Builder setErrorParser(final HttpJsonLroErrorParser errorParser) { + this.errorParser = errorParser; + return this; + } public Builder setName(String name) { this.name = name; @@ -153,11 +190,26 @@ private Builder setOperation(Operation operation) { this.errorCode = HttpJsonStatusCode.of(com.google.rpc.Code.forNumber(operation.getError().getCode())); this.errorMessage = operation.getError().getMessage(); + this.errorDetails = + ErrorDetails.builder().setRawErrorMessages(operation.getError().getDetailsList()).build(); return this; } public HttpJsonOperationSnapshot build() { - return new HttpJsonOperationSnapshot(name, metadata, done, response, errorCode, errorMessage); + ErrorDetails finalErrorDetails = this.errorDetails; + String finalErrorMessage = this.errorMessage; + if (errorParser != null && response != null) { + ErrorDetails parsedDetails = errorParser.parse(response); + if (parsedDetails != null) { + finalErrorDetails = parsedDetails; + } + String parsedMsg = errorParser.parseErrorMessage(response); + if (parsedMsg != null && !parsedMsg.isEmpty()) { + finalErrorMessage = parsedMsg; + } + } + return new HttpJsonOperationSnapshot( + name, metadata, done, response, errorCode, finalErrorMessage, finalErrorDetails); } } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ProtoOperationTransformers.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ProtoOperationTransformers.java index d5092cbad9ec..cc9200b87763 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ProtoOperationTransformers.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ProtoOperationTransformers.java @@ -63,7 +63,8 @@ public ResponseT apply(OperationSnapshot operationSnapshot) { + operationSnapshot.getErrorMessage(), null, operationSnapshot.getErrorCode(), - false); + false, + operationSnapshot.getErrorDetails()); } if (!(operationSnapshot.getResponse() instanceof Any)) { diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshotTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshotTest.java index 2dda22e61905..99e3f672bdd0 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshotTest.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshotTest.java @@ -33,8 +33,12 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode.Code; +import com.google.protobuf.Any; +import com.google.protobuf.Empty; import java.util.ArrayList; +import java.util.Collections; import org.junit.jupiter.api.Test; class HttpJsonOperationSnapshotTest { @@ -86,4 +90,22 @@ void newBuilderTestNotDone() { assertEquals(HttpJsonStatusCode.of(Code.OK), testOperationSnapshot.getErrorCode()); assertFalse(testOperationSnapshot.isDone()); } + + @Test + void newBuilderTestWithErrorDetails() { + ErrorDetails errorDetails = + ErrorDetails.builder() + .setRawErrorMessages(Collections.singletonList(Any.pack(Empty.getDefaultInstance()))) + .build(); + HttpJsonOperationSnapshot testOperationSnapshot = + HttpJsonOperationSnapshot.newBuilder() + .setName("snapshot-details") + .setMetadata("Dallas") + .setDone(true) + .setError(400, "Bad Request") + .setErrorDetails(errorDetails) + .build(); + + assertEquals(errorDetails, testOperationSnapshot.getErrorDetails()); + } } diff --git a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ProtoOperationTransformersTest.java b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ProtoOperationTransformersTest.java index 8220e5e3d4f9..3179bbbddc6c 100644 --- a/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ProtoOperationTransformersTest.java +++ b/sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/ProtoOperationTransformersTest.java @@ -34,15 +34,17 @@ import com.google.api.gax.httpjson.ProtoOperationTransformers.MetadataTransformer; import com.google.api.gax.httpjson.ProtoOperationTransformers.ResponseTransformer; import com.google.api.gax.longrunning.OperationSnapshot; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.UnavailableException; import com.google.api.gax.rpc.UnknownException; import com.google.common.truth.Truth; import com.google.longrunning.Operation; import com.google.protobuf.Any; import com.google.rpc.Code; +import com.google.rpc.ErrorInfo; import com.google.rpc.Status; -import com.google.type.Color; import com.google.type.Money; +import java.util.Collections; import org.junit.jupiter.api.Test; class ProtoOperationTransformersTest { @@ -96,11 +98,13 @@ void testAnyResponseTransformer_exception() { HttpJsonOperationSnapshot.create( Operation.newBuilder().setResponse(Any.pack(inputMoney)).setError(status).build()); - Exception exception = + UnavailableException exception = assertThrows(UnavailableException.class, () -> transformer.apply(operationSnapshot)); Truth.assertThat(exception) .hasMessageThat() .contains("failed with status = HttpJsonStatusCode{statusCode=UNAVAILABLE}"); + Truth.assertThat(exception.getErrorDetails()) + .isEqualTo(ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build()); } @Test @@ -110,7 +114,7 @@ void testAnyResponseTransformer_mismatchedTypes() { OperationSnapshot operationSnapshot = HttpJsonOperationSnapshot.create( Operation.newBuilder() - .setResponse(Any.pack(Color.getDefaultInstance())) + .setResponse(Any.pack(ErrorInfo.getDefaultInstance())) .setError(status) .build()); Exception exception = @@ -135,11 +139,32 @@ void testAnyMetadataTransformer_mismatchedTypes() { OperationSnapshot operationSnapshot = HttpJsonOperationSnapshot.create( Operation.newBuilder() - .setMetadata(Any.pack(Color.getDefaultInstance())) + .setMetadata(Any.pack(ErrorInfo.getDefaultInstance())) .setError(status) .build()); Exception exception = assertThrows(UnknownException.class, () -> transformer.apply(operationSnapshot)); Truth.assertThat(exception).hasMessageThat().contains("encountered a problem unpacking it"); } + + @Test + void testAnyResponseTransformer_exceptionWithErrorDetails() { + ResponseTransformer transformer = ResponseTransformer.create(Money.class); + Money inputMoney = Money.newBuilder().setCurrencyCode("USD").build(); + ErrorInfo errorInfo = + ErrorInfo.newBuilder().setReason("TEST_REASON").setDomain("googleapis.com").build(); + Status status = + Status.newBuilder() + .setCode(Code.UNAVAILABLE.getNumber()) + .addDetails(Any.pack(errorInfo)) + .build(); + OperationSnapshot operationSnapshot = + HttpJsonOperationSnapshot.create( + Operation.newBuilder().setResponse(Any.pack(inputMoney)).setError(status).build()); + + UnavailableException exception = + assertThrows(UnavailableException.class, () -> transformer.apply(operationSnapshot)); + Truth.assertThat(exception.getErrorDetails()).isNotNull(); + Truth.assertThat(exception.getErrorDetails().getErrorInfo()).isEqualTo(errorInfo); + } } diff --git a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/longrunning/OperationSnapshot.java b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/longrunning/OperationSnapshot.java index 92f7aa04cbf3..b8ff859ea26b 100644 --- a/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/longrunning/OperationSnapshot.java +++ b/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/longrunning/OperationSnapshot.java @@ -29,7 +29,9 @@ */ package com.google.api.gax.longrunning; +import com.google.api.gax.rpc.ErrorDetails; import com.google.api.gax.rpc.StatusCode; +import java.util.Collections; import org.jspecify.annotations.NullMarked; /** @@ -67,4 +69,14 @@ public interface OperationSnapshot { * or if it succeeded, returns null. */ String getErrorMessage(); + + /** + * If the operation is done and it failed, returns the ErrorDetails; if the operation is not done + * or if it succeeded, returns an empty ErrorDetails object. + * + * @return the error details if the operation failed, or an empty ErrorDetails object + */ + default ErrorDetails getErrorDetails() { + return ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build(); + } }