Skip to content

raise OAuthRefreshException when refresh token fails#102

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

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

Conversation

@simonc56
Copy link
Copy Markdown
Collaborator

No description provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Review Change Stack

Walkthrough

This PR enhances OAuth token refresh failure handling by rewriting OAuthRefreshException to safely parse error responses, implementing structured retry logic in TokenAuth.refresh_token, and validating token state throughout. Error details are extracted from HTTP responses, with fallbacks for missing JSON, and propagated through a new helper method to build exception instances.

Changes

OAuth Token Refresh Error Handling

Layer / File(s) Summary
OAuthRefreshException with safe JSON parsing
trakt/errors.py, tests/test_errors.py
Exception class safely loads response JSON with fallbacks, accepts optional error/error_description/cause parameters, exposes cached properties, and formats messages based on available fields. Tests validate extraction and formatting of error details.
TokenAuth refresh validation and retry logic
trakt/api.py
validate_token now validates expiry via OAUTH_EXPIRES_AT and raises OAuthRefreshException on missing expiry, with reliable TOKEN_UNDER_REFRESH cleanup. refresh_token implements bounded retry loop, captures and structures exceptions, logs per-attempt failures, validates response payload, ensures expiry persistence, and provides _build_refresh_exception helper to extract and propagate error details from OAuth responses.
Refresh failure integration test
tests/test_api.py
Test constructs config with expired refresh token, mocks OAuthException from HTTP client, and verifies TokenAuth.get_token() raises OAuthRefreshException with propagated error details and correct state flags.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • glensc/python-pytrakt#81: Directly modifies TokenAuth.refresh_token error handling to parse OAuth/HTTP response JSON and surface error/error_description fields with different exception wrapping approach.

Suggested reviewers

  • glensc

Poem

🐰 A token walks into the refresh booth,
But OAuth says "nope, not today, friend!"
We catch the error, parse the truth,
Retry with grace until the end.
Error details flow like carrots anew,
Exception handling, tried and true! 🥕

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

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.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess relevance to the changeset. Add a pull request description explaining the changes, motivation, and any relevant context for reviewers.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately summarizes the main change: introducing OAuthRefreshException when token refresh fails.
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
Comment on lines +79 to +81
self.data = self._load_data()
self._error = error or self.data.get("error")
self._error_description = error_description or self.data.get("error_description")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard against non-object JSON payloads in OAuthRefreshException.

Line 80 and Line 81 assume self.data is a dict. If response.json() returns a JSON array/string/number, this path raises AttributeError and hides the actual refresh failure context.

Proposed fix
     def _load_data(self):
         if self.response is None:
             return {}

         try:
-            return self.response.json()
-        except (AttributeError, ValueError):
+            data = self.response.json()
+            return data if isinstance(data, dict) else {}
+        except (AttributeError, ValueError, TypeError):
             return {}

Also applies to: 83-90

🤖 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/errors.py` around lines 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.

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.

1 participant