Skip to content

fix: stop mutating token refresh errors into auth errors#329

Open
aaguiarz wants to merge 6 commits intomainfrom
fix/issue-328-auth-error-retyping
Open

fix: stop mutating token refresh errors into auth errors#329
aaguiarz wants to merge 6 commits intomainfrom
fix/issue-328-auth-error-retyping

Conversation

@aaguiarz
Copy link
Member

@aaguiarz aaguiarz commented Feb 16, 2026

Summary

Replaces error-instance mutation in credential token refresh with construction of a real FgaApiAuthenticationError.

Changes

  • Added regression test reproducing failed toThrow(FgaApiAuthenticationError) when token endpoint returns 500
  • Replaced constructor/name property mutation with explicit creation of FgaApiAuthenticationError
  • Preserved auth context (client_id, audience, grant_type) in the generated error payload

Potential Breaking Change

  • Token refresh failures now throw a real FgaApiAuthenticationError instance rather than a mutated API error object.
  • Consumers that depended on the previous underlying runtime type/prototype or exact message text may need to update error handling.

Fixes #328

Summary by CodeRabbit

  • Bug Fixes

    • Authentication errors now include additional diagnostic context (client ID, audience, grant type) for improved troubleshooting.
    • Enhanced error message standardization and handling for various authentication failure scenarios.
    • Improved support for token refresh failure detection and reporting.
  • Tests

    • Added test coverage for authentication error handling scenarios.

Copilot AI review requested due to automatic review settings February 16, 2026 18:53
@aaguiarz aaguiarz requested a review from a team as a code owner February 16, 2026 18:53
@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 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.

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 changes fix issue #328 by replacing error object mutation with proper instantiation of FgaApiAuthenticationError. The error class is enhanced to accept both AxiosError and FgaApiError with optional authentication context fields, supported by a new helper function for standardized error messaging.

Changes

Cohort / File(s) Summary
Error Class Enhancement
errors.ts
Added getAuthenticationErrorMessage() helper function to standardize error messaging from AxiosError or FgaApiError sources. Updated FgaApiAuthenticationError constructor to accept both AxiosError and FgaApiError with optional context fields (clientId, audience, grantType) instead of only AxiosError.
Error Handling Integration
credentials/credentials.ts
Replaced error mutation pattern with instantiation of new FgaApiAuthenticationError, passing structured context payload (clientId, audience, grantType) to the constructor instead of mutating error properties.
Test Coverage
tests/credentials.test.ts
Added import for FgaApiAuthenticationError and new test case verifying that HTTP 404 token refresh failures reject with FgaApiAuthenticationError.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 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 summarizes the main change: replacing error mutation with proper FgaApiAuthenticationError construction.
Linked Issues check ✅ Passed The PR fully addresses issue #328 by constructing real FgaApiAuthenticationError instances instead of mutating FgaApiError, preserving auth context and fixing instanceof checks.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the error mutation issue: modified error handling in credentials.ts, enhanced FgaApiAuthenticationError constructor, added helper function, and included a regression test.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/issue-328-auth-error-retyping

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.

@dosubot
Copy link

dosubot bot commented Feb 16, 2026

Related Documentation

Checked 8 published document(s) in 1 knowledge base(s). No updates required.

How did I do? Any feedback?  Join Discord

@codecov-commenter
Copy link

codecov-commenter commented Feb 16, 2026

Codecov Report

❌ Patch coverage is 76.19048% with 10 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.96%. Comparing base (a7090e2) to head (b54331c).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
errors.ts 80.00% 2 Missing and 5 partials ⚠️
credentials/credentials.ts 57.14% 0 Missing and 3 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #329      +/-   ##
==========================================
+ Coverage   89.67%   89.96%   +0.28%     
==========================================
  Files          25       25              
  Lines        1492     1534      +42     
  Branches      279      299      +20     
==========================================
+ Hits         1338     1380      +42     
+ Misses         94       90       -4     
- Partials       60       64       +4     

☔ 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.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
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

This PR fixes token refresh failures being surfaced as mutated FgaApiError objects (breaking instanceof FgaApiAuthenticationError) by constructing and throwing a real FgaApiAuthenticationError, and adds a regression test for the failure mode described in #328.

Changes:

  • Add a regression test asserting token refresh failures throw a real FgaApiAuthenticationError.
  • Replace error-instance mutation in Credentials.refreshAccessToken() with explicit FgaApiAuthenticationError construction, preserving auth context fields.

Reviewed changes

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

File Description
tests/credentials.test.ts Adds a regression test ensuring token refresh failures reject with FgaApiAuthenticationError.
credentials/credentials.ts Stops mutating FgaApiError instances and instead throws a newly constructed FgaApiAuthenticationError with auth context.

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

