Skip to content

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

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

fix: attach Authorization header to streaming and ApiExecutor requests (#330)#331
cportcvent wants to merge 10 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

Release Notes

  • Bug Fixes

    • Authentication headers are now applied consistently at request construction time for all API calls, including streaming.
    • Improved error handling when obtaining tokens to avoid unexpected failures.
  • Refactor

    • Centralized outbound authentication logic and per-configuration token caching for correct isolation.
  • Tests

    • Added tests covering API token, client-credentials flows, token caching, and concurrent token exchange 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: e02bedea-e07c-44bc-8b64-09fb4983e721

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

Centralizes and applies outbound authentication for all request paths: adds ApiClient.applyAuthHeader(...), updates request builders (streaming and non-streaming) to call it, moves OAuth2 token management into ApiClient with caching, removes duplicate auth logic in OpenFgaApi, and adds tests for auth header behavior and concurrency.

Changes

Cohort / File(s) Summary
Request builders
src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java, src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java
Both builders now call apiClient.applyAuthHeader(httpRequestBuilder, configuration) during request construction so Authorization headers are attached before request interceptors and before building the request.
Api client & auth centralization
src/main/java/dev/openfga/sdk/api/client/ApiClient.java
Adds applyAuthHeader(HttpRequest.Builder, Configuration) to attach Authorization for API_TOKEN and CLIENT_CREDENTIALS; lazily creates/caches OAuth2Client instances keyed by credentials; maps concurrency and exceptions to ApiException.
OAuth2 token handling
src/main/java/dev/openfga/sdk/api/auth/OAuth2Client.java, src/main/java/dev/openfga/sdk/api/auth/TokenSnapshot.java
Refactors token caching to use immutable TokenSnapshot and in-flight deduplication so concurrent callers share one exchange; introduces TokenSnapshot record and validity logic with buffer/jitter.
High-level API changes
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java
Removes per-class OAuth2Client field and manual request-level auth header construction; delegates auth header application to ApiClient.applyAuthHeader(...).
Request builder signature
src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java
buildHttpRequest(...) now declares throws ApiException in addition to existing exceptions (propagating applyAuthHeader's checked exception).
Tests — ApiClient & executor
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/StreamingApiExecutorTest.java
Adds tests asserting applyAuthHeader behavior for missing creds, ApiToken, and ClientCredentials (including token exchange, caching, and header replacement) and verifies headers on raw and streaming requests.
Tests — OAuth2 concurrency & infra
src/test/java/dev/openfga/sdk/api/auth/OAuth2ClientTest.java, src/test/java/dev/openfga/sdk/api/auth/AccessTokenTest.java, src/test/java/dev/openfga/sdk/api/client/OpenFgaClientTest.java
Adds concurrent token-exchange test ensuring single exchange for concurrent calls; adapts tests to new TokenSnapshot and uses a real ApiClient in client tests.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant ApiClient
    participant RequestBuilder
    participant OAuth2Issuer
    participant RemoteAPI

    Client->>RequestBuilder: buildHttpRequest(configuration)
    RequestBuilder->>ApiClient: applyAuthHeader(requestBuilder, configuration)
    alt credentials method = API_TOKEN
        ApiClient-->>RequestBuilder: add Authorization: Bearer <api_token>
    else credentials method = CLIENT_CREDENTIALS
        ApiClient->>OAuth2Issuer: (cached?) getAccessToken()
        alt cached valid token
            OAuth2Issuer-->>ApiClient: return token
        else no valid token
            OAuth2Issuer->>OAuth2Issuer: in-flight dedupe + exchange
            OAuth2Issuer->>OAuth2Issuer: update TokenSnapshot
            OAuth2Issuer-->>ApiClient: return token
        end
        ApiClient-->>RequestBuilder: add Authorization: Bearer <exchanged_token>
    else no credentials
        ApiClient-->>RequestBuilder: no Authorization header
    end
    RequestBuilder->>RemoteAPI: send HttpRequest (with headers)
    RemoteAPI-->>Client: response / stream of data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • jimmyjames
  • rhamzeh
🚥 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 Title clearly summarizes the main fix: attaching Authorization headers to streaming and ApiExecutor requests, matching the core bug fix in the changeset.
Linked Issues check ✅ Passed All core coding requirements from #330 are met: Authorization header is attached to streaming requests via centralized apiClient.applyAuthHeader(), token caching is implemented with concurrent maps, OAuth2Client is shared on ApiClient, and tests validate the behavior across NONE, API_TOKEN, and CLIENT_CREDENTIALS methods.
Out of Scope Changes check ✅ Passed All changes directly support the core fix: centralizing auth header application, implementing token caching, and adding comprehensive tests. No unrelated modifications were introduced.

✏️ 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 75.00000% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 38.34%. Comparing base (2565cc8) to head (dfab5a5).

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 78.26% 4 Missing and 1 partial ⚠️

❌ Your project status has failed because the head coverage (38.34%) 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.34%   +0.28%     
- Complexity     1259     1266       +7     
============================================
  Files           198      199       +1     
  Lines          7646     7717      +71     
  Branches        885      901      +16     
============================================
+ Hits           2910     2959      +49     
- Misses         4598     4610      +12     
- Partials        138      148      +10     

☔ 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

@Test
public void stream_appliesApiTokenAuthHeader() throws Exception {
// Regression guard for openfga/java-sdk#330: the streaming path must attach
// Authorization: Bearer <token> when the configuration uses API_TOKEN credentials.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For this test
can we replace the doCallRealMethod() pattern with real ApiClient constructed from a fresh mock builder backed by mockHttpClient ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in 6298cb6

// after inFlight becomes null immediately sees a valid token.
snapshot.set(new TokenSnapshot(token, Instant.now().plusSeconds(response.getExpiresInSeconds())));

telemetry.metrics().credentialsRequest(1L, new HashMap<>());
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we move this telemetry.metrics().credentialsRequest(...) to after promise.complete(token) so a slow/throwing telemetry call can't stall waiting threads.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in f879fde

…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

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