package contract import ( "encoding/json" "math" "strings" "unicode/utf8" ) type ErrorCategory string const ( ErrorTimeout ErrorCategory = "timeout" ErrorRateLimited ErrorCategory = "rate_limited" ErrorAuthentication ErrorCategory = "authentication" ErrorValidation ErrorCategory = "validation" ErrorUpstream ErrorCategory = "upstream" ErrorTransport ErrorCategory = "transport" ErrorInternal ErrorCategory = "internal" ErrorOther ErrorCategory = "other" MaxErrorCategories = 8 MaxErrorMessageBytes = 2048 ) // NormalizeErrorCategory maps the existing consumer classifications into the // approved, bounded vocabulary. Never use an upstream message/code as a key. func NormalizeErrorCategory(value string) ErrorCategory { switch strings.ToLower(strings.TrimSpace(value)) { case "timeout": return ErrorTimeout case "rate_limited", "mobile_busy", "mobile_busy_exhausted", "channel_limited", "channel_blocked": return ErrorRateLimited case "authentication": return ErrorAuthentication case "validation": return ErrorValidation case "upstream", "business", "carrier_retry": return ErrorUpstream case "transport", "network": return ErrorTransport case "internal", "canceled": return ErrorInternal default: return ErrorOther } } func (c ErrorCategory) MarshalJSON() ([]byte, error) { return json.Marshal(string(NormalizeErrorCategory(string(c)))) } func (c *ErrorCategory) UnmarshalJSON(data []byte) error { var value string if err := json.Unmarshal(data, &value); err != nil { return invalid("error category must be a string: %v", err) } *c = NormalizeErrorCategory(value) return nil } // ErrorCounts is normalized on both JSON input and output, including map keys // (encoding/json does not invoke ErrorCategory.MarshalJSON for map keys). // Counts are per category, not card IDs or individual error messages. type ErrorCounts map[ErrorCategory]int64 func (counts ErrorCounts) Normalized() (ErrorCounts, error) { normalized := make(ErrorCounts) for category, count := range counts { key := NormalizeErrorCategory(string(category)) if count < 0 || count > math.MaxInt64-normalized[key] { return nil, invalid("error counts must be nonnegative and must not overflow") } normalized[key] += count } return normalized, nil } func (counts ErrorCounts) MarshalJSON() ([]byte, error) { normalized, err := counts.Normalized() if err != nil { return nil, err } type wireCounts map[ErrorCategory]int64 return json.Marshal(wireCounts(normalized)) } func (counts *ErrorCounts) UnmarshalJSON(data []byte) error { var decoded map[ErrorCategory]int64 if err := json.Unmarshal(data, &decoded); err != nil { return invalid("invalid error counts: %v", err) } normalized, err := ErrorCounts(decoded).Normalized() if err != nil { return err } *counts = normalized return nil } // BoundedErrorMessage makes new summary/action error messages valid UTF-8 and // caps their byte length. It does not modify legacy FailureRecord.Message. func BoundedErrorMessage(message string) string { message = strings.ToValidUTF8(message, "\uFFFD") if len(message) <= MaxErrorMessageBytes { return message } end := MaxErrorMessageBytes for !utf8.RuneStart(message[end]) { end-- } return message[:end] }