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