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/librarian.yaml b/librarian.yaml index a1128e1d24c9..719505e9bbac 100644 --- a/librarian.yaml +++ b/librarian.yaml @@ -1231,6 +1231,7 @@ 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/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/common/RetrySettingsComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/RetrySettingsComposer.java index 98784fc8818f..a9e680439f9a 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/RetrySettingsComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/RetrySettingsComposer.java @@ -30,10 +30,12 @@ import com.google.api.generator.engine.ast.Expr; import com.google.api.generator.engine.ast.ExprStatement; import com.google.api.generator.engine.ast.MethodInvocationExpr; +import com.google.api.generator.engine.ast.NewObjectExpr; import com.google.api.generator.engine.ast.PrimitiveValue; import com.google.api.generator.engine.ast.StringObjectValue; import com.google.api.generator.engine.ast.TypeNode; import com.google.api.generator.engine.ast.ValueExpr; +import com.google.api.generator.engine.ast.VaporReference; import com.google.api.generator.engine.ast.Variable; import com.google.api.generator.engine.ast.VariableExpr; import com.google.api.generator.gapic.composer.store.TypeStore; @@ -323,6 +325,26 @@ public static Expr createLroSettingsBuilderExpr( Variable.builder().setType(TypeNode.CLASS_OBJECT).setName("class").build()) .setStaticReferenceType(t) .build(); + + List createArgs = new ArrayList<>(); + createArgs.add(classFieldRefFn.apply(method.lro().responseType())); + if (service.pakkage().startsWith("com.google.cloud.compute.v1") + && !service.pakkage().startsWith("com.google.cloud.compute.v1small") + && operationResponseTransformer + .reference() + .pakkage() + .equals("com.google.api.gax.httpjson")) { + createArgs.add( + NewObjectExpr.builder() + .setType( + TypeNode.withReference( + VaporReference.builder() + .setName("ComputeLroErrorParser") + .setPakkage(service.pakkage() + ".stub") + .build())) + .build()); + } + builderSettingsExpr = MethodInvocationExpr.builder() .setExprReferenceExpr(builderSettingsExpr) @@ -331,7 +353,7 @@ public static Expr createLroSettingsBuilderExpr( MethodInvocationExpr.builder() .setStaticReferenceType(operationResponseTransformer) .setMethodName("create") - .setArguments(classFieldRefFn.apply(method.lro().responseType())) + .setArguments(createArgs) .build()) .build(); builderSettingsExpr = 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/ProtoOperationTransformers.java b/sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/ProtoOperationTransformers.java index cc9200b87763..747e642b01a3 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 @@ -32,11 +32,13 @@ import com.google.api.core.ApiFunction; import com.google.api.gax.longrunning.OperationSnapshot; 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.protobuf.Any; import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.Message; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** Public for technical reasons; intended for use by generated code. */ @NullMarked @@ -46,25 +48,46 @@ private ProtoOperationTransformers() {} public static class ResponseTransformer implements ApiFunction { private final AnyTransformer transformer; + @Nullable private final HttpJsonLroErrorParser errorParser; private ResponseTransformer(Class packedClass) { this.transformer = new AnyTransformer<>(packedClass); + this.errorParser = null; + } + + private ResponseTransformer( + Class packedClass, @Nullable HttpJsonLroErrorParser errorParser) { + this.transformer = new AnyTransformer<>(packedClass); + this.errorParser = errorParser; } @Override public ResponseT apply(OperationSnapshot operationSnapshot) { if (!operationSnapshot.getErrorCode().getCode().equals(Code.OK)) { + @Nullable ErrorDetails details = null; + String errorMessage = operationSnapshot.getErrorMessage(); + + if (errorParser != null && operationSnapshot.getResponse() != null) { + details = errorParser.parse(operationSnapshot.getResponse()); + String parsedMsg = errorParser.parseErrorMessage(operationSnapshot.getResponse()); + if (parsedMsg != null && !parsedMsg.isEmpty()) { + errorMessage = parsedMsg; + } + } else { + details = operationSnapshot.getErrorDetails(); + } + throw ApiExceptionFactory.createException( "Operation with name \"" + operationSnapshot.getName() + "\" failed with status = " + operationSnapshot.getErrorCode() + " and message = " - + operationSnapshot.getErrorMessage(), + + errorMessage, null, operationSnapshot.getErrorCode(), false, - operationSnapshot.getErrorDetails()); + details); } if (!(operationSnapshot.getResponse() instanceof Any)) { @@ -88,6 +111,11 @@ public static ResponseTransformer create( Class packedClass) { return new ResponseTransformer<>(packedClass); } + + public static ResponseTransformer create( + Class packedClass, @Nullable HttpJsonLroErrorParser errorParser) { + return new ResponseTransformer<>(packedClass, errorParser); + } } public static class MetadataTransformer