Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.9.3...HEAD)
- feat: Improve error messaging by parsing error details from resp bodies (#256)

## v0.9.3

Expand Down
99 changes: 93 additions & 6 deletions src/main/java/dev/openfga/sdk/errors/FgaError.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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

import com.fasterxml.jackson.databind.JsonNode;
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;
Expand All @@ -11,6 +13,8 @@
import java.util.Optional;

public class FgaError extends ApiException {
private static final ObjectMapper ERROR_MAPPER = new ObjectMapper();

private String method = null;
private String requestUrl = null;
private String clientId = null;
Expand All @@ -28,6 +32,62 @@ public FgaError(String message, int code, HttpHeaders responseHeaders, String re
super(message, code, responseHeaders, responseBody);
}

/**
* Parse the API error response body to extract the error message and code.
* @param methodName The API method name that was called
* @param responseBody The response body JSON string
* @return A descriptive error message
*/
private static String parseErrorMessage(String methodName, String responseBody) {
if (responseBody == null || responseBody.trim().isEmpty()) {
return methodName;
}

try {
JsonNode jsonNode = ERROR_MAPPER.readTree(responseBody);

// Try to extract message field
JsonNode messageNode = jsonNode.get("message");
String message = (messageNode != null && !messageNode.isNull()) ? messageNode.asText() : null;

// If we have a message, return it, otherwise fall back to method name
if (message != null && !message.trim().isEmpty()) {
return message;
}
} catch (Exception e) {
// If parsing fails, fall back to the method name
// This is intentional to ensure errors are still reported even if the response format is unexpected
}

return methodName;
}

/**
* Extract the API error code from the response body.
* @param responseBody The response body JSON string
* @return The error code, or null if not found
*/
private static String extractErrorCode(String responseBody) {
if (responseBody == null || responseBody.trim().isEmpty()) {
return null;
}

try {
JsonNode jsonNode = ERROR_MAPPER.readTree(responseBody);

// Try to extract code field
JsonNode codeNode = jsonNode.get("code");
if (codeNode != null && !codeNode.isNull()) {
return codeNode.asText();
}
} catch (Exception e) {
// If parsing fails, return null
// This is intentional - we still want to report the error even if we can't extract the code
}

return null;
}

public static Optional<FgaError> getError(
String name,
HttpRequest request,
Expand All @@ -43,25 +103,43 @@ public static Optional<FgaError> getError(

final String body = response.body();
final var headers = response.headers();

// Parse the error message from the response body
final String errorMessage = parseErrorMessage(name, body);
final FgaError error;

if (status == BAD_REQUEST || status == UNPROCESSABLE_ENTITY) {
error = new FgaApiValidationError(name, previousError, status, headers, body);
error = new FgaApiValidationError(errorMessage, previousError, status, headers, body);
} else if (status == UNAUTHORIZED || status == FORBIDDEN) {
error = new FgaApiAuthenticationError(name, previousError, status, headers, body);
error = new FgaApiAuthenticationError(errorMessage, previousError, status, headers, body);
} else if (status == NOT_FOUND) {
error = new FgaApiNotFoundError(name, previousError, status, headers, body);
error = new FgaApiNotFoundError(errorMessage, previousError, status, headers, body);
} else if (status == TOO_MANY_REQUESTS) {
error = new FgaApiRateLimitExceededError(name, previousError, status, headers, body);
error = new FgaApiRateLimitExceededError(errorMessage, previousError, status, headers, body);
} else if (isServerError(status)) {
error = new FgaApiInternalError(name, previousError, status, headers, body);
error = new FgaApiInternalError(errorMessage, previousError, status, headers, body);
} else {
error = new FgaError(name, previousError, status, headers, body);
error = new FgaError(errorMessage, previousError, status, headers, body);
}

error.setMethod(request.method());
error.setRequestUrl(configuration.getApiUrl());

// Extract and set API error code from response body
String apiErrorCode = extractErrorCode(body);
if (apiErrorCode != null) {
error.setApiErrorCode(apiErrorCode);
}

// Extract and set request ID from response headers if present
// Common request ID header names
Optional<String> requestId = headers.firstValue("X-Request-Id")
.or(() -> headers.firstValue("x-request-id"))
.or(() -> headers.firstValue("Request-Id"));
if (requestId.isPresent()) {
error.setRequestId(requestId.get());
}

// Extract and set Retry-After header if present
Optional<String> retryAfter = headers.firstValue(FgaConstants.RETRY_AFTER_HEADER_NAME);
if (retryAfter.isPresent()) {
Expand Down Expand Up @@ -135,6 +213,15 @@ public String getApiErrorCode() {
return apiErrorCode;
}

/**
* Get the API error code.
* This is an alias for getApiErrorCode() for convenience.
* @return The API error code from the response
*/
public String getCode() {
return apiErrorCode;
}

public void setRetryAfterHeader(String retryAfterHeader) {
this.retryAfterHeader = retryAfterHeader;
}
Expand Down
Loading
Loading