|
| 1 | +package api |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/json" |
| 5 | + "fmt" |
| 6 | +) |
| 7 | + |
| 8 | +type Error struct { |
| 9 | + // Only errorCode and errorMessage are returned by App Store Server API. |
| 10 | + errorCode int |
| 11 | + errorMessage string |
| 12 | +} |
| 13 | + |
| 14 | +func newError(errorCode int, errorMessage string) *Error { |
| 15 | + return &Error{ |
| 16 | + errorCode: errorCode, |
| 17 | + errorMessage: errorMessage, |
| 18 | + } |
| 19 | +} |
| 20 | + |
| 21 | +type appStoreAPIErrorResp struct { |
| 22 | + ErrorCode int `json:"errorCode"` |
| 23 | + ErrorMessage string `json:"errorMessage"` |
| 24 | +} |
| 25 | + |
| 26 | +func newErrorFromJSON(b []byte) (*Error, bool) { |
| 27 | + if len(b) == 0 { |
| 28 | + return nil, false |
| 29 | + } |
| 30 | + var rErr appStoreAPIErrorResp |
| 31 | + if err := json.Unmarshal(b, &rErr); err != nil { |
| 32 | + return nil, false |
| 33 | + } |
| 34 | + if rErr.ErrorCode == 0 { |
| 35 | + return nil, false |
| 36 | + } |
| 37 | + return &Error{errorCode: rErr.ErrorCode, errorMessage: rErr.ErrorMessage}, true |
| 38 | +} |
| 39 | + |
| 40 | +func (e *Error) Error() string { |
| 41 | + return fmt.Sprintf("errorCode: %d, errorMessage: %s", e.errorCode, e.errorMessage) |
| 42 | +} |
| 43 | + |
| 44 | +func (e *Error) As(target interface{}) bool { |
| 45 | + if targetErr, ok := target.(*Error); ok { |
| 46 | + *targetErr = *e |
| 47 | + return true |
| 48 | + } |
| 49 | + return false |
| 50 | +} |
| 51 | + |
| 52 | +func (e *Error) Is(target error) bool { |
| 53 | + if other, ok := target.(*Error); ok && other.errorCode == e.errorCode { |
| 54 | + return true |
| 55 | + } |
| 56 | + return false |
| 57 | +} |
| 58 | + |
| 59 | +func (e *Error) ErrorCode() int { |
| 60 | + return e.errorCode |
| 61 | +} |
| 62 | + |
| 63 | +func (e *Error) ErrorMessage() string { |
| 64 | + return e.errorMessage |
| 65 | +} |
| 66 | + |
| 67 | +func (e *Error) Retryable() bool { |
| 68 | + // NOTE: |
| 69 | + // RateLimitExceededError[1] could also be considered as a retryable error. |
| 70 | + // But limits are enforced on an hourly basis[2], so you should handle exceeded rate limits gracefully instead of retrying immediately. |
| 71 | + // Refs: |
| 72 | + // [1] https://developer.apple.com/documentation/appstoreserverapi/ratelimitexceedederror |
| 73 | + // [2] https://developer.apple.com/documentation/appstoreserverapi/identifying_rate_limits |
| 74 | + switch e.errorCode { |
| 75 | + case 4040002, 4040004, 5000001, 4040006: |
| 76 | + return true |
| 77 | + default: |
| 78 | + return false |
| 79 | + } |
| 80 | +} |
| 81 | + |
| 82 | +// All Error lists in https://developer.apple.com/documentation/appstoreserverapi/error_codes. |
| 83 | +var ( |
| 84 | + // Retryable errors |
| 85 | + AccountNotFoundRetryableError = newError(4040002, "Account not found. Please try again.") |
| 86 | + AppNotFoundRetryableError = newError(4040004, "App not found. Please try again.") |
| 87 | + GeneralInternalRetryableError = newError(5000001, "An unknown error occurred. Please try again.") |
| 88 | + OriginalTransactionIdNotFoundRetryableError = newError(4040006, "Original transaction id not found. Please try again.") |
| 89 | + // Errors |
| 90 | + AccountNotFoundError = newError(4040001, "Account not found.") |
| 91 | + AppNotFoundError = newError(4040003, "App not found.") |
| 92 | + FamilySharedSubscriptionExtensionIneligibleError = newError(4030007, "Subscriptions that users obtain through Family Sharing can't get a renewal date extension directly.") |
| 93 | + GeneralInternalError = newError(5000000, "An unknown error occurred.") |
| 94 | + GeneralBadRequestError = newError(4000000, "Bad request.") |
| 95 | + InvalidAppIdentifierError = newError(4000002, "Invalid request app identifier.") |
| 96 | + InvalidEmptyStorefrontCountryCodeListError = newError(4000027, "Invalid request. If provided, the list of storefront country codes must not be empty.") |
| 97 | + InvalidExtendByDaysError = newError(4000009, "Invalid extend by days value.") |
| 98 | + InvalidExtendReasonCodeError = newError(4000010, "Invalid extend reason code.") |
| 99 | + InvalidOriginalTransactionIdError = newError(4000008, "Invalid original transaction id.") |
| 100 | + InvalidRequestIdentifierError = newError(4000011, "Invalid request identifier.") |
| 101 | + InvalidRequestRevisionError = newError(4000005, "Invalid request revision.") |
| 102 | + InvalidRevokedError = newError(4000030, "Invalid request. The revoked parameter is invalid.") |
| 103 | + InvalidStatusError = newError(4000031, "Invalid request. The status parameter is invalid.") |
| 104 | + InvalidStorefrontCountryCodeError = newError(4000028, "Invalid request. A storefront country code was invalid.") |
| 105 | + InvalidTransactionIdError = newError(4000006, "Invalid transaction id.") |
| 106 | + OriginalTransactionIdNotFoundError = newError(4040005, "Original transaction id not found.") |
| 107 | + RateLimitExceededError = newError(4290000, "Rate limit exceeded.") |
| 108 | + StatusRequestNotFoundError = newError(4040009, "The server didn't find a subscription-renewal-date extension request for this requestIdentifier and productId combination.") |
| 109 | + SubscriptionExtensionIneligibleError = newError(4030004, "Forbidden - subscription state ineligible for extension.") |
| 110 | + SubscriptionMaxExtensionError = newError(4030005, "Forbidden - subscription has reached maximum extension count.") |
| 111 | + TransactionIdNotFoundError = newError(4040010, "Transaction id not found.") |
| 112 | + // Notification test and history errors |
| 113 | + InvalidEndDateError = newError(4000016, "Invalid request. The end date is not a timestamp value represented in milliseconds.") |
| 114 | + InvalidNotificationTypeError = newError(4000018, "Invalid request. The notification type or subtype is invalid.") |
| 115 | + InvalidPaginationTokenError = newError(4000014, "Invalid request. The pagination token is invalid.") |
| 116 | + InvalidStartDateError = newError(4000015, "Invalid request. The start date is not a timestamp value represented in milliseconds.") |
| 117 | + InvalidTestNotificationTokenError = newError(4000020, "Invalid request. The test notification token is invalid.") |
| 118 | + InvalidInAppOwnershipTypeError = newError(4000026, "Invalid request. The in-app ownership type parameter is invalid.") |
| 119 | + InvalidProductIdError = newError(4000023, "Invalid request. The product id parameter is invalid.") |
| 120 | + InvalidProductTypeError = newError(4000022, "Invalid request. The product type parameter is invalid.") |
| 121 | + InvalidSortError = newError(4000021, "Invalid request. The sort parameter is invalid.") |
| 122 | + InvalidSubscriptionGroupIdentifierError = newError(4000024, "Invalid request. The subscription group identifier parameter is invalid.") |
| 123 | + MultipleFiltersSuppliedError = newError(4000019, "Invalid request. Supply either a transaction id or a notification type, but not both.") |
| 124 | + PaginationTokenExpiredError = newError(4000017, "Invalid request. The pagination token is expired.") |
| 125 | + ServerNotificationURLNotFoundError = newError(4040007, "No App Store Server Notification URL found for provided app. Check that a URL is configured in App Store Connect for this environment.") |
| 126 | + StartDateAfterEndDateError = newError(4000013, "Invalid request. The end date precedes the start date or the dates are the same.") |
| 127 | + StartDateTooFarInPastError = newError(4000012, "Invalid request. The start date is earlier than the allowed start date.") |
| 128 | + TestNotificationNotFoundError = newError(4040008, "Either the test notification token is expired or the notification and status are not yet available.") |
| 129 | +) |
0 commit comments