22
33import static dev .openfga .sdk .errors .HttpStatusCode .*;
44
5+ import com .fasterxml .jackson .annotation .JsonInclude ;
6+ import com .fasterxml .jackson .core .JsonProcessingException ;
7+ import com .fasterxml .jackson .databind .DeserializationFeature ;
8+ import com .fasterxml .jackson .databind .ObjectMapper ;
9+ import com .fasterxml .jackson .databind .SerializationFeature ;
10+ import com .fasterxml .jackson .datatype .jsr310 .JavaTimeModule ;
511import dev .openfga .sdk .api .configuration .Configuration ;
612import dev .openfga .sdk .api .configuration .CredentialsMethod ;
713import dev .openfga .sdk .constants .FgaConstants ;
814import java .net .http .HttpHeaders ;
915import java .net .http .HttpRequest ;
1016import java .net .http .HttpResponse ;
1117import java .util .Optional ;
18+ import org .openapitools .jackson .nullable .JsonNullableModule ;
1219
1320public class FgaError extends ApiException {
21+ private static final String UNKNOWN_ERROR_CODE = "unknown_error" ;
22+
1423 private String method = null ;
1524 private String requestUrl = null ;
1625 private String clientId = null ;
@@ -19,6 +28,60 @@ public class FgaError extends ApiException {
1928 private String requestId = null ;
2029 private String apiErrorCode = null ;
2130 private String retryAfterHeader = null ;
31+ private String apiErrorMessage = null ;
32+ private String operationName = null ;
33+
34+ private static final ObjectMapper OBJECT_MAPPER = createConfiguredObjectMapper ();
35+
36+ private static ObjectMapper createConfiguredObjectMapper () {
37+ ObjectMapper mapper = new ObjectMapper ();
38+ mapper .setSerializationInclusion (JsonInclude .Include .NON_NULL );
39+ mapper .configure (DeserializationFeature .FAIL_ON_UNKNOWN_PROPERTIES , false );
40+ mapper .configure (DeserializationFeature .FAIL_ON_INVALID_SUBTYPE , false );
41+ mapper .disable (SerializationFeature .WRITE_DATES_AS_TIMESTAMPS );
42+ mapper .enable (SerializationFeature .WRITE_ENUMS_USING_TO_STRING );
43+ mapper .enable (DeserializationFeature .READ_ENUMS_USING_TO_STRING );
44+ mapper .disable (DeserializationFeature .ADJUST_DATES_TO_CONTEXT_TIME_ZONE );
45+ mapper .registerModule (new JavaTimeModule ());
46+ mapper .registerModule (new JsonNullableModule ());
47+ return mapper ;
48+ }
49+
50+ @ com .fasterxml .jackson .annotation .JsonIgnoreProperties (ignoreUnknown = true )
51+ private static class ApiErrorResponse {
52+ @ com .fasterxml .jackson .annotation .JsonProperty ("code" )
53+ private String code ;
54+
55+ @ com .fasterxml .jackson .annotation .JsonProperty ("message" )
56+ private String message ;
57+
58+ @ com .fasterxml .jackson .annotation .JsonProperty ("error" )
59+ private String error ;
60+
61+ public String getCode () {
62+ return code ;
63+ }
64+
65+ public void setCode (String code ) {
66+ this .code = code ;
67+ }
68+
69+ public String getMessage () {
70+ return message != null ? message : error ;
71+ }
72+
73+ public void setMessage (String message ) {
74+ this .message = message ;
75+ }
76+
77+ public String getError () {
78+ return error ;
79+ }
80+
81+ public void setError (String error ) {
82+ this .error = error ;
83+ }
84+ }
2285
2386 public FgaError (String message , Throwable cause , int code , HttpHeaders responseHeaders , String responseBody ) {
2487 super (message , cause , code , responseHeaders , responseBody );
@@ -53,7 +116,7 @@ public static Optional<FgaError> getError(
53116 error = new FgaApiNotFoundError (name , previousError , status , headers , body );
54117 } else if (status == TOO_MANY_REQUESTS ) {
55118 error = new FgaApiRateLimitExceededError (name , previousError , status , headers , body );
56- } else if (isServerError (status )) {
119+ } else if (HttpStatusCode . isServerError (status )) {
57120 error = new FgaApiInternalError (name , previousError , status , headers , body );
58121 } else {
59122 error = new FgaError (name , previousError , status , headers , body );
@@ -75,6 +138,27 @@ public static Optional<FgaError> getError(
75138 error .setAudience (clientCredentials .getApiAudience ());
76139 }
77140
141+ error .setOperationName (name );
142+
143+ // Parse API error response
144+ if (body != null && !body .trim ().isEmpty ()) {
145+ try {
146+ ApiErrorResponse resp = OBJECT_MAPPER .readValue (body , ApiErrorResponse .class );
147+ error .setApiErrorCode (resp .getCode ());
148+ error .setApiErrorMessage (resp .getMessage ());
149+ } catch (JsonProcessingException e ) {
150+ // Wrap unparseable response
151+ error .setApiErrorCode (UNKNOWN_ERROR_CODE );
152+ error .setApiErrorMessage ("Unable to parse error response. Raw response: " + body );
153+ }
154+ }
155+
156+ // Extract requestId from headers
157+ Optional <String > requestIdOpt = headers .firstValue ("x-request-id" );
158+ if (requestIdOpt .isPresent ()) {
159+ error .setRequestId (requestIdOpt .get ());
160+ }
161+
78162 // Unknown error
79163 return Optional .of (error );
80164 }
@@ -142,4 +226,143 @@ public void setRetryAfterHeader(String retryAfterHeader) {
142226 public String getRetryAfterHeader () {
143227 return retryAfterHeader ;
144228 }
229+
230+ public void setApiErrorMessage (String apiErrorMessage ) {
231+ this .apiErrorMessage = apiErrorMessage ;
232+ }
233+
234+ public String getApiErrorMessage () {
235+ return apiErrorMessage ;
236+ }
237+
238+ public void setOperationName (String operationName ) {
239+ this .operationName = operationName ;
240+ }
241+
242+ public String getOperationName () {
243+ return operationName ;
244+ }
245+
246+ /**
247+ * Returns a formatted error message for FgaError.
248+ * <p>
249+ * The message is formatted as:
250+ * <pre>
251+ * [operationName] HTTP statusCode apiErrorMessage (apiErrorCode) [request-id: requestId]
252+ * </pre>
253+ * Example: [write] HTTP 400 type 'invalid_type' not found (validation_error) [request-id: abc-123]
254+ * </p>
255+ *
256+ * @return the formatted error message string
257+ */
258+ @ Override
259+ public String getMessage () {
260+ // Use apiErrorMessage if available, otherwise fall back to the original message
261+ String message = (apiErrorMessage != null && !apiErrorMessage .isEmpty ()) ? apiErrorMessage : super .getMessage ();
262+
263+ StringBuilder sb = new StringBuilder ();
264+
265+ // [operationName]
266+ if (operationName != null && !operationName .isEmpty ()) {
267+ sb .append ("[" ).append (operationName ).append ("] " );
268+ }
269+
270+ // HTTP 400
271+ sb .append ("HTTP " ).append (getStatusCode ()).append (" " );
272+
273+ // type 'invalid_type' not found
274+ if (message != null && !message .isEmpty ()) {
275+ sb .append (message );
276+ }
277+
278+ // (validation_error)
279+ if (apiErrorCode != null && !apiErrorCode .isEmpty ()) {
280+ sb .append (" (" ).append (apiErrorCode ).append (")" );
281+ }
282+
283+ // [request-id: abc-123]
284+ if (requestId != null && !requestId .isEmpty ()) {
285+ sb .append (" [request-id: " ).append (requestId ).append ("]" );
286+ }
287+
288+ return sb .toString ().trim ();
289+ }
290+
291+ // --- Helper Methods ---
292+
293+ /**
294+ * Checks if this is a validation error.
295+ * Reliable error type checking based on error code.
296+ *
297+ * @return true if this is a validation error
298+ */
299+ public boolean isValidationError () {
300+ return "validation_error" .equals (apiErrorCode );
301+ }
302+
303+ /**
304+ * Checks if this is an unknown error due to unparseable response.
305+ * This occurs when the error response could not be parsed as JSON.
306+ *
307+ * @return true if this is an unknown error
308+ */
309+ public boolean isUnknownError () {
310+ return UNKNOWN_ERROR_CODE .equals (apiErrorCode );
311+ }
312+
313+ /**
314+ * Checks if this is a not found (404) error.
315+ *
316+ * @return true if this is a 404 error
317+ */
318+ public boolean isNotFoundError () {
319+ return getStatusCode () == NOT_FOUND ;
320+ }
321+
322+ /**
323+ * Checks if this is an authentication (401) error.
324+ *
325+ * @return true if this is a 401 error
326+ */
327+ public boolean isAuthenticationError () {
328+ return getStatusCode () == UNAUTHORIZED ;
329+ }
330+
331+ /**
332+ * Checks if this is a rate limit (429) error.
333+ *
334+ * @return true if this is a rate limit error
335+ */
336+ public boolean isRateLimitError () {
337+ return getStatusCode () == TOO_MANY_REQUESTS || "rate_limit_exceeded" .equals (apiErrorCode );
338+ }
339+
340+ /**
341+ * Checks if this error should be retried.
342+ * 429 (Rate Limit) and 5xx (Server Errors) are typically retryable.
343+ *
344+ * @return true if this error is retryable
345+ */
346+ public boolean isRetryable () {
347+ return HttpStatusCode .isRetryable (getStatusCode ());
348+ }
349+
350+ /**
351+ * Checks if this is a client error (4xx).
352+ *
353+ * @return true if this is a 4xx error
354+ */
355+ public boolean isClientError () {
356+ int status = getStatusCode ();
357+ return status >= 400 && status < 500 ;
358+ }
359+
360+ /**
361+ * Checks if this is a server error (5xx).
362+ *
363+ * @return true if this is a 5xx error
364+ */
365+ public boolean isServerError () {
366+ return HttpStatusCode .isServerError (getStatusCode ());
367+ }
145368}
0 commit comments