55import com .databricks .sdk .core .error .details .ErrorDetails ;
66import com .databricks .sdk .core .http .Response ;
77import com .fasterxml .jackson .databind .ObjectMapper ;
8- import java .io .*;
8+ import com .google .api .client .util .Strings ;
9+ import java .io .IOException ;
10+ import java .io .InputStream ;
911import java .nio .charset .StandardCharsets ;
12+ import java .util .Optional ;
1013import java .util .regex .Matcher ;
1114import java .util .regex .Pattern ;
1215import org .apache .commons .io .IOUtils ;
@@ -16,73 +19,103 @@ public class ApiErrors {
1619 private static final ObjectMapper MAPPER = new ObjectMapper ();
1720 private static final Pattern HTML_ERROR_REGEX = Pattern .compile ("<pre>(.*)</pre>" );
1821 private static final ErrorMapper ERROR_MAPPER = new ErrorMapper ();
22+ private static final String SCIM_NULL_DETAILS = "null" ;
1923
20- public static DatabricksError getDatabricksError (Response out , Exception error ) {
21- if (error != null ) {
22- // If the endpoint did not respond to the request, interpret the exception.
23- return new DatabricksError ("IO_ERROR" , 523 , error );
24- } else if (out .getStatusCode () == 429 ) {
25- return new DatabricksError ("TOO_MANY_REQUESTS" , "Current request has to be retried" , 429 );
24+ // Empty error details used to normalize errors with no error details.
25+ private static final ErrorDetails DEFAULT_ERROR_DETAILS = ErrorDetails .builder ().build ();
26+
27+ public static DatabricksError getDatabricksError (Response response ) {
28+ // Private link error handling depends purely on the response URL without
29+ // looking at the error body.
30+ if (PrivateLinkInfo .isPrivateLinkRedirect (response )) {
31+ return ERROR_MAPPER .apply (response , new ApiErrorBody ());
32+ }
33+
34+ Optional <ApiErrorBody > optionalErrorBody = parseApiError (response );
35+ if (!optionalErrorBody .isPresent ()) {
36+ // Purely infer the error based on its status code.
37+ return ERROR_MAPPER .apply (response , new ApiErrorBody ());
2638 }
2739
28- ApiErrorBody errorBody = readErrorFromResponse (out );
29- return ERROR_MAPPER .apply (out , errorBody );
40+ ApiErrorBody errorBody = optionalErrorBody .get ();
41+
42+ // TODO: normalization should ideally happen at API call level,
43+ // allowing each method to control its own normalization.
44+ normalizeError (errorBody );
45+
46+ return ERROR_MAPPER .apply (response , errorBody );
3047 }
3148
32- private static ApiErrorBody readErrorFromResponse (Response response ) {
33- // Private link error handling depends purely on the response URL.
34- if (PrivateLinkInfo .isPrivateLinkRedirect (response )) {
35- return new ApiErrorBody ();
49+ private static void normalizeError (ApiErrorBody errorBody ) {
50+ // Guarantee that all errors have an ErrorDetails container even
51+ // if it does not contains any specific error details.
52+ if (errorBody .getErrorDetails () == null ) {
53+ errorBody .setErrorDetails (DEFAULT_ERROR_DETAILS );
3654 }
37- ApiErrorBody errorBody = parseApiError (response );
3855
39- // Condense API v1.2 and SCIM error string and code into the message and errorCode fields of
40- // DatabricksApiException.
41- if (errorBody .getApi12Error () != null && !errorBody .getApi12Error ().isEmpty ()) {
56+ // Error messages from older APIs used a different field to pass the
57+ // error message. We keep this code for backward compatibility until
58+ // these APIs are fully deprecated.
59+ if (!Strings .isNullOrEmpty (errorBody .getApi12Error ())) {
4260 errorBody .setMessage (errorBody .getApi12Error ());
61+ return ;
62+ }
63+
64+ if (!Strings .isNullOrEmpty (errorBody .getMessage ())) {
65+ return ;
66+ }
67+
68+ // SCIM error handling.
69+ //
70+ // TODO: This code is brittle and should likely be refactored to a more
71+ // robust solution to detect SCIM errors. This will likely involve
72+ // parsing the SCIM error at the API call level rather than normalizing.
73+ if (Strings .isNullOrEmpty (errorBody .getScimDetail ())) {
74+ return ;
75+ }
76+ if (SCIM_NULL_DETAILS .equals (errorBody .getScimDetail ())) {
77+ errorBody .setMessage ("SCIM API Internal Error" );
78+ } else {
79+ errorBody .setMessage (errorBody .getScimDetail ());
4380 }
44- if (errorBody .getMessage () == null || errorBody .getMessage ().isEmpty ()) {
45- if (errorBody .getScimDetail () != null && !"null" .equals (errorBody .getScimDetail ())) {
46- errorBody .setMessage (errorBody .getScimDetail ());
47- } else {
48- errorBody .setMessage ("SCIM API Internal Error" );
49- }
81+ if (!Strings .isNullOrEmpty (errorBody .getScimType ())) {
5082 String message = errorBody .getScimType () + " " + errorBody .getMessage ();
5183 errorBody .setMessage (message .trim ());
52- errorBody .setErrorCode ("SCIM_" + errorBody .getScimStatus ());
5384 }
54- if (errorBody .getErrorDetails () == null ) {
55- errorBody .setErrorDetails ( ErrorDetails . builder (). build ());
85+ if (! Strings . isNullOrEmpty ( errorBody .getScimStatus ()) ) {
86+ errorBody .setErrorCode ( "SCIM_" + errorBody . getScimStatus ());
5687 }
57- return errorBody ;
5888 }
5989
6090 /**
6191 * The response is either a JSON response or a webpage. In the JSON case, it is parsed normally;
6292 * in the webpage case, the relevant error metadata is extracted from the response using a
6393 * heuristic-based approach.
6494 */
65- private static ApiErrorBody parseApiError (Response response ) {
95+ private static Optional <ApiErrorBody > parseApiError (Response response ) {
96+ InputStream in = response .getBody ();
97+ if (in == null ) {
98+ return Optional .empty ();
99+ }
100+
101+ // Read the body now, so we can try to parse as JSON and then
102+ // fallback to old error handling logic.
103+ String body ;
66104 try {
67- InputStream in = response .getBody ();
68- if (in == null ) {
69- ApiErrorBody errorBody = new ApiErrorBody ();
70- errorBody .setMessage (
71- String .format ("Status response from server: %s" , response .getStatus ()));
72- return errorBody ;
73- }
74-
75- // Read the body now, so we can try to parse as JSON and then fallback to old error handling
76- // logic.
77- String body = IOUtils .toString (in , StandardCharsets .UTF_8 );
78- try {
79- return MAPPER .readValue (body , ApiErrorBody .class );
80- } catch (IOException e ) {
81- return parseUnknownError (response , body , e );
82- }
105+ body = IOUtils .toString (in , StandardCharsets .UTF_8 );
83106 } catch (IOException e ) {
84107 throw new DatabricksException ("Unable to read response body" , e );
85108 }
109+
110+ if (Strings .isNullOrEmpty (body )) {
111+ return Optional .empty ();
112+ }
113+
114+ try {
115+ return Optional .of (MAPPER .readValue (body , ApiErrorBody .class ));
116+ } catch (IOException e ) {
117+ return Optional .of (parseUnknownError (response , body , e ));
118+ }
86119 }
87120
88121 private static ApiErrorBody parseUnknownError (Response response , String body , IOException err ) {
0 commit comments