Skip to content

Commit 2042e47

Browse files
committed
feat: improve error messaging
feat: parse error msg feat: update changelog Removed two feature entries from the unreleased section. feat: set operation name and err msg
1 parent ae02d65 commit 2042e47

File tree

3 files changed

+601
-16
lines changed

3 files changed

+601
-16
lines changed

CHANGELOG.md

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,7 @@
11
# Changelog
22

3-
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.9.3...HEAD)
4-
5-
## v0.9.3
6-
7-
### [0.9.3](https://github.com/openfga/java-sdk/compare/v0.9.2...v0.9.3)) (2025-11-10)
8-
9-
### Fixed
10-
11-
- fix: preserve response headers in transaction write operations (#254)
3+
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.9.2...HEAD)
4+
- feat: Improve error messaging by parsing error details from resp bodies (#256)
125

136
## v0.9.2
147

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

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import static dev.openfga.sdk.errors.HttpStatusCode.*;
44

5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.fasterxml.jackson.databind.ObjectMapper;
57
import dev.openfga.sdk.api.configuration.Configuration;
68
import dev.openfga.sdk.api.configuration.CredentialsMethod;
79
import dev.openfga.sdk.constants.FgaConstants;
@@ -11,13 +13,17 @@
1113
import java.util.Optional;
1214

1315
public class FgaError extends ApiException {
16+
private static final ObjectMapper ERROR_MAPPER = new ObjectMapper();
17+
1418
private String method = null;
1519
private String requestUrl = null;
1620
private String clientId = null;
1721
private String audience = null;
1822
private String grantType = null;
1923
private String requestId = null;
2024
private String apiErrorCode = null;
25+
private String apiErrorMessage = null;
26+
private String operationName = null;
2127
private String retryAfterHeader = null;
2228

2329
public FgaError(String message, Throwable cause, int code, HttpHeaders responseHeaders, String responseBody) {
@@ -28,6 +34,61 @@ public FgaError(String message, int code, HttpHeaders responseHeaders, String re
2834
super(message, code, responseHeaders, responseBody);
2935
}
3036

37+
/**
38+
* Parse the API error response body to extract the error message and code.
39+
* @param methodName The API method name that was called
40+
* @param responseBody The response body JSON string
41+
* @return A descriptive error message
42+
*/
43+
private static String parseErrorMessage(String methodName, String responseBody) {
44+
if (responseBody == null || responseBody.trim().isEmpty()) {
45+
return methodName;
46+
}
47+
48+
try {
49+
JsonNode jsonNode = ERROR_MAPPER.readTree(responseBody);
50+
51+
// Try to extract message field
52+
JsonNode messageNode = jsonNode.get("message");
53+
String message = (messageNode != null && !messageNode.isNull()) ? messageNode.asText() : null;
54+
55+
// If we have a message, return it, otherwise fall back to method name
56+
if (message != null && !message.trim().isEmpty()) {
57+
return message;
58+
}
59+
} catch (Exception e) {
60+
// If parsing fails, fall back to the method name
61+
// This is intentional to ensure errors are still reported even if the response format is unexpected
62+
}
63+
64+
return methodName;
65+
}
66+
67+
/**
68+
* Extract the API error code from the response body.
69+
* @param responseBody The response body JSON string
70+
* @return The error code, or null if not found
71+
*/
72+
private static String extractErrorCode(String responseBody) {
73+
if (responseBody == null || responseBody.trim().isEmpty()) {
74+
return null;
75+
}
76+
77+
try {
78+
JsonNode jsonNode = ERROR_MAPPER.readTree(responseBody);
79+
80+
JsonNode codeNode = jsonNode.get("code");
81+
if (codeNode != null && !codeNode.isNull()) {
82+
return codeNode.asText();
83+
}
84+
} catch (Exception e) {
85+
// If parsing fails, return null
86+
// This is intentional - we still want to report the error even if we can't extract the code
87+
}
88+
89+
return null;
90+
}
91+
3192
public static Optional<FgaError> getError(
3293
String name,
3394
HttpRequest request,
@@ -43,25 +104,54 @@ public static Optional<FgaError> getError(
43104

44105
final String body = response.body();
45106
final var headers = response.headers();
107+
108+
// Parse the error message from the response body
109+
final String errorMessage = parseErrorMessage(name, body);
46110
final FgaError error;
47111

48112
if (status == BAD_REQUEST || status == UNPROCESSABLE_ENTITY) {
49-
error = new FgaApiValidationError(name, previousError, status, headers, body);
113+
error = new FgaApiValidationError(errorMessage, previousError, status, headers, body);
50114
} else if (status == UNAUTHORIZED || status == FORBIDDEN) {
51-
error = new FgaApiAuthenticationError(name, previousError, status, headers, body);
115+
error = new FgaApiAuthenticationError(errorMessage, previousError, status, headers, body);
52116
} else if (status == NOT_FOUND) {
53-
error = new FgaApiNotFoundError(name, previousError, status, headers, body);
117+
error = new FgaApiNotFoundError(errorMessage, previousError, status, headers, body);
54118
} else if (status == TOO_MANY_REQUESTS) {
55-
error = new FgaApiRateLimitExceededError(name, previousError, status, headers, body);
119+
error = new FgaApiRateLimitExceededError(errorMessage, previousError, status, headers, body);
56120
} else if (isServerError(status)) {
57-
error = new FgaApiInternalError(name, previousError, status, headers, body);
121+
error = new FgaApiInternalError(errorMessage, previousError, status, headers, body);
58122
} else {
59-
error = new FgaError(name, previousError, status, headers, body);
123+
error = new FgaError(errorMessage, previousError, status, headers, body);
60124
}
61125

62126
error.setMethod(request.method());
63127
error.setRequestUrl(configuration.getApiUrl());
64128

129+
// Set the operation name
130+
error.setOperationName(name);
131+
132+
// Extract and set API error code from response body
133+
String apiErrorCode = extractErrorCode(body);
134+
if (apiErrorCode != null) {
135+
error.setApiErrorCode(apiErrorCode);
136+
}
137+
138+
// Set the API error message (same as what was parsed for the constructor)
139+
// This allows getMessage() to return a formatted version
140+
if (!errorMessage.equals(name)) {
141+
// Only set apiErrorMessage if we actually got a message from the API
142+
// (not just falling back to the operation name)
143+
error.setApiErrorMessage(errorMessage);
144+
}
145+
146+
// Extract and set request ID from response headers if present
147+
// Common request ID header names
148+
Optional<String> requestId = headers.firstValue("X-Request-Id")
149+
.or(() -> headers.firstValue("x-request-id"))
150+
.or(() -> headers.firstValue("Request-Id"));
151+
if (requestId.isPresent()) {
152+
error.setRequestId(requestId.get());
153+
}
154+
65155
// Extract and set Retry-After header if present
66156
Optional<String> retryAfter = headers.firstValue(FgaConstants.RETRY_AFTER_HEADER_NAME);
67157
if (retryAfter.isPresent()) {
@@ -135,11 +225,36 @@ public String getApiErrorCode() {
135225
return apiErrorCode;
136226
}
137227

228+
/**
229+
* Get the API error code.
230+
* This is an alias for getApiErrorCode() for convenience.
231+
* @return The API error code from the response
232+
*/
233+
public String getCode() {
234+
return apiErrorCode;
235+
}
236+
138237
public void setRetryAfterHeader(String retryAfterHeader) {
139238
this.retryAfterHeader = retryAfterHeader;
140239
}
141240

142241
public String getRetryAfterHeader() {
143242
return retryAfterHeader;
144243
}
145-
}
244+
245+
public void setApiErrorMessage(String apiErrorMessage) {
246+
this.apiErrorMessage = apiErrorMessage;
247+
}
248+
249+
public String getApiErrorMessage() {
250+
return apiErrorMessage;
251+
}
252+
253+
public void setOperationName(String operationName) {
254+
this.operationName = operationName;
255+
}
256+
257+
public String getOperationName() {
258+
return operationName;
259+
}
260+
}

0 commit comments

Comments
 (0)