Skip to content

fix: surface actual server error in OAuth code exchange UI - #7

Closed
superdav42 wants to merge 3 commits into
mainfrom
bugfix/oauth-error-display
Closed

superdav42 wants to merge 3 commits into
mainfrom
bugfix/oauth-error-display

Conversation

@superdav42

@superdav42 superdav42 commented Apr 8, 2026 •

Copy link
Copy Markdown
Contributor

Problem

After commit e1f3833 (scope validation), the OAuth code exchange can now fail with a 403 insufficient_scope error in addition to the existing 400 exchange_failed path. Both failures show the generic "Code exchange failed." message in the UI because the error handling used err instanceof Error — but apiFetch throws a plain object {code, message, data} for WP_Error responses, not an Error instance. The instanceof check always evaluated to false, swallowing the actual server message.

Changes

js/connector.jsx — replace err instanceof Error ? err.message : fallback with err?.message || fallback in both handleStartOAuth and handleExchangeCode. This surfaces the actual server error (e.g. the scope validation message) instead of the generic fallback.

src/OAuthPool/PoolManager.php — add WP_DEBUG error_log calls in exchangeCode() at each null-return point (wp_error, non-200 HTTP status, bad response body). Enables diagnosis of token endpoint failures without modifying production code.

Diagnosis

If you're seeing "Code exchange failed." after this PR, enable WP_DEBUG in wp-config.php and check the PHP error log. You'll see one of:

  • [AnthropicMax] Token exchange HTTP 400: {"error":"invalid_grant",...} — code is expired or already used
  • [AnthropicMax] Token exchange HTTP 4xx: ... — token endpoint rejected the request
  • [AnthropicMax] Token exchange bad response body: ... — unexpected response format

If the exchange succeeds (200) but scope validation fails, the UI will now show the full message: "Authorization succeeded but the token is missing the required 'user:inference' scope..." — which means the account doesn't have a Claude Max subscription.

Testing

  1. Enable WP_DEBUG in wp-config.php
  2. Go through the OAuth flow with an invalid/expired code — the UI should now show the actual error from the server
  3. Go through the OAuth flow with a valid code from a non-Max account — the UI should show the scope error message

Summary by CodeRabbit

  • Documentation

    • Added comprehensive developer guide for WordPress plugin development environment setup, including local installation configuration, site access URLs, WordPress version specifications, and WP-CLI command examples.
  • Bug Fixes

    • Improved error message handling in OAuth account connection to provide clearer feedback when authentication fails.
  • Chores

    • Enhanced diagnostic logging for OAuth token exchange operations to support troubleshooting.

Point wp-cli at shared WordPress 7.0-RC2 multisite dev install at
../wordpress (wordpress.local:8080). Documents reset workflow and
WP-CLI usage in AGENTS.md.
apiFetch throws a plain object {code, message, data} for WP_Error
responses, not an Error instance. The instanceof Error check always
evaluated to false, so the generic fallback message was always shown
instead of the actual server error (e.g. insufficient_scope 403).

Replace err instanceof Error ? err.message : fallback with
err?.message || fallback in both handleStartOAuth and handleExchangeCode.

Also add WP_DEBUG error_log calls in PoolManager::exchangeCode() to
surface the token endpoint HTTP status and body when the exchange fails,
making it easier to diagnose whether the failure is a network error,
a non-200 response, or a malformed response body.
@coderabbitai

coderabbitai Bot commented Apr 8, 2026 •

Copy link
Copy Markdown

Warning

Rate limit exceeded

@superdav42 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 11 minutes and 25 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 11 minutes and 25 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c70059a2-fd91-4743-aaff-323425ca49a0

📥 Commits

Reviewing files that changed from the base of the PR and between 95215be and d157329.

📒 Files selected for processing (2)
  • inc/admin.php
  • plugin.php
📝 Walkthrough

Walkthrough

Added documentation and configuration files for WordPress plugin development environment, enhanced error message extraction in OAuth form handling via optional chaining, and implemented WP_DEBUG-guarded error logging in the OAuth token exchange flow.

Changes

