Skip to content

Classify non-JSON and HTTP-200 refresh refusals as dead grants - #1783

Merged
RhysSullivan merged 3 commits into
mainfrom
fix/oauth-refresh-permanent-classification
Aug 27, 2026
Merged

Classify non-JSON and HTTP-200 refresh refusals as dead grants#1783
RhysSullivan merged 3 commits into
mainfrom
fix/oauth-refresh-permanent-classification

Conversation

@RhysSullivan

@RhysSullivan RhysSullivan commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Problem

A connection whose OAuth grant had died kept re-sending the same dead refresh token on every single use. Each attempt reached the agent as an opaque Internal tool error [id] and the health endpoint as a bare 500, while the connection still rendered as fine — a continuous stream of identical rejections against one authorization server, with no backoff and nothing telling the user to reconnect.

The refresh failure handler only treated a rejection as permanent when the response carried an RFC 6749 §5.2 error code. Most real refusals carry none: the OAuth client refuses to read a non-JSON error body at all (it content-type asserts first), and for a 200-that-is-not-a-token it throws with the parsed body and no response attached. So a text/plain 400, a text/plain 404, and a GitHub-style 200 {"error":"bad_refresh_token"} all arrived code-less, were classified as retryable storage failures, and the grant was never marked dead.

Fix

OAuth2Error now carries the token endpoint's HTTP status, recovered from the parsed-body failure shape too. isPermanentTokenRejection — a 4xx, or a 2xx that carried no usable token — drives the refresh handler to a reauth-required CredentialResolutionError, so the grant is marked dead once and the connection moves to needs-reauth. A 5xx or a transport failure still stays a retryable StorageError, and a 4xx that did name a code keeps its existing classification, so a fleet-wide invalid_client is not mistaken for one user's dead grant.

Testing

Primary verification is e2e/scenarios/oauth-refresh-rejected-non-json.test.ts: five black-box scenarios, each completing a real authorization-code flow against a test authorization server that refuses every refresh grant with one wire shape, then making three tool calls over MCP.

  • text/plain 400, text/plain 404, HTTP 200 with an error body, HTTP 200 with no usable access token — the agent gets an actionable oauth_reauth_required carrying the endpoint's own words, the connection reads as expired without a probe, and the authorization server's request ledger shows the grant left the building exactly once across all three calls.
  • HTTP 503, the control — no reconnect is demanded and the ledger shows three attempts, so a server having a bad minute is still retried.

Red/green: with the two product files reverted to the merge base and the scenarios in place, the four dead-grant scenarios fail (Internal tool error [id] from the MCP call) and the 503 control passes; with the fix, all five pass, alongside the pre-existing oauth-refresh-rejected and oauth-refresh-on-401 scenarios.

One unit case remains, in packages/core/sdk/src/oauth-helpers.test.ts: a token endpoint that never answers at all. The e2e authorization server can emit any response, but it cannot emit the absence of one, and "no answer" versus "the server said no" is exactly the boundary this classifier keys on.

Out of scope, tracked separately: healthFromCredentialResolutionError still re-wraps into StorageError, so the health endpoint's own response is a separate change.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Updated (UTC)
✅ Deployment successful!
View logs
executor-cloud 098ca81 Aug 27 2026, 10:24 PM

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 27, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
executor-marketing 098ca81 Commit Preview URL

Branch Preview URL
Aug 27 2026, 10:22 PM

@github-actions

github-actions Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Cloudflare preview

Torn down — the PR is closed.

@pkg-pr-new

pkg-pr-new Bot commented Aug 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@executor-js/cli

npm i https://pkg.pr.new/@executor-js/cli@1783

@executor-js/config

npm i https://pkg.pr.new/@executor-js/config@1783

@executor-js/execution

npm i https://pkg.pr.new/@executor-js/execution@1783

@executor-js/sdk

npm i https://pkg.pr.new/@executor-js/sdk@1783

@executor-js/codemode-core

npm i https://pkg.pr.new/@executor-js/codemode-core@1783

@executor-js/runtime-quickjs

npm i https://pkg.pr.new/@executor-js/runtime-quickjs@1783

executor

npm i https://pkg.pr.new/executor@1783

@executor-js/plugin-file-secrets

npm i https://pkg.pr.new/@executor-js/plugin-file-secrets@1783

@executor-js/plugin-graphql

npm i https://pkg.pr.new/@executor-js/plugin-graphql@1783

@executor-js/plugin-keychain

npm i https://pkg.pr.new/@executor-js/plugin-keychain@1783

@executor-js/plugin-mcp

npm i https://pkg.pr.new/@executor-js/plugin-mcp@1783

@executor-js/plugin-onepassword

npm i https://pkg.pr.new/@executor-js/plugin-onepassword@1783

@executor-js/plugin-openapi

npm i https://pkg.pr.new/@executor-js/plugin-openapi@1783

commit: 098ca81

@RhysSullivan
RhysSullivan force-pushed the fix/oauth-refresh-permanent-classification branch from 6a6f22c to c08f268 Compare August 27, 2026 20:30
@RhysSullivan
RhysSullivan marked this pull request as ready for review August 27, 2026 22:45
@RhysSullivan
RhysSullivan merged commit cd3d000 into main Aug 27, 2026
44 checks passed
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