Skip to content

Commit 81003d2

Browse files
authored
test(showcase): Add gRPC and HttpJson Showcase ITs to verify Error Details (#13928)
### Description This PR introduces E2E integration test coverage for standard and custom error details using the `FailEchoWithDetails` RPC in Showcase, validating the exception propagation and deserialization pipeline across both gRPC and HTTP/JSON transports. ### Key Changes * **gRPC Transport Verification**: * Verifies unpacking and correctness of all standard error details (e.g. `ErrorInfo`, `RetryInfo`, `DebugInfo`, `QuotaFailure`, `PreconditionFailure`, `BadRequest`, `RequestInfo`, `ResourceInfo`, `Help`, `LocalizedMessage`). * Verifies that Showcase-specific custom trailers (e.g. `PoetryError`) are successfully unpacked and mapped into the client-side `ErrorDetails`. * Verifies dynamic message propagation inside custom trailers when parameters are passed to the request. * **HTTP/JSON Transport Verification**: * Verifies that error status code mapping (e.g. `ABORTED`) propagates correctly. * Handles GAX's HTTP/JSON parser boundaries when dealing with unregistered custom types (like `PoetryError`) in JSON status payloads.
1 parent 716a52e commit 81003d2

1 file changed

Lines changed: 261 additions & 0 deletions

File tree

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
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.showcase.v1beta1.it;
18+
19+
import static com.google.common.truth.Truth.assertThat;
20+
import static org.junit.jupiter.api.Assertions.assertThrows;
21+
22+
import com.google.api.client.http.HttpResponseException;
23+
import com.google.api.gax.rpc.ApiException;
24+
import com.google.api.gax.rpc.ErrorDetails;
25+
import com.google.api.gax.rpc.StatusCode;
26+
import com.google.gson.JsonObject;
27+
import com.google.gson.JsonParser;
28+
import com.google.protobuf.TypeRegistry;
29+
import com.google.protobuf.util.JsonFormat;
30+
import com.google.rpc.BadRequest;
31+
import com.google.rpc.DebugInfo;
32+
import com.google.rpc.ErrorInfo;
33+
import com.google.rpc.Help;
34+
import com.google.rpc.LocalizedMessage;
35+
import com.google.rpc.PreconditionFailure;
36+
import com.google.rpc.QuotaFailure;
37+
import com.google.rpc.RequestInfo;
38+
import com.google.rpc.ResourceInfo;
39+
import com.google.rpc.RetryInfo;
40+
import com.google.rpc.Status;
41+
import com.google.showcase.v1beta1.EchoClient;
42+
import com.google.showcase.v1beta1.FailEchoWithDetailsRequest;
43+
import com.google.showcase.v1beta1.PoetryError;
44+
import com.google.showcase.v1beta1.it.util.TestClientInitializer;
45+
import java.util.concurrent.TimeUnit;
46+
import org.junit.jupiter.api.AfterAll;
47+
import org.junit.jupiter.api.BeforeAll;
48+
import org.junit.jupiter.api.Test;
49+
50+
/**
51+
* Integration tests for verifying that client libraries correctly propagate and deserialize
52+
* standard and custom error details from {@link ApiException} over gRPC and HTTP/JSON transports.
53+
*/
54+
class ITErrorDetails {
55+
56+
private static final TypeRegistry SHOWCASE_ERROR_TYPE_REGISTRY =
57+
TypeRegistry.newBuilder()
58+
.add(ErrorInfo.getDescriptor())
59+
.add(RetryInfo.getDescriptor())
60+
.add(DebugInfo.getDescriptor())
61+
.add(QuotaFailure.getDescriptor())
62+
.add(PreconditionFailure.getDescriptor())
63+
.add(BadRequest.getDescriptor())
64+
.add(RequestInfo.getDescriptor())
65+
.add(ResourceInfo.getDescriptor())
66+
.add(Help.getDescriptor())
67+
.add(LocalizedMessage.getDescriptor())
68+
.add(PoetryError.getDescriptor())
69+
.build();
70+
71+
private static EchoClient grpcClient;
72+
private static EchoClient httpjsonClient;
73+
74+
@BeforeAll
75+
static void createClients() throws Exception {
76+
grpcClient = TestClientInitializer.createGrpcEchoClient();
77+
httpjsonClient = TestClientInitializer.createHttpJsonEchoClient();
78+
}
79+
80+
@AfterAll
81+
static void destroyClients() throws InterruptedException {
82+
grpcClient.close();
83+
httpjsonClient.close();
84+
85+
grpcClient.awaitTermination(TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
86+
httpjsonClient.awaitTermination(
87+
TestClientInitializer.AWAIT_TERMINATION_SECONDS, TimeUnit.SECONDS);
88+
}
89+
90+
/**
91+
* Helper method to verify the content of error details. Transport-neutral validation of standard
92+
* and custom error packets.
93+
*/
94+
private void verifyErrorDetailsContent(ErrorDetails errorDetails, String expectedPoem) {
95+
// Verify standard error details are present and populated
96+
// We are assuming that the mock server's hardcoded return values will stay the same
97+
// https://github.com/googleapis/gapic-showcase/blob/b6c247f153369044d599969f6929ecdeb066c4c6/server/services/echo_service.go
98+
ErrorInfo errorInfo = errorDetails.getErrorInfo();
99+
assertThat(errorInfo).isNotNull();
100+
assertThat(errorInfo.getReason()).isEqualTo("some ErrorInfo reason");
101+
102+
RetryInfo retryInfo = errorDetails.getRetryInfo();
103+
assertThat(retryInfo).isNotNull();
104+
assertThat(retryInfo.getRetryDelay().getSeconds()).isEqualTo(11);
105+
106+
DebugInfo debugInfo = errorDetails.getDebugInfo();
107+
assertThat(debugInfo).isNotNull();
108+
assertThat(debugInfo.getDetail()).isEqualTo("a DebugInfo detail");
109+
110+
QuotaFailure quotaFailure = errorDetails.getQuotaFailure();
111+
assertThat(quotaFailure).isNotNull();
112+
assertThat(quotaFailure.getViolations(0).getDescription())
113+
.isEqualTo("First QuotaFailure description");
114+
115+
PreconditionFailure preconditionFailure = errorDetails.getPreconditionFailure();
116+
assertThat(preconditionFailure).isNotNull();
117+
assertThat(preconditionFailure.getViolations(0).getDescription())
118+
.isEqualTo("First PreconditionFailure description");
119+
120+
BadRequest badRequest = errorDetails.getBadRequest();
121+
assertThat(badRequest).isNotNull();
122+
assertThat(badRequest.getFieldViolations(0).getDescription())
123+
.isEqualTo("First BadRequest description");
124+
125+
RequestInfo requestInfo = errorDetails.getRequestInfo();
126+
assertThat(requestInfo).isNotNull();
127+
assertThat(requestInfo.getRequestId()).isEqualTo("RequestInfo: showcase-request-id");
128+
assertThat(requestInfo.getServingData()).isEqualTo("RequestInfo: showcase serving data");
129+
130+
ResourceInfo resourceInfo = errorDetails.getResourceInfo();
131+
assertThat(resourceInfo).isNotNull();
132+
assertThat(resourceInfo.getResourceType()).isEqualTo("ResourceInfo: showcase resource");
133+
134+
Help help = errorDetails.getHelp();
135+
assertThat(help).isNotNull();
136+
assertThat(help.getLinks(0).getDescription()).isEqualTo("Help: first showcase help link");
137+
138+
LocalizedMessage localizedMessage = errorDetails.getLocalizedMessage();
139+
assertThat(localizedMessage).isNotNull();
140+
assertThat(localizedMessage.getLocale()).isEqualTo("fr-CH");
141+
assertThat(localizedMessage.getMessage())
142+
.isEqualTo("This LocalizedMessage should be treated specially");
143+
144+
// Verify custom PoetryError can be unpacked and matches expected poem
145+
PoetryError poetryError = errorDetails.getMessage(PoetryError.class);
146+
assertThat(poetryError).isNotNull();
147+
assertThat(poetryError.getPoem()).isEqualTo(expectedPoem);
148+
}
149+
150+
// Verifies error details are correctly propagated and unpacked over standard gRPC protocol
151+
@Test
152+
void testGrpc_failEchoWithDetails() {
153+
FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build();
154+
ApiException exception =
155+
assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request));
156+
157+
assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED);
158+
assertThat(exception.getErrorDetails()).isNotNull();
159+
160+
// Reuse Transport-neutral Validation
161+
verifyErrorDetailsContent(exception.getErrorDetails(), "roses are red");
162+
}
163+
164+
// Verifies custom Error Details messages reflect user-defined inputs via gRPC
165+
@Test
166+
void testGrpc_failEchoWithDetails_customMessage() {
167+
String customMessage = "this is a custom message to echo back";
168+
FailEchoWithDetailsRequest request =
169+
FailEchoWithDetailsRequest.newBuilder().setMessage(customMessage).build();
170+
ApiException exception =
171+
assertThrows(ApiException.class, () -> grpcClient.failEchoWithDetails(request));
172+
173+
assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED);
174+
assertThat(exception.getErrorDetails()).isNotNull();
175+
176+
PoetryError poetryError = exception.getErrorDetails().getMessage(PoetryError.class);
177+
assertThat(poetryError).isNotNull();
178+
assertThat(poetryError.getPoem()).isEqualTo(customMessage);
179+
}
180+
181+
// Verifies error details are accessible in raw form over REST/HTTP and validates manually-parsed
182+
// decompression
183+
@Test
184+
void testHttpJson_failEchoWithDetails() throws Exception {
185+
FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest.newBuilder().build();
186+
ApiException exception =
187+
assertThrows(ApiException.class, () -> httpjsonClient.failEchoWithDetails(request));
188+
assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED);
189+
190+
// GAX HTTP/JSON parser limitation: Because the response contains a custom/unregistered type
191+
// (PoetryError) in the Any details list, HttpJsonErrorParser fails to parse the status payload,
192+
// resulting in empty ErrorDetails (where getErrorInfo() returns null).
193+
// Note: Standard Google Cloud services follow AIP-193 error details (ErrorInfo, RetryInfo,
194+
// etc.)
195+
// and do not use custom error payload types like PoetryError.
196+
ErrorDetails errorDetails = exception.getErrorDetails();
197+
if (errorDetails != null) {
198+
assertThat(errorDetails.getErrorInfo()).isNull();
199+
}
200+
201+
// Workaround REST limitation: Parse the raw HTTP JSON error response manually using a custom
202+
// TypeRegistry that registers standard types plus the showcase-specific PoetryError type.
203+
// This mimics the behavior of HttpJsonErrorParser, which currently does not support passing in
204+
// a custom TypeRegistry.
205+
assertThat(exception.getCause()).isInstanceOf(HttpResponseException.class);
206+
HttpResponseException httpException = (HttpResponseException) exception.getCause();
207+
String errorJson = httpException.getContent();
208+
assertThat(errorJson).isNotNull();
209+
210+
JsonFormat.Parser jsonParser =
211+
JsonFormat.parser().ignoringUnknownFields().usingTypeRegistry(SHOWCASE_ERROR_TYPE_REGISTRY);
212+
213+
// Parse the raw HTTP error JSON payload formatted per AIP-193 HTTP/1.1 representation
214+
// (rest_error.proto),
215+
// where fields are wrapped under the top-level "error" JSON key.
216+
JsonObject root = JsonParser.parseString(errorJson).getAsJsonObject();
217+
JsonObject errorObj = root.getAsJsonObject("error");
218+
Status.Builder statusBuilder = Status.newBuilder();
219+
jsonParser.merge(errorObj.toString(), statusBuilder);
220+
Status status = statusBuilder.build();
221+
222+
// Verify we can successfully unpack the details from our custom status instance
223+
ErrorDetails parsedDetails =
224+
ErrorDetails.builder().setRawErrorMessages(status.getDetailsList()).build();
225+
226+
// Reuse Transport-neutral Validation!
227+
verifyErrorDetailsContent(parsedDetails, "roses are red");
228+
}
229+
230+
// Verifies custom Error Details messages reflect user-defined inputs via HTTP/JSON
231+
@Test
232+
void testHttpJson_failEchoWithDetails_customMessage() throws Exception {
233+
String customMessage = "this is a custom message to echo back";
234+
FailEchoWithDetailsRequest request =
235+
FailEchoWithDetailsRequest.newBuilder().setMessage(customMessage).build();
236+
ApiException exception =
237+
assertThrows(ApiException.class, () -> httpjsonClient.failEchoWithDetails(request));
238+
239+
assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED);
240+
assertThat(exception.getCause()).isInstanceOf(HttpResponseException.class);
241+
242+
HttpResponseException httpException = (HttpResponseException) exception.getCause();
243+
String errorJson = httpException.getContent();
244+
assertThat(errorJson).isNotNull();
245+
246+
JsonFormat.Parser jsonParser =
247+
JsonFormat.parser().ignoringUnknownFields().usingTypeRegistry(SHOWCASE_ERROR_TYPE_REGISTRY);
248+
249+
JsonObject root = JsonParser.parseString(errorJson).getAsJsonObject();
250+
JsonObject errorObj = root.getAsJsonObject("error");
251+
Status.Builder statusBuilder = Status.newBuilder();
252+
jsonParser.merge(errorObj.toString(), statusBuilder);
253+
Status status = statusBuilder.build();
254+
255+
ErrorDetails parsedDetails =
256+
ErrorDetails.builder().setRawErrorMessages(status.getDetailsList()).build();
257+
PoetryError poetryError = parsedDetails.getMessage(PoetryError.class);
258+
assertThat(poetryError).isNotNull();
259+
assertThat(poetryError.getPoem()).isEqualTo(customMessage);
260+
}
261+
}

0 commit comments

Comments
 (0)