Skip to content

Commit

Permalink
Merge pull request #25 from openfga/feat/errors-and-retries
Browse files Browse the repository at this point in the history
feat: Introduce error modeling and retries
  • Loading branch information
adriantam authored Oct 6, 2023
2 parents 12af785 + a84a576 commit 38361ba
Show file tree
Hide file tree
Showing 17 changed files with 689 additions and 561 deletions.
6 changes: 6 additions & 0 deletions .openapi-generator/FILES
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ src/main/java/dev/openfga/sdk/api/client/ClientListRelationsRequest.java
src/main/java/dev/openfga/sdk/api/client/ClientReadRequest.java
src/main/java/dev/openfga/sdk/api/client/ClientTupleKey.java
src/main/java/dev/openfga/sdk/api/client/ClientWriteRequest.java
src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java
src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java
src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java
Expand Down Expand Up @@ -166,6 +167,11 @@ src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelRequest.java
src/main/java/dev/openfga/sdk/api/model/WriteAuthorizationModelResponse.java
src/main/java/dev/openfga/sdk/api/model/WriteRequest.java
src/main/java/dev/openfga/sdk/errors/ApiException.java
src/main/java/dev/openfga/sdk/errors/FgaApiAuthenticationError.java
src/main/java/dev/openfga/sdk/errors/FgaApiInternalError.java
src/main/java/dev/openfga/sdk/errors/FgaApiNotFoundError.java
src/main/java/dev/openfga/sdk/errors/FgaApiRateLimitExceededError.java
src/main/java/dev/openfga/sdk/errors/FgaApiValidationError.java
src/main/java/dev/openfga/sdk/errors/FgaInvalidParameterException.java
src/main/java/dev/openfga/sdk/util/Pair.java
src/main/java/dev/openfga/sdk/util/StringUtil.java
Expand Down
640 changes: 247 additions & 393 deletions src/main/java/dev/openfga/sdk/api/OpenFgaApi.java

Large diffs are not rendered by default.

33 changes: 7 additions & 26 deletions src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,23 +12,19 @@

package dev.openfga.sdk.api.auth;

import com.fasterxml.jackson.databind.ObjectMapper;
import dev.openfga.sdk.api.client.ApiClient;
import dev.openfga.sdk.api.client.HttpRequestAttempt;
import dev.openfga.sdk.api.configuration.*;
import dev.openfga.sdk.errors.ApiException;
import dev.openfga.sdk.errors.FgaInvalidParameterException;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Instant;
import java.util.concurrent.CompletableFuture;

