Skip to content

Commit ddcbd7e

Browse files
committed
non-transaction write error handling
1 parent 17c34db commit ddcbd7e

26 files changed

Lines changed: 494 additions & 2040 deletions

.openapi-generator/FILES

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,8 @@ src/main/java/dev/openfga/sdk/api/client/model/ClientWriteAssertionsResponse.jav
164164
src/main/java/dev/openfga/sdk/api/client/model/ClientWriteAuthorizationModelResponse.java
165165
src/main/java/dev/openfga/sdk/api/client/model/ClientWriteRequest.java
166166
src/main/java/dev/openfga/sdk/api/client/model/ClientWriteResponse.java
167+
src/main/java/dev/openfga/sdk/api/client/model/ClientWriteSingleResponse.java
168+
src/main/java/dev/openfga/sdk/api/client/model/ClientWriteStatus.java
167169
src/main/java/dev/openfga/sdk/api/configuration/AdditionalHeadersSupplier.java
168170
src/main/java/dev/openfga/sdk/api/configuration/ApiToken.java
169171
src/main/java/dev/openfga/sdk/api/configuration/BaseConfiguration.java

CHANGELOG.md

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,6 @@
22

33
## [Unreleased](https://github.com/openfga/java-sdk/compare/v0.8.3...HEAD)
44

5-
### Added
6-
- feat: RFC 9110 compliant Retry-After header support with exponential backoff and jitter
7-
- feat: Enhanced retry strategy with delay calculation
8-
- feat: Retry-After header value exposed in error objects for better observability
9-
- feat: Input validation for Configuration.minimumRetryDelay() to prevent negative values
10-
- feat: Default value (100ms) now explicitly set for minimumRetryDelay configuration
11-
12-
### Changed
13-
- **BREAKING**: Maximum allowable retry count is now enforced at 15 (default remains 3)
14-
- **BREAKING**: FgaError now exposes Retry-After header value via getRetryAfterHeader() method
15-
- **BREAKING**: Configuration.minimumRetryDelay() now requires non-null values and validates input, throwing IllegalArgumentException for null or negative values
16-
17-
### Technical Details
18-
- Implements RFC 9110 compliant Retry-After header parsing (supports both integer seconds and HTTP-date formats)
19-
- Adds exponential backoff with jitter (base delay: 2^retryCount * 100ms, capped at 120 seconds)
20-
- Validates Retry-After values between 1-1800 seconds (30 minutes maximum)
21-
- Prioritizes Retry-After header delays over exponential backoff when present
22-
- Unified retry behavior: All requests retry on 429s and 5xx errors (except 501 Not Implemented)
23-
24-
**Migration Guide**:
25-
- Update error handling code if using FgaError properties - new getRetryAfterHeader() method available
26-
- Note: Maximum allowable retries is now enforced at 15 (validation added to prevent exceeding this limit)
27-
- **IMPORTANT**: Configuration.minimumRetryDelay() now requires non-null values and validates input - ensure you're not passing null or negative Duration values, as this will now throw IllegalArgumentException. Previously null values were silently accepted and would fall back to default behavior at runtime.
28-
295
## v0.8.3
306

317
### [0.8.3](https://github.com/openfga/java-sdk/compare/v0.8.2...v0.8.3) (2025-07-15)

README.md

Lines changed: 6 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ libraryDependencies += "dev.openfga" % "openfga-sdk" % "0.8.3"
125125

126126
We strongly recommend you initialize the `OpenFgaClient` only once and then re-use it throughout your app, otherwise you will incur the cost of having to re-initialize multiple times or at every request, the cost of reduced connection pooling and re-use, and would be particularly costly in the client credentials flow, as that flow will be preformed on every request.
127127

128-
> The `Client` will by default retry API requests up to 3 times. Rate limiting (429) errors are always retried. Server errors (5xx) are retried for all operations, with delay calculation using `Retry-After` headers when provided or exponential backoff as fallback.
128+
> The `Client` will by default retry API requests up to 3 times on 429 and 5xx errors.
129129
130130
#### No Credentials
131131

