1919import static com .google .common .truth .Truth .assertThat ;
2020import static org .junit .jupiter .api .Assertions .assertThrows ;
2121
22+ import com .google .api .client .http .HttpResponseException ;
2223import com .google .api .gax .rpc .ApiException ;
2324import com .google .api .gax .rpc .ErrorDetails ;
2425import 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 ;
2530import com .google .rpc .BadRequest ;
2631import com .google .rpc .DebugInfo ;
2732import com .google .rpc .ErrorInfo ;
3237import com .google .rpc .RequestInfo ;
3338import com .google .rpc .ResourceInfo ;
3439import com .google .rpc .RetryInfo ;
40+ import com .google .rpc .Status ;
3541import com .google .showcase .v1beta1 .EchoClient ;
3642import com .google .showcase .v1beta1 .EchoResponse ;
3743import com .google .showcase .v1beta1 .FailEchoWithDetailsRequest ;
4248import org .junit .jupiter .api .BeforeAll ;
4349import org .junit .jupiter .api .Test ;
4450
51+ /**
52+ * Integration tests for verifying that client libraries correctly propagate and deserialize
53+ * standard and custom error details from {@link ApiException} over gRPC and HTTP/JSON transports.
54+ */
4555class ITErrorDetails {
4656
4757 private static EchoClient grpcClient ;
@@ -63,12 +73,11 @@ static void destroyClients() throws InterruptedException {
6373 TestClientInitializer .AWAIT_TERMINATION_SECONDS , TimeUnit .SECONDS );
6474 }
6575
66- private void verifyErrorDetailsGrpc (ApiException exception ) {
67- assertThat (exception .getStatusCode ().getCode ()).isEqualTo (StatusCode .Code .ABORTED );
68-
69- ErrorDetails errorDetails = exception .getErrorDetails ();
70- assertThat (errorDetails ).isNotNull ();
71-
76+ /**
77+ * Helper method to verify the content of error details. Transport-neutral validation of standard
78+ * and custom error packets.
79+ */
80+ private void verifyErrorDetailsContent (ErrorDetails errorDetails , String expectedPoem ) {
7281 // Verify standard error details are present and populated
7382 // We are assuming that the mock server's hardcoded return values will stay the same
7483 // https://github.com/googleapis/gapic-showcase/blob/b6c247f153369044d599969f6929ecdeb066c4c6/server/services/echo_service.go
@@ -118,44 +127,50 @@ private void verifyErrorDetailsGrpc(ApiException exception) {
118127 assertThat (localizedMessage .getMessage ())
119128 .isEqualTo ("This LocalizedMessage should be treated specially" );
120129
121- // Verify custom PoetryError can be unpacked
130+ // Verify custom PoetryError can be unpacked and matches expected poem
122131 PoetryError poetryError = errorDetails .getMessage (PoetryError .class );
123132 assertThat (poetryError ).isNotNull ();
124- assertThat (poetryError .getPoem ()).isEqualTo ("roses are red" );
133+ assertThat (poetryError .getPoem ()).isEqualTo (expectedPoem );
125134
126135 // Verify mismatched type returns null safely (mismatch unpacking)
127136 EchoResponse mismatchedDetail = errorDetails .getMessage (EchoResponse .class );
128137 assertThat (mismatchedDetail ).isNull ();
129138 }
130139
140+ // Verifies error details are correctly propagated and unpacked over standard gRPC protocol
131141 @ Test
132142 void testGrpc_failEchoWithDetails () {
133143 FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest .newBuilder ().build ();
134144 ApiException exception =
135145 assertThrows (ApiException .class , () -> grpcClient .failEchoWithDetails (request ));
136- verifyErrorDetailsGrpc (exception );
146+
147+ assertThat (exception .getStatusCode ().getCode ()).isEqualTo (StatusCode .Code .ABORTED );
148+ assertThat (exception .getErrorDetails ()).isNotNull ();
149+
150+ // Reuse Transport-neutral Validation
151+ verifyErrorDetailsContent (exception .getErrorDetails (), "roses are red" );
137152 }
138153
154+ // Verifies custom Error Details messages reflect user-defined inputs via gRPC
139155 @ Test
140156 void testGrpc_failEchoWithDetails_customMessage () {
141157 String customMessage = "this is a custom message to echo back" ;
142158 FailEchoWithDetailsRequest request =
143159 FailEchoWithDetailsRequest .newBuilder ().setMessage (customMessage ).build ();
144160 ApiException exception =
145161 assertThrows (ApiException .class , () -> grpcClient .failEchoWithDetails (request ));
146- assertThat (exception .getStatusCode ().getCode ()).isEqualTo (StatusCode .Code .ABORTED );
147162
148- ErrorDetails errorDetails = exception .getErrorDetails ( );
149- assertThat (errorDetails ).isNotNull ();
163+ assertThat ( exception .getStatusCode (). getCode ()). isEqualTo ( StatusCode . Code . ABORTED );
164+ assertThat (exception . getErrorDetails () ).isNotNull ();
150165
151- // Verify custom PoetryError can be unpacked and contains the custom message
152- PoetryError poetryError = errorDetails .getMessage (PoetryError .class );
153- assertThat (poetryError ).isNotNull ();
154- assertThat (poetryError .getPoem ()).isEqualTo (customMessage );
166+ // Reuse Transport-neutral Validation
167+ verifyErrorDetailsContent (exception .getErrorDetails (), customMessage );
155168 }
156169
170+ // Verifies error details are accessible in raw form over REST/HTTP and validates manually-parsed
171+ // decompression
157172 @ Test
158- void testHttpJson_failEchoWithDetails () {
173+ void testHttpJson_failEchoWithDetails () throws Exception {
159174 FailEchoWithDetailsRequest request = FailEchoWithDetailsRequest .newBuilder ().build ();
160175 ApiException exception =
161176 assertThrows (ApiException .class , () -> httpjsonClient .failEchoWithDetails (request ));
@@ -168,5 +183,43 @@ void testHttpJson_failEchoWithDetails() {
168183 if (errorDetails != null ) {
169184 assertThat (errorDetails .getErrorInfo ()).isNull ();
170185 }
186+
187+ // Workaround REST limitation: Parse the raw HTTP JSON error response manually using a custom
188+ // TypeRegistry that registers standard types plus the showcase-specific PoetryError type.
189+ assertThat (exception .getCause ()).isInstanceOf (HttpResponseException .class );
190+ HttpResponseException httpException = (HttpResponseException ) exception .getCause ();
191+ String errorJson = httpException .getContent ();
192+ assertThat (errorJson ).isNotNull ();
193+
194+ TypeRegistry typeRegistry =
195+ TypeRegistry .newBuilder ()
196+ .add (ErrorInfo .getDescriptor ())
197+ .add (RetryInfo .getDescriptor ())
198+ .add (DebugInfo .getDescriptor ())
199+ .add (QuotaFailure .getDescriptor ())
200+ .add (PreconditionFailure .getDescriptor ())
201+ .add (BadRequest .getDescriptor ())
202+ .add (RequestInfo .getDescriptor ())
203+ .add (ResourceInfo .getDescriptor ())
204+ .add (Help .getDescriptor ())
205+ .add (LocalizedMessage .getDescriptor ())
206+ .add (PoetryError .getDescriptor ())
207+ .build ();
208+ JsonFormat .Parser jsonParser =
209+ JsonFormat .parser ().ignoringUnknownFields ().usingTypeRegistry (typeRegistry );
210+
211+ // Parse the AIP-193 "error" JSON object into a status builder
212+ JsonObject root = JsonParser .parseString (errorJson ).getAsJsonObject ();
213+ JsonObject errorObj = root .getAsJsonObject ("error" );
214+ Status .Builder statusBuilder = Status .newBuilder ();
215+ jsonParser .merge (errorObj .toString (), statusBuilder );
216+ Status status = statusBuilder .build ();
217+
218+ // Verify we can successfully unpack the details from our custom status instance
219+ ErrorDetails parsedDetails =
220+ ErrorDetails .builder ().setRawErrorMessages (status .getDetailsList ()).build ();
221+
222+ // Reuse Transport-neutral Validation!
223+ verifyErrorDetailsContent (parsedDetails , "roses are red" );
171224 }
172225}
0 commit comments