diff --git a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java
index 462e5bfb..5d8cbfa4 100644
--- a/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java
+++ b/src/main/java/dev/openfga/sdk/api/client/model/ClientBatchCheckClientResponse.java
@@ -4,6 +4,8 @@
import dev.openfga.sdk.errors.FgaError;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CompletionException;
+import java.util.concurrent.ExecutionException;
import java.util.function.BiFunction;
public class ClientBatchCheckClientResponse extends CheckResponse {
@@ -18,19 +20,24 @@ public ClientBatchCheckClientResponse(
this.request = request;
this.throwable = throwable;
+ Throwable cause = throwable instanceof CompletionException || throwable instanceof ExecutionException
+ ? throwable.getCause()
+ : throwable;
+
if (clientCheckResponse != null) {
this.statusCode = clientCheckResponse.getStatusCode();
this.headers = clientCheckResponse.getHeaders();
this.rawResponse = clientCheckResponse.getRawResponse();
this.setAllowed(clientCheckResponse.getAllowed());
this.setResolution(clientCheckResponse.getResolution());
- } else if (throwable instanceof FgaError) {
- FgaError error = (FgaError) throwable;
+ } else if (cause instanceof FgaError) {
+ FgaError error = (FgaError) cause;
this.statusCode = error.getStatusCode();
- this.headers = error.getResponseHeaders().map();
+ var responseHeaders = error.getResponseHeaders();
+ this.headers = responseHeaders != null ? responseHeaders.map() : null;
this.rawResponse = error.getResponseData();
} else {
- // Should be unreachable, but required for type completion
+ // no HTTP response available, e.g. the request never reached the server
this.statusCode = null;
this.headers = null;
this.rawResponse = null;
@@ -68,14 +75,30 @@ public Throwable getThrowable() {
return throwable;
}
- public int getStatusCode() {
+ /**
+ * Returns the HTTP status code of the check response.
+ *
+ * If no HTTP response was received — for example, the request never reached the server because of a
+ * network failure (connection refused, timeout, DNS failure) and all retries were exhausted — this
+ * returns {@code null}. In that case the underlying cause can be examined with
+ * {@link ClientBatchCheckClientResponse#getThrowable()}.
+ *
+ * @return the HTTP status code, or {@code null} if no HTTP response was received.
+ */
+ public Integer getStatusCode() {
return statusCode;
}
+ /**
+ * @return the HTTP response headers, or {@code null} if no HTTP response was received.
+ */
public Map> getHeaders() {
return headers;
}
+ /**
+ * @return the raw HTTP response body, or {@code null} if no HTTP response was received.
+ */
public String getRawResponse() {
return rawResponse;
}
diff --git a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
index bad5b5fd..425b0c4b 100644
--- a/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
+++ b/src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
@@ -10,6 +10,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.tomakehurst.wiremock.client.WireMock;
+import com.github.tomakehurst.wiremock.http.Fault;
import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo;
import com.github.tomakehurst.wiremock.junit5.WireMockTest;
import com.pgssoft.httpclient.HttpClientMock;
@@ -18,6 +19,7 @@
import dev.openfga.sdk.api.model.*;
import dev.openfga.sdk.constants.FgaConstants;
import dev.openfga.sdk.errors.*;
+import java.io.IOException;
import java.net.http.HttpClient;
import java.time.Duration;
import java.time.OffsetDateTime;
@@ -2066,6 +2068,11 @@ public void clientBatchCheck_400() throws Exception {
assertNotNull(response);
assertEquals(1, response.size());
assertNull(response.get(0).getAllowed());
+ assertEquals(400, response.get(0).getStatusCode());
+ assertEquals(
+ "{\"code\":\"validation_error\",\"message\":\"Generic validation error\"}",
+ response.get(0).getRawResponse());
+ assertNotNull(response.get(0).getHeaders());
Throwable execException = response.get(0).getThrowable();
var exception = assertInstanceOf(FgaApiValidationError.class, execException.getCause());
assertEquals(400, exception.getStatusCode());
@@ -2092,6 +2099,11 @@ public void clientBatchCheck_404() throws Exception {
assertNotNull(response);
assertEquals(1, response.size());
assertNull(response.get(0).getAllowed());
+ assertEquals(404, response.get(0).getStatusCode());
+ assertEquals(
+ "{\"code\":\"undefined_endpoint\",\"message\":\"Endpoint not enabled\"}",
+ response.get(0).getRawResponse());
+ assertNotNull(response.get(0).getHeaders());
Throwable execException = response.get(0).getThrowable();
var exception = assertInstanceOf(FgaApiNotFoundError.class, execException.getCause());
assertEquals(404, exception.getStatusCode());
@@ -2118,6 +2130,11 @@ public void clientBatchCheck_500() throws Exception {
assertNotNull(response);
assertEquals(1, response.size());
assertNull(response.get(0).getAllowed());
+ assertEquals(500, response.get(0).getStatusCode());
+ assertEquals(
+ "{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}",
+ response.get(0).getRawResponse());
+ assertNotNull(response.get(0).getHeaders());
Throwable execException = response.get(0).getThrowable();
var exception = assertInstanceOf(FgaApiInternalError.class, execException.getCause());
assertEquals(500, exception.getStatusCode());
@@ -2125,6 +2142,41 @@ public void clientBatchCheck_500() throws Exception {
"{\"code\":\"internal_error\",\"message\":\"Internal Server Error\"}", exception.getResponseData());
}
+ @Test
+ public void clientBatchCheck_networkError(WireMockRuntimeInfo wireMockRuntimeInfo) throws Exception {
+ // Given
+ String httpBaseUrl = wireMockRuntimeInfo.getHttpBaseUrl();
+ var fga = new OpenFgaClient(clientConfiguration.apiUrl(httpBaseUrl), new ApiClient());
+ String postUrl = String.format("/stores/%s/check", DEFAULT_STORE_ID);
+ WireMock.stubFor(
+ WireMock.post(postUrl).willReturn(WireMock.aResponse().withFault(Fault.CONNECTION_RESET_BY_PEER)));
+
+ // When
+ List response = fga.clientBatchCheck(
+ List.of(new ClientCheckRequest()), new ClientBatchCheckClientOptions())
+ .join();
+
+ // Then
+ // Network errors are retried (1 initial + 3 retries = 4 total)
+ WireMock.verify(4, WireMock.postRequestedFor(WireMock.urlEqualTo(postUrl)));
+ assertNotNull(response);
+ assertEquals(1, response.size());
+ assertNull(response.get(0).getAllowed());
+ // No HTTP response was received, so status code, headers and body are null
+ assertNull(response.get(0).getStatusCode());
+ assertNull(response.get(0).getHeaders());
+ assertNull(response.get(0).getRawResponse());
+ Throwable execException = response.get(0).getThrowable();
+ assertNotNull(execException);
+ var exception = assertInstanceOf(ApiException.class, execException.getCause());
+ assertFalse(exception instanceof FgaError);
+ Throwable rootCause = exception;
+ while (rootCause.getCause() != null) {
+ rootCause = rootCause.getCause();
+ }
+ assertInstanceOf(IOException.class, rootCause);
+ }
+
@Test
public void shouldThrowExceptionWhenCorrelationIdsAreDuplicated() {
// Given