22
33import static dev .openfga .sdk .errors .HttpStatusCode .*;
44
5+ import com .fasterxml .jackson .databind .JsonNode ;
6+ import com .fasterxml .jackson .databind .ObjectMapper ;
57import dev .openfga .sdk .api .configuration .Configuration ;
68import dev .openfga .sdk .api .configuration .CredentialsMethod ;
79import dev .openfga .sdk .constants .FgaConstants ;
1113import java .util .Optional ;
1214
1315public class FgaError extends ApiException {
16+ private static final ObjectMapper ERROR_MAPPER = new ObjectMapper ();
17+
1418 private String method = null ;
1519 private String requestUrl = null ;
1620 private String clientId = null ;
1721 private String audience = null ;
1822 private String grantType = null ;
1923 private String requestId = null ;
2024 private String apiErrorCode = null ;
25+ private String apiErrorMessage = null ;
26+ private String operationName = null ;
2127 private String retryAfterHeader = null ;
2228
2329 public FgaError (String message , Throwable cause , int code , HttpHeaders responseHeaders , String responseBody ) {
@@ -28,6 +34,61 @@ public FgaError(String message, int code, HttpHeaders responseHeaders, String re
2834 super (message , code , responseHeaders , responseBody );
2935 }
3036
37+ /**
38+ * Parse the API error response body to extract the error message and code.
39+ * @param methodName The API method name that was called
40+ * @param responseBody The response body JSON string
41+ * @return A descriptive error message
42+ */
43+ private static String parseErrorMessage (String methodName , String responseBody ) {
44+ if (responseBody == null || responseBody .trim ().isEmpty ()) {
45+ return methodName ;
46+ }
47+
48+ try {
49+ JsonNode jsonNode = ERROR_MAPPER .readTree (responseBody );
50+
51+ // Try to extract message field
52+ JsonNode messageNode = jsonNode .get ("message" );
53+ String message = (messageNode != null && !messageNode .isNull ()) ? messageNode .asText () : null ;
54+
55+ // If we have a message, return it, otherwise fall back to method name
56+ if (message != null && !message .trim ().isEmpty ()) {
57+ return message ;
58+ }
59+ } catch (Exception e ) {
60+ // If parsing fails, fall back to the method name
61+ // This is intentional to ensure errors are still reported even if the response format is unexpected
62+ }
63+
64+ return methodName ;
65+ }
66+
67+ /**
68+ * Extract the API error code from the response body.
69+ * @param responseBody The response body JSON string
70+ * @return The error code, or null if not found
71+ */
72+ private static String extractErrorCode (String responseBody ) {
73+ if (responseBody == null || responseBody .trim ().isEmpty ()) {
74+ return null ;
75+ }
76+
77+ try {
78+ JsonNode jsonNode = ERROR_MAPPER .readTree (responseBody );
79+
80+ JsonNode codeNode = jsonNode .get ("code" );
81+ if (codeNode != null && !codeNode .isNull ()) {
82+ return codeNode .asText ();
83+ }
84+ } catch (Exception e ) {
85+ // If parsing fails, return null
86+ // This is intentional - we still want to report the error even if we can't extract the code
87+ }
88+
89+ return null ;
90+ }
91+
3192 public static Optional <FgaError > getError (
3293 String name ,
3394 HttpRequest request ,
@@ -43,25 +104,54 @@ public static Optional<FgaError> getError(
43104
44105 final String body = response .body ();
45106 final var headers = response .headers ();
107+
108+ // Parse the error message from the response body
109+ final String errorMessage = parseErrorMessage (name , body );
46110 final FgaError error ;
47111
48112 if (status == BAD_REQUEST || status == UNPROCESSABLE_ENTITY ) {
49- error = new FgaApiValidationError (name , previousError , status , headers , body );
113+ error = new FgaApiValidationError (errorMessage , previousError , status , headers , body );
50114 } else if (status == UNAUTHORIZED || status == FORBIDDEN ) {
51- error = new FgaApiAuthenticationError (name , previousError , status , headers , body );
115+ error = new FgaApiAuthenticationError (errorMessage , previousError , status , headers , body );
52116 } else if (status == NOT_FOUND ) {
53- error = new FgaApiNotFoundError (name , previousError , status , headers , body );
117+ error = new FgaApiNotFoundError (errorMessage , previousError , status , headers , body );
54118 } else if (status == TOO_MANY_REQUESTS ) {
55- error = new FgaApiRateLimitExceededError (name , previousError , status , headers , body );
119+ error = new FgaApiRateLimitExceededError (errorMessage , previousError , status , headers , body );
56120 } else if (isServerError (status )) {
57- error = new FgaApiInternalError (name , previousError , status , headers , body );
121+ error = new FgaApiInternalError (errorMessage , previousError , status , headers , body );
58122 } else {
59- error = new FgaError (name , previousError , status , headers , body );
123+ error = new FgaError (errorMessage , previousError , status , headers , body );
60124 }
61125
62126 error .setMethod (request .method ());
63127 error .setRequestUrl (configuration .getApiUrl ());
64128
129+ // Set the operation name
130+ error .setOperationName (name );
131+
132+ // Extract and set API error code from response body
133+ String apiErrorCode = extractErrorCode (body );
134+ if (apiErrorCode != null ) {
135+ error .setApiErrorCode (apiErrorCode );
136+ }
137+
138+ // Set the API error message (same as what was parsed for the constructor)
139+ // This allows getMessage() to return a formatted version
140+ if (!errorMessage .equals (name )) {
141+ // Only set apiErrorMessage if we actually got a message from the API
142+ // (not just falling back to the operation name)
143+ error .setApiErrorMessage (errorMessage );
144+ }
145+
146+ // Extract and set request ID from response headers if present
147+ // Common request ID header names
148+ Optional <String > requestId = headers .firstValue ("X-Request-Id" )
149+ .or (() -> headers .firstValue ("x-request-id" ))
150+ .or (() -> headers .firstValue ("Request-Id" ));
151+ if (requestId .isPresent ()) {
152+ error .setRequestId (requestId .get ());
153+ }
154+
65155 // Extract and set Retry-After header if present
66156 Optional <String > retryAfter = headers .firstValue (FgaConstants .RETRY_AFTER_HEADER_NAME );
67157 if (retryAfter .isPresent ()) {
@@ -135,11 +225,36 @@ public String getApiErrorCode() {
135225 return apiErrorCode ;
136226 }
137227
228+ /**
229+ * Get the API error code.
230+ * This is an alias for getApiErrorCode() for convenience.
231+ * @return The API error code from the response
232+ */
233+ public String getCode () {
234+ return apiErrorCode ;
235+ }
236+
138237 public void setRetryAfterHeader (String retryAfterHeader ) {
139238 this .retryAfterHeader = retryAfterHeader ;
140239 }
141240
142241 public String getRetryAfterHeader () {
143242 return retryAfterHeader ;
144243 }
145- }
244+
245+ public void setApiErrorMessage (String apiErrorMessage ) {
246+ this .apiErrorMessage = apiErrorMessage ;
247+ }
248+
249+ public String getApiErrorMessage () {
250+ return apiErrorMessage ;
251+ }
252+
253+ public void setOperationName (String operationName ) {
254+ this .operationName = operationName ;
255+ }
256+
257+ public String getOperationName () {
258+ return operationName ;
259+ }
260+ }
0 commit comments