charmbracelet/crush · error

token introspection failed: status %d body %q

Error message

token introspection failed: status %d body %q

What it means

IntrospectToken returns 'token introspection failed: status %d body %q' when /token/introspect responds with any non-200 status. Unlike the token-exchange path (which uses oauth.TokenExchangeError), this is a plain formatted error embedding the status and up to the response body for diagnosis.

Source

Thrown at internal/oauth/hyper/device.go:242

	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "crush")

	client := &http.Client{Timeout: 30 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return nil, fmt.Errorf("execute request: %w", err)
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
	if err != nil {
		return nil, fmt.Errorf("read response: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("token introspection failed: status %d body %q", resp.StatusCode, string(body))
	}

	var result IntrospectTokenResponse
	if err := json.Unmarshal(body, &result); err != nil {
		return nil, fmt.Errorf("unmarshal response: %w", err)
	}

	return &result, nil
}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read the status code and embedded body in the error message — 401/403 usually means the token is invalid or expired and should trigger a re-login (device flow restart)
  2. Verify the deployment supports POST /token/introspect (404 → wrong BaseURL or outdated server)
  3. Back off and retry on 429/5xx
  4. Handle this error distinctly from transport errors in loginHyper so users see 'token invalid' vs 'network down'

Example fix

// before
if resp.StatusCode != http.StatusOK {
    return nil, fmt.Errorf("token introspection failed: status %d body %q", resp.StatusCode, string(body))
}
// after
if resp.StatusCode != http.StatusOK {
    return nil, &oauth.IntrospectError{StatusCode: resp.StatusCode, Body: string(body)} // typed, matches TokenExchangeError pattern
}
Defensive patterns

Strategy: try-catch

Validate before calling

// pre-check token shape locally before network introspection
if len(accessToken) < 20 || !strings.HasPrefix(accessToken, "hype") {
    return fmt.Errorf("access token is malformed; re-authenticate")
}

Type guard

func isAuthFailure(err error) bool {
    var te *oauth.TokenExchangeError
    matched := errors.As(err, &te)
    return matched || strings.Contains(err.Error(), "status 401") || strings.Contains(err.Error(), "status 403")
}

Try / catch

res, err := IntrospectToken(ctx, tok)
if err != nil {
    var authErr = strings.Contains(err.Error(), "status 401") || strings.Contains(err.Error(), "status 403")
    if authErr {
        return restartDeviceLogin(ctx) // token invalid/expired → re-login
    }
    if strings.Contains(err.Error(), "status 429") || strings.Contains(err.Error(), "status 5") {
        return retryWithBackoff(3, func() (*IntrospectTokenResponse, error) { return IntrospectToken(ctx, tok) })
    }
    return err
}

Prevention

When it happens

Trigger: The introspection endpoint replies 4xx/5xx: 401/403 for an invalid/expired access token being introspected, 404 if the endpoint doesn't exist on that deployment, 429 rate limiting, or 5xx server error.

Common situations: Introspecting a revoked or expired Hyper access token, hitting a server version where /token/introspect moved or was removed, API version mismatch between client and server, or rate limits after polling loops.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/6aff8798ed5b708b. Report an issue: GitHub.