Comment on lines 568 to 569
await expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError);
expect(scope.isDone()).toBe(true);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

This test triggers the full retry loop (4 requests) and will sleep for the exponential backoff delays (hundreds of ms up to >1s), which can noticeably slow the suite and add timing flakiness. Consider using Jest fake timers (or mocking the retry delay) so the retries complete without real time passing while still asserting the thrown error type.

Suggested change
await expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError);
expect(scope.isDone()).toBe(true);
jest.useFakeTimers();
try {
const accessTokenPromise = credentials.getAccessTokenHeader();
jest.runAllTimers();
await expect(accessTokenPromise).rejects.toThrow(FgaApiAuthenticationError);
expect(scope.isDone()).toBe(true);
} finally {
jest.useRealTimers();
}

Copilot uses AI. Check for mistakes.
status: err.statusCode,
statusText: err.statusText,
data: err.responseData,
headers: err.responseHeader,
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

FgaApiAuthenticationError’s constructor calls getResponseHeaders(err), which assumes err.response.headers is an object. Here headers is set to err.responseHeader, which is optional on FgaApiError; if it’s undefined this can throw at runtime. Consider defaulting headers to {} (and/or omitting the response object entirely when there is no response) before constructing the auth error.

Suggested change
headers: err.responseHeader,
headers: err.responseHeader ?? {},

Copilot uses AI. Check for mistakes.
Comment on lines 240 to 256
const authError = new FgaApiAuthenticationError({
response: {
status: err.statusCode,
statusText: err.statusText,
data: err.responseData,
headers: err.responseHeader,
},
config: {
url: err.requestURL,
method: err.method,
data: JSON.stringify({
client_id: clientCredentials.clientId,
audience: clientCredentials.apiAudience,
grant_type: "client_credentials",
}),
},
} as any);
Copy link

Copilot AI Feb 16, 2026

Choose a reason for hiding this comment

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

This creates an AxiosError-like object and then casts it to any to satisfy new FgaApiAuthenticationError(...). That removes type safety and makes it easy to drift from AxiosError shape over time. A more robust approach would be to introduce a small helper/factory (or overload) that builds an FgaApiAuthenticationError directly from an existing FgaApiError + auth context, avoiding as any and manual AxiosError shaping.

Copilot uses AI. Check for mistakes.
@aaguiarz
Copy link
Member Author

Addressed the review comments and pushed commit 8cfe19f.

What changed:

  • Added a direct FgaApiError -> FgaApiAuthenticationError constructor path in errors.ts.
    • This removes the manual Axios-shape object cast from credentials.ts.
    • It preserves stack/request/auth context while staying type-safe at the call site.
  • Updated token refresh error mapping in credentials.ts to throw:
    • new FgaApiAuthenticationError(err, { clientId, audience, grantType })
  • Updated the regression test to avoid retry backoff sleep overhead by using a non-retryable API failure (single 404 response), while still validating the instanceof FgaApiAuthenticationError behavior.

Validated locally:

  • npm test -- tests/credentials.test.ts -t "real FgaApiAuthenticationError"
  • npm test -- tests/index.test.ts -t "401 during authentication should result in FgaApiAuthenticationError"

Copy link

@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.

🤖 Fix all issues with AI agents
Verify each finding against the current code and only fix it if needed.


In `@errors.ts`:
- Around line 217-221: The constructor for the error class currently ignores the
optional context when err is an AxiosError; update the AxiosError branch in the
constructor to apply context overrides (clientId, audience, grantType) after
extracting values from the parsed request data so that if
context.clientId/audience/grantType is provided it replaces the parsed value;
locate the AxiosError handling block in the constructor and merge values (e.g.,
finalClientId = context?.clientId ?? parsedClientId) for each of the three
fields and use those merged values where the code currently uses the parsed
ones.

In `@tests/credentials.test.ts`:
- Around line 541-569: Add assertions to the existing test that checks thrown
error properties: after awaiting
expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError)
also capture the rejected error and assert its context fields and status code
are set correctly (e.g., verify error.clientId equals OPENFGA_CLIENT_ID,
error.audience equals OPENFGA_API_AUDIENCE, error.grantType equals
CredentialsMethod.ClientCredentials or the literal grant type string used, and
error.statusCode equals 404). Use the same test-local variables (apiTokenIssuer,
OPENFGA_CLIENT_ID, OPENFGA_API_AUDIENCE) and the
Credentials.getAccessTokenHeader / FgaApiAuthenticationError symbols to locate
where to add these assertions.
🧹 Nitpick comments (2)
🤖 Fix all nitpicks with AI agents
Verify each finding against the current code and only fix it if needed.


