{"record":{"id":"6aff8798ed5b708b","repo":"charmbracelet/crush","slug":"token-introspection-failed-status-d-body-q","errorCode":null,"errorMessage":"token introspection failed: status %d body %q","messagePattern":"token introspection failed: status (.+?) body %q","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/oauth/hyper/device.go","lineNumber":242,"sourceCode":"\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"User-Agent\", \"crush\")\n\n\tclient := &http.Client{Timeout: 30 * time.Second}\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"execute request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\treturn nil, fmt.Errorf(\"token introspection failed: status %d body %q\", resp.StatusCode, string(body))\n\t}\n\n\tvar result IntrospectTokenResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"unmarshal response: %w\", err)\n\t}\n\n\treturn &result, nil\n}\n","sourceCodeStart":224,"sourceCodeEnd":252,"githubUrl":"https://github.com/charmbracelet/crush/blob/7944b8e52225d8805e31eacbf7ef24856b0dfb7a/internal/oauth/hyper/device.go#L224-L252","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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'"],"exampleFix":"// before\nif resp.StatusCode != http.StatusOK {\n    return nil, fmt.Errorf(\"token introspection failed: status %d body %q\", resp.StatusCode, string(body))\n}\n// after\nif resp.StatusCode != http.StatusOK {\n    return nil, &oauth.IntrospectError{StatusCode: resp.StatusCode, Body: string(body)} // typed, matches TokenExchangeError pattern\n}","handlingStrategy":"try-catch","validationCode":"// pre-check token shape locally before network introspection\nif len(accessToken) < 20 || !strings.HasPrefix(accessToken, \"hype\") {\n    return fmt.Errorf(\"access token is malformed; re-authenticate\")\n}","typeGuard":"func isAuthFailure(err error) bool {\n    var te *oauth.TokenExchangeError\n    matched := errors.As(err, &te)\n    return matched || strings.Contains(err.Error(), \"status 401\") || strings.Contains(err.Error(), \"status 403\")\n}","tryCatchPattern":"res, err := IntrospectToken(ctx, tok)\nif err != nil {\n    var authErr = strings.Contains(err.Error(), \"status 401\") || strings.Contains(err.Error(), \"status 403\")\n    if authErr {\n        return restartDeviceLogin(ctx) // token invalid/expired → re-login\n    }\n    if strings.Contains(err.Error(), \"status 429\") || strings.Contains(err.Error(), \"status 5\") {\n        return retryWithBackoff(3, func() (*IntrospectTokenResponse, error) { return IntrospectToken(ctx, tok) })\n    }\n    return err\n}","preventionTips":["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"],"tags":["http","oauth","api-error","status-code"],"backgroundTag":"http-non-2xx-response","analyzedSha":"7944b8e52225d8805e31eacbf7ef24856b0dfb7a","analyzedAt":"2026-08-29T12:48:59.079Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}