-
Notifications
You must be signed in to change notification settings - Fork 21
feat: Improve error messaging #260
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 7 commits
26d603b
8c4898d
c03e301
a9617a6
a016a32
9dec79d
f869a62
13eadc9
26ec73f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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,46 @@ 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; | ||
|
|
||
| private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); | ||
SoulPancake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| @com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true) | ||
| public static class ApiErrorResponse { | ||
| @com.fasterxml.jackson.annotation.JsonProperty("code") | ||
| private String code; | ||
|
|
||
| @com.fasterxml.jackson.annotation.JsonProperty("message") | ||
| private String message; | ||
|
|
||
| @com.fasterxml.jackson.annotation.JsonProperty("error") | ||
| private String error; | ||
|
|
||
| public String getCode() { | ||
| return code; | ||
| } | ||
|
|
||
| public void setCode(String code) { | ||
| this.code = code; | ||
| } | ||
|
|
||
| public String getMessage() { | ||
| 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) { | ||
| super(message, cause, code, responseHeaders, responseBody); | ||
|
|
@@ -53,7 +95,7 @@ public static Optional<FgaError> 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); | ||
|
|
@@ -75,6 +117,26 @@ public static Optional<FgaError> getError( | |
| error.setAudience(clientCredentials.getApiAudience()); | ||
| } | ||
|
|
||
| error.setOperationName(name); | ||
|
|
||
| // Parse API error response | ||
| if (body != null && !body.trim().isEmpty()) { | ||
| try { | ||
| 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()); | ||
|
||
| } | ||
| } | ||
|
|
||
| // Extract requestId from headers | ||
| Optional<String> requestIdOpt = headers.firstValue("x-request-id"); | ||
| if (requestIdOpt.isPresent()) { | ||
| error.setRequestId(requestIdOpt.get()); | ||
| } | ||
|
|
||
| // Unknown error | ||
| return Optional.of(error); | ||
| } | ||
|
|
@@ -142,4 +204,136 @@ 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; | ||
| } | ||
|
|
||
| /** | ||
| * Returns a formatted error message for FgaError. | ||
| * <p> | ||
| * The message is formatted as: | ||
| * <pre> | ||
| * [operationName] HTTP statusCode apiErrorMessage (apiErrorCode) [request-id: requestId] | ||
| * </pre> | ||
| * Example: [write] HTTP 400 type 'invalid_type' not found (validation_error) [request-id: abc-123] | ||
| * </p> | ||
| * | ||
| * @return the formatted error message string | ||
| */ | ||
| @Override | ||
| public String 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); | ||
SoulPancake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| /** | ||
| * 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; | ||
| } | ||
SoulPancake marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.