In `@errors.ts`:
- Around line 217-221: The constructor for the error class currently ignores the
optional context when err is an AxiosError; update the AxiosError branch in the
constructor to apply context overrides (clientId, audience, grantType) after
extracting values from the parsed request data so that if
context.clientId/audience/grantType is provided it replaces the parsed value;
locate the AxiosError handling block in the constructor and merge values (e.g.,
finalClientId = context?.clientId ?? parsedClientId) for each of the three
fields and use those merged values where the code currently uses the parsed
ones.

In `@tests/credentials.test.ts`:
- Around line 541-569: Add assertions to the existing test that checks thrown
error properties: after awaiting
expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError)
also capture the rejected error and assert its context fields and status code
are set correctly (e.g., verify error.clientId equals OPENFGA_CLIENT_ID,
error.audience equals OPENFGA_API_AUDIENCE, error.grantType equals
CredentialsMethod.ClientCredentials or the literal grant type string used, and
error.statusCode equals 404). Use the same test-local variables (apiTokenIssuer,
OPENFGA_CLIENT_ID, OPENFGA_API_AUDIENCE) and the
Credentials.getAccessTokenHeader / FgaApiAuthenticationError symbols to locate
where to add these assertions.
errors.ts (1)

217-221: context parameter is silently ignored when err is an AxiosError.

The AxiosError branch (lines 264–272) extracts clientId, audience, and grantType only from parsed request data, ignoring the context argument. Today no caller passes context with an AxiosError, but a future caller could hit this silently. Consider applying context overrides in the AxiosError branch as well, or documenting that context is only used for FgaApiError inputs.

Proposed fix
-    this.clientId = data?.client_id;
-    this.audience = data?.audience;
-    this.grantType = data?.grant_type;
+    this.clientId = context?.clientId || data?.client_id;
+    this.audience = context?.audience || data?.audience;
+    this.grantType = context?.grantType || data?.grant_type;

Also applies to: 253-272

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

In `@errors.ts` around lines 217 - 221, The constructor for the error class
currently ignores the optional context when err is an AxiosError; update the
AxiosError branch in the constructor to apply context overrides (clientId,
audience, grantType) after extracting values from the parsed request data so
that if context.clientId/audience/grantType is provided it replaces the parsed
value; locate the AxiosError handling block in the constructor and merge values
(e.g., finalClientId = context?.clientId ?? parsedClientId) for each of the
three fields and use those merged values where the code currently uses the
parsed ones.
tests/credentials.test.ts (1)

541-569: Good regression test — consider asserting error properties too.

The instanceof check directly validates the fix for #328. For stronger coverage, you could also verify that context fields (clientId, audience, grantType) and statusCode are correctly propagated on the thrown error.

