Classify non-JSON and HTTP-200 refresh refusals as dead grants - #1783
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | 098ca81 | Aug 27 2026, 10:24 PM |
Deploying with
|
| 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 |
Contributor
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
executor
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
commit: |
RhysSullivan
force-pushed
the
fix/oauth-refresh-permanent-classification
branch
from
August 27, 2026 20:30
6a6f22c to
c08f268
Compare
RhysSullivan
marked this pull request as ready for review
August 27, 2026 22:45
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/plain400, atext/plain404, and a GitHub-style200 {"error":"bad_refresh_token"}all arrived code-less, were classified as retryable storage failures, and the grant was never marked dead.Fix
OAuth2Errornow 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-requiredCredentialResolutionError, so the grant is marked dead once and the connection moves to needs-reauth. A 5xx or a transport failure still stays a retryableStorageError, and a 4xx that did name a code keeps its existing classification, so a fleet-wideinvalid_clientis 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/plain400,text/plain404, HTTP 200 with an error body, HTTP 200 with no usable access token — the agent gets an actionableoauth_reauth_requiredcarrying 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.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-existingoauth-refresh-rejectedandoauth-refresh-on-401scenarios.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:
healthFromCredentialResolutionErrorstill re-wraps intoStorageError, so the health endpoint's own response is a separate change.