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/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-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 07debc4f117d..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 @@ -37,6 +37,7 @@ 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. @@ -130,6 +131,7 @@ public static class Builder { private String errorMessage; private ErrorDetails errorDetails = ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build(); + private @Nullable HttpJsonLroErrorParser errorParser; /** * Sets the LRO error details. @@ -142,6 +144,17 @@ Builder setErrorDetails(final 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; return this; @@ -183,8 +196,20 @@ private Builder setOperation(Operation operation) { } public HttpJsonOperationSnapshot build() { + 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, errorMessage, errorDetails); + name, metadata, done, response, errorCode, finalErrorMessage, finalErrorDetails); } } }