Checklist
Description
OpenFgaClient#streamedListObjects sends HTTPS requests without the Authorization: Bearer <token> header, so every call against an FGA deployment that requires auth (e.g. FGA Cloud via api.us1.fga.dev) fails with:
dev.openfga.sdk.errors.ApiException: API error: 401
Every other method on the same OpenFgaClient instance — check, batchCheck, listObjects, read, etc. — succeeds with the identical ClientCredentials. A plain curl POST to /stores/{storeId}/streamed-list-objects with a manually-obtained bearer token succeeds. The issue is reproducible on 100% of calls.
Root cause (confirmed by reading sources of 0.9.7 and main):
Non-streaming calls route through OpenFgaApi.buildHttpRequestWithPublisher, which attaches the bearer token:
// src/main/java/dev/openfga/sdk/api/OpenFgaApi.java (0.9.7), around L1297–L1300
if (configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE) {
String accessToken = getAccessToken(configuration);
httpRequest.header("Authorization", "Bearer " + accessToken);
}
OpenFgaApi is the only class that constructs and owns an OAuth2Client.
Streaming calls take a different path:
OpenFgaClient.streamedListObjects (src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java, around L1317) instantiates StreamedListObjectsApi directly with (configuration, apiClient) — it does not route through OpenFgaApi, and has no reference to an OAuth2Client.
StreamedListObjectsApi.streamedListObjects calls BaseStreamingApi.buildHttpRequest.
BaseStreamingApi.buildHttpRequest calls ApiClient.requestBuilder(method, path, bodyBytes, configuration), which sets only accept, content-type, URI, method, and timeout. It never reads credentials, never consults an OAuth2Client, and never adds an Authorization header. It then applies apiClient.getRequestInterceptor() if one is set, which is the only user-facing escape hatch — but by default no such interceptor exists.
The same omission exists in the newer StreamingApiExecutor / ApiExecutorRequestBuilder.buildHttpRequest (merged via PR #296), which likewise calls ApiClient.requestBuilder(...) without attaching auth.
Version of SDK
0.9.7
Version of OpenFGA (if known)
Auth0 FGA (> v1.5.x)
OpenFGA Flags/Custom Configuration Applicable
N/A
Reproduction
Consistent on every call. Full standalone Maven project below — single class, no dependency on anything else.
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>local.repro</groupId>
<artifactId>openfga-streaming-repro</artifactId>
<version>0.1.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>dev.openfga</groupId>
<artifactId>openfga-sdk</artifactId>
<version>0.9.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>3.6.3</version>
<configuration>
<mainClass>Repro</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
src/main/java/Repro.java:
// Repro.java — run against any FGA deployment requiring auth (e.g. FGA Cloud).
// Requires dev.openfga:openfga-sdk:0.9.7 on the classpath.
import dev.openfga.sdk.api.client.OpenFgaClient;
import dev.openfga.sdk.api.client.model.ClientCheckRequest;
import dev.openfga.sdk.api.client.model.ClientListObjectsRequest;
import dev.openfga.sdk.api.configuration.ClientConfiguration;
import dev.openfga.sdk.api.configuration.ClientCredentials;
import dev.openfga.sdk.api.configuration.Credentials;
public final class Repro {
public static void main(String[] args) throws Exception {
ClientConfiguration cfg = new ClientConfiguration()
.apiUrl(getenvOrDefault("FGA_API_URL", "https://api.us1.fga.dev"))
.storeId(System.getenv("FGA_STORE_ID"))
.credentials(new Credentials(new ClientCredentials()
.apiTokenIssuer(getenvOrDefault("FGA_API_TOKEN_ISSUER", "auth.fga.dev"))
.apiAudience(getenvOrDefault("FGA_API_AUDIENCE", "https://api.us1.fga.dev/"))
.clientId(System.getenv("FGA_CLIENT_ID"))
.clientSecret(System.getenv("FGA_CLIENT_SECRET"))));
OpenFgaClient client = new OpenFgaClient(cfg);
// A. Non-streaming check — succeeds, bearer token attached.
var check = client.check(new ClientCheckRequest()
.user("user:baz")
.relation("owner")
._object("foo:0016a5ab-38ac-49c4-86c7-0ece9c212ea6")).get();
System.out.println("check OK, allowed=" + check.getAllowed());
// B. Non-streaming listObjects — succeeds, bearer token attached.
var list = client.listObjects(new ClientListObjectsRequest()
.user("user:baz")
.relation("owner")
.type("foo")).get();
System.out.println("listObjects OK, count=" + list.getObjects().size());
// C. Streaming — fails with API error: 401. Request goes out with no Authorization header.
var req = new ClientListObjectsRequest()
.user("user:baz")
.relation("owner")
.type("foo");
client.streamedListObjects(req, resp -> System.out.println("got " + resp.getObject()))
.get(); // throws ExecutionException -> ApiException: API error: 401
}
private static String getenvOrDefault(String name, String defaultValue) {
String value = System.getenv(name);
return value != null ? value : defaultValue;
}
}
Run:
export FGA_API_URL='https://api.us1.fga.dev'
export FGA_STORE_ID='<store id>'
export FGA_API_TOKEN_ISSUER='auth.fga.dev'
export FGA_API_AUDIENCE='https://api.us1.fga.dev/'
export FGA_CLIENT_ID='<client id>'
export FGA_CLIENT_SECRET='<client secret>'
mvn -q compile exec:java
Steps:
- Given an FGA store with any valid authorization model and at least one tuple.
- When
streamedListObjects is invoked with ClientCredentials auth.
- Then the outgoing HTTPS POST to
/stores/{store_id}/streamed-list-objects contains no Authorization header and FGA returns 401.
The same configuration works for check (step A) and listObjects (step B) on the very same OpenFgaClient instance, proving the credentials themselves are valid.
OpenFGA SDK version
dev.openfga:openfga-sdk:0.9.7 — also confirmed on main as of 2026-04-21.
OpenFGA version
FGA Cloud (api.us1.fga.dev). Also reproducible against any self-hosted OpenFGA that requires a bearer token.
SDK Configuration
new ClientConfiguration()
.apiUrl("https://api.us1.fga.dev")
.storeId("<redacted>")
.credentials(new Credentials(new ClientCredentials()
.apiTokenIssuer("auth.fga.dev")
.apiAudience("https://api.us1.fga.dev/")
.clientId("<redacted>")
.clientSecret("<redacted>")));
Backtrace
Actual output of the repro above against FGA Cloud:
check OK, allowed=true
listObjects OK, count=1234
Exception in thread "main" java.util.concurrent.ExecutionException: java.lang.RuntimeException: dev.openfga.sdk.errors.ApiException: API error: 401
at java.base/java.util.concurrent.CompletableFuture.reportGet(CompletableFuture.java:396)
at java.base/java.util.concurrent.CompletableFuture.get(CompletableFuture.java:2073)
at Repro.main(Repro.java:46)
Caused by: java.lang.RuntimeException: dev.openfga.sdk.errors.ApiException: API error: 401
at dev.openfga.sdk.api.BaseStreamingApi.lambda$processStreamingResponse$2(BaseStreamingApi.java:113)
at java.base/java.util.concurrent.CompletableFuture.uniHandle(CompletableFuture.java:934)
at java.base/java.util.concurrent.CompletableFuture$UniHandle.tryFire(CompletableFuture.java:911)
at java.base/java.util.concurrent.CompletableFuture.postComplete(CompletableFuture.java:510)
at java.base/java.util.concurrent.CompletableFuture.postFire(CompletableFuture.java:614)
at java.base/java.util.concurrent.CompletableFuture$UniWhenComplete.tryFire(CompletableFuture.java:844)
at java.base/java.util.concurrent.CompletableFuture$Completion.exec(CompletableFuture.java:483)
at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)
at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)
at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)
at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)
at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
Caused by: dev.openfga.sdk.errors.ApiException: API error: 401
at dev.openfga.sdk.api.BaseStreamingApi.lambda$processStreamingResponse$1(BaseStreamingApi.java:81)
at java.base/java.util.concurrent.CompletableFuture$UniCompose.tryFire(CompletableFuture.java:1150)
... 9 more
BaseStreamingApi.java:81 is the status-code check in processStreamingResponse that raises the ApiException for the 401 response.
Capturing the outgoing request via an ApiClient#setRequestInterceptor confirms the streaming call sends only accept: application/json and content-type: application/json — no Authorization header.
Expectation
OpenFgaClient#streamedListObjects should attach Authorization: Bearer <token> to the outgoing HTTP request whenever configuration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE, mirroring the behavior of OpenFgaApi.buildHttpRequestWithPublisher. With the same ClientCredentials that already work for listObjects, streamedListObjects should stream results without any 401.
References
Suggested fix direction (maintainers may prefer otherwise)
Centralize auth once so that every request-builder path benefits. Concretely:
- Consolidate
OpenFgaApi#getAccessToken in a reusable fashion, eg. on OAuth2Client itself.
- Store the
OAuth2Client instance on ApiClient (setOAuth2Client / getOAuth2Client) so the cached token is shared across every request path that uses the same ApiClient. OpenFgaApi's constructor registers its OAuth2Client there.
- Call equivalent
#buildHttpRequestWithPublisher to assess auth method and credentials, apply headers, from the three request-builder paths — OpenFgaApi.buildHttpRequestWithPublisher, BaseStreamingApi.buildHttpRequest, and ApiExecutorRequestBuilder.buildHttpRequest.
Happy to open a PR against the current main if that would be useful and encouraged.
Checklist
Description
OpenFgaClient#streamedListObjectssends HTTPS requests without theAuthorization: Bearer <token>header, so every call against an FGA deployment that requires auth (e.g. FGA Cloud viaapi.us1.fga.dev) fails with:Every other method on the same
OpenFgaClientinstance —check,batchCheck,listObjects,read, etc. — succeeds with the identicalClientCredentials. A plaincurlPOST to/stores/{storeId}/streamed-list-objectswith a manually-obtained bearer token succeeds. The issue is reproducible on 100% of calls.Root cause (confirmed by reading sources of 0.9.7 and
main):Non-streaming calls route through
OpenFgaApi.buildHttpRequestWithPublisher, which attaches the bearer token:OpenFgaApiis the only class that constructs and owns anOAuth2Client.Streaming calls take a different path:
OpenFgaClient.streamedListObjects(src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.java, around L1317) instantiatesStreamedListObjectsApidirectly with(configuration, apiClient)— it does not route throughOpenFgaApi, and has no reference to anOAuth2Client.StreamedListObjectsApi.streamedListObjectscallsBaseStreamingApi.buildHttpRequest.BaseStreamingApi.buildHttpRequestcallsApiClient.requestBuilder(method, path, bodyBytes, configuration), which sets onlyaccept,content-type, URI, method, and timeout. It never reads credentials, never consults anOAuth2Client, and never adds anAuthorizationheader. It then appliesapiClient.getRequestInterceptor()if one is set, which is the only user-facing escape hatch — but by default no such interceptor exists.The same omission exists in the newer
StreamingApiExecutor/ApiExecutorRequestBuilder.buildHttpRequest(merged via PR #296), which likewise callsApiClient.requestBuilder(...)without attaching auth.Version of SDK
0.9.7Version of OpenFGA (if known)
Auth0 FGA (
> v1.5.x)OpenFGA Flags/Custom Configuration Applicable
N/A
Reproduction
Consistent on every call. Full standalone Maven project below — single class, no dependency on anything else.
pom.xml:src/main/java/Repro.java:Run:
Steps:
streamedListObjectsis invoked withClientCredentialsauth./stores/{store_id}/streamed-list-objectscontains noAuthorizationheader and FGA returns 401.The same configuration works for
check(step A) andlistObjects(step B) on the very sameOpenFgaClientinstance, proving the credentials themselves are valid.OpenFGA SDK version
dev.openfga:openfga-sdk:0.9.7— also confirmed onmainas of 2026-04-21.OpenFGA version
FGA Cloud (
api.us1.fga.dev). Also reproducible against any self-hosted OpenFGA that requires a bearer token.SDK Configuration
Backtrace
Actual output of the repro above against FGA Cloud:
BaseStreamingApi.java:81is the status-code check inprocessStreamingResponsethat raises theApiExceptionfor the 401 response.Capturing the outgoing request via an
ApiClient#setRequestInterceptorconfirms the streaming call sends onlyaccept: application/jsonandcontent-type: application/json— noAuthorizationheader.Expectation
OpenFgaClient#streamedListObjectsshould attachAuthorization: Bearer <token>to the outgoing HTTP request wheneverconfiguration.getCredentials().getCredentialsMethod() != CredentialsMethod.NONE, mirroring the behavior ofOpenFgaApi.buildHttpRequestWithPublisher. With the sameClientCredentialsthat already work forlistObjects,streamedListObjectsshould stream results without any 401.References
src/main/java/dev/openfga/sdk/api/OpenFgaApi.java— working auth path (lines ~1284–1320 in 0.9.7).src/main/java/dev/openfga/sdk/api/BaseStreamingApi.java— brokenbuildHttpRequest(identical onmain).src/main/java/dev/openfga/sdk/api/client/ApiClient.java—requestBuilderdoes not setAuthorizationand has no credentials awareness.src/main/java/dev/openfga/sdk/api/client/OpenFgaClient.javaline ~1317 — instantiatesStreamedListObjectsApiwithout going throughOpenFgaApi.src/main/java/dev/openfga/sdk/api/client/ApiExecutorRequestBuilder.java— same missing-auth gap asBaseStreamingApi(affectsApiExecutorand the newerStreamingApiExecutorintroduced in PR feat: streaming APIExecutor #296).Suggested fix direction (maintainers may prefer otherwise)
Centralize auth once so that every request-builder path benefits. Concretely:
OpenFgaApi#getAccessTokenin a reusable fashion, eg. onOAuth2Clientitself.OAuth2Clientinstance onApiClient(setOAuth2Client/getOAuth2Client) so the cached token is shared across every request path that uses the sameApiClient.OpenFgaApi's constructor registers itsOAuth2Clientthere.#buildHttpRequestWithPublisherto assess auth method and credentials, apply headers, from the three request-builder paths —OpenFgaApi.buildHttpRequestWithPublisher,BaseStreamingApi.buildHttpRequest, andApiExecutorRequestBuilder.buildHttpRequest.Happy to open a PR against the current
mainif that would be useful and encouraged.