From 26d603b5647c36f1cda9d0766c93bd0e76e19331 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Fri, 14 Nov 2025 12:14:30 +0530
Subject: [PATCH 01/10] feat: improve errors, relay api err msg
---
.../java/dev/openfga/sdk/errors/FgaError.java | 55 +++++
.../sdk/errors/FgaErrorIntegrationTest.java | 211 ++++++++++++++++++
2 files changed, 266 insertions(+)
create mode 100644 src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 85f54d88..24d19cbc 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -2,6 +2,8 @@
import static dev.openfga.sdk.errors.HttpStatusCode.*;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.api.configuration.CredentialsMethod;
import dev.openfga.sdk.constants.FgaConstants;
@@ -19,6 +21,13 @@ public class FgaError extends ApiException {
private String requestId = null;
private String apiErrorCode = null;
private String retryAfterHeader = null;
+ private String apiErrorMessage = null;
+ private String operationName = null;
+
+ public static class ApiErrorResponse {
+ public String code;
+ public String message;
+ }
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, cause, code, responseHeaders, responseBody);
@@ -75,6 +84,26 @@ public static Optional getError(
error.setAudience(clientCredentials.getApiAudience());
}
+ error.setOperationName(name);
+
+ // Parse API error response
+ if (body != null && !body.trim().isEmpty()) {
+ ObjectMapper mapper = new ObjectMapper();
+ try {
+ ApiErrorResponse resp = mapper.readValue(body, ApiErrorResponse.class);
+ error.setApiErrorCode(resp.code);
+ error.setApiErrorMessage(resp.message);
+ } catch (JsonProcessingException e) {
+ // Fall back, do nothing
+ }
+ }
+
+ // Extract requestId from headers
+ Optional requestIdOpt = headers.firstValue("x-request-id");
+ if (requestIdOpt.isPresent()) {
+ error.setRequestId(requestIdOpt.get());
+ }
+
// Unknown error
return Optional.of(error);
}
@@ -142,4 +171,30 @@ public void setRetryAfterHeader(String retryAfterHeader) {
public String getRetryAfterHeader() {
return retryAfterHeader;
}
+
+ public void setApiErrorMessage(String apiErrorMessage) {
+ this.apiErrorMessage = apiErrorMessage;
+ }
+
+ public String getApiErrorMessage() {
+ return apiErrorMessage;
+ }
+
+ public void setOperationName(String operationName) {
+ this.operationName = operationName;
+ }
+
+ public String getOperationName() {
+ return operationName;
+ }
+
+ @Override
+ public String getMessage() {
+ if (apiErrorMessage != null && !apiErrorMessage.isEmpty() && operationName != null) {
+ String codePart = (apiErrorCode != null && !apiErrorCode.isEmpty()) ? " (" + apiErrorCode + ")" : "";
+ return String.format("[%s] %s%s", operationName, apiErrorMessage, codePart);
+ } else {
+ return super.getMessage();
+ }
+ }
}
diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
new file mode 100644
index 00000000..f56c2329
--- /dev/null
+++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
@@ -0,0 +1,211 @@
+package dev.openfga.sdk.errors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import dev.openfga.sdk.api.client.OpenFgaClient;
+import dev.openfga.sdk.api.client.model.ClientTupleKey;
+import dev.openfga.sdk.api.client.model.ClientWriteRequest;
+import dev.openfga.sdk.api.configuration.ClientConfiguration;
+import dev.openfga.sdk.api.model.CreateStoreRequest;
+import dev.openfga.sdk.api.model.WriteAuthorizationModelRequest;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.openfga.OpenFGAContainer;
+
+@TestInstance(Lifecycle.PER_CLASS)
+@Testcontainers
+public class FgaErrorIntegrationTest {
+
+ @Container
+ private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2");
+
+ private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
+ private String authModelJson;
+ private OpenFgaClient fga;
+
+ @BeforeAll
+ public void loadAuthModelJson() throws IOException {
+ authModelJson = Files.readString(Paths.get("src", "test-integration", "resources", "auth-model.json"));
+ }
+
+ @BeforeEach
+ public void initializeApi() throws Exception {
+ ClientConfiguration apiConfig = new ClientConfiguration().apiUrl(openfga.getHttpEndpoint());
+ fga = new OpenFgaClient(apiConfig);
+
+ // Create a store
+ String storeName = "ErrorTestStore";
+ var createStoreResponse =
+ fga.createStore(new CreateStoreRequest().name(storeName)).get();
+ String storeId = createStoreResponse.getId();
+ fga.setStoreId(storeId);
+
+ // Write the authorization model
+ WriteAuthorizationModelRequest writeModelRequest =
+ mapper.readValue(authModelJson, WriteAuthorizationModelRequest.class);
+ fga.writeAuthorizationModel(writeModelRequest).get();
+ }
+
+ @Test
+ public void testWriteValidationError() throws Exception {
+ // Try to write a tuple with invalid type
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+ assertTrue(exception.getMessage().contains("type 'invalid_type' not found"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getApiErrorMessage().contains("type 'invalid_type' not found"));
+ assertEquals("write", exception.getOperationName());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testWriteValidationErrorWithInvalidRelation() throws Exception {
+ // Try to write a tuple with valid type but invalid relation
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("document:readme")
+ .relation("invalid_relation")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+ // Verify the formatted message includes operation name and API error details
+ assertTrue(exception.getMessage().contains("write"));
+ assertTrue(exception.getMessage().contains("relation 'document#invalid_relation' not found"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertEquals("write", exception.getOperationName());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testErrorMessageFormattingWithAllFields() throws Exception {
+ // Verify that when all fields are present, the message is properly formatted
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+
+ // Verify the message follows the format: [operationName] apiErrorMessage (apiErrorCode)
+ String message = exception.getMessage();
+ assertTrue(message.startsWith("[write]"), "Message should start with [write]");
+ assertTrue(message.contains("type 'invalid_type' not found"), "Message should contain the API error message");
+ assertTrue(message.endsWith("(validation_error)"), "Message should end with (validation_error)");
+
+ // Verify individual fields are accessible
+ assertEquals("write", exception.getOperationName());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testCheckValidationError() throws Exception {
+ // Test error handling for check operation to verify operation name is set correctly
+ var checkRequest = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne");
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.check(checkRequest).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+
+ // Verify operation name is "check" not "write"
+ assertEquals("check", exception.getOperationName());
+ assertTrue(exception.getMessage().contains("[check]"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testErrorDetailsAreNotLostInStackTrace() throws Exception {
+ // Verify that error details are preserved when exception is thrown
+ // This addresses the issue where details were "buried down in the exception stack"
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ try {
+ fga.write(request).get();
+ fail("Expected ExecutionException to be thrown");
+ } catch (ExecutionException e) {
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, e.getCause());
+
+ // Verify that calling toString() or getMessage() gives useful information
+ String errorString = exception.toString();
+ assertTrue(
+ errorString.contains("type 'invalid_type' not found"),
+ "toString() should contain the API error message");
+
+ String errorMessage = exception.getMessage();
+ assertTrue(errorMessage.contains("[write]"), "getMessage() should contain operation name");
+ assertTrue(errorMessage.contains("validation_error"), "getMessage() should contain error code");
+
+ // Verify the response body is still accessible for custom parsing if needed
+ assertNotNull(exception.getResponseData(), "Response body should be available");
+ }
+ }
+
+ @Test
+ public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
+ // Test that validation errors for multiple tuples still show useful information
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(
+ new ClientTupleKey()
+ ._object("invalid_type1:readme")
+ .relation("viewer")
+ .user("user:anne"),
+ new ClientTupleKey()
+ ._object("invalid_type2:readme")
+ .relation("viewer")
+ .user("user:bob")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+
+ // The error message should be informative even for batch operations
+ assertTrue(exception.getMessage().contains("write"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+}
From 8c4898d2810f762215c4ae5a503ecfd88fa85a72 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Fri, 14 Nov 2025 13:09:02 +0530
Subject: [PATCH 02/10] feat: more tcs
---
.../sdk/errors/FgaErrorIntegrationTest.java | 170 ++++++++++++++++++
1 file changed, 170 insertions(+)
diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
index f56c2329..da35119f 100644
--- a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
+++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
@@ -208,4 +208,174 @@ public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
+
+ @Test
+ public void testNotFoundError() throws Exception {
+ ClientConfiguration badConfig = new ClientConfiguration()
+ .apiUrl(openfga.getHttpEndpoint())
+ .storeId("01HVJPQR3TXYZ9NQXABCDEFGHI"); // Non-existent store ID
+ OpenFgaClient badClient = new OpenFgaClient(badConfig);
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ badClient.getStore().get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("getStore")
+ || exception.getMessage().contains("[get"));
+ assertNotNull(exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testInvalidParameterException() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest(); // Empty request, no writes or deletes
+
+ // This should throw FgaInvalidParameterException before making the API call
+ dev.openfga.sdk.errors.FgaInvalidParameterException exception =
+ assertThrows(dev.openfga.sdk.errors.FgaInvalidParameterException.class, () -> {
+ // Trying to write with null storeId by creating a new client without storeId
+ ClientConfiguration config = new ClientConfiguration().apiUrl(openfga.getHttpEndpoint());
+ OpenFgaClient client = new OpenFgaClient(config);
+ client.write(request).get();
+ });
+
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("storeId")
+ || exception.getMessage().contains("parameter"));
+ }
+
+ @Test
+ public void testAuthenticationError() throws Exception {
+ // Test FgaApiAuthenticationError when store doesn't exist (403/401 from API)
+ // Use a non-existent store ID to trigger authentication/authorization error
+ ClientConfiguration badConfig =
+ new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("non-existent-store-id-12345");
+ OpenFgaClient badClient = new OpenFgaClient(badConfig);
+
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("document:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ badClient.write(request).get();
+ });
+
+ // Could be NotFoundError (404) or AuthenticationError depending on API version
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ // Verify error details are populated
+ assertNotNull(exception.getMessage());
+ assertTrue(exception.getMessage().contains("[write]")
+ || exception.getMessage().contains("write"));
+ assertNotNull(exception.getApiErrorCode());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testReadOperationError() throws Exception {
+ // Test that read operations also surface error details correctly
+ // Read requires both user and object when object is a type prefix
+ var readRequest = new dev.openfga.sdk.api.client.model.ClientReadRequest()
+ ._object("invalid_type:"); // Type prefix without user will fail
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.read(readRequest).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+
+ // Verify operation name is "read"
+ assertTrue(exception.getOperationName().equals("read")
+ || exception.getMessage().contains("read"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testExpandOperationError() throws Exception {
+ // Test that expand operations surface error details correctly
+ var expandRequest = new dev.openfga.sdk.api.client.model.ClientExpandRequest()
+ ._object("invalid_type:readme")
+ .relation("viewer");
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.expand(expandRequest).get();
+ });
+
+ FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
+
+ // Verify operation name is "expand"
+ assertTrue(exception.getOperationName().equals("expand")
+ || exception.getMessage().contains("expand"));
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ }
+
+ @Test
+ public void testDifferentErrorCodesAreSurfaced() throws Exception {
+ // Test that different validation_error subcases are properly surfaced
+ // Case 1: Invalid type
+ ClientWriteRequest request1 = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException ex1 =
+ assertThrows(ExecutionException.class, () -> fga.write(request1).get());
+ FgaApiValidationError error1 = assertInstanceOf(FgaApiValidationError.class, ex1.getCause());
+ assertTrue(error1.getApiErrorMessage().contains("type"));
+
+ // Case 2: Invalid relation
+ ClientWriteRequest request2 = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("document:readme")
+ .relation("invalid_relation")
+ .user("user:anne")));
+
+ ExecutionException ex2 =
+ assertThrows(ExecutionException.class, () -> fga.write(request2).get());
+ FgaApiValidationError error2 = assertInstanceOf(FgaApiValidationError.class, ex2.getCause());
+ assertTrue(error2.getApiErrorMessage().contains("relation"));
+
+ // Both should have the same error code but different messages
+ assertEquals(error1.getApiErrorCode(), error2.getApiErrorCode());
+ assertNotEquals(error1.getApiErrorMessage(), error2.getApiErrorMessage());
+ }
+
+ @Test
+ public void testErrorMessageContainsOperationContext() throws Exception {
+ // Verify that different operations have their names in the error message
+
+ ClientWriteRequest writeReq = new ClientWriteRequest()
+ .writes(List.of(
+ new ClientTupleKey()._object("invalid:x").relation("r").user("user:x")));
+
+ ExecutionException writeEx =
+ assertThrows(ExecutionException.class, () -> fga.write(writeReq).get());
+ FgaError writeError = assertInstanceOf(FgaError.class, writeEx.getCause());
+ assertTrue(writeError.getMessage().contains("[write]"), "Write error should contain [write] in message");
+
+ // Check operation
+ var checkReq = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
+ ._object("invalid:x")
+ .relation("r")
+ .user("user:x");
+
+ ExecutionException checkEx =
+ assertThrows(ExecutionException.class, () -> fga.check(checkReq).get());
+ FgaError checkError = assertInstanceOf(FgaError.class, checkEx.getCause());
+ assertTrue(checkError.getMessage().contains("[check]"), "Check error should contain [check] in message");
+
+ // Verify they have different operation names
+ assertNotEquals(writeError.getOperationName(), checkError.getOperationName());
+ }
}
From c03e30170280cc865b7ab00e04adf50a09e4f488 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Mon, 17 Nov 2025 17:22:13 +0530
Subject: [PATCH 03/10] feat: log the exception for json processing
---
src/main/java/dev/openfga/sdk/errors/FgaError.java | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 24d19cbc..7989c8fc 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -94,7 +94,8 @@ public static Optional getError(
error.setApiErrorCode(resp.code);
error.setApiErrorMessage(resp.message);
} catch (JsonProcessingException e) {
- // Fall back, do nothing
+ // Fall back, do nothing - log the exception for debugging
+ System.err.println("Failed to parse API error response JSON: " + e.getMessage());
}
}
From a9617a6e3add3b464ff1dba7c0e84d2474e8c1d7 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Wed, 19 Nov 2025 13:59:03 +0530
Subject: [PATCH 04/10] feat: address copilot comments
---
.../java/dev/openfga/sdk/errors/FgaError.java | 50 ++++++++++++++++---
.../sdk/errors/FgaErrorIntegrationTest.java | 4 +-
2 files changed, 45 insertions(+), 9 deletions(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 7989c8fc..7fdf8e63 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -24,9 +24,30 @@ public class FgaError extends ApiException {
private String apiErrorMessage = null;
private String operationName = null;
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
public static class ApiErrorResponse {
- public String code;
- public String message;
+ @com.fasterxml.jackson.annotation.JsonProperty("code")
+ private String code;
+
+ @com.fasterxml.jackson.annotation.JsonProperty("message")
+ private String message;
+
+ public String getCode() {
+ return code;
+ }
+
+ public void setCode(String code) {
+ this.code = code;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public void setMessage(String message) {
+ this.message = message;
+ }
}
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
@@ -88,11 +109,10 @@ public static Optional getError(
// Parse API error response
if (body != null && !body.trim().isEmpty()) {
- ObjectMapper mapper = new ObjectMapper();
try {
- ApiErrorResponse resp = mapper.readValue(body, ApiErrorResponse.class);
- error.setApiErrorCode(resp.code);
- error.setApiErrorMessage(resp.message);
+ ApiErrorResponse resp = OBJECT_MAPPER.readValue(body, ApiErrorResponse.class);
+ error.setApiErrorCode(resp.getCode());
+ error.setApiErrorMessage(resp.getMessage());
} catch (JsonProcessingException e) {
// Fall back, do nothing - log the exception for debugging
System.err.println("Failed to parse API error response JSON: " + e.getMessage());
@@ -189,9 +209,25 @@ public String getOperationName() {
return operationName;
}
+ /**
+ * Returns a formatted error message for FgaError.
+ *
+ * If both {@code apiErrorMessage} and {@code operationName} are present and non-empty, the message is formatted as:
+ *
+ * [operationName] apiErrorMessage (apiErrorCode)
+ *
+ * where {@code apiErrorCode} is included only if present and non-empty.
+ * Otherwise, falls back to the default parent message.
+ *
+ *
+ * @return the formatted error message string
+ */
@Override
public String getMessage() {
- if (apiErrorMessage != null && !apiErrorMessage.isEmpty() && operationName != null) {
+ if (apiErrorMessage != null
+ && !apiErrorMessage.isEmpty()
+ && operationName != null
+ && !operationName.isEmpty()) {
String codePart = (apiErrorCode != null && !apiErrorCode.isEmpty()) ? " (" + apiErrorCode + ")" : "";
return String.format("[%s] %s%s", operationName, apiErrorMessage, codePart);
} else {
diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
index da35119f..c6a6af01 100644
--- a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
+++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
@@ -291,7 +291,7 @@ public void testReadOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
// Verify operation name is "read"
- assertTrue(exception.getOperationName().equals("read")
+ assertTrue("read".equals(exception.getOperationName())
|| exception.getMessage().contains("read"));
assertEquals("validation_error", exception.getApiErrorCode());
assertNotNull(exception.getApiErrorMessage());
@@ -312,7 +312,7 @@ public void testExpandOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
// Verify operation name is "expand"
- assertTrue(exception.getOperationName().equals("expand")
+ assertTrue("expand".equals(exception.getOperationName())
|| exception.getMessage().contains("expand"));
assertEquals("validation_error", exception.getApiErrorCode());
assertNotNull(exception.getApiErrorMessage());
From a016a328adee19766a84a048cf496d4848ea8c36 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Wed, 19 Nov 2025 14:52:47 +0530
Subject: [PATCH 05/10] feat: add changelog
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6171468c..ed9fb31f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.9.3...HEAD)
+- Improved error handling and integration test coverage for FgaError and related classes. (#260)
+
## v0.9.3
### [0.9.3](https://github.com/openfga/java-sdk/compare/v0.9.2...v0.9.3)) (2025-11-10)
From 9dec79d1fea1b6eae540e76a85ba73eb3d6acc8a Mon Sep 17 00:00:00 2001
From: Anurag Bandyopadhyay
Date: Wed, 19 Nov 2025 20:58:22 +0530
Subject: [PATCH 06/10] feat: thorough tests and sync with rfc
---
.../java/dev/openfga/sdk/errors/FgaError.java | 130 +++++++-
.../sdk/errors/FgaErrorIntegrationTest.java | 309 ++++++++++++++----
.../sdk/api/auth/OAuth2ClientTest.java | 4 +-
3 files changed, 368 insertions(+), 75 deletions(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 7fdf8e63..f1e1299c 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -26,6 +26,7 @@ public class FgaError extends ApiException {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)
public static class ApiErrorResponse {
@com.fasterxml.jackson.annotation.JsonProperty("code")
private String code;
@@ -33,6 +34,9 @@ public static class ApiErrorResponse {
@com.fasterxml.jackson.annotation.JsonProperty("message")
private String message;
+ @com.fasterxml.jackson.annotation.JsonProperty("error")
+ private String error;
+
public String getCode() {
return code;
}
@@ -42,12 +46,20 @@ public void setCode(String code) {
}
public String getMessage() {
- return message;
+ return message != null ? message : error;
}
public void setMessage(String message) {
this.message = message;
}
+
+ public String getError() {
+ return error;
+ }
+
+ public void setError(String error) {
+ this.error = error;
+ }
}
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
@@ -83,7 +95,7 @@ public static Optional getError(
error = new FgaApiNotFoundError(name, previousError, status, headers, body);
} else if (status == TOO_MANY_REQUESTS) {
error = new FgaApiRateLimitExceededError(name, previousError, status, headers, body);
- } else if (isServerError(status)) {
+ } else if (HttpStatusCode.isServerError(status)) {
error = new FgaApiInternalError(name, previousError, status, headers, body);
} else {
error = new FgaError(name, previousError, status, headers, body);
@@ -212,26 +224,116 @@ public String getOperationName() {
/**
* Returns a formatted error message for FgaError.
*
- * If both {@code apiErrorMessage} and {@code operationName} are present and non-empty, the message is formatted as:
+ * The message is formatted as:
*
- * [operationName] apiErrorMessage (apiErrorCode)
+ * [operationName] HTTP statusCode apiErrorMessage (apiErrorCode) [request-id: requestId]
*
- * where {@code apiErrorCode} is included only if present and non-empty.
- * Otherwise, falls back to the default parent message.
+ * Example: [write] HTTP 400 type 'invalid_type' not found (validation_error) [request-id: abc-123]
*
*
* @return the formatted error message string
*/
@Override
public String getMessage() {
- if (apiErrorMessage != null
- && !apiErrorMessage.isEmpty()
- && operationName != null
- && !operationName.isEmpty()) {
- String codePart = (apiErrorCode != null && !apiErrorCode.isEmpty()) ? " (" + apiErrorCode + ")" : "";
- return String.format("[%s] %s%s", operationName, apiErrorMessage, codePart);
- } else {
- return super.getMessage();
+ // Use apiErrorMessage if available, otherwise fall back to the original message
+ String message = (apiErrorMessage != null && !apiErrorMessage.isEmpty()) ? apiErrorMessage : super.getMessage();
+
+ StringBuilder sb = new StringBuilder();
+
+ // [operationName]
+ if (operationName != null && !operationName.isEmpty()) {
+ sb.append("[").append(operationName).append("] ");
+ }
+
+ // HTTP 400
+ sb.append("HTTP ").append(getStatusCode()).append(" ");
+
+ // type 'invalid_type' not found
+ if (message != null && !message.isEmpty()) {
+ sb.append(message);
+ }
+
+ // (validation_error)
+ if (apiErrorCode != null && !apiErrorCode.isEmpty()) {
+ sb.append(" (").append(apiErrorCode).append(")");
}
+
+ // [request-id: abc-123]
+ if (requestId != null && !requestId.isEmpty()) {
+ sb.append(" [request-id: ").append(requestId).append("]");
+ }
+
+ return sb.toString().trim();
+ }
+
+ // --- Helper Methods ---
+
+ /**
+ * Checks if this is a validation error.
+ * Reliable error type checking based on error code.
+ *
+ * @return true if this is a validation error
+ */
+ public boolean isValidationError() {
+ return "validation_error".equals(apiErrorCode);
+ }
+
+ /**
+ * Checks if this is a not found (404) error.
+ *
+ * @return true if this is a 404 error
+ */
+ public boolean isNotFoundError() {
+ return getStatusCode() == NOT_FOUND;
+ }
+
+ /**
+ * Checks if this is an authentication (401) error.
+ *
+ * @return true if this is a 401 error
+ */
+ public boolean isAuthenticationError() {
+ return getStatusCode() == UNAUTHORIZED;
+ }
+
+ /**
+ * Checks if this is a rate limit (429) error.
+ *
+ * @return true if this is a rate limit error
+ */
+ public boolean isRateLimitError() {
+ return getStatusCode() == TOO_MANY_REQUESTS || "rate_limit_exceeded".equals(apiErrorCode);
+ }
+
+ /**
+ * Checks if this error should be retried.
+ * 429 (Rate Limit) and 5xx (Server Errors) are typically retryable.
+ *
+ * @return true if this error is retryable
+ */
+ public boolean isRetryable() {
+ int status = getStatusCode();
+ // 429 (Rate Limit) and 5xx (Server Errors) are typically retryable.
+ return status == TOO_MANY_REQUESTS || (status >= 500 && status < 600);
+ }
+
+ /**
+ * Checks if this is a client error (4xx).
+ *
+ * @return true if this is a 4xx error
+ */
+ public boolean isClientError() {
+ int status = getStatusCode();
+ return status >= 400 && status < 500;
+ }
+
+ /**
+ * Checks if this is a server error (5xx).
+ *
+ * @return true if this is a 5xx error
+ */
+ public boolean isServerError() {
+ int status = getStatusCode();
+ return status >= 500 && status < 600;
}
}
diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
index c6a6af01..dc33bd11 100644
--- a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
+++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
@@ -103,7 +103,6 @@ public void testWriteValidationErrorWithInvalidRelation() throws Exception {
@Test
public void testErrorMessageFormattingWithAllFields() throws Exception {
- // Verify that when all fields are present, the message is properly formatted
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -116,22 +115,23 @@ public void testErrorMessageFormattingWithAllFields() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- // Verify the message follows the format: [operationName] apiErrorMessage (apiErrorCode)
String message = exception.getMessage();
- assertTrue(message.startsWith("[write]"), "Message should start with [write]");
- assertTrue(message.contains("type 'invalid_type' not found"), "Message should contain the API error message");
- assertTrue(message.endsWith("(validation_error)"), "Message should end with (validation_error)");
+ assertTrue(message.startsWith("[write]"));
+ assertTrue(message.contains("HTTP 400"));
+ assertTrue(message.contains("type 'invalid_type' not found"));
+ assertTrue(message.contains("(validation_error)"));
+ assertTrue(message.contains("[request-id: "));
+ assertTrue(message.endsWith("]"));
- // Verify individual fields are accessible
assertEquals("write", exception.getOperationName());
assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(400, exception.getStatusCode());
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@Test
public void testCheckValidationError() throws Exception {
- // Test error handling for check operation to verify operation name is set correctly
var checkRequest = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
._object("invalid_type:readme")
.relation("viewer")
@@ -143,18 +143,17 @@ public void testCheckValidationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- // Verify operation name is "check" not "write"
assertEquals("check", exception.getOperationName());
- assertTrue(exception.getMessage().contains("[check]"));
+ assertEquals(400, exception.getStatusCode());
assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getMessage().contains("[check]"));
+ assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@Test
public void testErrorDetailsAreNotLostInStackTrace() throws Exception {
- // Verify that error details are preserved when exception is thrown
- // This addresses the issue where details were "buried down in the exception stack"
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -167,24 +166,21 @@ public void testErrorDetailsAreNotLostInStackTrace() throws Exception {
} catch (ExecutionException e) {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, e.getCause());
- // Verify that calling toString() or getMessage() gives useful information
String errorString = exception.toString();
- assertTrue(
- errorString.contains("type 'invalid_type' not found"),
- "toString() should contain the API error message");
+ assertTrue(errorString.contains("type 'invalid_type' not found"));
String errorMessage = exception.getMessage();
- assertTrue(errorMessage.contains("[write]"), "getMessage() should contain operation name");
- assertTrue(errorMessage.contains("validation_error"), "getMessage() should contain error code");
+ assertTrue(errorMessage.contains("[write]"));
+ assertTrue(errorMessage.contains("HTTP 400"));
+ assertTrue(errorMessage.contains("validation_error"));
- // Verify the response body is still accessible for custom parsing if needed
- assertNotNull(exception.getResponseData(), "Response body should be available");
+ assertEquals(400, exception.getStatusCode());
+ assertNotNull(exception.getResponseData());
}
}
@Test
public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
- // Test that validation errors for multiple tuples still show useful information
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
@@ -202,18 +198,18 @@ public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- // The error message should be informative even for batch operations
- assertTrue(exception.getMessage().contains("write"));
+ assertEquals(400, exception.getStatusCode());
assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getMessage().contains("[write]"));
+ assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@Test
- public void testNotFoundError() throws Exception {
- ClientConfiguration badConfig = new ClientConfiguration()
- .apiUrl(openfga.getHttpEndpoint())
- .storeId("01HVJPQR3TXYZ9NQXABCDEFGHI"); // Non-existent store ID
+ public void testGetStoreWithInvalidStoreIdFormat() throws Exception {
+ ClientConfiguration badConfig =
+ new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("01HVJPQR3TXYZ9NQXABCDEFGHI");
OpenFgaClient badClient = new OpenFgaClient(badConfig);
ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
@@ -222,22 +218,20 @@ public void testNotFoundError() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertNotNull(exception.getMessage());
- assertTrue(exception.getMessage().contains("getStore")
- || exception.getMessage().contains("[get"));
- assertNotNull(exception.getApiErrorCode());
- assertNotNull(exception.getApiErrorMessage());
+ assertEquals(400, exception.getStatusCode());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getApiErrorMessage().contains("does not match regex pattern"));
assertNotNull(exception.getRequestId());
+ assertTrue(exception.getMessage().contains("HTTP 400"));
+ assertTrue(exception.getMessage().contains("[getStore]"));
}
@Test
public void testInvalidParameterException() throws Exception {
- ClientWriteRequest request = new ClientWriteRequest(); // Empty request, no writes or deletes
+ ClientWriteRequest request = new ClientWriteRequest();
- // This should throw FgaInvalidParameterException before making the API call
dev.openfga.sdk.errors.FgaInvalidParameterException exception =
assertThrows(dev.openfga.sdk.errors.FgaInvalidParameterException.class, () -> {
- // Trying to write with null storeId by creating a new client without storeId
ClientConfiguration config = new ClientConfiguration().apiUrl(openfga.getHttpEndpoint());
OpenFgaClient client = new OpenFgaClient(config);
client.write(request).get();
@@ -249,9 +243,7 @@ public void testInvalidParameterException() throws Exception {
}
@Test
- public void testAuthenticationError() throws Exception {
- // Test FgaApiAuthenticationError when store doesn't exist (403/401 from API)
- // Use a non-existent store ID to trigger authentication/authorization error
+ public void testInvalidStoreIdFormat() throws Exception {
ClientConfiguration badConfig =
new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("non-existent-store-id-12345");
OpenFgaClient badClient = new OpenFgaClient(badConfig);
@@ -266,23 +258,19 @@ public void testAuthenticationError() throws Exception {
badClient.write(request).get();
});
- // Could be NotFoundError (404) or AuthenticationError depending on API version
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- // Verify error details are populated
- assertNotNull(exception.getMessage());
- assertTrue(exception.getMessage().contains("[write]")
- || exception.getMessage().contains("write"));
- assertNotNull(exception.getApiErrorCode());
+ assertEquals(400, exception.getStatusCode());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getMessage().contains("[write]"));
+ assertTrue(exception.getMessage().contains("HTTP 400"));
+ assertTrue(exception.getMessage().contains("StoreId"));
assertNotNull(exception.getRequestId());
}
@Test
public void testReadOperationError() throws Exception {
- // Test that read operations also surface error details correctly
- // Read requires both user and object when object is a type prefix
- var readRequest = new dev.openfga.sdk.api.client.model.ClientReadRequest()
- ._object("invalid_type:"); // Type prefix without user will fail
+ var readRequest = new dev.openfga.sdk.api.client.model.ClientReadRequest()._object("invalid_type:");
ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
fga.read(readRequest).get();
@@ -290,17 +278,17 @@ public void testReadOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- // Verify operation name is "read"
- assertTrue("read".equals(exception.getOperationName())
- || exception.getMessage().contains("read"));
+ assertEquals("read", exception.getOperationName());
+ assertEquals(400, exception.getStatusCode());
assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getMessage().contains("[read]"));
+ assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@Test
public void testExpandOperationError() throws Exception {
- // Test that expand operations surface error details correctly
var expandRequest = new dev.openfga.sdk.api.client.model.ClientExpandRequest()
._object("invalid_type:readme")
.relation("viewer");
@@ -311,10 +299,11 @@ public void testExpandOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- // Verify operation name is "expand"
- assertTrue("expand".equals(exception.getOperationName())
- || exception.getMessage().contains("expand"));
+ assertEquals("expand", exception.getOperationName());
+ assertEquals(400, exception.getStatusCode());
assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.getMessage().contains("[expand]"));
+ assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@@ -353,8 +342,6 @@ public void testDifferentErrorCodesAreSurfaced() throws Exception {
@Test
public void testErrorMessageContainsOperationContext() throws Exception {
- // Verify that different operations have their names in the error message
-
ClientWriteRequest writeReq = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()._object("invalid:x").relation("r").user("user:x")));
@@ -362,9 +349,11 @@ public void testErrorMessageContainsOperationContext() throws Exception {
ExecutionException writeEx =
assertThrows(ExecutionException.class, () -> fga.write(writeReq).get());
FgaError writeError = assertInstanceOf(FgaError.class, writeEx.getCause());
- assertTrue(writeError.getMessage().contains("[write]"), "Write error should contain [write] in message");
+ assertEquals("write", writeError.getOperationName());
+ assertEquals(400, writeError.getStatusCode());
+ assertTrue(writeError.getMessage().contains("[write]"));
+ assertTrue(writeError.getMessage().contains("HTTP 400"));
- // Check operation
var checkReq = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
._object("invalid:x")
.relation("r")
@@ -373,9 +362,209 @@ public void testErrorMessageContainsOperationContext() throws Exception {
ExecutionException checkEx =
assertThrows(ExecutionException.class, () -> fga.check(checkReq).get());
FgaError checkError = assertInstanceOf(FgaError.class, checkEx.getCause());
- assertTrue(checkError.getMessage().contains("[check]"), "Check error should contain [check] in message");
+ assertEquals("check", checkError.getOperationName());
+ assertEquals(400, checkError.getStatusCode());
+ assertTrue(checkError.getMessage().contains("[check]"));
+ assertTrue(checkError.getMessage().contains("HTTP 400"));
- // Verify they have different operation names
assertNotEquals(writeError.getOperationName(), checkError.getOperationName());
}
+
+ // --- Tests for New Helper Methods ---
+
+ @Test
+ public void testIsValidationErrorHelper() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.isValidationError());
+ assertTrue(exception.isClientError());
+ assertFalse(exception.isServerError());
+ assertFalse(exception.isRetryable());
+ }
+
+ @Test
+ public void testIsNotFoundErrorHelper() throws Exception {
+ var tempStoreResponse = fga.createStore(new CreateStoreRequest().name("TempStoreForNotFoundTest"))
+ .get();
+ String tempStoreId = tempStoreResponse.getId();
+
+ ClientConfiguration tempConfig = new ClientConfiguration().apiUrl(openfga.getHttpEndpoint());
+ OpenFgaClient tempClient = new OpenFgaClient(tempConfig);
+ tempClient.setStoreId(tempStoreId);
+ tempClient.deleteStore().get();
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ tempClient.getStore().get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(404, exception.getStatusCode());
+ assertEquals("store_id_not_found", exception.getApiErrorCode());
+ assertTrue(exception.isNotFoundError());
+ assertTrue(exception.isClientError());
+ assertFalse(exception.isServerError());
+ assertFalse(exception.isRetryable());
+ assertFalse(exception.isValidationError());
+ assertTrue(exception.getMessage().contains("HTTP 404"));
+ assertTrue(exception.getMessage().contains("[getStore]"));
+ }
+
+ @Test
+ public void testIsClientErrorHelper() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertTrue(exception.isClientError());
+ assertFalse(exception.isServerError());
+ }
+
+ @Test
+ public void testErrorCategorizationHelpers() throws Exception {
+ ClientConfiguration badConfig =
+ new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("non-existent-store-id");
+ OpenFgaClient badClient = new OpenFgaClient(badConfig);
+
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("document:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ badClient.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertTrue(exception.isClientError());
+ assertTrue(exception.isValidationError());
+ assertFalse(exception.isServerError());
+ assertFalse(exception.isRetryable());
+ assertFalse(exception.isAuthenticationError());
+ assertFalse(exception.isNotFoundError());
+ }
+
+ @Test
+ public void testIsRetryableHelper() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertFalse(exception.isRetryable());
+ }
+
+ @Test
+ public void testHelperMethodsConsistency() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertTrue(exception.isClientError());
+ assertFalse(exception.isServerError());
+ assertTrue(exception.isValidationError());
+ assertFalse(exception.isRetryable());
+ }
+
+ @Test
+ public void testErrorCodeFieldsAccessibility() throws Exception {
+ ClientWriteRequest request = new ClientWriteRequest()
+ .writes(List.of(new ClientTupleKey()
+ ._object("invalid_type:readme")
+ .relation("viewer")
+ .user("user:anne")));
+
+ ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
+ fga.write(request).get();
+ });
+
+ FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
+
+ assertEquals(400, exception.getStatusCode());
+ assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals("write", exception.getOperationName());
+ assertNotNull(exception.getApiErrorMessage());
+ assertNotNull(exception.getRequestId());
+ assertTrue(exception.getRequestId().matches("[a-zA-Z0-9-]+"));
+ }
+
+ @Test
+ public void testMessageFormatConsistency() throws Exception {
+ ClientWriteRequest writeReq = new ClientWriteRequest()
+ .writes(List.of(
+ new ClientTupleKey()._object("invalid:x").relation("r").user("user:x")));
+
+ ExecutionException writeEx =
+ assertThrows(ExecutionException.class, () -> fga.write(writeReq).get());
+ FgaError writeError = assertInstanceOf(FgaError.class, writeEx.getCause());
+
+ var checkReq = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
+ ._object("invalid:x")
+ .relation("r")
+ .user("user:x");
+
+ ExecutionException checkEx =
+ assertThrows(ExecutionException.class, () -> fga.check(checkReq).get());
+ FgaError checkError = assertInstanceOf(FgaError.class, checkEx.getCause());
+
+ String writeMsg = writeError.getMessage();
+ String checkMsg = checkError.getMessage();
+
+ assertTrue(writeMsg.matches("\\[\\w+\\] HTTP \\d{3} .+"));
+ assertTrue(checkMsg.matches("\\[\\w+\\] HTTP \\d{3} .+"));
+
+ assertEquals(400, writeError.getStatusCode());
+ assertEquals(400, checkError.getStatusCode());
+ assertTrue(writeMsg.contains("[write]"));
+ assertTrue(writeMsg.contains("HTTP 400"));
+ assertTrue(writeMsg.contains("(validation_error)"));
+ assertTrue(writeMsg.contains("[request-id: "));
+
+ assertTrue(checkMsg.contains("[check]"));
+ assertTrue(checkMsg.contains("HTTP 400"));
+ assertTrue(checkMsg.contains("(validation_error)"));
+ assertTrue(checkMsg.contains("[request-id: "));
+ }
}
diff --git a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
index 8fcae337..45c112ed 100644
--- a/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
+++ b/src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
@@ -160,7 +160,9 @@ public void exchangeOAuth2TokenWithRetriesFailure(WireMockRuntimeInfo wm) throws
var exception = assertThrows(java.util.concurrent.ExecutionException.class, () -> auth0.getAccessToken()
.get());
- assertEquals("dev.openfga.sdk.errors.FgaApiRateLimitExceededError: exchangeToken", exception.getMessage());
+ assertTrue(exception.getMessage().contains("FgaApiRateLimitExceededError"));
+ assertTrue(exception.getMessage().contains("exchangeToken"));
+ assertTrue(exception.getMessage().contains("HTTP 429"));
verify(3, postRequestedFor(urlEqualTo("/oauth/token")));
}
From 13eadc9f4ce60c05dacb87eaec7c3757be66d7e8 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Wed, 26 Nov 2025 12:08:38 +0530
Subject: [PATCH 07/10] feat: address comments and use consts
---
.../java/dev/openfga/sdk/errors/FgaError.java | 30 +++-
.../sdk/errors/FgaErrorIntegrationTest.java | 143 ++++++++++--------
2 files changed, 100 insertions(+), 73 deletions(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index f1e1299c..1ebdb2f7 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -2,8 +2,12 @@
import static dev.openfga.sdk.errors.HttpStatusCode.*;
+import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import dev.openfga.sdk.api.configuration.Configuration;
import dev.openfga.sdk.api.configuration.CredentialsMethod;
import dev.openfga.sdk.constants.FgaConstants;
@@ -11,6 +15,7 @@
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.Optional;
+import org.openapitools.jackson.nullable.JsonNullableModule;
public class FgaError extends ApiException {
private String method = null;
@@ -24,10 +29,24 @@ public class FgaError extends ApiException {
private String apiErrorMessage = null;
private String operationName = null;
- private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+ private static final ObjectMapper OBJECT_MAPPER = createConfiguredObjectMapper();
+
+ private static ObjectMapper createConfiguredObjectMapper() {
+ ObjectMapper mapper = new ObjectMapper();
+ mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+ mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+ mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false);
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
+ mapper.enable(DeserializationFeature.READ_ENUMS_USING_TO_STRING);
+ mapper.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
+ mapper.registerModule(new JavaTimeModule());
+ mapper.registerModule(new JsonNullableModule());
+ return mapper;
+ }
@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)
- public static class ApiErrorResponse {
+ private static class ApiErrorResponse {
@com.fasterxml.jackson.annotation.JsonProperty("code")
private String code;
@@ -312,9 +331,7 @@ public boolean isRateLimitError() {
* @return true if this error is retryable
*/
public boolean isRetryable() {
- int status = getStatusCode();
- // 429 (Rate Limit) and 5xx (Server Errors) are typically retryable.
- return status == TOO_MANY_REQUESTS || (status >= 500 && status < 600);
+ return HttpStatusCode.isRetryable(getStatusCode());
}
/**
@@ -333,7 +350,6 @@ public boolean isClientError() {
* @return true if this is a 5xx error
*/
public boolean isServerError() {
- int status = getStatusCode();
- return status >= 500 && status < 600;
+ return HttpStatusCode.isServerError(getStatusCode());
}
}
diff --git a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
index dc33bd11..21b7aec3 100644
--- a/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
+++ b/src/test-integration/java/dev/openfga/sdk/errors/FgaErrorIntegrationTest.java
@@ -31,6 +31,17 @@ public class FgaErrorIntegrationTest {
private static final OpenFGAContainer openfga = new OpenFGAContainer("openfga/openfga:v1.10.2");
private static final ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
+
+ // Test constants
+ private static final String ERROR_CODE_VALIDATION_ERROR = "validation_error";
+ private static final String ERROR_CODE_STORE_ID_NOT_FOUND = "store_id_not_found";
+ private static final String OPERATION_WRITE = "write";
+ private static final String OPERATION_CHECK = "check";
+ private static final String OPERATION_READ = "read";
+ private static final String OPERATION_EXPAND = "expand";
+ private static final int HTTP_STATUS_BAD_REQUEST = 400;
+ private static final int HTTP_STATUS_NOT_FOUND = 404;
+
private String authModelJson;
private OpenFgaClient fga;
@@ -58,7 +69,7 @@ public void initializeApi() throws Exception {
}
@Test
- public void testWriteValidationError() throws Exception {
+ public void writeValidationError() throws Exception {
// Try to write a tuple with invalid type
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
@@ -72,14 +83,14 @@ public void testWriteValidationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
assertTrue(exception.getMessage().contains("type 'invalid_type' not found"));
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getApiErrorMessage().contains("type 'invalid_type' not found"));
- assertEquals("write", exception.getOperationName());
+ assertEquals(OPERATION_WRITE, exception.getOperationName());
assertNotNull(exception.getRequestId());
}
@Test
- public void testWriteValidationErrorWithInvalidRelation() throws Exception {
+ public void writeValidationErrorWithInvalidRelation() throws Exception {
// Try to write a tuple with valid type but invalid relation
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
@@ -95,14 +106,14 @@ public void testWriteValidationErrorWithInvalidRelation() throws Exception {
// Verify the formatted message includes operation name and API error details
assertTrue(exception.getMessage().contains("write"));
assertTrue(exception.getMessage().contains("relation 'document#invalid_relation' not found"));
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertNotNull(exception.getApiErrorMessage());
- assertEquals("write", exception.getOperationName());
+ assertEquals(OPERATION_WRITE, exception.getOperationName());
assertNotNull(exception.getRequestId());
}
@Test
- public void testErrorMessageFormattingWithAllFields() throws Exception {
+ public void errorMessageFormattingWithAllFields() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -119,19 +130,19 @@ public void testErrorMessageFormattingWithAllFields() throws Exception {
assertTrue(message.startsWith("[write]"));
assertTrue(message.contains("HTTP 400"));
assertTrue(message.contains("type 'invalid_type' not found"));
- assertTrue(message.contains("(validation_error)"));
+ assertTrue(message.contains("(" + ERROR_CODE_VALIDATION_ERROR + ")"));
assertTrue(message.contains("[request-id: "));
assertTrue(message.endsWith("]"));
- assertEquals("write", exception.getOperationName());
- assertEquals("validation_error", exception.getApiErrorCode());
- assertEquals(400, exception.getStatusCode());
+ assertEquals(OPERATION_WRITE, exception.getOperationName());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
}
@Test
- public void testCheckValidationError() throws Exception {
+ public void checkValidationError() throws Exception {
var checkRequest = new dev.openfga.sdk.api.client.model.ClientCheckRequest()
._object("invalid_type:readme")
.relation("viewer")
@@ -143,9 +154,9 @@ public void testCheckValidationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- assertEquals("check", exception.getOperationName());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(OPERATION_CHECK, exception.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getMessage().contains("[check]"));
assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
@@ -153,7 +164,7 @@ public void testCheckValidationError() throws Exception {
}
@Test
- public void testErrorDetailsAreNotLostInStackTrace() throws Exception {
+ public void errorDetailsAreNotLostInStackTrace() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -172,15 +183,15 @@ public void testErrorDetailsAreNotLostInStackTrace() throws Exception {
String errorMessage = exception.getMessage();
assertTrue(errorMessage.contains("[write]"));
assertTrue(errorMessage.contains("HTTP 400"));
- assertTrue(errorMessage.contains("validation_error"));
+ assertTrue(errorMessage.contains(ERROR_CODE_VALIDATION_ERROR));
- assertEquals(400, exception.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
assertNotNull(exception.getResponseData());
}
}
@Test
- public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
+ public void multipleTupleErrorsShowDetailedMessage() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()
@@ -198,8 +209,8 @@ public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getMessage().contains("[write]"));
assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
@@ -207,7 +218,7 @@ public void testMultipleTupleErrorsShowDetailedMessage() throws Exception {
}
@Test
- public void testGetStoreWithInvalidStoreIdFormat() throws Exception {
+ public void getStoreWithInvalidStoreIdFormat() throws Exception {
ClientConfiguration badConfig =
new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("01HVJPQR3TXYZ9NQXABCDEFGHI");
OpenFgaClient badClient = new OpenFgaClient(badConfig);
@@ -218,8 +229,8 @@ public void testGetStoreWithInvalidStoreIdFormat() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getApiErrorMessage().contains("does not match regex pattern"));
assertNotNull(exception.getRequestId());
assertTrue(exception.getMessage().contains("HTTP 400"));
@@ -227,7 +238,7 @@ public void testGetStoreWithInvalidStoreIdFormat() throws Exception {
}
@Test
- public void testInvalidParameterException() throws Exception {
+ public void invalidParameterException() throws Exception {
ClientWriteRequest request = new ClientWriteRequest();
dev.openfga.sdk.errors.FgaInvalidParameterException exception =
@@ -243,7 +254,7 @@ public void testInvalidParameterException() throws Exception {
}
@Test
- public void testInvalidStoreIdFormat() throws Exception {
+ public void invalidStoreIdFormat() throws Exception {
ClientConfiguration badConfig =
new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("non-existent-store-id-12345");
OpenFgaClient badClient = new OpenFgaClient(badConfig);
@@ -260,8 +271,8 @@ public void testInvalidStoreIdFormat() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getMessage().contains("[write]"));
assertTrue(exception.getMessage().contains("HTTP 400"));
assertTrue(exception.getMessage().contains("StoreId"));
@@ -269,7 +280,7 @@ public void testInvalidStoreIdFormat() throws Exception {
}
@Test
- public void testReadOperationError() throws Exception {
+ public void readOperationError() throws Exception {
var readRequest = new dev.openfga.sdk.api.client.model.ClientReadRequest()._object("invalid_type:");
ExecutionException executionException = assertThrows(ExecutionException.class, () -> {
@@ -278,9 +289,9 @@ public void testReadOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- assertEquals("read", exception.getOperationName());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(OPERATION_READ, exception.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getMessage().contains("[read]"));
assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
@@ -288,7 +299,7 @@ public void testReadOperationError() throws Exception {
}
@Test
- public void testExpandOperationError() throws Exception {
+ public void expandOperationError() throws Exception {
var expandRequest = new dev.openfga.sdk.api.client.model.ClientExpandRequest()
._object("invalid_type:readme")
.relation("viewer");
@@ -299,9 +310,9 @@ public void testExpandOperationError() throws Exception {
FgaApiValidationError exception = assertInstanceOf(FgaApiValidationError.class, executionException.getCause());
- assertEquals("expand", exception.getOperationName());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(OPERATION_EXPAND, exception.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.getMessage().contains("[expand]"));
assertTrue(exception.getMessage().contains("HTTP 400"));
assertNotNull(exception.getApiErrorMessage());
@@ -309,7 +320,7 @@ public void testExpandOperationError() throws Exception {
}
@Test
- public void testDifferentErrorCodesAreSurfaced() throws Exception {
+ public void differentErrorCodesAreSurfaced() throws Exception {
// Test that different validation_error subcases are properly surfaced
// Case 1: Invalid type
ClientWriteRequest request1 = new ClientWriteRequest()
@@ -341,7 +352,7 @@ public void testDifferentErrorCodesAreSurfaced() throws Exception {
}
@Test
- public void testErrorMessageContainsOperationContext() throws Exception {
+ public void errorMessageContainsOperationContext() throws Exception {
ClientWriteRequest writeReq = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()._object("invalid:x").relation("r").user("user:x")));
@@ -349,8 +360,8 @@ public void testErrorMessageContainsOperationContext() throws Exception {
ExecutionException writeEx =
assertThrows(ExecutionException.class, () -> fga.write(writeReq).get());
FgaError writeError = assertInstanceOf(FgaError.class, writeEx.getCause());
- assertEquals("write", writeError.getOperationName());
- assertEquals(400, writeError.getStatusCode());
+ assertEquals(OPERATION_WRITE, writeError.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, writeError.getStatusCode());
assertTrue(writeError.getMessage().contains("[write]"));
assertTrue(writeError.getMessage().contains("HTTP 400"));
@@ -362,8 +373,8 @@ public void testErrorMessageContainsOperationContext() throws Exception {
ExecutionException checkEx =
assertThrows(ExecutionException.class, () -> fga.check(checkReq).get());
FgaError checkError = assertInstanceOf(FgaError.class, checkEx.getCause());
- assertEquals("check", checkError.getOperationName());
- assertEquals(400, checkError.getStatusCode());
+ assertEquals(OPERATION_CHECK, checkError.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, checkError.getStatusCode());
assertTrue(checkError.getMessage().contains("[check]"));
assertTrue(checkError.getMessage().contains("HTTP 400"));
@@ -373,7 +384,7 @@ public void testErrorMessageContainsOperationContext() throws Exception {
// --- Tests for New Helper Methods ---
@Test
- public void testIsValidationErrorHelper() throws Exception {
+ public void isValidationErrorHelper() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -386,8 +397,8 @@ public void testIsValidationErrorHelper() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.isValidationError());
assertTrue(exception.isClientError());
assertFalse(exception.isServerError());
@@ -395,7 +406,7 @@ public void testIsValidationErrorHelper() throws Exception {
}
@Test
- public void testIsNotFoundErrorHelper() throws Exception {
+ public void isNotFoundErrorHelper() throws Exception {
var tempStoreResponse = fga.createStore(new CreateStoreRequest().name("TempStoreForNotFoundTest"))
.get();
String tempStoreId = tempStoreResponse.getId();
@@ -411,8 +422,8 @@ public void testIsNotFoundErrorHelper() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(404, exception.getStatusCode());
- assertEquals("store_id_not_found", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_NOT_FOUND, exception.getStatusCode());
+ assertEquals(ERROR_CODE_STORE_ID_NOT_FOUND, exception.getApiErrorCode());
assertTrue(exception.isNotFoundError());
assertTrue(exception.isClientError());
assertFalse(exception.isServerError());
@@ -423,7 +434,7 @@ public void testIsNotFoundErrorHelper() throws Exception {
}
@Test
- public void testIsClientErrorHelper() throws Exception {
+ public void isClientErrorHelper() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -436,13 +447,13 @@ public void testIsClientErrorHelper() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
assertTrue(exception.isClientError());
assertFalse(exception.isServerError());
}
@Test
- public void testErrorCategorizationHelpers() throws Exception {
+ public void errorCategorizationHelpers() throws Exception {
ClientConfiguration badConfig =
new ClientConfiguration().apiUrl(openfga.getHttpEndpoint()).storeId("non-existent-store-id");
OpenFgaClient badClient = new OpenFgaClient(badConfig);
@@ -459,8 +470,8 @@ public void testErrorCategorizationHelpers() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
assertTrue(exception.isClientError());
assertTrue(exception.isValidationError());
assertFalse(exception.isServerError());
@@ -470,7 +481,7 @@ public void testErrorCategorizationHelpers() throws Exception {
}
@Test
- public void testIsRetryableHelper() throws Exception {
+ public void isRetryableHelper() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -483,12 +494,12 @@ public void testIsRetryableHelper() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
assertFalse(exception.isRetryable());
}
@Test
- public void testHelperMethodsConsistency() throws Exception {
+ public void helperMethodsConsistency() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -501,7 +512,7 @@ public void testHelperMethodsConsistency() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
assertTrue(exception.isClientError());
assertFalse(exception.isServerError());
assertTrue(exception.isValidationError());
@@ -509,7 +520,7 @@ public void testHelperMethodsConsistency() throws Exception {
}
@Test
- public void testErrorCodeFieldsAccessibility() throws Exception {
+ public void errorCodeFieldsAccessibility() throws Exception {
ClientWriteRequest request = new ClientWriteRequest()
.writes(List.of(new ClientTupleKey()
._object("invalid_type:readme")
@@ -522,16 +533,16 @@ public void testErrorCodeFieldsAccessibility() throws Exception {
FgaError exception = assertInstanceOf(FgaError.class, executionException.getCause());
- assertEquals(400, exception.getStatusCode());
- assertEquals("validation_error", exception.getApiErrorCode());
- assertEquals("write", exception.getOperationName());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, exception.getStatusCode());
+ assertEquals(ERROR_CODE_VALIDATION_ERROR, exception.getApiErrorCode());
+ assertEquals(OPERATION_WRITE, exception.getOperationName());
assertNotNull(exception.getApiErrorMessage());
assertNotNull(exception.getRequestId());
assertTrue(exception.getRequestId().matches("[a-zA-Z0-9-]+"));
}
@Test
- public void testMessageFormatConsistency() throws Exception {
+ public void messageFormatConsistency() throws Exception {
ClientWriteRequest writeReq = new ClientWriteRequest()
.writes(List.of(
new ClientTupleKey()._object("invalid:x").relation("r").user("user:x")));
@@ -555,16 +566,16 @@ public void testMessageFormatConsistency() throws Exception {
assertTrue(writeMsg.matches("\\[\\w+\\] HTTP \\d{3} .+"));
assertTrue(checkMsg.matches("\\[\\w+\\] HTTP \\d{3} .+"));
- assertEquals(400, writeError.getStatusCode());
- assertEquals(400, checkError.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, writeError.getStatusCode());
+ assertEquals(HTTP_STATUS_BAD_REQUEST, checkError.getStatusCode());
assertTrue(writeMsg.contains("[write]"));
assertTrue(writeMsg.contains("HTTP 400"));
- assertTrue(writeMsg.contains("(validation_error)"));
+ assertTrue(writeMsg.contains("(" + ERROR_CODE_VALIDATION_ERROR + ")"));
assertTrue(writeMsg.contains("[request-id: "));
assertTrue(checkMsg.contains("[check]"));
assertTrue(checkMsg.contains("HTTP 400"));
- assertTrue(checkMsg.contains("(validation_error)"));
+ assertTrue(checkMsg.contains("(" + ERROR_CODE_VALIDATION_ERROR + ")"));
assertTrue(checkMsg.contains("[request-id: "));
}
}
From 26ec73f962f321b61e03c54a93b9352dec72f7b1 Mon Sep 17 00:00:00 2001
From: Anurag Bandyopadhyay
Date: Wed, 26 Nov 2025 14:38:00 +0530
Subject: [PATCH 08/10] feat: apply changelog suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 45894923..f2a2095a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.9.3...HEAD)
+### Changed
- Improved error handling and integration test coverage for FgaError and related classes. (#260)
## v0.9.3
From bef34e9a45bc968c16ac5401c64498f2e3474adf Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Fri, 28 Nov 2025 14:56:43 +0530
Subject: [PATCH 09/10] feat: fallback to using resp body as err msg when
parsing fails
---
src/main/java/dev/openfga/sdk/errors/FgaError.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 1ebdb2f7..37bd59f1 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -145,8 +145,8 @@ public static Optional getError(
error.setApiErrorCode(resp.getCode());
error.setApiErrorMessage(resp.getMessage());
} catch (JsonProcessingException e) {
- // Fall back, do nothing - log the exception for debugging
- System.err.println("Failed to parse API error response JSON: " + e.getMessage());
+ // Fall back to using raw response body as error message if parsing fails
+ error.setApiErrorMessage(body);
}
}
From cdb089c212cd1411fb98a37cde21aee1fe2a2902 Mon Sep 17 00:00:00 2001
From: SoulPancake
Date: Fri, 28 Nov 2025 15:14:21 +0530
Subject: [PATCH 10/10] fix: use unknown err code and msg raw resp
---
.../java/dev/openfga/sdk/errors/FgaError.java | 17 +++++++++++++++--
1 file changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/main/java/dev/openfga/sdk/errors/FgaError.java b/src/main/java/dev/openfga/sdk/errors/FgaError.java
index 37bd59f1..ef23ef9f 100644
--- a/src/main/java/dev/openfga/sdk/errors/FgaError.java
+++ b/src/main/java/dev/openfga/sdk/errors/FgaError.java
@@ -18,6 +18,8 @@
import org.openapitools.jackson.nullable.JsonNullableModule;
public class FgaError extends ApiException {
+ private static final String UNKNOWN_ERROR_CODE = "unknown_error";
+
private String method = null;
private String requestUrl = null;
private String clientId = null;
@@ -145,8 +147,9 @@ public static Optional getError(
error.setApiErrorCode(resp.getCode());
error.setApiErrorMessage(resp.getMessage());
} catch (JsonProcessingException e) {
- // Fall back to using raw response body as error message if parsing fails
- error.setApiErrorMessage(body);
+ // Wrap unparseable response
+ error.setApiErrorCode(UNKNOWN_ERROR_CODE);
+ error.setApiErrorMessage("Unable to parse error response. Raw response: " + body);
}
}
@@ -297,6 +300,16 @@ public boolean isValidationError() {
return "validation_error".equals(apiErrorCode);
}
+ /**
+ * Checks if this is an unknown error due to unparseable response.
+ * This occurs when the error response could not be parsed as JSON.
+ *
+ * @return true if this is an unknown error
+ */
+ public boolean isUnknownError() {
+ return UNKNOWN_ERROR_CODE.equals(apiErrorCode);
+ }
+
/**
* Checks if this is a not found (404) error.
*