@@ -22,12 +22,130 @@ export interface ErrorResponse {
2222 */
2323export enum ErrorType {
2424 API_ERROR = "API_ERROR" ,
25+ RATE_LIMIT = "RATE_LIMIT" ,
2526 VALIDATION_ERROR = "VALIDATION_ERROR" ,
2627 NOT_FOUND = "NOT_FOUND" ,
2728 NETWORK_ERROR = "NETWORK_ERROR" ,
2829 UNKNOWN_ERROR = "UNKNOWN_ERROR" ,
2930}
3031
32+ type RetryAwareError = {
33+ hevyRetryCount ?: number ;
34+ hevyRetryExhausted ?: boolean ;
35+ } ;
36+
37+ function normalizeHeaderValue ( value : unknown ) : string | undefined {
38+ if ( typeof value === "string" ) {
39+ const trimmed = value . trim ( ) ;
40+ return trimmed . length > 0 ? trimmed : undefined ;
41+ }
42+
43+ if ( typeof value === "number" && Number . isFinite ( value ) ) {
44+ return String ( value ) ;
45+ }
46+
47+ if ( Array . isArray ( value ) && value . length > 0 ) {
48+ return normalizeHeaderValue ( value [ 0 ] ) ;
49+ }
50+
51+ return undefined ;
52+ }
53+
54+ function getHeaderValue ( headers : unknown , key : string ) : string | undefined {
55+ if ( ! headers || typeof headers !== "object" ) {
56+ return undefined ;
57+ }
58+
59+ if (
60+ "get" in headers &&
61+ typeof ( headers as { get ?: unknown } ) . get === "function"
62+ ) {
63+ const value = ( headers as { get : ( headerName : string ) => unknown } ) . get (
64+ key ,
65+ ) ;
66+ return normalizeHeaderValue ( value ) ;
67+ }
68+
69+ const headerRecord = headers as Record < string , unknown > ;
70+ return normalizeHeaderValue (
71+ headerRecord [ key ] ??
72+ headerRecord [ key . toLowerCase ( ) ] ??
73+ headerRecord [ key . toUpperCase ( ) ] ,
74+ ) ;
75+ }
76+
77+ function formatSecondsLabel ( seconds : number ) : string {
78+ const roundedSeconds = Math . max ( 0 , Math . round ( seconds ) ) ;
79+ const suffix = roundedSeconds === 1 ? "" : "s" ;
80+ return `${ roundedSeconds } second${ suffix } ` ;
81+ }
82+
83+ function getRateLimitMessage ( error : unknown ) : string {
84+ if ( ! isAxiosError ( error ) ) {
85+ return "Rate limited by Hevy. Please wait and retry." ;
86+ }
87+
88+ const retryAfterHeader = getHeaderValue (
89+ error . response ?. headers ,
90+ "retry-after" ,
91+ ) ;
92+ if ( ! retryAfterHeader ) {
93+ return "Rate limited by Hevy (HTTP 429). Please wait and retry your request." ;
94+ }
95+
96+ const seconds = Number ( retryAfterHeader ) ;
97+ if ( Number . isFinite ( seconds ) && seconds >= 0 ) {
98+ return `Rate limited by Hevy (HTTP 429). Please wait about ${ formatSecondsLabel ( seconds ) } before retrying.` ;
99+ }
100+
101+ const retryAtMillis = Date . parse ( retryAfterHeader ) ;
102+ if ( ! Number . isNaN ( retryAtMillis ) ) {
103+ const secondsUntilRetry = Math . ceil (
104+ Math . max ( 0 , retryAtMillis - Date . now ( ) ) / 1000 ,
105+ ) ;
106+ return `Rate limited by Hevy (HTTP 429). Please wait about ${ formatSecondsLabel ( secondsUntilRetry ) } before retrying.` ;
107+ }
108+
109+ return "Rate limited by Hevy (HTTP 429). Please wait and retry your request." ;
110+ }
111+
112+ function isRetryExhaustedError ( error : unknown ) : boolean {
113+ return (
114+ ! ! error &&
115+ typeof error === "object" &&
116+ ( error as RetryAwareError ) . hevyRetryExhausted === true
117+ ) ;
118+ }
119+
120+ function getRetryExhaustedMessage ( error : unknown ) : string {
121+ const retryCount =
122+ typeof error === "object" && error !== null
123+ ? ( error as RetryAwareError ) . hevyRetryCount
124+ : undefined ;
125+ const attemptCount =
126+ typeof retryCount === "number" && Number . isFinite ( retryCount )
127+ ? retryCount + 1
128+ : undefined ;
129+
130+ if ( attemptCount ) {
131+ return `Unable to complete the request after ${ attemptCount } attempts to the Hevy API due to transient failures. Please try again shortly.` ;
132+ }
133+
134+ return "Unable to complete the request after multiple attempts to the Hevy API due to transient failures. Please try again shortly." ;
135+ }
136+
137+ function getUserFacingMessage ( error : unknown , defaultMessage : string ) : string {
138+ if ( isRetryExhaustedError ( error ) ) {
139+ return getRetryExhaustedMessage ( error ) ;
140+ }
141+
142+ if ( isAxiosError ( error ) && error . response ?. status === 429 ) {
143+ return getRateLimitMessage ( error ) ;
144+ }
145+
146+ return defaultMessage ;
147+ }
148+
31149/**
32150 * Enhanced error response with type categorization
33151 */
@@ -79,6 +197,8 @@ export function createErrorResponse(
79197 errorMessage = stringifyErrorData ( axiosErrorContext . data ) ;
80198 }
81199
200+ errorMessage = getUserFacingMessage ( error , errorMessage ) ;
201+
82202 // Extract error code if available (for logging purposes)
83203 const errorCode =
84204 error instanceof Error && "code" in error
@@ -210,6 +330,14 @@ function stringifyErrorData(data: unknown): string {
210330 * Determine the type of error based on error characteristics
211331 */
212332function determineErrorType ( error : unknown , message : string ) : ErrorType {
333+ if ( isRetryExhaustedError ( error ) ) {
334+ return ErrorType . NETWORK_ERROR ;
335+ }
336+
337+ if ( isAxiosError ( error ) && error . response ?. status === 429 ) {
338+ return ErrorType . RATE_LIMIT ;
339+ }
340+
213341 const messageLower = message . toLowerCase ( ) ;
214342 const nameLower = error instanceof Error ? error . name . toLowerCase ( ) : "" ;
215343
0 commit comments