Skip to content

raise OAuthRefreshException when refresh token fails#102

Open
simonc56 wants to merge 4 commits into
glensc:mainfrom
simonc56:feat/raise-oauth-refresh-exception
Open

raise OAuthRefreshException when refresh token fails#102
simonc56 wants to merge 4 commits into
glensc:mainfrom
simonc56:feat/raise-oauth-refresh-exception

Conversation

@simonc56
Copy link
Copy Markdown
Collaborator

@simonc56 simonc56 commented May 21, 2026

OAuthRefreshException exists in codebase but is never raised.
This PR makes the OAuthRefreshException be raised when refreshing OAuth token fails, instead of silently continuing with an expired token.

Needed in :

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

Warning

Review limit reached

@simonc56, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 55 minutes and 9 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f56c0ebd-2213-4a54-8961-a5ec1711d8fd

📥 Commits

Reviewing files that changed from the base of the PR and between c577875 and 37c4a69.

📒 Files selected for processing (4)
  • tests/test_api.py
  • tests/test_errors.py
  • trakt/api.py
  • trakt/errors.py

Walkthrough

Rewrites OAuthRefreshException for safe JSON handling, adds retry-and-structured-exception logic to TokenAuth.refresh_token (with expiry validation and config persistence), and adds tests verifying exception detail extraction and refresh-failure behavior.

Changes

OAuth Token Refresh Error Handling

Layer / File(s) Summary
OAuthRefreshException with safe JSON parsing
trakt/errors.py, tests/test_errors.py
Exception now accepts optional error/error_description/cause, defensively loads response JSON, exposes cached error/error_description properties, and formats __str__; tests validate extraction and message formatting.
TokenAuth validate/refresh and retry logic
trakt/api.py
Adds OAuthRefreshException import; validate_token requires persisted OAUTH_EXPIRES_AT and reliably clears TOKEN_UNDER_REFRESH. refresh_token implements a bounded retry loop, builds and raises OAuthRefreshException with API error details, validates non-empty payload and persisted expiry, and stores updated config via _build_refresh_exception.
Refresh failure integration test
tests/test_api.py
New test constructs expired AuthConfig, mocks HTTP client to raise OAuthException, invokes TokenAuth.get_token(), and asserts OAuthRefreshException contains API error fields and TokenAuth flags indicate failed refresh.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • glensc

Poem

🐇 I nibbled logs and chased a flaky thread,
A refresh token tired, its timer fled.
I catch the error, parse what it will say,
Retry like springs until the break of day.
Carrots of details shown — now tests can play.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% 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
Title check ✅ Passed The title accurately describes the main objective of the PR—raising OAuthRefreshException when OAuth token refresh fails.
Description check ✅ Passed The description clearly explains the problem being solved and references why the change is needed, making it relevant to the changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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

@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

Caution

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

⚠️ Outside diff range comments (1)
trakt/api.py (1)

284-295: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate required refresh fields before computing OAUTH_EXPIRES_AT.

Line 284 only guards None. An empty dict or missing created_at / expires_in reaches Line 294 and can raise TypeError, escaping the dedicated OAuthRefreshException flow.

Proposed fix
         if response is None:
             self.OAUTH_TOKEN_VALID = False
             raise OAuthRefreshException(
                 error='empty_refresh_response',
                 error_description='OAuth token refresh completed without a response payload.',
             )
