Skip to content

fix: attach Authorization header to streaming and ApiExecutor requests (#330)#331

Open
cportcvent wants to merge 11 commits intoopenfga:mainfrom
cportcvent:fix/issue-330
Open

fix: attach Authorization header to streaming and ApiExecutor requests (#330)#331
cportcvent wants to merge 11 commits intoopenfga:mainfrom
cportcvent:fix/issue-330

Conversation

@cportcvent
Copy link
Copy Markdown

@cportcvent cportcvent commented Apr 22, 2026

Description

What problem is being solved?

OpenFgaClient#streamedListObjects — and every other code path that flows through StreamingApiExecutor, ApiExecutorRequestBuilder, or BaseStreamingApi — sends requests without the Authorization: Bearer <token> header. Against any FGA deployment that requires auth (FGA Cloud, or any self-hosted OpenFGA with bearer auth), every streaming call fails with ApiException: API error: 401, even though non-streaming calls on the same OpenFgaClient with the same ClientCredentials succeed.

Closes #330.

How is it being solved?

Auth used to live in exactly one place — OpenFgaApi.buildHttpRequestWithPublisher — which also privately owned the only OAuth2Client. Any request builder that did not route through OpenFgaApi (streaming, the newer ApiExecutor) had no way to reach that code and shipped unauthenticated.

This PR centralizes auth on ApiClient as the single entry point and routes every request-builder path through it:

  • New ApiClient#applyAuthHeader(HttpRequest.Builder, Configuration) applies Authorization: Bearer <token> based on Credentials:
    • NONE (or null credentials / null method) → no header.
    • API_TOKEN → static token from configuration.
    • CLIENT_CREDENTIALS → lazily creates an OAuth2Client on first use, cached on the ApiClient in an AtomicReference, so the exchanged token is reused across every request path that uses the same ApiClient.
  • OpenFgaApi drops its private OAuth2Client field and getAccessToken(...) helper and delegates to apiClient.applyAuthHeader(...).
  • BaseStreamingApi.buildHttpRequest and ApiExecutorRequestBuilder.buildHttpRequest now call apiClient.applyAuthHeader(...) before running user-supplied request interceptors — fixing the 401 for streamedListObjects and every other streaming / ApiExecutor-backed call.

No public API is removed. ApiClient gains one new method; OpenFgaApi's public surface is unchanged. Lazy initialization means existing ApiClient consumers that never use CLIENT_CREDENTIALS pay nothing.

What changes are made to solve it?

  • src/main/java/dev/openfga/sdk/api/client/ApiClient.java — adds applyAuthHeader(...) and a lazy AtomicReference<OAuth2Client> with double-checked initialization via compareAndSet.
  • src/main/java/dev/openfga/sdk/api/OpenFgaApi.java — removes oAuth2Client field and getAccessToken(...); replaces inline header logic with a call to apiClient.applyAuthHeader(httpRequest, configuration).
  • src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java — invokes apiClient.applyAuthHeader(requestBuilder, configuration) immediately after building the request.
  • src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java — invokes apiClient.applyAuthHeader(...) before running request interceptors; method now declares ApiException in addition to FgaInvalidParameterException and JsonProcessingException.
  • Tests:
    • ApiClientTest — unit coverage for applyAuthHeader across NONE, API_TOKEN, and CLIENT_CREDENTIALS, including verification that the OAuth2 token exchange happens exactly once and is cached for subsequent calls.
    • ApiExecutorTest — WireMock-backed regression tests that prove the ApiExecutor request path attaches Authorization for both API_TOKEN and CLIENT_CREDENTIALS.
    • StreamingApiExecutorTest — regression guard tied directly to streamedListObjects omits Authorization header — 401 against FGA Cloud with ClientCredentials #330: the streaming path now attaches Authorization: Bearer <token> when API_TOKEN credentials are configured.
    • OpenFgaClientTest — switches from a mocked ApiClient to a real ApiClient wrapping a mocked HttpClient.Builder, so tests exercise the real applyAuthHeader wiring.

References

Review Checklist

  • I have clicked on "allow edits by maintainers".
  • I have added documentation for new/changed functionality in this PR or in a PR to openfga.dev — no doc change required: this is a bug fix; the public OpenFgaClient / OpenFgaApi surface is unchanged.
  • The correct base branch is being used, if not main — base is main.
  • I have added tests to validate that the change in functionality is working as expected — unit + integration coverage for all three request-builder paths (OpenFgaApi, ApiExecutor, StreamingApiExecutor) plus a direct #330 regression guard in StreamingApiExecutorTest.

Summary by CodeRabbit

  • Refactor

    • Centralized authentication header handling across all API call types (streaming and regular requests).
    • Improved OAuth2 token caching with concurrent request deduplication for better performance and thread-safety.
  • Tests

    • Added comprehensive authentication behavior validation across API call types.
    • Added concurrent token exchange testing to ensure thread-safe behavior.

…try point for attaching OAuth2 Authorization header.

Includes lazy OAuth2Client initialization on detected CLIENT_CREDENTIALS use.
Copilot AI review requested due to automatic review settings April 22, 2026 16:12
@cportcvent cportcvent requested a review from a team as a code owner April 22, 2026 16:12
@linux-foundation-easycla
Copy link
Copy Markdown

linux-foundation-easycla Bot commented Apr 22, 2026

CLA Signed

The committers listed above are authorized under a signed CLA.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6449ae2b-e80d-413e-8dec-94e52a4d3ad3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

The PR centralizes authentication header application by introducing ApiClient.applyAuthHeader(), which handles both API token and OAuth2 client credentials flows. This method is now invoked from both streaming (BaseStreamingApi, ApiExecutorRequestBuilder) and non-streaming request construction paths, ensuring consistent Authorization header application across all request types and addressing the missing headers bug in streaming requests.

Changes

Cohort / File(s) Summary
Core Auth Centralization
src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Introduces new public applyAuthHeader(HttpRequest.Builder, Configuration) method that applies authentication headers based on configured credentials: uses static tokens for API_TOKEN mode, exchanges client credentials for OAuth2 tokens with per-ApiClient caching via CredentialsCacheKey, and handles exception conversion for threading and execution errors.
Auth Application Points
src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java, src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java
Updated to call applyAuthHeader() immediately after creating the request builder, before applying interceptors, ensuring auth headers are present in all outbound requests (both streaming and non-streaming).
Legacy Auth Removal
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java
Removes inline OAuth2Client field, token-exchange logic, and manual Authorization header construction; delegates to centralized apiClient.applyAuthHeader() instead.
Thread-Safe Token Caching
src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Refactors getAccessToken() to use immutable token snapshot caching and atomic deduplication of concurrent token exchanges via inFlight ConcurrentMap, preventing redundant OAuth2 requests when multiple threads request tokens simultaneously.
Immutable AccessToken
src/main/java/dev/openfga/sdk/api/auth/AccessToken.java
Converts from mutable class with setters to immutable Java record (record AccessToken(String token, Instant expiresAt)), makes isValid() package-private, removes public accessors/mutators, and switches to ThreadLocalRandom for jitter generation.
Auth Header Tests
src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java
Adds comprehensive ApplyAuthHeader test suite validating header behavior across credentials modes: omits headers when missing, applies static bearer tokens for API_TOKEN, exchanges and caches tokens for CLIENT_CREDENTIALS, and ensures header replacement (not appending) on reapplication.
Raw API & Streaming Tests
src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java, src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java
Add end-to-end auth header validation for raw API calls and streaming paths, confirming both ApiToken and ClientCredentials modes correctly attach Authorization headers in outbound requests.
OAuth2 Concurrency Test
src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
Adds concurrent token-exchange test verifying that multiple simultaneous getAccessToken() calls trigger only one underlying OAuth2 token endpoint request.
Test Infrastructure
src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java, src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java
Updates test doubles and assertions: extends HttpClient.Builder mocking with connectTimeout(), replaces mocked ApiClient with real instance, adjusts AccessToken assertions for immutable record construction.

Sequence Diagram

sequenceDiagram
    participant Client as Client Code
    participant Builder as Request Builder
    participant ApiClient as ApiClient
    participant OAuth2 as OAuth2Client
    participant HttpClient as HTTP Client
    participant TokenEndpoint as OAuth Token Endpoint

    Client->>Builder: buildHttpRequest(configuration, apiClient)
    Builder->>Builder: Create HttpRequest.Builder<br/>(headers, body, etc.)
    Builder->>ApiClient: applyAuthHeader(requestBuilder, configuration)
    
    alt Configuration.Credentials == null or NONE
        ApiClient->>Builder: (no modification)
    else CredentialsMethod == API_TOKEN
        ApiClient->>ApiClient: Extract static token
        ApiClient->>Builder: Set Authorization: Bearer {token}
    else CredentialsMethod == CLIENT_CREDENTIALS
        ApiClient->>ApiClient: Check cache by CredentialsCacheKey
        alt Token in cache and valid
            ApiClient->>Builder: Set Authorization: Bearer {cached_token}
        else Token missing or expired
            ApiClient->>OAuth2: getAccessToken()
            OAuth2->>OAuth2: Check/create inFlight ConcurrentMap entry
            alt Atomic winner (first thread to set inFlight)
                OAuth2->>TokenEndpoint: POST /oauth/token<br/>(client_credentials, client_id, secret)
                TokenEndpoint->>OAuth2: {access_token, expires_in}
                OAuth2->>OAuth2: Create immutable AccessToken snapshot<br/>with computed expiry
                OAuth2->>OAuth2: Cache in per-ApiClient ConcurrentMap
                OAuth2->>OAuth2: Clear inFlight entry
            else Atomic loser (subsequent threads)
                OAuth2->>OAuth2: Reuse in-flight CompletableFuture
            end
            OAuth2->>ApiClient: CompletableFuture.get() {access_token}
            ApiClient->>Builder: Set Authorization: Bearer {token}
        end
    end
    
    Builder->>Builder: Apply request interceptors
    Builder->>Builder: Build HttpRequest
    Builder->>HttpClient: send/sendAsync(request)
    HttpClient->>Client: Response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • feat: streaming APIExecutor #296: Modifies streaming request construction pipeline (ApiExecutorRequestBuilder/BaseStreamingApi) with this PR adding centralized applyAuthHeader() call during request building, directly related to streaming executor implementation.
  • feat: APIExecutor for calling raw arbitrary endpoints #273: Introduces ApiExecutorRequestBuilder and raw-request API path; this PR extends that work by updating buildHttpRequest() to invoke ApiClient.applyAuthHeader() in the same class/method signature.

Suggested reviewers

  • jimmyjames
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: attaching Authorization headers to streaming and ApiExecutor requests, directly addressing the bug reported in issue #330.
Linked Issues check ✅ Passed All objectives from issue #330 are met: Authorization headers are attached to streaming/ApiExecutor requests, token retrieval is centralized in ApiClient.applyAuthHeader, caching is implemented, and tests validate the fix across auth methods.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the linked issue: refactoring authentication handling, centralizing OAuth2Client, and updating request builders to apply auth headers. AccessToken was refactored to immutability as a supporting improvement per reviewer feedback.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java (1)

167-181: ⚠️ Potential issue | 🟠 Major

Preserve auth exceptions instead of re-wrapping them.

Line 171 can now throw ApiException or FgaInvalidParameterException, but the broad catch on Line 180 wraps both as a new ApiException. That loses the original auth failure type/details for streaming requests.

Proposed fix
         try {
             byte[] bodyBytes = objectMapper.writeValueAsBytes(body);
             HttpRequest.Builder requestBuilder = ApiClient.requestBuilder(method, path, bodyBytes, configuration);
 
             apiClient.applyAuthHeader(requestBuilder, configuration);
 
             // Apply request interceptors if any
             var interceptor = apiClient.getRequestInterceptor();
             if (interceptor != null) {
                 interceptor.accept(requestBuilder);
             }
 
             return requestBuilder.build();
+        } catch (ApiException | FgaInvalidParameterException e) {
+            throw e;
         } catch (Exception e) {
             throw new ApiException(e);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java` around lines 167 -
181, The catch-all in BaseStreamingApi around objectMapper.writeValueAsBytes /
ApiClient.requestBuilder / apiClient.applyAuthHeader is re-wrapping auth-related
exceptions (e.g., FgaInvalidParameterException) into a generic ApiException and
losing original details; update the catch block to rethrow the original
exception when it's already an ApiException or FgaInvalidParameterException (or
other auth-specific exception types), and only wrap unknown exceptions into a
new ApiException so callers of the BaseStreamingApi methods receive the original
auth failure type and message.
🧹 Nitpick comments (1)
src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java (1)

360-390: Optional: extend streaming regression to CLIENT_CREDENTIALS.

Issue #330 specifically called out streamedListObjects failing with ClientCredentials. The API_TOKEN test is a solid guard, but the exact reproduction scenario (CLIENT_CREDENTIALS on the streaming path) is only covered transitively via ApiExecutorTest.rawApi_appliesClientCredentialsAuthHeader + ApiClientTest caching. A WireMock-backed streaming variant (similar to rawApi_appliesClientCredentialsAuthHeader) would give a direct regression guard for the exact originally-reported flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java`
around lines 360 - 390, Add a new streaming unit test mirroring
stream_appliesApiTokenAuthHeader but using ClientCredentials: create a
ClientConfiguration with credentials(new Credentials(new
ClientCredentials(clientId, clientSecret))) (or the existing ClientCredentials
fixture), call doCallRealMethod().when(mockApiClient).applyAuthHeader(...) as in
the API_TOKEN test, construct an OpenFgaClient with that config, mock the
HttpClient.sendAsync to return a Stream response, call
streamingApiExecutor(...).stream(...).get(), capture the HttpRequest via
ArgumentCaptor and assert the Authorization header produced by the streaming
path equals the expected bearer token created/attached by applyAuthHeader (i.e.,
the same pattern and assertions as in stream_appliesApiTokenAuthHeader but with
ClientCredentials to directly guard the CLIENT_CREDENTIALS streaming
regression).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java`:
- Around line 390-397: The cached OAuth2Client is currently returned regardless
of the passed Configuration; change ensureOAuth2Client to validate the existing
oAuth2Client against the credential identity from the incoming Configuration
(e.g. clientId, clientSecret/token issuer/audience or any combination that
defines the credential identity) and only reuse it if the identity matches;
otherwise construct a new OAuth2Client and atomically replace the cached
reference (use a compare-and-set loop on the oAuth2Client atomic reference) so
callers using ConfigurationOverride or different credentials get a client tied
to those credentials; implement a small identity/key extractor on Configuration
and/or add a getter on OAuth2Client to compare the identity before returning the
cached instance.
- Around line 372-375: The OAuth token refresh path is not thread-safe:
ensureOAuth2Client() returns a shared OAuth2Client whose getAccessToken()
mutates shared fields (token, expiresAt) and can be invoked concurrently; update
OAuth2Client.getAccessToken() to serialize refreshes (e.g., add a private final
ReentrantLock or synchronized block inside getAccessToken(), or implement a
single-flight promise so only one thread calls exchangeToken() while others
wait) and ensure access to token and expiresAt is guarded; keep
ensureOAuth2Client() as-is but protect the mutating calls (exchangeToken()) and
the subsequent writes to token/expiresAt inside the lock so concurrent requests
don’t perform duplicate exchanges or corrupt state.

In `@src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java`:
- Around line 209-211: The builder currently appends user headers with
headers.forEach(httpRequestBuilder::header) then calls
apiClient.applyAuthHeader(httpRequestBuilder, configuration), which can produce
duplicate Authorization headers; update either ApiExecutorRequestBuilder or the
applyAuthHeader implementation to avoid duplication: detect existing
"Authorization" on the HttpRequest.Builder (or in the headers map passed to
ApiExecutorRequestBuilder) and skip adding the SDK token if present, or
conversely remove/override any user-supplied Authorization before calling
apiClient.applyAuthHeader; refer to the headers.forEach loop, the
httpRequestBuilder::header usage, and the apiClient.applyAuthHeader(...) method
when making the change and add a short Javadoc note on Authorization being
SDK-managed if you choose to reserve the header instead.

---

Outside diff comments:
In `@src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java`:
- Around line 167-181: The catch-all in BaseStreamingApi around
objectMapper.writeValueAsBytes / ApiClient.requestBuilder /
apiClient.applyAuthHeader is re-wrapping auth-related exceptions (e.g.,
FgaInvalidParameterException) into a generic ApiException and losing original
details; update the catch block to rethrow the original exception when it's
already an ApiException or FgaInvalidParameterException (or other auth-specific
exception types), and only wrap unknown exceptions into a new ApiException so
callers of the BaseStreamingApi methods receive the original auth failure type
and message.

---

Nitpick comments:
In `@src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java`:
- Around line 360-390: Add a new streaming unit test mirroring
stream_appliesApiTokenAuthHeader but using ClientCredentials: create a
ClientConfiguration with credentials(new Credentials(new
ClientCredentials(clientId, clientSecret))) (or the existing ClientCredentials
fixture), call doCallRealMethod().when(mockApiClient).applyAuthHeader(...) as in
the API_TOKEN test, construct an OpenFgaClient with that config, mock the
HttpClient.sendAsync to return a Stream response, call
streamingApiExecutor(...).stream(...).get(), capture the HttpRequest via
ArgumentCaptor and assert the Authorization header produced by the streaming
path equals the expected bearer token created/attached by applyAuthHeader (i.e.,
the same pattern and assertions as in stream_appliesApiTokenAuthHeader but with
ClientCredentials to directly guard the CLIENT_CREDENTIALS streaming
regression).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 39bae369-0896-409a-ae4f-5939c2a72286

📥 Commits

Reviewing files that changed from the base of the PR and between 2565cc8 and 11ab8e4.

📒 Files selected for processing (8)
  • src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java
  • src/main/java/dev/openfga/sdk/api/OpenFgaApi.java
  • src/main/java/dev/openfga/sdk/api/client/ApiClient.java
  • src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java
  • src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java
  • src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java
  • src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
  • src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java

Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes missing Authorization: Bearer <token> propagation on streaming and ApiExecutor request paths by centralizing auth header application in ApiClient, ensuring authenticated requests across OpenFgaApi, BaseStreamingApi, and ApiExecutorRequestBuilder.

Changes:

  • Add ApiClient.applyAuthHeader(...) with lazy OAuth2Client initialization/caching for CLIENT_CREDENTIALS.
  • Route OpenFgaApi, streaming request building, and ApiExecutorRequestBuilder through applyAuthHeader(...).
  • Add/adjust unit + WireMock tests to cover auth header attachment and token exchange caching.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/main/java/dev/openfga/sdk/api/client/ApiClient.java Introduces centralized auth header application + cached OAuth2 client.
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java Delegates auth header logic to ApiClient.applyAuthHeader(...).
src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java Ensures streaming request builder applies auth header.
src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java Applies auth header for ApiExecutor-built requests and updates throws list.
src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java Adds unit tests for applyAuthHeader behaviors + OAuth2 exchange caching.
src/test/java/dev/openfga/sdk/api/client/ApiExecutorTest.java Adds WireMock regression tests for auth headers on ApiExecutor path.
src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java Adds regression guard verifying auth header on streaming path.
src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java Switches to a real ApiClient in tests to exercise auth wiring.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java Outdated
Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Apr 22, 2026

Codecov Report

❌ Patch coverage is 73.62637% with 24 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.44%. Comparing base (2565cc8) to head (93de0d7).

Files with missing lines Patch % Lines
...ain/java/dev/openfga/sdk/api/client/ApiClient.java 66.03% 7 Missing and 11 partials ⚠️
...in/java/dev/openfga/sdk/api/auth/OAuth2Client.java 80.00% 5 Missing and 1 partial ⚠️

❌ Your project status has failed because the head coverage (38.44%) is below the target coverage (80.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files
@@             Coverage Diff              @@
##               main     #331      +/-   ##
============================================
+ Coverage     38.05%   38.44%   +0.38%     
- Complexity     1259     1265       +6     
============================================
  Files           198      198              
  Lines          7646     7705      +59     
  Branches        885      900      +15     
============================================
+ Hits           2910     2962      +52     
+ Misses         4598     4594       -4     
- Partials        138      149      +11     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@SoulPancake
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java (1)

174-210: Solid concurrency test; consider pooling threads for CI stability.

The test faithfully reproduces the race and correctly asserts single-flight via verify(1, postRequestedFor(...)). Two small nits if you want to harden it:

  • Using new Thread(...).start() without joining means threads may still be running when the test returns on timeout; wrapping in a try/finally that interrupts survivors on failure would avoid leaked threads across tests.
  • A small ExecutorService with a shutdownNow() in finally would be cleaner than raw threads.

Neither is blocking — the done latch is sufficient for the assertion semantics.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java` around lines
174 - 210, The test exchangeOAuth2Token_concurrentRequests_singleExchange
creates raw Threads that may leak across tests; replace the manual thread
creation with an ExecutorService (e.g., fixed thread pool of size threadCount)
and submit the same runnables that await startGate and count down done, then in
a finally block call executor.shutdownNow() and await termination to ensure all
worker threads are stopped; alternatively, if you prefer to keep Threads, store
them in a list and after done.await(...) join each thread and interrupt any
still-alive threads in a finally block so no threads leak between tests (refer
to startGate, done, tokens, failures, and the test method name).
src/main/java/dev/openfga/sdk/api/auth/TokenSnapshot.java (1)

25-38: Per-call randomized jitter makes isValid() non-deterministic across successive calls.

Because ThreadLocalRandom.current().nextInt(EXPIRY_JITTER_SECS) is evaluated on every invocation, two calls a second apart near the expiry boundary can return different verdicts (one valid, the next invalid). Combined with the inFlight single-flight gate this is benign — the first losing call triggers a refresh that others piggyback on — but it does mean a burst of callers can deterministically force an early refresh even when the token is still well within its buffer window.

If the intent is "randomize the refresh boundary per token" rather than "per call", consider capturing the jitter once (e.g., compute an effectiveExpiresAt in the compact constructor) so all callers agree on validity for a given snapshot:

♻️ Sketch
-record TokenSnapshot(String token, Instant expiresAt) {
+record TokenSnapshot(String token, Instant expiresAt, Instant effectiveExpiresAt) {
     ...
     TokenSnapshot {
-        expiresAt = expiresAt != null ? expiresAt.truncatedTo(ChronoUnit.SECONDS) : null;
+        expiresAt = expiresAt != null ? expiresAt.truncatedTo(ChronoUnit.SECONDS) : null;
+        effectiveExpiresAt = expiresAt == null ? null : expiresAt
+                .minusSeconds(EXPIRY_BUFFER_SECS)
+                .minusSeconds(ThreadLocalRandom.current().nextInt(EXPIRY_JITTER_SECS));
     }
     boolean isValid() {
         if (isNullOrWhitespace(token)) return false;
-        if (expiresAt == null) return true;
-        ...
+        if (effectiveExpiresAt == null) return true;
+        return Instant.now().isBefore(effectiveExpiresAt);
     }
 }

Separately, if TOKEN_EXPIRY_JITTER_IN_SEC is ever set to 0, nextInt(0) throws IllegalArgumentException — a defensive Math.max(1, EXPIRY_JITTER_SECS) or an explicit guard would prevent a nasty regression if the constant is tuned down.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/auth/TokenSnapshot.java` around lines 25 -
38, The isValid() method in TokenSnapshot uses
ThreadLocalRandom.nextInt(EXPIRY_JITTER_SECS) on every call making validity
checks non-deterministic; instead compute a per-snapshot jitter once (e.g., add
an effectiveExpiresAt field or compute and store jitter in the TokenSnapshot
constructor) and have isValid() use that stored value (use expiresAt,
EXPIRY_BUFFER_SECS and the stored jitter to derive effectiveExpiresAt) so all
calls agree; also defensively handle EXPIRY_JITTER_SECS == 0 by using
Math.max(1, EXPIRY_JITTER_SECS) or an explicit guard when computing the one-time
jitter.
src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java (1)

41-43: LGTM — parameterized cases correctly migrated to TokenSnapshot.

The parameter set still covers the same validity boundaries and the TokenSnapshot constructor applies second-truncation consistently for these cases.

Consider renaming the class to TokenSnapshotTest in a follow-up since the class is no longer exercising AccessToken.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java` around lines 41
- 43, The test class name still references AccessToken but now tests
TokenSnapshot; rename the test class from AccessTokenTest to TokenSnapshotTest
(update the public class declaration and the file name accordingly) so the test
class name matches the exercised type (TokenSnapshot) while leaving the
parameterized test method testTokenValid and its TokenSnapshot usage unchanged.
src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java (1)

58-270: Good coverage of the new applyAuthHeader contract.

The suite exercises the essential branches (NONE, null credentials, null method, API_TOKEN, token replacement on retry, CC failure → ApiException, CC success, CC caching, CC per-credential isolation). The replace-header test directly guards against the regression that led to setHeader being used in production — nice regression coverage.

Minor: mockHttpClientBuilder only stubs build() and executor(...). If ApiClient ever starts invoking other HttpClient.Builder fluent methods (e.g., connectTimeout, version) in the ctor, those will silently return null and NPE at the next chained call. Not a problem today, but a one-line Mockito.when(builder.<anyMethod>(any())).thenReturn(builder) default or using a real HttpClient.newBuilder() with a mocked httpClient would be more future-proof.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java` around lines 58
- 270, Test helper mockHttpClientBuilder currently only stubs build() and
executor(...), so if ApiClient's constructor ever calls other HttpClient.Builder
fluent methods (e.g., connectTimeout, version, followRedirects) the chain will
return null and cause NPEs; update mockHttpClientBuilder to either (a) use a
real HttpClient.newBuilder() and spy it while stubbing build() to return the
HttpClientMock, or (b) add a default Mockito stub that returns the same mock
builder for any fluent method (e.g.,
Mockito.when(builder.connectTimeout(any())).thenReturn(builder),
Mockito.when(builder.version(any())).thenReturn(builder), or a catch-all pattern
to return builder) so the builder remains chainable when used by ApiClient (see
mockHttpClientBuilder, ApiClient constructor, and HttpClient.Builder fluent
methods).
src/main/java/dev/openfga/sdk/api/client/ApiClient.java (1)

396-449: Cache key design looks good; minor suggestion on lookup path.

SHA-256 hashing the secret (rather than retaining the raw value as a key field) is the right call, and including clientId, issuer, audience, and scopes gives clean tenant isolation. The ConcurrentHashMap + putIfAbsent pattern is correct.

Optional: computeIfAbsent would collapse the two-step get/put and avoid constructing a throwaway OAuth2Client on the losing side of a race, at the cost of needing to wrap FgaInvalidParameterException (e.g., via a try/catch + sneaky-throw helper or an unchecked sentinel). Given how infrequently this path races in practice, the current form is fine.

Also note: entries are never evicted. If an application rotates client secrets at runtime (a new Configuration with the same clientId/issuer/audience/scopes but a new secret), the map grows by one per rotation. Not a leak in typical usage, but worth calling out if long-running servers do key rotation without restarting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java` around lines 396 -
449, The ensureOAuth2Client two-step get/putIfAbsent should be simplified to use
oAuth2Clients.computeIfAbsent to avoid constructing a throwaway OAuth2Client on
races: change ensureOAuth2Client to call oAuth2Clients.computeIfAbsent(key, k ->
{ try { return new OAuth2Client(configuration, this); } catch
(FgaInvalidParameterException e) { throw new RuntimeException(e); } }) and then
unwrap the RuntimeException to rethrow the original
FgaInvalidParameterException; additionally consider replacing the plain
ConcurrentHashMap with a bounded/expiring cache (e.g., Caffeine) or add
TTL/eviction logic for CredentialsCacheKey to avoid unbounded growth on
client-secret rotations (referencing ensureOAuth2Client, CredentialsCacheKey,
oAuth2Clients, and OAuth2Client).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java`:
- Around line 373-391: Unwrap the ExecutionException returned by
ensureOAuth2Client(...).getAccessToken().get() so the original cause (e.g.,
FgaApi*Error or ApiException from the HTTP attempt) is propagated to callers: if
ExecutionException.getCause() is an ApiException rethrow it, otherwise wrap the
cause in a new ApiException preserving the cause; also preserve
InterruptedException handling as-is. Add defensive null checks before
dereferencing credentials.getApiToken() and
configuration.getCredentials().getClientCredentials() (and their
getToken()/token fields) and throw a clear validation exception (e.g.,
FgaInvalidParameterException or ApiException with a descriptive message) when
those sub-objects or tokens are null instead of letting an NPE surface;
reference the switch block in ApiClient.java, the calls to
credentials.getApiToken().getToken(),
ensureOAuth2Client(...).getAccessToken().get(), and
configuration.getCredentials().getClientCredentials() when making these changes.

---

Nitpick comments:
In `@src/main/java/dev/openfga/sdk/api/auth/TokenSnapshot.java`:
- Around line 25-38: The isValid() method in TokenSnapshot uses
ThreadLocalRandom.nextInt(EXPIRY_JITTER_SECS) on every call making validity
checks non-deterministic; instead compute a per-snapshot jitter once (e.g., add
an effectiveExpiresAt field or compute and store jitter in the TokenSnapshot
constructor) and have isValid() use that stored value (use expiresAt,
EXPIRY_BUFFER_SECS and the stored jitter to derive effectiveExpiresAt) so all
calls agree; also defensively handle EXPIRY_JITTER_SECS == 0 by using
Math.max(1, EXPIRY_JITTER_SECS) or an explicit guard when computing the one-time
jitter.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java`:
- Around line 396-449: The ensureOAuth2Client two-step get/putIfAbsent should be
simplified to use oAuth2Clients.computeIfAbsent to avoid constructing a
throwaway OAuth2Client on races: change ensureOAuth2Client to call
oAuth2Clients.computeIfAbsent(key, k -> { try { return new
OAuth2Client(configuration, this); } catch (FgaInvalidParameterException e) {
throw new RuntimeException(e); } }) and then unwrap the RuntimeException to
rethrow the original FgaInvalidParameterException; additionally consider
replacing the plain ConcurrentHashMap with a bounded/expiring cache (e.g.,
Caffeine) or add TTL/eviction logic for CredentialsCacheKey to avoid unbounded
growth on client-secret rotations (referencing ensureOAuth2Client,
CredentialsCacheKey, oAuth2Clients, and OAuth2Client).

In `@src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java`:
- Around line 41-43: The test class name still references AccessToken but now
tests TokenSnapshot; rename the test class from AccessTokenTest to
TokenSnapshotTest (update the public class declaration and the file name
accordingly) so the test class name matches the exercised type (TokenSnapshot)
while leaving the parameterized test method testTokenValid and its TokenSnapshot
usage unchanged.

In `@src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java`:
- Around line 174-210: The test
exchangeOAuth2Token_concurrentRequests_singleExchange creates raw Threads that
may leak across tests; replace the manual thread creation with an
ExecutorService (e.g., fixed thread pool of size threadCount) and submit the
same runnables that await startGate and count down done, then in a finally block
call executor.shutdownNow() and await termination to ensure all worker threads
are stopped; alternatively, if you prefer to keep Threads, store them in a list
and after done.await(...) join each thread and interrupt any still-alive threads
in a finally block so no threads leak between tests (refer to startGate, done,
tokens, failures, and the test method name).

In `@src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java`:
- Around line 58-270: Test helper mockHttpClientBuilder currently only stubs
build() and executor(...), so if ApiClient's constructor ever calls other
HttpClient.Builder fluent methods (e.g., connectTimeout, version,
followRedirects) the chain will return null and cause NPEs; update
mockHttpClientBuilder to either (a) use a real HttpClient.newBuilder() and spy
it while stubbing build() to return the HttpClientMock, or (b) add a default
Mockito stub that returns the same mock builder for any fluent method (e.g.,
Mockito.when(builder.connectTimeout(any())).thenReturn(builder),
Mockito.when(builder.version(any())).thenReturn(builder), or a catch-all pattern
to return builder) so the builder remains chainable when used by ApiClient (see
mockHttpClientBuilder, ApiClient constructor, and HttpClient.Builder fluent
methods).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3d860d14-bf0f-4a09-bcfd-519efb78f367

📥 Commits

Reviewing files that changed from the base of the PR and between 11ab8e4 and e13bebc.

📒 Files selected for processing (6)
  • src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
  • src/main/java/dev/openfga/sdk/api/auth/TokenSnapshot.java
  • src/main/java/dev/openfga/sdk/api/client/ApiClient.java
  • src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java
  • src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java
  • src/test/java/dev/openfga/sdk/api/client/ApiClientTest.java

Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java
@SoulPancake
Copy link
Copy Markdown
Member

It seems to work well!
@jimmyjames Do you wanna have a look at this once?

@SoulPancake SoulPancake requested a review from jimmyjames April 23, 2026 05:12
Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java Outdated
Copy link
Copy Markdown
Member

@SoulPancake SoulPancake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since accessToken.java is now unused, maybe we can remove it?
And rename AccessTokenTest to TokenSnapshotTest

Comment thread src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java Outdated
…g a forked TokenSnapshot

Also improve stream API executor tests per reviewer comments.
… stability during OAuth2 exchange handling
@cportcvent
Copy link
Copy Markdown
Author

cportcvent commented Apr 24, 2026

Since accessToken.java is now unused, maybe we can remove it? And rename AccessTokenTest to TokenSnapshotTest

Good catch - I went with the other direction: preserve the existing AccessToken history, but make it immutable (essentially roll TokenSnapshot into AccessToken as a modest refactor rather than a new concept)

Addressed in 6298cb6

Copy link
Copy Markdown
Member

@SoulPancake SoulPancake left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Works perfectly!

@SoulPancake SoulPancake requested a review from Copilot April 26, 2026 17:30
@SoulPancake
Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 26, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/dev/openfga/sdk/api/OpenFgaApi.java
Comment thread src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java
Comment thread src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Comment thread src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (4)
src/main/java/dev/openfga/sdk/api/client/ApiClient.java (3)

341-361: Nit: document the assertValid() precondition on the public method.

applyAuthHeader is public and dereferences credentials.getApiToken().getToken() (line 376) and credentials.getClientCredentials() inside ensureOAuth2Client (line 401) without null guards. In-tree this is safe because every call site invokes configuration.assertValid() first (see learning), but since this is a new public surface, a one-line Javadoc note would prevent external callers from tripping on an NPE if they ever reach this method without having validated the configuration.

Based on learnings: Credentials.assertValid() ensures apiToken != null for API_TOKEN and clientCredentials != null for CLIENT_CREDENTIALS, and all in-tree call sites for ApiClient.applyAuthHeader() invoke configuration.assertValid() beforehand.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java` around lines 341 -
361, applyAuthHeader is a public API that dereferences credential fields without
null checks; update its Javadoc to require callers to call
configuration.assertValid() (or equivalently document that
Configuration.assertValid() / Credentials.assertValid() must have been invoked)
so external callers won't hit NPEs; reference the method name applyAuthHeader
and the helper ensureOAuth2Client (which uses getClientCredentials) and the use
of getApiToken/getToken in the doc comment to make the precondition explicit.

385-391: Minor: also unwrap FgaInvalidParameterException from ExecutionException.getCause().

applyAuthHeader already declares throws FgaInvalidParameterException, and OAuth2Client.exchangeToken() declares it too — so it's possible (e.g., a config defect surfaced asynchronously) for an FgaInvalidParameterException to be the cause. Today it gets wrapped in ApiException, which loses the precise type for callers that distinguish the two. Cheap to fix.

♻️ Proposed change
                 } catch (ExecutionException e) {
                     Throwable cause = e.getCause();
                     if (cause instanceof ApiException) {
                         throw (ApiException) cause;
                     }
+                    if (cause instanceof FgaInvalidParameterException) {
+                        throw (FgaInvalidParameterException) cause;
+                    }
                     throw new ApiException(cause != null ? cause : e);
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java` around lines 385 -
391, In the ExecutionException catch block inside ApiClient (e.g., in
applyAuthHeader where OAuth2Client.exchangeToken() may be called), check if
e.getCause() is an instance of FgaInvalidParameterException and if so rethrow it
directly before the ApiException handling; otherwise keep the existing behavior
of rethrowing ApiException when cause is ApiException or wrapping into a new
ApiException for other throwables. This preserves the specific
FgaInvalidParameterException type for callers that rely on it.

412-453: LGTM on CredentialsCacheKey.

Hashing the client secret (rather than retaining plaintext in the key) is a nice touch for cache-isolation hygiene, and the Arrays.equals/Arrays.hashCode handling on the byte array is correct. One thing to keep in mind operationally: if your deployment ever rotates client credentials in-place against the same ApiClient instance, stale entries will accumulate in oAuth2Clients — likely not a concern in typical usage (one credential per app), but worth noting if the SDK is ever embedded in a multi-tenant credential-rotating context.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java` around lines 412 -
453, The review notes stale cache entries can accumulate in the oAuth2Clients
map when client credentials are rotated (new secret => new CredentialsCacheKey)
— fix by giving the cache eviction semantics: replace the plain Map with an
expiring/loading cache (e.g., Caffeine or a ConcurrentHashMap with scheduled TTL
eviction) or implement explicit removal when credentials are updated; locate
usages of oAuth2Clients and the CredentialsCacheKey class and change the cache
implementation to support time-based expiry or an update/remove pathway so
rotating client secrets do not leak stale entries.
src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java (1)

60-65: Optional: consider a CAS retry loop instead of recursion.

The fallback existing != null ? existing : getAccessToken() at line 64 recurses when the racing thread cleared inFlight before our get(). In practice the recursion terminates immediately (snapshot is set before the in-flight gate is cleared on success), but a tight CAS-retry loop would be more idiomatic and remove the (theoretical) stack-growth concern under sustained failure churn.

♻️ Sketch
-    public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterException, ApiException {
-        AccessToken current = snapshot.get();
-        if (current.isValid()) {
-            return CompletableFuture.completedFuture(current.token());
-        }
-
-        CompletableFuture<String> promise = new CompletableFuture<>();
-        if (!inFlight.compareAndSet(null, promise)) {
-            // Another thread won the race — join its exchange rather than starting a new one.
-            CompletableFuture<String> existing = inFlight.get();
-            return existing != null ? existing : getAccessToken();
-        }
+    public CompletableFuture<String> getAccessToken() throws FgaInvalidParameterException, ApiException {
+        CompletableFuture<String> promise;
+        while (true) {
+            AccessToken current = snapshot.get();
+            if (current.isValid()) {
+                return CompletableFuture.completedFuture(current.token());
+            }
+            promise = new CompletableFuture<>();
+            if (inFlight.compareAndSet(null, promise)) {
+                break;
+            }
+            CompletableFuture<String> existing = inFlight.get();
+            if (existing != null) {
+                return existing;
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java` around lines 60 -
65, The fallback in OAuth2Client.getAccessToken currently recurses via "existing
!= null ? existing : getAccessToken()" when inFlight was cleared between the
compareAndSet and the subsequent get(), which risks theoretical stack growth;
change this to a CAS-retry loop: allocate a new CompletableFuture<String>
promise, attempt inFlight.compareAndSet(null, promise) and if it fails obtain
the current inFlight via inFlight.get() and if that is non-null return it,
otherwise retry the compareAndSet until it succeeds (then proceed with the token
exchange and complete/clear inFlight), using the existing inFlight, promise,
compareAndSet and getAccessToken logic but without recursion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java`:
- Around line 60-65: The fallback in OAuth2Client.getAccessToken currently
recurses via "existing != null ? existing : getAccessToken()" when inFlight was
cleared between the compareAndSet and the subsequent get(), which risks
theoretical stack growth; change this to a CAS-retry loop: allocate a new
CompletableFuture<String> promise, attempt inFlight.compareAndSet(null, promise)
and if it fails obtain the current inFlight via inFlight.get() and if that is
non-null return it, otherwise retry the compareAndSet until it succeeds (then
proceed with the token exchange and complete/clear inFlight), using the existing
inFlight, promise, compareAndSet and getAccessToken logic but without recursion.

In `@src/main/java/dev/openfga/sdk/api/client/ApiClient.java`:
- Around line 341-361: applyAuthHeader is a public API that dereferences
credential fields without null checks; update its Javadoc to require callers to
call configuration.assertValid() (or equivalently document that
Configuration.assertValid() / Credentials.assertValid() must have been invoked)
so external callers won't hit NPEs; reference the method name applyAuthHeader
and the helper ensureOAuth2Client (which uses getClientCredentials) and the use
of getApiToken/getToken in the doc comment to make the precondition explicit.
- Around line 385-391: In the ExecutionException catch block inside ApiClient
(e.g., in applyAuthHeader where OAuth2Client.exchangeToken() may be called),
check if e.getCause() is an instance of FgaInvalidParameterException and if so
rethrow it directly before the ApiException handling; otherwise keep the
existing behavior of rethrowing ApiException when cause is ApiException or
wrapping into a new ApiException for other throwables. This preserves the
specific FgaInvalidParameterException type for callers that rely on it.
- Around line 412-453: The review notes stale cache entries can accumulate in
the oAuth2Clients map when client credentials are rotated (new secret => new
CredentialsCacheKey) — fix by giving the cache eviction semantics: replace the
plain Map with an expiring/loading cache (e.g., Caffeine or a ConcurrentHashMap
with scheduled TTL eviction) or implement explicit removal when credentials are
updated; locate usages of oAuth2Clients and the CredentialsCacheKey class and
change the cache implementation to support time-based expiry or an update/remove
pathway so rotating client secrets do not leak stale entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 5e483e90-b78e-4e7e-bd66-e375203f1813

📥 Commits

Reviewing files that changed from the base of the PR and between e13bebc and f879fde.

📒 Files selected for processing (5)
  • src/main/java/dev/openfga/sdk/api/auth/AccessToken.java
  • src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java
  • src/main/java/dev/openfga/sdk/api/client/ApiClient.java
  • src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java
  • src/test/java/dev/openfga/sdk/api/client/StreamingApiExecutorTest.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java

…tor to strict mutex; includes regression test
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

streamedListObjects omits Authorization header — 401 against FGA Cloud with ClientCredentials

4 participants