ruvnet/ruflo · error · OAuthError
unexpected_shape
unexpected_shape
Error message
unexpected response shape from the server
What it means
parseTokenResponse got a 2xx from the token endpoint but resp.json() failed — the body isn't JSON — so it throws OAuthError code 'unexpected_shape'. The client only speaks the JSON token format, so a 200 with an HTML/text body means something other than the OAuth server answered.
Source
Thrown at v3/@claude-flow/security/src/oauth/client.ts:75
/** Builds the `/oauth/authorize` URL for the standard loopback-redirect flow. */
export function authorizeUrl(redirectUri: string, state: string, codeChallenge: string): string {
const url = new URL(`${authBaseUrl()}/oauth/authorize`);
url.searchParams.set('response_type', 'code');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', redirectUri);
url.searchParams.set('scope', SCOPE);
url.searchParams.set('state', state);
url.searchParams.set('code_challenge', codeChallenge);
url.searchParams.set('code_challenge_method', 'S256');
return url.toString();
}
async function parseTokenResponse(resp: Response): Promise<TokenResponse> {
if (resp.ok) {
try {
return (await resp.json()) as TokenResponse;
} catch {
throw new OAuthError('unexpected response shape from the server', 'unexpected_shape');
}
}
try {
const body = (await resp.json()) as OAuthErrorBody;
throw new OAuthError(
`oauth error: ${body.error} — ${body.error_description}`,
'protocol',
body.error,
body.error_description,
);
} catch (e) {
if (e instanceof OAuthError) throw e;
throw new OAuthError('unexpected response shape from the server', 'unexpected_shape');
}
}
async function postForm(path: string, form: Record<string, string>, base = authBaseUrl()): Promise<TokenResponse> {
let resp: Response;View on GitHub (pinned to fa13ee4ad6)
Solutions
- Unset or fix COGNITUM_AUTH_URL — the default is https://auth.cognitum.one
- Reproduce manually: curl -i -X POST $COGNITUM_AUTH_URL/oauth/token and inspect status, content-type, and body
- Rule out HTTPS_PROXY/captive-portal interception; retry from a clean network
- If you operate the server, never return 200 with non-JSON on the token route
Example fix
# before export COGNITUM_AUTH_URL=http://localhost:3000 # dev stub returning HTML → unexpected_shape # after unset COGNITUM_AUTH_URL # target https://auth.cognitum.one
Defensive patterns
Strategy: try-catch
Type guard
function isOAuthError(e: unknown, code?: string): boolean {
return e instanceof Error && e.name === 'OAuthError'
&& (code === undefined || (e as { code?: string }).code === code);
} Try / catch
try {
return await postToken(form);
} catch (e) {
if (isOAuthError(e, 'unexpected_shape')) {
// something answered 200 that isn't the OAuth server: check COGNITUM_AUTH_URL / proxies
throw new Error(`Auth server returned non-JSON 200 — check COGNITUM_AUTH_URL and network interception`);
}
throw e;
} Prevention
- Point COGNITUM_AUTH_URL only at servers that actually implement the OAuth token endpoints
- Reproduce unexpected shapes with curl -i before guessing at code fixes
- Distrust 200 responses with non-JSON content-type in OAuth clients generally
When it happens
Trigger: A captive portal or transparent proxy intercepting the POST and returning a 200 login page; COGNITUM_AUTH_URL pointing at a server that doesn't implement POST /oauth/token; an API gateway returning 200 with an HTML error shell.
Common situations: Corporate networks with SSL inspection portals; leftover COGNITUM_AUTH_URL from local testing aimed at a stub; a misconfigured load balancer answering 200 for every path.
Related errors
- Routes config must be a flat array of routes
- Invalid route entry: ${JSON.stringify(r)}
- --token-stdin: JSON is missing required field "access_token"
- network
- Invalid completion type
AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18).
Data as JSON: /api/errors/c67eb9bca10654eb.
Report an issue: GitHub.