+
+        required = ("access_token", "refresh_token", "created_at", "expires_in")
+        if any(response.get(k) is None for k in required):
+            self.OAUTH_TOKEN_VALID = False
+            raise OAuthRefreshException(
+                error='invalid_refresh_payload',
+                error_description='OAuth token refresh response is missing required fields.',
+            )
+
+        try:
+            expires_at = int(response.get("created_at")) + int(response.get("expires_in"))
+        except (TypeError, ValueError):
+            self.OAUTH_TOKEN_VALID = False
+            raise OAuthRefreshException(
+                error='invalid_refresh_payload',
+                error_description='OAuth token refresh response contains invalid expiry values.',
+            )

         self.config.update(
             OAUTH_TOKEN=response.get("access_token"),
             OAUTH_REFRESH=response.get("refresh_token"),
-            OAUTH_EXPIRES_AT=response.get("created_at") + response.get("expires_in"),
+            OAUTH_EXPIRES_AT=expires_at,
         )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@trakt/api.py` around lines 284 - 295, The code only checks response is not
None but then assumes response contains "created_at" and "expires_in"; update
the refresh handling in the method that sets self.config (use the same response
variable and the config.update block) to validate required keys ("access_token",
"refresh_token", "created_at", "expires_in") and their types before computing
OAUTH_EXPIRES_AT; if any are missing or invalid, set self.OAUTH_TOKEN_VALID =
False and raise OAuthRefreshException with an informative error and
error_description instead of letting a TypeError propagate, otherwise compute
OAUTH_EXPIRES_AT as created_at + expires_in and proceed to update OAUTH_TOKEN
and OAUTH_REFRESH.
🧹 Nitpick comments (1)
tests/test_errors.py (1)

33-46: ⚡ Quick win

Add a fallback-path test for non-JSON refresh error responses.

This test validates the happy path, but not the safe-parse fallback. Add one case where response.json() fails to ensure the exception still formats safely.

Proposed test addition
+def test_oauth_refresh_exception_handles_non_json_response():
+    response = Mock()
+    response.json.side_effect = ValueError("invalid json")
+
+    texc = OAuthRefreshException(response=response)
+
+    assert texc.http_code == 401
+    assert texc.error is None
+    assert texc.error_description is None
+    assert str(texc) == 'Unauthorized - OAuth token refresh failed'
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_errors.py` around lines 33 - 46, Add a new unit test that verifies
OAuthRefreshException correctly handles non-JSON error responses by making the
mocked response.json() raise (e.g., ValueError or json.JSONDecodeError) and
asserting the exception still sets a safe http_code (401), uses fallback/default
values for error and error_description (or empty strings), and produces a safe
str() message like "Unauthorized - OAuth token refresh failed" without crashing;
target the OAuthRefreshException constructor and __str__ behavior in
tests/test_errors.py to ensure the safe-parse fallback path is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@trakt/errors.py`:
- Around line 79-81: OAuthRefreshException assumes self.data is a dict and calls
.get, which will raise AttributeError for non-object JSON payloads; change the
initialization in the constructor that calls self._load_data() to defensively
coerce non-dict JSON into an empty dict (e.g., data = self._load_data(); if not
isinstance(data, dict): data = {}), then use that safe `data` for assigning
self._error, self._error_description and any other attributes set via data.get
in the block around OAuthRefreshException (lines setting
_error/_error_description and the subsequent .get uses between ~80-90), so all
.get calls operate on a dict fallback rather than potentially non-object JSON.

---

Outside diff comments:
In `@trakt/api.py`:
- Around line 284-295: The code only checks response is not None but then
assumes response contains "created_at" and "expires_in"; update the refresh
handling in the method that sets self.config (use the same response variable and
the config.update block) to validate required keys ("access_token",
"refresh_token", "created_at", "expires_in") and their types before computing
OAUTH_EXPIRES_AT; if any are missing or invalid, set self.OAUTH_TOKEN_VALID =
False and raise OAuthRefreshException with an informative error and
error_description instead of letting a TypeError propagate, otherwise compute
OAUTH_EXPIRES_AT as created_at + expires_in and proceed to update OAUTH_TOKEN
and OAUTH_REFRESH.

---

Nitpick comments:
In `@tests/test_errors.py`:
- Around line 33-46: Add a new unit test that verifies OAuthRefreshException
correctly handles non-JSON error responses by making the mocked response.json()
raise (e.g., ValueError or json.JSONDecodeError) and asserting the exception
still sets a safe http_code (401), uses fallback/default values for error and
error_description (or empty strings), and produces a safe str() message like
"Unauthorized - OAuth token refresh failed" without crashing; target the
OAuthRefreshException constructor and __str__ behavior in tests/test_errors.py
to ensure the safe-parse fallback path is covered.
🪄 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: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 51991ce0-2bf3-4dad-af96-fd3e64806234

📥 Commits

Reviewing files that changed from the base of the PR and between 2c7091c and 491a85e.

📒 Files selected for processing (4)
  • tests/test_api.py
  • tests/test_errors.py
  • trakt/api.py
  • trakt/errors.py

Comment thread trakt/errors.py
@glensc
Copy link
Copy Markdown
Owner

glensc commented May 27, 2026

@simonc56 please fill pr body, add references. also resolve discussions with coderabbit before requesting a review.

currently the pr contains too many changes i don't think i want to waste my personal time reviewing such llm produced noise.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 28, 2026

Actionable comments posted: 0

@simonc56 simonc56 force-pushed the feat/raise-oauth-refresh-exception branch from c577875 to 37c4a69 Compare May 28, 2026 20:57
@simonc56
Copy link
Copy Markdown
Collaborator Author

currently the pr contains too many changes

It contains only changes needed to fix issue described in first post.
What do you mean "too many" ?

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.

2 participants