Skip to content

Commit b38fbcc

Browse files
committed
feat: implement snapshot-integrated custom LRO error parsing
1 parent 5bd14bf commit b38fbcc

6 files changed

Lines changed: 164 additions & 3 deletions

File tree

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.google.cloud.compute.v1.stub;
18+
19+
import com.google.api.core.BetaApi;
20+
import com.google.api.gax.httpjson.HttpJsonLroErrorParser;
21+
import com.google.api.gax.rpc.ErrorDetails;
22+
import com.google.cloud.compute.v1.Errors;
23+
import com.google.cloud.compute.v1.Operation;
24+
import com.google.protobuf.Any;
25+
import com.google.rpc.ErrorInfo;
26+
import java.util.ArrayList;
27+
import java.util.List;
28+
29+
@BetaApi("The surface for custom LRO error parsing is not stable yet and may change.")
30+
class ComputeLroErrorParser implements HttpJsonLroErrorParser {
31+
32+
@Override
33+
public ErrorDetails parse(Object response) {
34+
if (!(response instanceof Operation)) {
35+
return null;
36+
}
37+
Operation operation = ((Operation) response);
38+
if (!operation.hasError()) {
39+
return null;
40+
}
41+
List<Any> rawErrorMessages = new ArrayList<>();
42+
for (Errors error : operation.getError().getErrorsList()) {
43+
ErrorInfo errorInfo =
44+
ErrorInfo.newBuilder()
45+
.setReason(error.getCode())
46+
.setDomain("googleapis.com")
47+
.putMetadata("message", error.getMessage())
48+
.putMetadata("location", error.getLocation())
49+
.build();
50+
rawErrorMessages.add(Any.pack(errorInfo));
51+
}
52+
return ErrorDetails.builder().setRawErrorMessages(rawErrorMessages).build();
53+
}
54+
55+
@Override
56+
public String parseErrorMessage(Object response) {
57+
if (!(response instanceof Operation)) {
58+
return null;
59+
}
60+
Operation operation = ((Operation) response);
61+
if (!operation.hasError() || operation.getError().getErrorsCount() == 0) {
62+
return null;
63+
}
64+
StringBuilder sb = new StringBuilder();
65+
for (Errors error : operation.getError().getErrorsList()) {
66+
if (sb.length() > 0) {
67+
sb.append("; ");
68+
}
69+
sb.append(error.getCode()).append(": ").append(error.getMessage());
70+
}
71+
return sb.toString();
72+
}
73+
}

sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ protected Statement createMethodDescriptorVariableDecl(
170170
methodMaker
171171
.apply(
172172
"setOperationSnapshotFactory",
173-
setOperationSnapshotFactoryExpr(protoMethod, messageTypes))
173+
setOperationSnapshotFactoryExpr(service, protoMethod, messageTypes))
174174
.apply(expr);
175175
}
176176

@@ -453,7 +453,7 @@ private MethodInvocationExpr getExpr(VariableExpr var, String num) {
453453
}
454454

