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

  1. Unset or fix COGNITUM_AUTH_URL — the default is https://auth.cognitum.one
  2. Reproduce manually: curl -i -X POST $COGNITUM_AUTH_URL/oauth/token and inspect status, content-type, and body
  3. Rule out HTTPS_PROXY/captive-portal interception; retry from a clean network
  4. 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

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


AI-assisted analysis of ruvnet/ruflo@fa13ee4ad6 (2026-08-18). Data as JSON: /api/errors/c67eb9bca10654eb. Report an issue: GitHub.