Skip to content

Commit 8ad5010

Browse files
authored
fix: update StorageException translation of an ApiException to include error details (#2872)
Update StorageException logic for coalescing ApiExceptions to add a formatted string including fields from error details as a suppressed exception on the api exception. This keeps the diagnostic information at the "gapic layer" in the printed stacktrace similar to the json document being on the "apiary layer" of the cause.
1 parent c49fd08 commit 8ad5010

File tree

2 files changed

+132
-3
lines changed

2 files changed

+132
-3
lines changed

google-cloud-storage/src/main/java/com/google/cloud/storage/StorageException.java

+39-3
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,19 @@
2222
import com.google.api.gax.grpc.GrpcStatusCode;
2323
import com.google.api.gax.rpc.ApiException;
2424
import com.google.api.gax.rpc.ApiExceptions;
25+
import com.google.api.gax.rpc.ErrorDetails;
2526
import com.google.api.gax.rpc.StatusCode;
2627
import com.google.cloud.BaseServiceException;
2728
import com.google.cloud.RetryHelper.RetryHelperException;
2829
import com.google.cloud.http.BaseHttpServiceException;
2930
import com.google.common.collect.ImmutableSet;
31+
import com.google.protobuf.TextFormat;
3032
import io.grpc.StatusException;
3133
import io.grpc.StatusRuntimeException;
3234
import java.io.IOException;
35+
import java.util.Objects;
3336
import java.util.Set;
37+
import java.util.stream.Stream;
3438
import org.checkerframework.checker.nullness.qual.Nullable;
3539

3640
/**
@@ -127,9 +131,6 @@ static BaseServiceException coalesce(Throwable t) {
127131
static StorageException asStorageException(ApiException apiEx) {
128132
// https://cloud.google.com/storage/docs/json_api/v1/status-codes
129133
// https://cloud.google.com/apis/design/errors#http_mapping
130-
// https://cloud.google.com/apis/design/errors#error_payloads
131-
// TODO: flush this out more to wire through "error" and "details" when we're able to get real
132-
// errors from GCS
133134
int httpStatusCode = 0;
134135
StatusCode statusCode = apiEx.getStatusCode();
135136
if (statusCode instanceof GrpcStatusCode) {
@@ -155,12 +156,41 @@ static StorageException asStorageException(ApiException apiEx) {
155156
message = "Error: " + statusCodeName;
156157
}
157158

159+
// https://cloud.google.com/apis/design/errors#error_payloads
160+
attachErrorDetails(apiEx);
161+
158162
// It'd be better to use ExceptionData and BaseServiceException#<init>(ExceptionData) but,
159163
// BaseHttpServiceException does not pass that through so we're stuck using this for now.
160164
// TODO: When we can break the coupling to BaseHttpServiceException replace this
161165
return new StorageException(httpStatusCode, message, apiEx.getReason(), apiEx);
162166
}
163167

168+
private static void attachErrorDetails(ApiException ae) {
169+
if (ae != null && ae.getErrorDetails() != null) {
170+
final StringBuilder sb = new StringBuilder();
171+
ErrorDetails ed = ae.getErrorDetails();
172+
sb.append("ErrorDetails {\n");
173+
Stream.of(
174+
ed.getErrorInfo(),
175+
ed.getDebugInfo(),
176+
ed.getQuotaFailure(),
177+
ed.getPreconditionFailure(),
178+
ed.getBadRequest(),
179+
ed.getHelp())
180+
.filter(Objects::nonNull)
181+
.forEach(
182+
msg ->
183+
sb.append("\t\t")
184+
.append(msg.getClass().getSimpleName())
185+
.append(": { ")
186+
.append(TextFormat.printer().shortDebugString(msg))
187+
.append(" }\n"));
188+
sb.append("\t}");
189+
190+
ae.addSuppressed(new ApiExceptionErrorDetailsComment(sb.toString()));
191+
}
192+
}
193+
164194
/**
165195
* Translate IOException to a StorageException representing the cause of the error. This method
166196
* defaults to idempotent always being {@code true}. Additionally, this method translates
@@ -222,4 +252,10 @@ interface IOExceptionCallable<T> {
222252
interface IOExceptionRunnable {
223253
void run() throws IOException;
224254
}
255+
256+
private static final class ApiExceptionErrorDetailsComment extends Throwable {
257+
private ApiExceptionErrorDetailsComment(String message) {
258+
super(message, null, true, false);
259+
}
260+
}
225261
}

google-cloud-storage/src/test/java/com/google/cloud/storage/StorageExceptionGrpcCompatibilityTest.java

+93
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package com.google.cloud.storage;
1818

19+
import static com.google.cloud.storage.TestUtils.assertAll;
1920
import static com.google.common.truth.Truth.assertThat;
2021

2122
import com.google.api.gax.grpc.GrpcStatusCode;
@@ -25,11 +26,21 @@
2526
import com.google.cloud.BaseServiceException;
2627
import com.google.common.collect.ImmutableList;
2728
import com.google.protobuf.Any;
29+
import com.google.protobuf.TextFormat;
30+
import com.google.protobuf.TextFormat.Printer;
31+
import com.google.rpc.BadRequest;
32+
import com.google.rpc.BadRequest.FieldViolation;
2833
import com.google.rpc.DebugInfo;
2934
import com.google.rpc.ErrorInfo;
35+
import com.google.rpc.Help;
36+
import com.google.rpc.Help.Link;
37+
import com.google.rpc.LocalizedMessage;
38+
import com.google.rpc.PreconditionFailure;
39+
import com.google.rpc.QuotaFailure;
3040
import io.grpc.Status;
3141
import io.grpc.Status.Code;
3242
import io.grpc.StatusRuntimeException;
43+
import java.util.List;
3344
import org.junit.Test;
3445

3546
public final class StorageExceptionGrpcCompatibilityTest {
@@ -114,6 +125,88 @@ public void testCoalesce_UNAUTHENTICATED() {
114125
doTestCoalesce(401, Code.UNAUTHENTICATED);
115126
}
116127

128+
@Test
129+
public void apiExceptionErrorDetails() throws Exception {
130+
ErrorInfo errorInfo =
131+
ErrorInfo.newBuilder()
132+
.setReason("STACKOUT")
133+
.setDomain("spanner.googlepais.com")
134+
.putMetadata("availableRegions", "us-central1,us-east2")
135+
.build();
136+
DebugInfo debugInfo =
137+
DebugInfo.newBuilder()
138+
.addStackEntries("HEAD")
139+
.addStackEntries("HEAD~1")
140+
.addStackEntries("HEAD~2")
141+
.addStackEntries("HEAD~3")
142+
.setDetail("some detail")
143+
.build();
144+
QuotaFailure quotaFailure =
145+
QuotaFailure.newBuilder()
146+
.addViolations(
147+
QuotaFailure.Violation.newBuilder()
148+
.setSubject("clientip:127.0.3.3")
149+
.setDescription("Daily limit")
150+
.build())
151+
.build();
152+
PreconditionFailure preconditionFailure =
153+
PreconditionFailure.newBuilder()
154+
.addViolations(
155+
PreconditionFailure.Violation.newBuilder()
156+
.setType("TOS")
157+
.setSubject("google.com/cloud")
158+
.setDescription("Terms of service not accepted")
159+
.build())
160+
.build();
161+
BadRequest badRequest =
162+
BadRequest.newBuilder()
163+
.addFieldViolations(
164+
FieldViolation.newBuilder()
165+
.setField("email_addresses[3].type[2]")
166+
.setDescription("duplicate value 'WORK'")
167+
.setReason("INVALID_EMAIL_ADDRESS_TYPE")
168+
.setLocalizedMessage(
169+
LocalizedMessage.newBuilder()
170+
.setLocale("en-US")
171+
.setMessage("Invalid email type: duplicate value")
172+
.build())
173+
.build())
174+
.build();
175+
Help help =
176+
Help.newBuilder()
177+
.addLinks(
178+
Link.newBuilder().setDescription("link1").setUrl("https://google.com").build())
179+
.build();
180+
List<Any> errors =
181+
ImmutableList.of(
182+
Any.pack(errorInfo),
183+
Any.pack(debugInfo),
184+
Any.pack(quotaFailure),
185+
Any.pack(preconditionFailure),
186+
Any.pack(badRequest),
187+
Any.pack(help));
188+
ErrorDetails errorDetails = ErrorDetails.builder().setRawErrorMessages(errors).build();
189+
ApiException ae =
190+
ApiExceptionFactory.createException(
191+
Code.OUT_OF_RANGE.toStatus().asRuntimeException(),
192+
GrpcStatusCode.of(Code.OUT_OF_RANGE),
193+
false,
194+
errorDetails);
195+
196+
BaseServiceException se = StorageException.coalesce(ae);
197+
String message = se.getCause().getSuppressed()[0].getMessage();
198+
Printer printer = TextFormat.printer();
199+
assertAll(
200+
() -> assertThat(message).contains("ErrorDetails {"),
201+
() -> assertThat(message).contains(printer.shortDebugString(errorInfo)),
202+
() -> assertThat(message).contains(printer.shortDebugString(debugInfo)),
203+
() -> assertThat(message).contains(printer.shortDebugString(quotaFailure)),
204+
() -> assertThat(message).contains(printer.shortDebugString(preconditionFailure)),
205+
() -> assertThat(message).contains(printer.shortDebugString(badRequest)),
206+
() -> assertThat(message).contains(printer.shortDebugString(help)),
207+
() -> assertThat(message).contains("\t}"));
208+
}
209+
117210
private void doTestCoalesce(int expectedCode, Code code) {
118211
Status status = code.toStatus();
119212
GrpcStatusCode statusCode = GrpcStatusCode.of(code);

0 commit comments

Comments
 (0)