Skip to content

Commit dc192fc

Browse files
committed
fix(gax-httpjson): gracefully handle error details with unrecognized types
1 parent 0c481bd commit dc192fc

4 files changed

Lines changed: 103 additions & 10 deletions

File tree

‎java-showcase/gapic-showcase/src/test/java/com/google/showcase/v1beta1/it/ITErrorDetails.java‎

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -187,16 +187,12 @@ void testHttpJson_failEchoWithDetails() throws Exception {
187187
assertThrows(ApiException.class, () -> httpjsonClient.failEchoWithDetails(request));
188188
assertThat(exception.getStatusCode().getCode()).isEqualTo(StatusCode.Code.ABORTED);
189189

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.
190+
// The response contains a custom/unregistered type (PoetryError) in the Any details list.
191+
// HttpJsonErrorParser filters out unrecognized details so standard ones (e.g. ErrorInfo) are
192+
// still parsed.
196193
ErrorDetails errorDetails = exception.getErrorDetails();
197-
if (errorDetails != null) {
198-
assertThat(errorDetails.getErrorInfo()).isNull();
199-
}
194+
assertThat(errorDetails).isNotNull();
195+
assertThat(errorDetails.getErrorInfo()).isNotNull();
200196

201197
// Workaround REST limitation: Parse the raw HTTP JSON error response manually using a custom
202198
// TypeRegistry that registers standard types plus the showcase-specific PoetryError type.

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
import com.google.api.core.InternalApi;
3434
import com.google.api.gax.rpc.ErrorDetails;
35+
import com.google.common.collect.Iterables;
3536
import com.google.gson.JsonElement;
3637
import com.google.gson.JsonObject;
3738
import com.google.gson.JsonParser;
@@ -112,13 +113,34 @@ static Status parseStatus(String errorJson) {
112113
return Status.getDefaultInstance();
113114
}
114115

116+
JsonObject errorObject = errorElement.getAsJsonObject();
117+
JsonElement detailsElement = errorObject.get("details");
118+
if (detailsElement != null && detailsElement.isJsonArray()) {
119+
Iterables.removeIf(detailsElement.getAsJsonArray(), detail -> !isRecognizedDetail(detail));
120+
}
121+
115122
Status.Builder statusBuilder = Status.newBuilder();
116123
try {
117-
JSON_PARSER.merge(errorElement.toString(), statusBuilder);
124+
JSON_PARSER.merge(errorObject.toString(), statusBuilder);
118125
} catch (InvalidProtocolBufferException e) {
119126
return Status.getDefaultInstance();
120127
}
121128

122129
return statusBuilder.build();
123130
}
131+
132+
private static boolean isRecognizedDetail(JsonElement detail) {
133+
if (!detail.isJsonObject()) {
134+
return false;
135+
}
136+
JsonElement typeElement = detail.getAsJsonObject().get("@type");
137+
if (typeElement == null || !typeElement.isJsonPrimitive()) {
138+
return false;
139+
}
140+
try {
141+
return STANDARD_ERROR_TYPES.getDescriptorForTypeUrl(typeElement.getAsString()) != null;
142+
} catch (InvalidProtocolBufferException e) {
143+
return false;
144+
}
145+
}
124146
}

‎sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonApiExceptionFactoryTest.java‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,4 +148,43 @@ void testCreate_withoutErrorDetails() {
148148
assertThat(apiException.getErrorDetails()).isNotNull();
149149
assertThat(apiException.getErrorDetails().getErrorInfo()).isNull();
150150
}
151+
152+
@Test
153+
void testCreate_unrecognizedProtoInDetails_preservesMessageAndOtherDetails() {
154+
String payload =
155+
"{\n"
156+
+ " \"error\": {\n"
157+
+ " \"code\": 400,\n"
158+
+ " \"message\": \"Request contains an invalid argument.\",\n"
159+
+ " \"details\": [\n"
160+
+ " {\n"
161+
+ " \"@type\": \"type.googleapis.com/custom.UnrecognizedDetail\",\n"
162+
+ " \"detailMessage\": \"'123-invalid' is not a valid customer ID.\"\n"
163+
+ " },\n"
164+
+ " {\n"
165+
+ " \"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\",\n"
166+
+ " \"reason\": \"INVALID_CUSTOMER_ID\",\n"
167+
+ " \"domain\": \"googleapis.com\"\n"
168+
+ " }\n"
169+
+ " ]\n"
170+
+ " }\n"
171+
+ "}";
172+
173+
// HttpResponseException(HttpResponse) populates message with the status line + content.
174+
HttpResponseException exception =
175+
new HttpResponseException.Builder(400, "Bad Request", new HttpHeaders())
176+
.setContent(payload)
177+
.setMessage("400 Bad Request\n" + payload)
178+
.build();
179+
180+
HttpJsonApiExceptionFactory factory =
181+
new HttpJsonApiExceptionFactory(ImmutableSet.of(Code.UNAVAILABLE));
182+
ApiException apiException = factory.create(exception);
183+
184+
assertThat(apiException.getStatusCode().getCode()).isEqualTo(Code.INVALID_ARGUMENT);
185+
assertThat(apiException.getMessage()).contains("Request contains an invalid argument.");
186+
assertThat(apiException.getReason()).isEqualTo("INVALID_CUSTOMER_ID");
187+
assertThat(((HttpResponseException) apiException.getCause()).getContent())
188+
.contains("'123-invalid' is not a valid customer ID.");
189+
}
151190
}

‎sdk-platform-java/gax-java/gax-httpjson/src/test/java/com/google/api/gax/httpjson/HttpJsonErrorParserTest.java‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,40 @@ void parseStatus_arrayInError() {
135135
assertThat(HttpJsonErrorParser.parseStatus(payload))
136136
.isEqualTo(com.google.rpc.Status.getDefaultInstance());
137137
}
138+
139+
@Test
140+
void parseStatus_unrecognizedOrMalformedDetails_ignoresAndPreservesOthers() {
141+
String payload =
142+
"{\n"
143+
+ " \"error\": {\n"
144+
+ " \"code\": 400,\n"
145+
+ " \"message\": \"Invalid request payload.\",\n"
146+
+ " \"details\": [\n"
147+
+ " \"not-an-object\",\n"
148+
+ " {\n"
149+
+ " \"customField\": \"missingAtType\"\n"
150+
+ " },\n"
151+
+ " {\n"
152+
+ " \"@type\": \"malformed-type-url-without-slash\"\n"
153+
+ " },\n"
154+
+ " {\n"
155+
+ " \"@type\": \"type.googleapis.com/custom.UnrecognizedErrorDetail\",\n"
156+
+ " \"customField\": \"customValue\"\n"
157+
+ " },\n"
158+
+ " {\n"
159+
+ " \"@type\": \"type.googleapis.com/google.rpc.RetryInfo\"\n"
160+
+ " }\n"
161+
+ " ]\n"
162+
+ " }\n"
163+
+ "}";
164+
165+
com.google.rpc.Status status = HttpJsonErrorParser.parseStatus(payload);
166+
assertThat(status.getCode()).isEqualTo(400);
167+
assertThat(status.getMessage()).isEqualTo("Invalid request payload.");
168+
assertThat(status.getDetailsCount()).isEqualTo(1);
169+
170+
ErrorDetails errorDetails =
171+
ErrorDetails.builder().setRawErrorMessages(status.getDetailsList()).build();
172+
assertThat(errorDetails.getRetryInfo()).isNotNull();
173+
}
138174
}

0 commit comments

Comments
 (0)