Skip to content

Commit 00052d4

Browse files
Add ErrorInfo to API errors (#153)
## Changes Databricks API can return error details in case of errors. In some cases, users need to access such details to be able to solve the issue. This is the case for the errors of type ErrorInfo used by the Settings Platform. This PR adds the necessary fields to the APIError to Unmarshal the ErrorDetails and to expose the details of type ErrorInfo to the users. ## Tests - [X] Unit test - [X] `make fmt` - [X] Integration test
1 parent 0337b77 commit 00052d4

File tree

6 files changed

+186
-13
lines changed

6 files changed

+186
-13
lines changed

databricks-sdk-java/src/main/java/com/databricks/sdk/core/DatabricksError.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.databricks.sdk.core;
22

3+
import com.databricks.sdk.core.error.ErrorDetail;
34
import java.net.ConnectException;
45
import java.net.SocketException;
56
import java.net.SocketTimeoutException;
67
import java.util.Arrays;
8+
import java.util.Collections;
79
import java.util.List;
10+
import java.util.stream.Collectors;
811
import org.slf4j.Logger;
912
import org.slf4j.LoggerFactory;
1013

@@ -17,6 +20,7 @@
1720
* unrecoverable way and this exception should be thrown, potentially wrapped in another exception.
1821
*/
1922
public class DatabricksError extends DatabricksException {
23+
private static final String ERROR_INFO_TYPE = "type.googleapis.com/google.rpc.ErrorInfo";
2024
private final Logger LOG = LoggerFactory.getLogger(getClass().getName());
2125

2226
/** Errors returned by Databricks services which are known to be retriable. */
@@ -40,28 +44,45 @@ public class DatabricksError extends DatabricksException {
4044
private final String errorCode;
4145
private final int statusCode;
4246

47+
private final List<ErrorDetail> details;
48+
4349
public DatabricksError(int statusCode) {
44-
this("", "OK", statusCode, null);
50+
this("", "OK", statusCode, null, Collections.emptyList());
4551
}
4652

4753
public DatabricksError(String errorCode, String message) {
48-
this(errorCode, message, 400, null);
54+
this(errorCode, message, 400, null, Collections.emptyList());
4955
}
5056

5157
public DatabricksError(String errorCode, String message, int statusCode) {
52-
this(errorCode, message, statusCode, null);
58+
this(errorCode, message, statusCode, null, Collections.emptyList());
5359
}
5460

5561
public DatabricksError(String errorCode, int statusCode, Throwable cause) {
56-
this(errorCode, cause.getMessage(), statusCode, cause);
62+
this(errorCode, cause.getMessage(), statusCode, cause, Collections.emptyList());
63+
}
64+
65+
public DatabricksError(
66+
String errorCode, String message, int statusCode, List<ErrorDetail> details) {
67+
this(errorCode, message, statusCode, null, details);
5768
}
5869

59-
private DatabricksError(String errorCode, String message, int statusCode, Throwable cause) {
70+
private DatabricksError(
71+
String errorCode,
72+
String message,
73+
int statusCode,
74+
Throwable cause,
75+
List<ErrorDetail> details) {
6076
super(message, cause);
6177
this.errorCode = errorCode;
6278
this.message = message;
6379
this.cause = cause;
6480
this.statusCode = statusCode;
81+
this.details = details == null ? Collections.emptyList() : details;
82+
}
83+
84+
public List<ErrorDetail> getErrorInfo() {
85+
return this.getDetailsByType(ERROR_INFO_TYPE);
6586
}
6687

6788
public String getErrorCode() {
@@ -99,6 +120,10 @@ public boolean isRetriable() {
99120
return false;
100121
}
101122

123+
List<ErrorDetail> getDetailsByType(String type) {
124+
return this.details.stream().filter(e -> e.getType().equals(type)).collect(Collectors.toList());
125+
}
126+
102127
private static boolean isCausedBy(Throwable throwable, Class<? extends Throwable> clazz) {
103128
if (throwable == null) {
104129
return false;

databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrorBody.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
44
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.util.List;
56

67
/**
78
* The union of all JSON error responses from the Databricks APIs, not including HTML responses.
@@ -20,6 +21,7 @@ public class ApiErrorBody {
2021
private String scimStatus;
2122
private String scimType;
2223
private String api12Error;
24+
private List<ErrorDetail> errorDetails;
2325

2426
public ApiErrorBody() {}
2527

@@ -29,13 +31,23 @@ public ApiErrorBody(
2931
@JsonProperty("detail") String scimDetail,
3032
@JsonProperty("status") String scimStatus,
3133
@JsonProperty("scimType") String scimType,
32-
@JsonProperty("error") String api12Error) {
34+
@JsonProperty("error") String api12Error,
35+
@JsonProperty("details") List<ErrorDetail> errorDetails) {
3336
this.errorCode = errorCode;
3437
this.message = message;
3538
this.scimDetail = scimDetail;
3639
this.scimStatus = scimStatus;
3740
this.scimType = scimType;
3841
this.api12Error = api12Error;
42+
this.errorDetails = errorDetails;
43+
}
44+
45+
public List<ErrorDetail> getErrorDetails() {
46+
return errorDetails;
47+
}
48+
49+
public void setErrorDetails(List<ErrorDetail> errorDetails) {
50+
this.errorDetails = errorDetails;
3951
}
4052

4153
public String getErrorCode() {

databricks-sdk-java/src/main/java/com/databricks/sdk/core/error/ApiErrors.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import java.io.*;
88
import java.nio.charset.StandardCharsets;
9+
import java.util.Collections;
910
import java.util.regex.Matcher;
1011
import java.util.regex.Pattern;
1112
import org.apache.commons.io.IOUtils;
@@ -47,8 +48,14 @@ private static DatabricksError readErrorFromResponse(Response response) {
4748
errorBody.setMessage(message.trim());
4849
errorBody.setErrorCode("SCIM_" + errorBody.getScimStatus());
4950
}
51+
if (errorBody.getErrorDetails() == null) {
52+
errorBody.setErrorDetails(Collections.emptyList());
53+
}
5054
return new DatabricksError(
51-
errorBody.getErrorCode(), errorBody.getMessage(), response.getStatusCode());
55+
errorBody.getErrorCode(),
56+
errorBody.getMessage(),
57+
response.getStatusCode(),
58+
errorBody.getErrorDetails());
5259
}
5360

5461
/**
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.databricks.sdk.core.error;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import java.util.Collections;
6+
import java.util.Map;
7+
8+
@JsonIgnoreProperties(ignoreUnknown = true)
9+
public class ErrorDetail {
10+
11+
private String type;
12+
13+
private String reason;
14+
15+
private String domain;
16+
17+
private Map<String, String> metadata;
18+
19+
public ErrorDetail() {}
20+
21+
public ErrorDetail(
22+
@JsonProperty("@type") String type,
23+
@JsonProperty("reason") String reason,
24+
@JsonProperty("domain") String domain,
25+
@JsonProperty("metadata") Map<String, String> metadata) {
26+
this.type = type;
27+
this.reason = reason;
28+
this.domain = domain;
29+
this.metadata = Collections.unmodifiableMap(metadata);
30+
}
31+
32+
public String getType() {
33+
return type;
34+
}
35+
36+
public String getReason() {
37+
return reason;
38+
}
39+
40+
public Map<String, String> getMetadata() {
41+
return metadata;
42+
}
43+
44+
public String getDomain() {
45+
return domain;
46+
}
47+
48+
@Override
49+
public String toString() {
50+
return "ErrorDetails{"
51+
+ "type='"
52+
+ type
53+
+ '\''
54+
+ ", reason='"
55+
+ reason
56+
+ '\''
57+
+ ", domain='"
58+
+ domain
59+
+ '\''
60+
+ ", metadata="
61+
+ metadata
62+
+ '}';
63+
}
64+
}

databricks-sdk-java/src/test/java/com/databricks/sdk/core/ApiClientTest.java

Lines changed: 51 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static org.junit.jupiter.api.Assertions.assertThrows;
55

66
import com.databricks.sdk.core.error.ApiErrorBody;
7+
import com.databricks.sdk.core.error.ErrorDetail;
78
import com.databricks.sdk.core.http.Request;
89
import com.databricks.sdk.core.http.Response;
910
import com.databricks.sdk.core.utils.FakeTimer;
@@ -67,14 +68,19 @@ private <T> void runApiClientTest(
6768

6869
private void runFailingApiClientTest(
6970
Request request, List<ResponseProvider> responses, Class<?> clazz, String expectedMessage) {
70-
ApiClient client = getApiClient(request, responses);
7171
DatabricksException exception =
72-
assertThrows(
73-
DatabricksException.class,
74-
() -> client.GET(request.getUri().getPath(), clazz, Collections.emptyMap()));
72+
runFailingApiClientTest(request, responses, clazz, DatabricksException.class);
7573
assertEquals(exception.getMessage(), expectedMessage);
7674
}
7775

76+
private <T extends Throwable> T runFailingApiClientTest(
77+
Request request, List<ResponseProvider> responses, Class<?> clazz, Class<T> exceptionClass) {
78+
ApiClient client = getApiClient(request, responses);
79+
return assertThrows(
80+
exceptionClass,
81+
() -> client.GET(request.getUri().getPath(), clazz, Collections.emptyMap()));
82+
}
83+
7884
private Request getBasicRequest() {
7985
return new Request("GET", "http://my.host/api/my/endpoint");
8086
}
@@ -173,12 +179,51 @@ void retryDatabricksApi12RetriableError() throws JsonProcessingException {
173179
null,
174180
null,
175181
null,
176-
"Workspace 123 does not have any associated worker environments")),
182+
"Workspace 123 does not have any associated worker environments",
183+
null)),
177184
getSuccessResponse(req)),
178185
MyEndpointResponse.class,
179186
new MyEndpointResponse().setKey("value"));
180187
}
181188

189+
@Test
190+
void errorDetails() throws JsonProcessingException {
191+
Request req = getBasicRequest();
192+
193+
Map<String, String> metadata = new HashMap<>();
194+
metadata.put("etag", "value");
195+
ErrorDetail errorDetails =
196+
new ErrorDetail("type.googleapis.com/google.rpc.ErrorInfo", "reason", "domain", metadata);
197+
ErrorDetail unrelatedDetails =
198+
new ErrorDetail("unrelated", "wrong", "wrongDomain", new HashMap<>());
199+
200+
DatabricksError error =
201+
runFailingApiClientTest(
202+
req,
203+
Arrays.asList(
204+
getTransientError(
205+
req,
206+
401,
207+
new ApiErrorBody(
208+
"ERROR",
209+
null,
210+
null,
211+
null,
212+
null,
213+
null,
214+
Arrays.asList(errorDetails, unrelatedDetails))),
215+
getSuccessResponse(req)),
216+
MyEndpointResponse.class,
217+
DatabricksError.class);
218+
List<ErrorDetail> responseErrors = error.getErrorInfo();
219+
assertEquals(responseErrors.size(), 1);
220+
ErrorDetail responseError = responseErrors.get(0);
221+
assertEquals(errorDetails.getType(), responseError.getType());
222+
assertEquals(errorDetails.getReason(), responseError.getReason());
223+
assertEquals(errorDetails.getMetadata(), responseError.getMetadata());
224+
assertEquals(errorDetails.getDomain(), responseError.getDomain());
225+
}
226+
182227
@Test
183228
void retryDatabricksRetriableError() throws JsonProcessingException {
184229
Request req = getBasicRequest();
@@ -196,6 +241,7 @@ void retryDatabricksRetriableError() throws JsonProcessingException {
196241
null,
197242
null,
198243
null,
244+
null,
199245
null)),
200246
getSuccessResponse(req)),
201247
MyEndpointResponse.class,

databricks-sdk-java/src/test/java/com/databricks/sdk/core/error/ApiErrorBodyDeserializationSuite.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import com.fasterxml.jackson.core.JsonProcessingException;
66
import com.fasterxml.jackson.databind.ObjectMapper;
7+
import java.util.*;
78
import org.junit.jupiter.api.Test;
89

910
public class ApiErrorBodyDeserializationSuite {
@@ -21,13 +22,31 @@ void deserializeErrorResponse() throws JsonProcessingException {
2122
assertEquals(error.getApi12Error(), "theerror");
2223
}
2324

25+
@Test
26+
void deserializeDetailedResponse() throws JsonProcessingException {
27+
ObjectMapper mapper = new ObjectMapper();
28+
String rawResponse =
29+
"{\"error_code\":\"theerrorcode\",\"message\":\"themessage\","
30+
+ "\"details\":["
31+
+ "{\"@type\": \"type.googleapis.com/google.rpc.ErrorInfo\", \"reason\":\"detailreason\", \"domain\":\"detaildomain\",\"metadata\":{\"etag\":\"detailsetag\"}}"
32+
+ "]}";
33+
ApiErrorBody error = mapper.readValue(rawResponse, ApiErrorBody.class);
34+
Map<String, String> metadata = new HashMap<>();
35+
metadata.put("etag", "detailsetag");
36+
ErrorDetail errorDetails = error.getErrorDetails().get(0);
37+
assertEquals(errorDetails.getType(), "type.googleapis.com/google.rpc.ErrorInfo");
38+
assertEquals(errorDetails.getReason(), "detailreason");
39+
assertEquals(errorDetails.getDomain(), "detaildomain");
40+
assertEquals(errorDetails.getMetadata(), metadata);
41+
}
42+
2443
// Test that an ApiErrorBody can be deserialized, even if the response includes unexpected
2544
// parameters.
2645
@Test
2746
void handleUnexpectedFieldsInErrorResponse() throws JsonProcessingException {
2847
ObjectMapper mapper = new ObjectMapper();
2948
String rawResponse =
30-
"{\"error_code\":\"theerrorcode\",\"message\":\"themessage\",\"details\":[\"unexpected\"]}";
49+
"{\"error_code\":\"theerrorcode\",\"message\":\"themessage\",\"unexpectedField\":[\"unexpected\"]}";
3150
ApiErrorBody error = mapper.readValue(rawResponse, ApiErrorBody.class);
3251
assertEquals(error.getErrorCode(), "theerrorcode");
3352
assertEquals(error.getMessage(), "themessage");

0 commit comments

Comments
 (0)