Example: assert error properties
-      await expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError);
+      await expect(credentials.getAccessTokenHeader()).rejects.toSatisfy((err: FgaApiAuthenticationError) => {
+        expect(err).toBeInstanceOf(FgaApiAuthenticationError);
+        expect(err.statusCode).toBe(404);
+        expect(err.clientId).toBe(OPENFGA_CLIENT_ID);
+        expect(err.audience).toBe(OPENFGA_API_AUDIENCE);
+        expect(err.grantType).toBe("client_credentials");
+        return true;
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/credentials.test.ts` around lines 541 - 569, Add assertions to the
existing test that checks thrown error properties: after awaiting
expect(credentials.getAccessTokenHeader()).rejects.toThrow(FgaApiAuthenticationError)
also capture the rejected error and assert its context fields and status code
are set correctly (e.g., verify error.clientId equals OPENFGA_CLIENT_ID,
error.audience equals OPENFGA_API_AUDIENCE, error.grantType equals
CredentialsMethod.ClientCredentials or the literal grant type string used, and
error.statusCode equals 404). Use the same test-local variables (apiTokenIssuer,
OPENFGA_CLIENT_ID, OPENFGA_API_AUDIENCE) and the
Credentials.getAccessTokenHeader / FgaApiAuthenticationError symbols to locate
where to add these assertions.

@aaguiarz
Copy link
Member Author

aaguiarz commented Feb 16, 2026

Addressed the remaining CodeRabbit suggestions in commit f9d8594.

Changes made:

  • Updated the AxiosError branch in FgaApiAuthenticationError to apply context overrides for clientId, audience, and grantType using nullish coalescing.
  • Strengthened the regression test in tests/credentials.test.ts to assert propagated auth error fields and status code (404) in addition to type.

Validation run:

  • npm test -- tests/credentials.test.ts -t "real FgaApiAuthenticationError"

Copy link
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 3 out of 3 changed files in this pull request and generated 2 comments.


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

errors.ts Outdated
Comment on lines 243 to 245
this.clientId = context?.clientId || data?.client_id;
this.audience = context?.audience || data?.audience;
this.grantType = context?.grantType || data?.grant_type;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

In the FgaApiError wrapping branch, the context fallbacks use || (e.g., context?.clientId || data?.client_id). This treats empty strings as “unset” and will fall back to request data, which is inconsistent with the AxiosError branch below that uses nullish coalescing (??). Prefer using ?? here as well for consistent semantics and to avoid surprising overrides when a context value is intentionally provided but falsy.

Suggested change
this.clientId = context?.clientId || data?.client_id;
this.audience = context?.audience || data?.audience;
this.grantType = context?.grantType || data?.grant_type;
this.clientId = context?.clientId ?? data?.client_id;
this.audience = context?.audience ?? data?.audience;
this.grantType = context?.grantType ?? data?.grant_type;

Copilot uses AI. Check for mistakes.
@aaguiarz
Copy link
Member Author

Followed up on Copilot review #329 (review) in commit 40c23cc.

Actionable fixes applied:

  • errors.ts: switched the FgaApiError-branch context fallbacks from || to ?? for clientId, audience, and grantType to match Axios-branch semantics and preserve intentional falsy values.
  • tests/credentials.test.ts: aligned the regression scenario with PR intent by stubbing token endpoint failure as 500 (internal error), updated expected status to 500, and accounted for built-in retries with .times(4).

Validation run:

  • npm test -- tests/credentials.test.ts

Copy link
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 3 out of 3 changed files in this pull request and generated 3 comments.


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

Comment on lines 238 to 247
} catch (err: unknown) {
if (err instanceof FgaApiError) {
(err as any).constructor = FgaApiAuthenticationError;
(err as any).name = "FgaApiAuthenticationError";
(err as any).clientId = clientCredentials.clientId;
(err as any).audience = clientCredentials.apiAudience;
(err as any).grantType = "client_credentials";
throw new FgaApiAuthenticationError(err, {
clientId: clientCredentials.clientId,
audience: clientCredentials.apiAudience,
grantType: "client_credentials",
});
}

throw err;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

refreshAccessToken() only wraps errors that are instanceof FgaApiError. However attemptHttpRequest() throws FgaApiAuthenticationError directly for 401/403, so token-refresh failures with those statuses will bypass this wrapper and lose the intended auth context (clientId, audience, grantType). Consider also catching/wrapping FgaApiAuthenticationError here (or adjusting the retry helper) so token endpoint 401/403 include the same context as other failures.

Copilot uses AI. Check for mistakes.
errors.ts Outdated
Comment on lines +264 to +272
this.grantType = data?.grant_type;
this.clientId = context?.clientId ?? data?.client_id;
this.audience = context?.audience ?? data?.audience;
this.grantType = context?.grantType ?? data?.grant_type;
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

In the AxiosError branch, the code assumes err.config.data is JSON and parses it with JSON.parse(). For token refresh requests, axios commonly stores this as a URL-encoded string, so parsing fails and the error won’t capture clientId/audience/grantType unless context was explicitly passed. Consider parsing err.config.data as URLSearchParams when it’s a string and JSON parsing fails.

Copilot uses AI. Check for mistakes.
throw new FgaApiAuthenticationError(err, {
clientId: clientCredentials.clientId,
audience: clientCredentials.apiAudience,
grantType: "client_credentials",
Copy link

Copilot AI Feb 17, 2026

Choose a reason for hiding this comment

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

Minor: grantType is passed as the string literal "client_credentials" here. Using CredentialsMethod.ClientCredentials would keep this aligned with the enum and the test expectation, and avoids duplicating the raw value in multiple places.

Suggested change
grantType: "client_credentials",
grantType: CredentialsMethod.ClientCredentials,

Copilot uses AI. Check for mistakes.
@aaguiarz
Copy link
Member Author

Addressed actionable feedback from #329 (review) in commit b54331c.

Applied changes:

  • credentials/credentials.ts
    • Added explicit handling for FgaApiAuthenticationError in refreshAccessToken() to preserve/enrich auth context (clientId, audience, grantType) for 401/403 token-endpoint failures.
    • Replaced raw grant type literal with CredentialsMethod.ClientCredentials.
  • errors.ts
    • Added robust request-data parsing helper that supports both JSON and URL-encoded payloads (URLSearchParams) so clientId/audience/grantType extraction works when axios stores form data as querystring text.
  • tests/credentials.test.ts
    • Added regression test: should preserve auth context when token endpoint returns 401.

Validation run:

  • npm test -- tests/credentials.test.ts

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.

fix: token refresh error mutation breaks instanceof FgaApiAuthenticationError

2 participants