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
- 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)
- Verify the deployment supports POST /token/introspect (404 → wrong BaseURL or outdated server)
- Back off and retry on 429/5xx
- 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
- Check token expiry locally before introspecting to avoid predictable 401s
- Branch on the embedded status code: 401/403 → re-auth, 429/5xx → retry
- Refresh device credentials proactively before they expire
- Log the response body snippet from the error for server-side diagnostics
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
- failed to get MCP auth URL: status code %d
- failed to authenticate MCP: status code %d
- failed to refresh MCP prompts: status code %d
- unexpected status code: %d
- request failed with status code: %d
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/6aff8798ed5b708b.
Report an issue: GitHub.