github.com/anthropics/anthropic-sdk-go@v1.70.1/internal/auth/error.go (about)

     1  package auth
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"github.com/anthropics/anthropic-sdk-go/config"
    10  )
    11  
    12  // ErrNoCredentials is the sentinel for the case where the default credential
    13  // chain produced no usable credential. Every [NoCredentialsError] value
    14  // matches this sentinel via [errors.Is], so callers can branch on the
    15  // class of failure without a type assertion.
    16  var ErrNoCredentials = errors.New("no Anthropic credentials found")
    17  
    18  // OAuthTokenError is returned when an OAuth token request (workload identity
    19  // exchange or authorized_user refresh) fails with a non-200 status.
    20  type OAuthTokenError struct {
    21  	StatusCode int
    22  	Body       string
    23  	// RequestID is the value of the server's Request-Id response header, if
    24  	// any. Empty when the response had no such header. Included in the
    25  	// formatted error so support can correlate user reports with server logs.
    26  	RequestID string
    27  	// Hint is an optional caller-supplied diagnostic appended verbatim to
    28  	// the error message. Callers gate it on the response status and their
    29  	// own state — Error does not inspect StatusCode or Body to decide
    30  	// whether to include it.
    31  	Hint string
    32  	// WorkloadIdentity is set by the OIDC federation (jwt-bearer) flow to
    33  	// suppress the "re-run `ant auth login`" remediation suffix.
    34  	// That suffix only makes sense for the interactive user_oauth flow;
    35  	// machine credentials have no browser login to re-run, and suggesting
    36  	// one sends operators down the wrong path. The zero value keeps the
    37  	// suffix so the user_oauth flow needs no change at its call site.
    38  	WorkloadIdentity bool
    39  }
    40  
    41  // rfc6749ErrorBody is the subset of the RFC 6749 OAuth error response the SDK
    42  // surfaces in user-facing error messages. Extra fields are ignored.
    43  type rfc6749ErrorBody struct {
    44  	Error            string `json:"error"`
    45  	ErrorDescription string `json:"error_description"`
    46  }
    47  
    48  func (e *OAuthTokenError) Error() string {
    49  	parts := []string{fmt.Sprintf("oauth token request failed (status %d)", e.StatusCode)}
    50  	if e.RequestID != "" {
    51  		parts = append(parts, fmt.Sprintf("request id %s", e.RequestID))
    52  	}
    53  
    54  	var parsed rfc6749ErrorBody
    55  	if err := json.Unmarshal([]byte(e.Body), &parsed); err == nil && parsed.Error != "" {
    56  		if parsed.ErrorDescription != "" {
    57  			parts = append(parts, fmt.Sprintf("%s: %s", parsed.Error, parsed.ErrorDescription))
    58  		} else {
    59  			parts = append(parts, parsed.Error)
    60  		}
    61  	} else {
    62  		parts = append(parts, config.RedactOAuthErrorBody(e.Body))
    63  	}
    64  
    65  	msg := strings.Join(parts, "; ")
    66  	if !e.WorkloadIdentity && shouldSuggestRelogin(e.StatusCode, parsed.Error) {
    67  		msg += " — re-run `ant auth login` to re-authenticate"
    68  	}
    69  	if e.Hint != "" {
    70  		msg += ". " + e.Hint
    71  	}
    72  	return msg
    73  }
    74  
    75  // shouldSuggestRelogin returns true when a token-endpoint failure is most
    76  // likely caused by an expired or revoked refresh credential. 5xx is treated
    77  // as transient; blaming the user's credentials on a server outage is wrong.
    78  func shouldSuggestRelogin(status int, rfcCode string) bool {
    79  	if rfcCode == "invalid_grant" {
    80  		return true
    81  	}
    82  	return status == 400 || status == 401 || status == 403
    83  }
    84  
    85  // CredentialSourceState categorizes the outcome of one entry in the default
    86  // credential chain when no source produced a usable credential.
    87  type CredentialSourceState int
    88  
    89  const (
    90  	CredentialSourceNotSet CredentialSourceState = iota
    91  	CredentialSourceNotFound
    92  	CredentialSourceLoadFailed
    93  	CredentialSourcePartial
    94  )
    95  
    96  // CredentialSourceStatus records what the default resolution chain observed
    97  // for one credential source when all sources failed. Detail is the load
    98  // error for [CredentialSourceLoadFailed], the remediation hint for
    99  // [CredentialSourceNotFound], or the missing-var list for
   100  // [CredentialSourcePartial]; it is empty for [CredentialSourceNotSet].
   101  type CredentialSourceStatus struct {
   102  	Name   string
   103  	State  CredentialSourceState
   104  	Detail string
   105  }
   106  
   107  // NoCredentialsError is returned by the default credential chain when no
   108  // source produced a usable credential. Its Error method renders every
   109  // source the SDK tried and a brief remediation hint.
   110  type NoCredentialsError struct {
   111  	Sources []CredentialSourceStatus
   112  }
   113  
   114  // Is reports whether target is [ErrNoCredentials]. Enables callers to
   115  // detect the no-credentials class with [errors.Is] in addition to
   116  // [errors.As] against the concrete type.
   117  func (e *NoCredentialsError) Is(target error) bool {
   118  	return target == ErrNoCredentials
   119  }
   120  
   121  func (e *NoCredentialsError) Error() string {
   122  	var b strings.Builder
   123  	b.WriteString("no Anthropic credentials found. The SDK tried these sources in order:")
   124  	for i, s := range e.Sources {
   125  		fmt.Fprintf(&b, "\n  %d. %s: %s", i+1, s.Name, formatSourceState(s))
   126  	}
   127  	b.WriteString("\nTo fix:")
   128  	b.WriteString("\n  - run `ant auth login` to interactively authenticate, or")
   129  	b.WriteString("\n  - set ANTHROPIC_API_KEY, or")
   130  	b.WriteString("\n  - configure workload identity federation with ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE")
   131  	return b.String()
   132  }
   133  
   134  func formatSourceState(s CredentialSourceStatus) string {
   135  	switch s.State {
   136  	case CredentialSourceNotFound:
   137  		if s.Detail != "" {
   138  			return "not found (" + s.Detail + ")"
   139  		}
   140  		return "not found"
   141  	case CredentialSourceLoadFailed:
   142  		return "load failed: " + s.Detail
   143  	case CredentialSourcePartial:
   144  		return s.Detail
   145  	default:
   146  		return "not set"
   147  	}
   148  }
   149  
   150  // CredentialResolutionError is returned when a credential provider cannot be
   151  // constructed from the available configuration.
   152  type CredentialResolutionError struct {
   153  	Message string
   154  	Err     error
   155  }
   156  
   157  func (e *CredentialResolutionError) Error() string {
   158  	if e.Err != nil {
   159  		return fmt.Sprintf("credential resolution error: %s: %v", e.Message, e.Err)
   160  	}
   161  	return fmt.Sprintf("credential resolution error: %s", e.Message)
   162  }
   163  
   164  func (e *CredentialResolutionError) Unwrap() error {
   165  	return e.Err
   166  }