classifyByMessage matches well-known substrings emitted by upstream SDKs that wrap errors with %v (dropping the chain). Returns err unchanged when no pattern matches.
(err error)
| 62 | // that wrap errors with %v (dropping the chain). Returns err unchanged |
| 63 | // when no pattern matches. |
| 64 | func classifyByMessage(err error) error { |
| 65 | msg := err.Error() |
| 66 | lower := strings.ToLower(msg) |
| 67 | |
| 68 | switch { |
| 69 | case strings.Contains(lower, "failed to send initialized notification"): |
| 70 | return wrap(ErrInitNotification, err) |
| 71 | case strings.Contains(lower, "session missing"), |
| 72 | strings.Contains(lower, "session not found"): |
| 73 | return wrap(ErrSessionMissing, err) |
| 74 | case strings.Contains(lower, "connection reset"), |
| 75 | strings.Contains(lower, "connection refused"), |
| 76 | strings.Contains(lower, "broken pipe"), |
| 77 | strings.Contains(msg, "EOF"): |
| 78 | return wrap(ErrTransport, err) |
| 79 | // Map server-side OAuth token rejection to ErrAuthRequired. We match |
| 80 | // "invalid_token" (RFC 6750 §3.1 canonical error code) and its space- |
| 81 | // separated variant. We deliberately do NOT match bare "unauthorized" |
| 82 | // here to avoid classifying application-level 401s (unrelated to OAuth) |
| 83 | // as permanent auth failures; the token-was-attached gating in |
| 84 | // oauthTransport.roundTrip is the correct place for that check. |
| 85 | case strings.Contains(lower, "invalid_token"), |
| 86 | strings.Contains(lower, "invalid token"): |
| 87 | return wrap(ErrAuthRequired, err) |
| 88 | } |
| 89 | return err |
| 90 | } |
| 91 | |
| 92 | // IsTransient reports whether err wraps a sentinel that warrants a retry. |
| 93 | func IsTransient(err error) bool { |