Cohort / File(s) Summary
Development Configuration & Documentation
AGENTS.md, wp-cli.yml
Added setup documentation specifying local WordPress dev install location, access URLs, plugin symlink conventions, and WP-CLI configuration with path and site URL settings.
Error Handling Improvements
js/connector.jsx, src/OAuthPool/PoolManager.php
Updated OAuth error handling in frontend form to use optional chaining (err?.message) instead of Error instance checks; added backend diagnostic logging for token exchange failures (WP_Error, HTTP status, JSON parsing) gated by WP_DEBUG.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

Poem

🐰 With logs now shining bright and clear,
And docs to guide us through the year,
The OAuth flow's more transparent still,
Configuration bends to DevOps will!
A tidy setup, debugging delight. ✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: enabling the UI to display actual server error messages during OAuth code exchange instead of generic fallback messages.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/oauth-error-display

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
js/connector.jsx (1)

333-339: Inconsistent error handling in handleRemove and handleRefresh.

These two handlers still use the old err instanceof Error pattern (lines 337 and 366), while handleStartOAuth and handleExchangeCode were updated to use err?.message. For consistency and to surface server error messages in all cases, consider applying the same fix here.

♻️ Proposed fix for consistency
 	const handleRemove = async ( email ) => {
 		setIsBusy( true );
 		setNotice( null );
 		try {
 			await apiFetch( {
 				method: 'DELETE',
 				path:
 					'/anthropic-max-pool/v1/accounts/' +
 					encodeURIComponent( email ),
 			} );
 			await fetchAccounts();
 		} catch ( err ) {
 			setNotice( {
 				status: 'error',
-				message:
-					err instanceof Error
-						? err.message
-						: __( 'Failed to remove account.' ),
+				message: err?.message || __( 'Failed to remove account.' ),
 			} );
 		} finally {
 			setIsBusy( false );
 		}
 	};

 	const handleRefresh = async ( email ) => {
 		setIsBusy( true );
 		setNotice( null );
 		try {
 			await apiFetch( {
 				method: 'POST',
 				path:
 					'/anthropic-max-pool/v1/accounts/' +
 					encodeURIComponent( email ) +
 					'/refresh',
 			} );
 			setNotice( {
 				status: 'success',
 				message: __( 'Token refreshed.' ),
 			} );
 			await fetchAccounts();
 		} catch ( err ) {
 			setNotice( {
 				status: 'error',
-				message:
-					err instanceof Error
-						? err.message
-						: __( 'Refresh failed.' ),
+				message: err?.message || __( 'Refresh failed.' ),
 			} );
 		} finally {
 			setIsBusy( false );
 		}
 	};

Also applies to: 362-369

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