455455
private List<Expr> setOperationSnapshotFactoryExpr(
456-
Method protoMethod, Map<String, Message> messageTypes) {
456+
Service service, Method protoMethod, Map<String, Message> messageTypes) {
457457

458458
// Generate input variables for create()
459459
VariableExpr requestVarExpr =
@@ -598,6 +598,20 @@ private List<Expr> setOperationSnapshotFactoryExpr(
598598
.apply("setError", Arrays.asList(getHttpErrorStatusCodeExpr, getHttpErrorMessageExpr))
599599
.apply(newBuilderExpr);
600600

601+
if (service.pakkage().startsWith("com.google.cloud.compute.v1")) {
602+
TypeNode parserType =
603+
TypeNode.withReference(
604+
VaporReference.builder()
605+
.setName("ComputeLroErrorParser")
606+
.setPakkage(service.pakkage() + ".stub")
607+
.build());
608+
Expr newParserExpr = NewObjectExpr.builder().setType(parserType).build();
609+
newBuilderExpr =
610+
methodMaker
611+
.apply("setErrorParser", Collections.singletonList(newParserExpr))
612+
.apply(newBuilderExpr);
613+
}
614+
601615
buildExpr =
602616
MethodInvocationExpr.builder()
603617
.setExprReferenceExpr(newBuilderExpr)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/*
2+
* Copyright 2026 Google LLC
3+
*
4+
* Redistribution and use in source and binary forms, with or without
5+
* modification, are permitted provided that the following conditions are
6+
* met:
7+
*
8+
* * Redistributions of source code must retain the above copyright
9+
* notice, this list of conditions and the following disclaimer.
10+
* * Redistributions in binary form must reproduce the above
11+
* copyright notice, this list of conditions and the following disclaimer
12+
* in the documentation and/or other materials provided with the
13+
* distribution.
14+
* * Neither the name of Google LLC nor the names of its
15+
* contributors may be used to endorse or promote products derived from
16+
* this software without specific prior written permission.
17+
*
18+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19+
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20+
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21+
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22+
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23+
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24+
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25+
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26+
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27+
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29+
*/
30+
31+
package com.google.api.gax.httpjson;
32+
33+
import com.google.api.core.BetaApi;
34+
import com.google.api.gax.rpc.ErrorDetails;
35+
import org.jspecify.annotations.NullMarked;
36+
import org.jspecify.annotations.Nullable;
37+
38+
@NullMarked
39+
@BetaApi("The surface for custom LRO error parsing is not stable yet and may change.")
40+
public interface HttpJsonLroErrorParser {
41+
/** Parses custom LRO response object into standard ErrorDetails. */
42+
@Nullable ErrorDetails parse(Object response);
43+
44+
/** Concatenates custom LRO response errors into a single descriptive message. */
45+
@Nullable String parseErrorMessage(Object response);
46+
}

sdk-platform-java/gax-java/gax-httpjson/src/main/java/com/google/api/gax/httpjson/HttpJsonOperationSnapshot.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import com.google.longrunning.Operation;
3838
import java.util.Collections;
3939
import org.jspecify.annotations.NullMarked;
40+
import org.jspecify.annotations.Nullable;
4041

4142
/**
4243
* Implementation of OperationSnapshot based on REST transport.
@@ -130,6 +131,7 @@ public static class Builder {
130131
private String errorMessage;
131132
private ErrorDetails errorDetails =
132133
ErrorDetails.builder().setRawErrorMessages(Collections.emptyList()).build();
134+
private @Nullable HttpJsonLroErrorParser errorParser;
133135

134136
/**
135137
* Sets the LRO error details.
@@ -142,6 +144,17 @@ Builder setErrorDetails(final ErrorDetails errorDetails) {
142144
return this;
143145
}
144146

147+
/**
148+
* Sets the LRO error parser.
149+
*
150+
* @param errorParser the LRO error parser
151+
* @return the builder instance
152+
*/
153+
public Builder setErrorParser(final HttpJsonLroErrorParser errorParser) {
154+
this.errorParser = errorParser;
155+
return this;
156+
}
157+
145158
public Builder setName(String name) {
146159
this.name = name;
147160
return this;
@@ -183,8 +196,20 @@ private Builder setOperation(Operation operation) {
183196
}
184197

185198
public HttpJsonOperationSnapshot build() {
199+
ErrorDetails finalErrorDetails = this.errorDetails;
200+
String finalErrorMessage = this.errorMessage;
201+
if (errorParser != null && response != null) {
202+
ErrorDetails parsedDetails = errorParser.parse(response);
203+
if (parsedDetails != null) {
204+
finalErrorDetails = parsedDetails;
205+
}
206+
String parsedMsg = errorParser.parseErrorMessage(response);
207+
if (parsedMsg != null && !parsedMsg.isEmpty()) {
208+
finalErrorMessage = parsedMsg;
209+
}
210+
}
186211
return new HttpJsonOperationSnapshot(
187-
name, metadata, done, response, errorCode, errorMessage, errorDetails);
212+
name, metadata, done, response, errorCode, finalErrorMessage, finalErrorDetails);
188213
}
189214
}
190215
}

sdk-platform-java/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonAddressesStub.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ public class HttpJsonAddressesStub extends AddressesStub {
161161
.setDone(Status.DONE.equals(response.getStatus()))
162162
.setResponse(response)
163163
.setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage())
164+
.setErrorParser(new ComputeLroErrorParser())
164165
.build();
165166
})
166167
.build();
@@ -213,6 +214,7 @@ public class HttpJsonAddressesStub extends AddressesStub {
213214
.setDone(Status.DONE.equals(response.getStatus()))
214215
.setResponse(response)
215216
.setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage())
217+
.setErrorParser(new ComputeLroErrorParser())
216218
.build();
217219
})
218220
.build();

sdk-platform-java/test/integration/goldens/compute/src/com/google/cloud/compute/v1small/stub/HttpJsonRegionOperationsStub.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ public class HttpJsonRegionOperationsStub extends RegionOperationsStub {
101101
.setDone(Status.DONE.equals(response.getStatus()))
102102
.setResponse(response)
103103
.setError(response.getHttpErrorStatusCode(), response.getHttpErrorMessage())
104+
.setErrorParser(new ComputeLroErrorParser())
104105
.build();
105106
})
106107
.setPollingRequestFactory(

0 commit comments

Comments
 (0)