public class OAuth2Client {
private final HttpClient httpClient;
private final ApiClient apiClient;
private final Credentials credentials;
private final ObjectMapper mapper;
private final AccessToken token = new AccessToken();
private final CredentialsFlowRequest authRequest;
private final String apiTokenIssuer;
Expand All @@ -37,14 +33,11 @@ public class OAuth2Client {
* Initializes a new instance of the {@link OAuth2Client} class
*
* @param configuration Configuration, including credentials, that can be used to retrieve an access tokens
* @param httpClient Http client
*/
public OAuth2Client(Configuration configuration, HttpClient httpClient, ObjectMapper mapper)
throws FgaInvalidParameterException {
public OAuth2Client(Configuration configuration, ApiClient apiClient) throws FgaInvalidParameterException {
this.credentials = configuration.getCredentials();

this.httpClient = httpClient;
this.mapper = mapper;
this.apiClient = apiClient;
this.apiTokenIssuer = credentials.getClientCredentials().getApiTokenIssuer();
this.authRequest = new CredentialsFlowRequest();
this.authRequest.setClientId(credentials.getClientCredentials().getClientId());
Expand Down Expand Up @@ -78,27 +71,15 @@ public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterExce
private CompletableFuture<CredentialsFlowResponse> exchangeToken()
throws ApiException, FgaInvalidParameterException {
try {
byte[] body = mapper.writeValueAsBytes(authRequest);
byte[] body = apiClient.getObjectMapper().writeValueAsBytes(authRequest);

Configuration config = new Configuration().apiUrl("https://" + apiTokenIssuer);

HttpRequest request = ApiClient.requestBuilder("POST", "/oauth/token", body, config)
.build();

return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenCompose(httpResponse -> {
if (httpResponse.statusCode() != HttpURLConnection.HTTP_OK) {
return CompletableFuture.failedFuture(new ApiException("exchangeToken", httpResponse));
}
try {
CredentialsFlowResponse response =
mapper.readValue(httpResponse.body(), CredentialsFlowResponse.class);
return CompletableFuture.completedFuture(response);
} catch (Exception e) {
return CompletableFuture.failedFuture(e);
}
});
return new HttpRequestAttempt<>(request, "exchangeToken", CredentialsFlowResponse.class, apiClient, config)
.attemptHttpRequest();
} catch (IOException e) {
throw new ApiException(e);
}
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,17 @@ public HttpClient getHttpClient() {
return builder.build();
}

/**
* Get the current {@link HttpClient.Builder}.
*
* <p>The returned object is immutable and thread-safe.</p>
*
* @return The HTTP client.
*/
public HttpClient.Builder getHttpClientBuilder() {
return builder;
}

/**
* Set a custom {@link ObjectMapper} to serialize and deserialize the request
* and response bodies.
Expand Down
111 changes: 111 additions & 0 deletions src/main/java/dev/openfga/sdk/api/client/HttpRequestAttempt.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package dev.openfga.sdk.api.client;

import static dev.openfga.sdk.util.StringUtil.isNullOrWhitespace;

import dev.openfga.sdk.api.configuration.BaseConfiguration;
import dev.openfga.sdk.errors.*;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.concurrent.*;

public class HttpRequestAttempt<T> {
private final ApiClient apiClient;
private final BaseConfiguration configuration;
private final Class<T> clazz;
private final String name;
private final HttpRequest request;

public HttpRequestAttempt(
HttpRequest request, String name, Class<T> clazz, ApiClient apiClient, BaseConfiguration configuration)
throws FgaInvalidParameterException {
if (configuration.getMaxRetries() == null) {
throw new FgaInvalidParameterException("maxRetries", "Configuration");
}
this.apiClient = apiClient;
this.configuration = configuration;
this.name = name;
this.request = request;
this.clazz = clazz;
}

public CompletableFuture<T> attemptHttpRequest() throws ApiException {
int retryNumber = 0;
return attemptHttpRequest(apiClient.getHttpClient(), retryNumber, null);
}

private CompletableFuture<T> attemptHttpRequest(HttpClient httpClient, int retryNumber, Throwable previousError) {
return httpClient
.sendAsync(request, HttpResponse.BodyHandlers.ofString())
.thenCompose(response -> {
try {
checkStatus(name, response, previousError);
} catch (FgaApiRateLimitExceededError | FgaApiInternalError retryableError) {
if (retryNumber < configuration.getMaxRetries()) {
HttpClient delayingClient = getDelayedHttpClient();
return attemptHttpRequest(delayingClient, retryNumber + 1, retryableError);
}
return CompletableFuture.failedFuture(retryableError);
} catch (ApiException e) {
return CompletableFuture.failedFuture(e);
}

return deserializeResponse(response);
});
}

private CompletableFuture<T> deserializeResponse(HttpResponse<String> response) {
if (clazz == Void.class && isNullOrWhitespace(response.body())) {
return CompletableFuture.completedFuture(null);
}

try {
T deserialized = apiClient.getObjectMapper().readValue(response.body(), clazz);
return CompletableFuture.completedFuture(deserialized);
} catch (IOException e) {
// Malformed response.
return CompletableFuture.failedFuture(new ApiException(e));
}
}

private HttpClient getDelayedHttpClient() {
Duration retryDelay = configuration.getMinimumRetryDelay();
return apiClient
.getHttpClientBuilder()
.executor(CompletableFuture.delayedExecutor(retryDelay.toNanos(), TimeUnit.NANOSECONDS))
.build();
}

private static void checkStatus(String name, HttpResponse<String> response, Throwable previousError)
throws ApiException {
int status = response.statusCode();
String body = response.body();

switch (status) {
case HttpURLConnection.HTTP_BAD_REQUEST:
case 422: // HTTP 422 Unprocessable Entity
throw new FgaApiValidationError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_UNAUTHORIZED:
case HttpURLConnection.HTTP_FORBIDDEN:
throw new FgaApiAuthenticationError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_NOT_FOUND:
throw new FgaApiNotFoundError(name, previousError, status, response.headers(), body);

case 429: // HTTP 429 Too Many Requests
throw new FgaApiRateLimitExceededError(name, previousError, status, response.headers(), body);

case HttpURLConnection.HTTP_INTERNAL_ERROR:
throw new FgaApiInternalError(name, previousError, status, response.headers(), body);
}

// FGA and OAuth2 servers are only expected to return HTTP 2xx responses.
if (status < 200 || 300 <= status) {
throw new ApiException(name, previousError, status, response.headers(), body);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,8 @@ public interface BaseConfiguration {
Duration getReadTimeout();

Duration getConnectTimeout();

Integer getMaxRetries();

Duration getMinimumRetryDelay();
}
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,16 @@ public ClientConfiguration connectTimeout(Duration connectTimeout) {
super.connectTimeout(connectTimeout);
return this;
}

@Override
public ClientConfiguration maxRetries(int maxRetries) {
super.maxRetries(maxRetries);
return this;
}

@Override
public ClientConfiguration minimumRetryDelay(Duration minimumRetryDelay) {
super.minimumRetryDelay(minimumRetryDelay);
return this;
}
}
28 changes: 28 additions & 0 deletions src/main/java/dev/openfga/sdk/api/configuration/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ public class Configuration implements BaseConfiguration {
private String userAgent;
private Duration readTimeout;
private Duration connectTimeout;
private int maxRetries;
private Duration minimumRetryDelay;

public Configuration() {
this.apiUrl = DEFAULT_API_URL;
Expand Down Expand Up @@ -101,6 +103,12 @@ public Configuration override(ConfigurationOverride configurationOverride) {
Duration overrideConnectTimeout = configurationOverride.getConnectTimeout();
result.connectTimeout(overrideConnectTimeout != null ? overrideConnectTimeout : connectTimeout);

Integer overrideMaxRetries = configurationOverride.getMaxRetries();
result.maxRetries(overrideMaxRetries != null ? overrideMaxRetries : maxRetries);

Duration overrideMinimumRetryDelay = configurationOverride.getMinimumRetryDelay();
result.minimumRetryDelay(overrideMinimumRetryDelay != null ? overrideMinimumRetryDelay : minimumRetryDelay);

return result;
}

Expand Down Expand Up @@ -231,4 +239,24 @@ public Configuration connectTimeout(Duration connectTimeout) {
public Duration getConnectTimeout() {
return connectTimeout;
}

public Configuration maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}

@Override
public Integer getMaxRetries() {
return maxRetries;
}

public Configuration minimumRetryDelay(Duration minimumRetryDelay) {
this.minimumRetryDelay = minimumRetryDelay;
return this;
}

@Override
public Duration getMinimumRetryDelay() {
return minimumRetryDelay;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public class ConfigurationOverride implements BaseConfiguration {
private String userAgent;
private Duration readTimeout;
private Duration connectTimeout;
private Integer maxRetries;
private Duration minimumRetryDelay;

public ConfigurationOverride() {
this.apiUrl = null;
Expand Down Expand Up @@ -157,4 +159,24 @@ public ConfigurationOverride connectTimeout(Duration connectTimeout) {
public Duration getConnectTimeout() {
return connectTimeout;
}

public ConfigurationOverride maxRetries(int maxRetries) {
this.maxRetries = maxRetries;
return this;
}

@Override
public Integer getMaxRetries() {
return maxRetries;
}

public ConfigurationOverride minimumRetryDelay(Duration minimumRetryDelay) {
this.minimumRetryDelay = minimumRetryDelay;
return this;
}

@Override
public Duration getMinimumRetryDelay() {
return minimumRetryDelay;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiAuthenticationError extends ApiException {
public FgaApiAuthenticationError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiAuthenticationError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiInternalError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiInternalError extends ApiException {
public FgaApiInternalError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiInternalError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/openfga/sdk/errors/FgaApiNotFoundError.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiNotFoundError extends ApiException {
public FgaApiNotFoundError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiNotFoundError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package dev.openfga.sdk.errors;

import java.net.http.HttpHeaders;

public class FgaApiRateLimitExceededError extends ApiException {
public FgaApiRateLimitExceededError(
String message, Throwable throwable, int code, HttpHeaders responseHeaders, String responseBody) {
super(message, code, responseHeaders, responseBody);
}

public FgaApiRateLimitExceededError(String message, int code, HttpHeaders responseHeaders, String responseBody) {
this(message, (Throwable) null, code, responseHeaders, responseBody);
}
}
Loading

0 comments on commit 38361ba

Please sign in to comment.