Skip to content

Commit b5cc8c1

Browse files
committed
feat: address copilot comments
1 parent 7a2c8bd commit b5cc8c1

2 files changed

Lines changed: 118 additions & 29 deletions

File tree

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
11
package dev.openfga.sdk.errors;
22

33
import com.fasterxml.jackson.databind.JsonNode;
4-
import com.fasterxml.jackson.databind.ObjectMapper;
54
import java.net.http.HttpHeaders;
65

76
public class FgaApiValidationError extends FgaError {
8-
private static final ObjectMapper MAPPER = new ObjectMapper();
7+
8+
// String prefixes for parsing error messages
9+
private static final String RELATION_PREFIX = "relation '";
10+
private static final String TYPE_PREFIX = "type '";
11+
private static final String CHECK_REQUEST_TUPLE_KEY_PREFIX = "CheckRequestTupleKey.";
12+
private static final String TUPLE_KEY_PREFIX = "TupleKey.";
13+
private static final String QUOTE_SUFFIX = "'";
14+
private static final String NOT_FOUND_SUFFIX = "' not found";
15+
private static final String MUST_NOT_BE_EMPTY = "must not be empty";
916

1017
private String invalidField;
1118
private String invalidValue;
12-
private String expectedFormat;
1319

1420
public FgaApiValidationError(
1521
String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
@@ -23,22 +29,28 @@ public FgaApiValidationError(String message, int code, HttpHeaders responseHeade
2329
}
2430

2531
/**
26-
* Try to extract specific validation details from the error message
32+
* Try to extract specific validation details from the error message.
33+
* <p>
34+
* This parsing is best-effort and based on current OpenFGA API error message formats.
35+
* If the message format changes or doesn't match expected patterns, fields will be null.
36+
* The application should not rely on these fields for critical logic.
37+
*
38+
* @param responseBody The API error response body
2739
*/
2840
private void parseValidationDetails(String responseBody) {
2941
if (responseBody == null || responseBody.trim().isEmpty()) {
3042
return;
3143
}
3244

3345
try {
34-
JsonNode root = MAPPER.readTree(responseBody);
46+
JsonNode root = getErrorMapper().readTree(responseBody);
3547
String message = root.has("message") ? root.get("message").asText() : null;
3648

3749
if (message != null) {
3850
// Parse patterns like: "relation 'document#invalid_relation' not found"
39-
if (message.contains("relation '") && message.contains("' not found")) {
40-
int start = message.indexOf("relation '") + 10;
41-
int end = message.indexOf("'", start);
51+
if (message.contains(RELATION_PREFIX) && message.contains(NOT_FOUND_SUFFIX)) {
52+
int start = message.indexOf(RELATION_PREFIX) + RELATION_PREFIX.length();
53+
int end = message.indexOf(QUOTE_SUFFIX, start);
4254
if (end > start) {
4355
this.invalidField = "relation";
4456
this.invalidValue = message.substring(start, end);
@@ -47,9 +59,9 @@ private void parseValidationDetails(String responseBody) {
4759
}
4860
}
4961
// Parse patterns like: "type 'invalid_type' not found"
50-
else if (message.contains("type '") && message.contains("' not found")) {
51-
int start = message.indexOf("type '") + 6;
52-
int end = message.indexOf("'", start);
62+
else if (message.contains(TYPE_PREFIX) && message.contains(NOT_FOUND_SUFFIX)) {
63+
int start = message.indexOf(TYPE_PREFIX) + TYPE_PREFIX.length();
64+
int end = message.indexOf(QUOTE_SUFFIX, start);
5365
if (end > start) {
5466
this.invalidField = "type";
5567
this.invalidValue = message.substring(start, end);
@@ -58,26 +70,27 @@ else if (message.contains("type '") && message.contains("' not found")) {
5870
}
5971
}
6072
// Parse patterns like: "invalid CheckRequestTupleKey.User: value does not match regex..."
61-
else if (message.contains("invalid CheckRequestTupleKey.")) {
62-
int start = message.indexOf("CheckRequestTupleKey.") + 21;
73+
else if (message.contains(CHECK_REQUEST_TUPLE_KEY_PREFIX)) {
74+
int start =
75+
message.indexOf(CHECK_REQUEST_TUPLE_KEY_PREFIX) + CHECK_REQUEST_TUPLE_KEY_PREFIX.length();
6376
int end = message.indexOf(":", start);
6477
if (end > start) {
6578
this.invalidField = message.substring(start, end);
6679
addMetadata("invalid_field", invalidField);
6780
}
6881
}
6982
// Parse patterns like: "invalid TupleKey.User: value does not match regex..."
70-
else if (message.contains("invalid TupleKey.")) {
71-
int start = message.indexOf("TupleKey.") + 9;
83+
else if (message.contains(TUPLE_KEY_PREFIX)) {
84+
int start = message.indexOf(TUPLE_KEY_PREFIX) + TUPLE_KEY_PREFIX.length();
7285
int end = message.indexOf(":", start);
7386
if (end > start) {
7487
this.invalidField = message.substring(start, end);
7588
addMetadata("invalid_field", invalidField);
7689
}
7790
}
7891
// Parse patterns like: "object must not be empty"
79-
else if (message.contains("must not be empty")) {
80-
String[] parts = message.split(" ");
92+
else if (message.contains(MUST_NOT_BE_EMPTY)) {
93+
String[] parts = message.trim().split("\\s+");
8194
if (parts.length > 0 && !parts[0].isEmpty()) {
8295
this.invalidField = parts[0];
8396
addMetadata("invalid_field", invalidField);
@@ -89,15 +102,21 @@ else if (message.contains("must not be empty")) {
89102
}
90103
}
91104

105+
/**
106+
* Gets the field name that failed validation, if it could be parsed from the error message.
107+
*
108+
* @return The invalid field name (e.g., "relation", "type", "User"), or null if not parsed
109+
*/
92110
public String getInvalidField() {
93111
return invalidField;
94112
}
95113

114+
/**
115+
* Gets the invalid value that caused the validation error, if available.
116+
*
117+
* @return The invalid value, or null if not parsed from the error message
118+
*/
96119
public String getInvalidValue() {
97120
return invalidValue;
98121
}
99-
100-
public String getExpectedFormat() {
101-
return expectedFormat;
102-
}
103122
}

src/main/java/dev/openfga/sdk/errors/FgaError.java

Lines changed: 78 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
import java.util.Optional;
1616

1717
public class FgaError extends ApiException {
18+
/**
19+
* Shared ObjectMapper instance for parsing error responses.
20+
* ObjectMapper is thread-safe for read operations (parsing JSON).
21+
* This instance is shared across all error classes to reduce memory overhead.
22+
*/
1823
private static final ObjectMapper ERROR_MAPPER = new ObjectMapper();
1924

2025
private String method = null;
@@ -27,7 +32,14 @@ public class FgaError extends ApiException {
2732
private String apiErrorMessage = null;
2833
private String operationName = null;
2934
private String retryAfterHeader = null;
30-
private Map<String, Object> metadata = null;
35+
36+
/**
37+
* Metadata map for additional error context.
38+
* <p>
39+
* Note: Error instances follow a single-threaded lifecycle (create → populate → throw → catch).
40+
* They are not shared between threads, so thread-safety is not required.
41+
*/
42+
private final Map<String, Object> metadata = new HashMap<>();
3143

3244
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
3345
super(message, cause, code, responseHeaders, responseBody);
@@ -176,6 +188,11 @@ public void setMethod(String method) {
176188
this.method = method;
177189
}
178190

191+
/**
192+
* Gets the HTTP method used for the request that caused this error.
193+
*
194+
* @return The HTTP method (e.g., "GET", "POST"), or null if not set
195+
*/
179196
public String getMethod() {
180197
return method;
181198
}
@@ -184,6 +201,11 @@ public void setRequestUrl(String requestUrl) {
184201
this.requestUrl = requestUrl;
185202
}
186203

204+
/**
205+
* Gets the API URL for the request that caused this error.
206+
*
207+
* @return The request URL, or null if not set
208+
*/
187209
public String getRequestUrl() {
188210
return requestUrl;
189211
}
@@ -192,6 +214,11 @@ public void setClientId(String clientId) {
192214
this.clientId = clientId;
193215
}
194216

217+
/**
218+
* Gets the OAuth2 client ID used in the request, if client credentials authentication was used.
219+
*
220+
* @return The client ID, or null if not using client credentials or not set
221+
*/
195222
public String getClientId() {
196223
return clientId;
197224
}
@@ -200,6 +227,11 @@ public void setAudience(String audience) {
200227
this.audience = audience;
201228
}
202229

230+
/**
231+
* Gets the OAuth2 audience used in the request, if client credentials authentication was used.
232+
*
233+
* @return The audience, or null if not using client credentials or not set
234+
*/
203235
public String getAudience() {
204236
return audience;
205237
}
@@ -208,6 +240,11 @@ public void setGrantType(String grantType) {
208240
this.grantType = grantType;
209241
}
210242

243+
/**
244+
* Gets the OAuth2 grant type used in the request.
245+
*
246+
* @return The grant type, or null if not set
247+
*/
211248
public String getGrantType() {
212249
return grantType;
213250
}
@@ -216,6 +253,11 @@ public void setRequestId(String requestId) {
216253
this.requestId = requestId;
217254
}
218255

256+
/**
257+
* Gets the request ID from the response headers, useful for debugging and support.
258+
*
259+
* @return The request ID (from X-Request-Id header), or null if not present
260+
*/
219261
public String getRequestId() {
220262
return requestId;
221263
}
@@ -224,6 +266,11 @@ public void setApiErrorCode(String apiErrorCode) {
224266
this.apiErrorCode = apiErrorCode;
225267
}
226268

269+
/**
270+
* Gets the error code returned by the API in the response body.
271+
*
272+
* @return The API error code, or null if not available in the response
273+
*/
227274
public String getApiErrorCode() {
228275
return apiErrorCode;
229276
}
@@ -241,6 +288,11 @@ public void setRetryAfterHeader(String retryAfterHeader) {
241288
this.retryAfterHeader = retryAfterHeader;
242289
}
243290

291+
/**
292+
* Gets the Retry-After header value from rate limit responses.
293+
*
294+
* @return The Retry-After header value (in seconds or HTTP date), or null if not present
295+
*/
244296
public String getRetryAfterHeader() {
245297
return retryAfterHeader;
246298
}
@@ -249,6 +301,11 @@ public void setApiErrorMessage(String apiErrorMessage) {
249301
this.apiErrorMessage = apiErrorMessage;
250302
}
251303

304+
/**
305+
* Gets the error message parsed from the API response body.
306+
*
307+
* @return The API error message, or null if not available in the response
308+
*/
252309
public String getApiErrorMessage() {
253310
return apiErrorMessage;
254311
}
@@ -257,25 +314,38 @@ public void setOperationName(String operationName) {
257314
this.operationName = operationName;
258315
}
259316

317+
/**
318+
* Gets the operation name that resulted in this error.
319+
*
320+
* @return The operation name (e.g., "check", "write"), or null if not set
321+
*/
260322
public String getOperationName() {
261323
return operationName;
262324
}
263325

264-
public void setMetadata(Map<String, Object> metadata) {
265-
this.metadata = metadata;
266-
}
267-
326+
/**
327+
* Gets the metadata map containing additional error context.
328+
*
329+
* @return A map of metadata key-value pairs (never null)
330+
*/
268331
public Map<String, Object> getMetadata() {
269-
if (metadata == null) {
270-
metadata = new HashMap<>();
271-
}
272332
return metadata;
273333
}
274334

275335
public void addMetadata(String key, Object value) {
276336
getMetadata().put(key, value);
277337
}
278338

339+
/**
340+
* Provides access to the shared ObjectMapper for subclasses.
341+
* This mapper is thread-safe for read operations.
342+
*
343+
* @return The shared ObjectMapper instance
344+
*/
345+
protected static ObjectMapper getErrorMapper() {
346+
return ERROR_MAPPER;
347+
}
348+
279349
/**
280350
* Override getMessage() to return the actual API error message
281351
* instead of the generic operation name.

0 commit comments

Comments
 (0)