@@ -714,7 +714,7 @@ response.getResult() = [{
714714
715715
If you are using an OpenFGA version less than 1.8.0, you can use `clientBatchCheck`,
716716
which calls `check` in parallel. It will return `allowed: false` if it encounters an error, and will return the error in the body.
717-
If 429s are encountered, the underlying check will retry up to 3 times. For 5xx errors, all requests will retry with delay calculation using `Retry-After` headers when provided or exponential backoff as fallback.
717+
If 429s or 5xxs are encountered, the underlying check will retry up to 3 times before giving up.
718718
719719
```
720720
var request = List.of(
@@ -965,28 +965,9 @@ fgaClient.writeAssertions(assertions, options).get();
965965
966966
### Retries
967967
968-
The SDK implements RFC 9110 compliant retry behavior with support for the `Retry-After` header. By default, the SDK will automatically retry failed requests up to **3 times** with delay calculation (maximum allowable: 15 retries).
968+
If a network request fails with a 429 or 5xx error from the server, the SDK will automatically retry the request up to 3 times with a minimum wait time of 100 milliseconds between each attempt.
969969
970-
#### Retry Behavior
971-
972-
**Rate Limiting (429 errors):** Always retried regardless of HTTP method.
973-
974-
**Server Errors (5xx):** All requests are retried on 5xx errors (except 501 Not Implemented) regardless of HTTP method:
975-
- **All operations** (GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS): Always retried on 5xx errors with delay calculation
976-
977-
#### Delay Calculation
978-
979-
1. **Retry-After header present**: Uses the server-specified delay (supports both integer seconds and HTTP-date formats)
980-
2. **No Retry-After header**: Uses exponential backoff with jitter (base delay: 2^retryCount * 100ms, capped at 120 seconds)
981-
3. **Minimum delay**: Respects the configured `minimumRetryDelay` as a floor value
982-
983-
#### Configuration
984-
985-
Customize retry behavior using the `ClientConfiguration` builder. The SDK enforces a maximum of 15 retries to prevent accidental server overload:
986-
987-
**⚠️ Breaking Changes:**
988-
- Configuration validation now prevents setting `maxRetries` above 15
989-
- `FgaError` now exposes the `Retry-After` header value via `getRetryAfterHeader()`
970+
To customize this behavior, call `maxRetries` and `minimumRetryDelay` on the `ClientConfiguration` builder. `maxRetries` determines the maximum number of retries (up to 15), while `minimumRetryDelay` sets the minimum wait time between retries in milliseconds.
990971
991972
```java
992973
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -1000,37 +981,15 @@ public class Example {
1000981
.apiUrl(System.getenv("FGA_API_URL")) // If not specified, will default to "http://localhost:8080"
1001982
.storeId(System.getenv("FGA_STORE_ID")) // Not required when calling createStore() or listStores()
1002983
.authorizationModelId(System.getenv("FGA_MODEL_ID")) // Optional, can be overridden per request
1003-
.maxRetries(3) // retry up to 3 times on API requests (default: 3, maximum: 15)
1004-
.minimumRetryDelay(100); // minimum wait time between retries in milliseconds (default: 100ms)
984+
.maxRetries(3) // retry up to 3 times on API requests
985+
.minimumRetryDelay(250); // wait a minimum of 250 milliseconds between requests
1005986

1006987
var fgaClient = new OpenFgaClient(config);
1007988
var response = fgaClient.readAuthorizationModels().get();
1008989
}
1009990
}
1010991
```
1011992

1012-
#### Error Handling with Retry Information
1013-
1014-
When handling errors, you can access the `Retry-After` header value for debugging or custom retry logic:
1015-
1016-
```java
1017-
try {
1018-
var response = fgaClient.check(request).get();
1019-
} catch (ExecutionException e) {
1020-
if (e.getCause() instanceof FgaError) {
1021-
FgaError error = (FgaError) e.getCause();
1022-
1023-
// Access Retry-After header if present
1024-
String retryAfter = error.getRetryAfterHeader();
1025-
if (retryAfter != null) {
1026-
System.out.println("Server requested retry after: " + retryAfter + " seconds");
1027-
}
1028-
1029-
System.out.println("Error: " + error.getMessage());
1030-
}
1031-
}
1032-
```
1033-
1034993
### API Endpoints
1035994

1036995
| Method | HTTP request | Description |

build.gradle

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ plugins {
44
// Quality
55
id 'jacoco'
66
id 'jvm-test-suite'
7-
id 'com.diffplug.spotless' version '7.2.1'
7+
id 'com.diffplug.spotless' version '7.0.4'
88

99
// IDE
1010
id 'idea'
@@ -55,7 +55,7 @@ jacocoTestReport {
5555
}
5656

5757
ext {
58-
jackson_version = "2.19.2"
58+
jackson_version = "2.19.1"
5959
}
6060

6161
dependencies {
@@ -65,7 +65,7 @@ dependencies {
6565
implementation "com.fasterxml.jackson.core:jackson-databind:$jackson_version"
6666
implementation "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:$jackson_version"
6767
implementation "org.openapitools:jackson-databind-nullable:0.2.6"
68-
implementation platform("io.opentelemetry:opentelemetry-bom:1.52.0")
68+
implementation platform("io.opentelemetry:opentelemetry-bom:1.51.0")
6969
implementation "io.opentelemetry:opentelemetry-api"
7070
}
7171

@@ -76,7 +76,7 @@ testing {
7676
dependencies {
7777
implementation 'org.assertj:assertj-core:3.27.3'
7878
implementation 'org.mockito:mockito-core:5.18.0'
79-
implementation 'org.junit.jupiter:junit-jupiter:5.13.4'
79+
implementation 'org.junit.jupiter:junit-jupiter:5.13.2'
8080
implementation 'org.wiremock:wiremock:3.13.1'
8181

8282
runtimeOnly 'org.junit.platform:junit-platform-launcher'

example/example1/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
plugins {
22
id 'application'
3-
id 'com.diffplug.spotless' version '7.2.1'
3+
id 'com.diffplug.spotless' version '7.0.4'
44
id 'org.jetbrains.kotlin.jvm' version '2.2.0'
55
}
66

@@ -19,7 +19,7 @@ repositories {
1919
}
2020

2121
ext {
22-
jacksonVersion = "2.19.2"
22+
jacksonVersion = "2.19.1"
2323
}
2424

2525
dependencies {

example/example1/src/main/java/dev/openfga/sdk/example/Example1.java

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ public void run(String apiUrl) throws Exception {
1818
if (System.getenv("FGA_CLIENT_ID") != null) {
1919
credentials = new Credentials(new ClientCredentials()
2020
.apiAudience(System.getenv("FGA_API_AUDIENCE"))
21-
.apiTokenIssuer(System.getenv("FGA_TOKEN_ISSUER"))
22-
.clientId("FGA_CLIENT_ID")
23-
.clientSecret("FGA_CLIENT_SECRET"));
21+
.apiTokenIssuer(System.getenv("FGA_API_TOKEN_ISSUER"))
22+
.clientId(System.getenv("FGA_CLIENT_ID"))
23+
.clientSecret(System.getenv("FGA_CLIENT_SECRET")));
2424
} else {
2525
System.out.println("Proceeding with no credentials (expecting localhost)");
2626
}
@@ -102,10 +102,20 @@ public void run(String apiUrl) throws Exception {
102102
fgaClient
103103
.write(
104104
new ClientWriteRequest()
105-
.writes(List.of(new ClientTupleKey()
106-
.user("user:anne")
107-
.relation("writer")
108-
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"))),
105+
.writes(List.of(
106+
new ClientTupleKey()
107+
.user("user:anne")
108+
.relation("writer")
109+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"),
110+
new ClientTupleKey()
111+
.user("user:anne")
112+
.relation("writer")
113+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a"), // duplicate
114+
new ClientTupleKey()
115+
.user("user:anne")
116+
.relation("owner")
117+
._object("document:0192ab2a-d83f-756d-9397-c5ed9f3cb69a") // different relation
118+
)),
109119
new ClientWriteOptions()
110120
.disableTransactions(true)
111121
.authorizationModelId(authorizationModel.getAuthorizationModelId()))

settings.gradle

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
pluginManagement {
2+
repositories {
3+
gradlePluginPortal()
4+
mavenCentral()
5+
}
6+
}
7+
18
rootProject.name = 'openfga-sdk'

0 commit comments

Comments
 (0)