In `@js/connector.jsx` around lines 333 - 339, The catch blocks in handleRemove
and handleRefresh use the old err instanceof Error pattern; update them to use
the optional chaining pattern similar to handleStartOAuth/handleExchangeCode by
setting the error message to err?.message with a fallback (e.g., __('Failed to
remove account.') or __('Failed to refresh account.')), and keep the setNotice
call and status:'error' unchanged so server-sent error strings surface
consistently; locate the catch blocks inside handleRemove and handleRefresh and
replace the ternary err instanceof Error ? err.message : ... with err?.message
?? __('...') as appropriate.
src/OAuthPool/PoolManager.php (2)

716-718: Consider redacting or truncating the response body before logging.

The response body from the token endpoint may contain sensitive information (e.g., error details that include partial tokens, account identifiers, or other diagnostic data). Even with WP_DEBUG gated, this could end up in production logs if debug mode is accidentally left enabled.

Consider truncating or sanitizing the body before logging:

🛡️ Proposed safer logging
         if (defined('WP_DEBUG') && WP_DEBUG) {
-            error_log('[AnthropicMax] Token exchange HTTP ' . $status . ': ' . wp_remote_retrieve_body($response));
+            $body = wp_remote_retrieve_body($response);
+            $truncated = strlen($body) > 500 ? substr($body, 0, 500) . '...[truncated]' : $body;
+            error_log('[AnthropicMax] Token exchange HTTP ' . $status . ': ' . $truncated);
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OAuthPool/PoolManager.php` around lines 716 - 718, The log currently
writes the full HTTP response body from wp_remote_retrieve_body($response) when
WP_DEBUG is true; change this to sanitize and/or truncate before calling
error_log: parse the body (json_decode if JSON) and remove/replace sensitive
fields like access_token, refresh_token, id_token, or any keys matching
/(token|secret|key|id)/i, then stringify and truncate to a safe length (e.g.,
200 chars) and log that sanitized string along with $status; update the
error_log call that uses WP_DEBUG, wp_remote_retrieve_body, $response and
$status to use the sanitized/truncated value instead.

724-726: Same truncation consideration applies here.

This also logs the full response body, which could be large or contain unexpected content. Consider applying the same truncation approach for consistency.

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

In `@src/OAuthPool/PoolManager.php` around lines 724 - 726, In PoolManager (the
token exchange error handling block around the if (defined('WP_DEBUG') &&
WP_DEBUG) check) avoid logging the entire remote response body; instead apply
the same truncation logic used elsewhere (e.g., a helper or use mb_substr/strlen
checks) to limit the logged length and append an ellipsis when truncated, then
log that truncated string in the processLogger/error_log call so large or
unexpected response bodies are not fully emitted.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@js/connector.jsx`:
- Around line 333-339: The catch blocks in handleRemove and handleRefresh use
the old err instanceof Error pattern; update them to use the optional chaining
pattern similar to handleStartOAuth/handleExchangeCode by setting the error
message to err?.message with a fallback (e.g., __('Failed to remove account.')
or __('Failed to refresh account.')), and keep the setNotice call and
status:'error' unchanged so server-sent error strings surface consistently;
locate the catch blocks inside handleRemove and handleRefresh and replace the
ternary err instanceof Error ? err.message : ... with err?.message ?? __('...')
as appropriate.

In `@src/OAuthPool/PoolManager.php`:
- Around line 716-718: The log currently writes the full HTTP response body from
wp_remote_retrieve_body($response) when WP_DEBUG is true; change this to
sanitize and/or truncate before calling error_log: parse the body (json_decode
if JSON) and remove/replace sensitive fields like access_token, refresh_token,
id_token, or any keys matching /(token|secret|key|id)/i, then stringify and
truncate to a safe length (e.g., 200 chars) and log that sanitized string along
with $status; update the error_log call that uses WP_DEBUG,
wp_remote_retrieve_body, $response and $status to use the sanitized/truncated
value instead.
- Around line 724-726: In PoolManager (the token exchange error handling block
around the if (defined('WP_DEBUG') && WP_DEBUG) check) avoid logging the entire
remote response body; instead apply the same truncation logic used elsewhere
(e.g., a helper or use mb_substr/strlen checks) to limit the logged length and
append an ellipsis when truncated, then log that truncated string in the
processLogger/error_log call so large or unexpected response bodies are not
fully emitted.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 530c40c3-4245-4a82-a6fa-5622e39e7484

📥 Commits

Reviewing files that changed from the base of the PR and between af4ce5e and 95215be.

📒 Files selected for processing (4)
  • AGENTS.md
  • js/connector.jsx
  • src/OAuthPool/PoolManager.php
  • wp-cli.yml

plugins_url() with __DIR__ from inc/admin.php fails when the plugin
directory is a symlink to a path outside WP_PLUGIN_DIR (e.g. a git
worktree). WordPress cannot walk up to find the plugin root from the
real path.

Fix: define ANTHROPIC_MAX_AI_PROVIDER_URL in plugin.php using
plugin_dir_url(__FILE__), which resolves via the symlink at load time
and always produces the correct URL regardless of where the real files
live. Use this constant in admin.php instead of plugins_url().

Also switch the script version from the static plugin version string
to filemtime() so the browser cache is busted on every build.
@superdav42

Copy link
Copy Markdown
Contributor Author

Superseded — the fixes in this PR are inferior duplicates of already-merged PR #5 (getJsonSchema, #state fragment stripping). Local main was 3 commits behind origin and the session was patching stale code. Closing and resetting local main to origin/main.

@superdav42 superdav42 closed this Apr 9, 2026
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