| 125 | * - 5xx (server errors): Retry 3 times - server might recover |
| 126 | */ |
| 127 | export class HTTPError extends AppError { |
| 128 | readonly maxRetries: number |
| 129 | |
| 130 | constructor(config: AppErrorConfig) { |
| 131 | super(config) |
| 132 | |
| 133 | // 5xx errors are retryable (server might recover) |
| 134 | // 4xx errors are not retryable (client error - permanent) |
| 135 | this.maxRetries = this.isRetryable ? 3 : 0 |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * Create HTTPError from fetch Response |
| 140 | */ |
| 141 | static async fromResponse(response: Response, endpoint: string): Promise<HTTPError> { |
| 142 | const status = response.status |
| 143 | const config = HTTPError.getErrorConfig(status) |
| 144 | const isDevelopment = process.env.NODE_ENV === 'development' |
| 145 | |
| 146 | // Try to extract error details from response body |
| 147 | let responseBody: unknown |
| 148 | try { |
| 149 | responseBody = await response.json() |
| 150 | } catch { |
| 151 | // Response body is not JSON - ignore |
| 152 | } |
| 153 | let serverMessage = |
| 154 | typeof (responseBody as any)?.error === 'string' ? (responseBody as any).error : |
| 155 | typeof (responseBody as any)?.message === 'string' ? (responseBody as any).message : |
| 156 | typeof (responseBody as any)?.detail === 'string' ? (responseBody as any).detail : |
| 157 | '' |
| 158 | try { |
| 159 | serverMessage = JSON.parse(serverMessage)?.error || serverMessage |
| 160 | } catch {} |
| 161 | |
| 162 | return new HTTPError({ |
| 163 | ...config, |
| 164 | message: serverMessage || config.message, |
| 165 | userMessage: serverMessage || config.userMessage, |
| 166 | statusCode: status, |
| 167 | technicalDetails: isDevelopment |
| 168 | ? { |
| 169 | status, |
| 170 | statusText: response.statusText, |
| 171 | ...(responseBody ? { response: JSON.stringify(responseBody).slice(0, 200) } : {}), |
| 172 | } |
| 173 | : undefined, |
| 174 | }) |
| 175 | } |
| 176 | |
| 177 | /** |
| 178 | * Map HTTP status codes to error configurations |
| 179 | * Following industry-standard error messages |
| 180 | */ |
| 181 | private static getErrorConfig(status: number): Omit<AppErrorConfig, 'statusCode' | 'technicalDetails'> { |
| 182 | // 4xx Client Errors (permanent - no retry) |
| 183 | if (status === 400) { |
| 184 | return { |
nothing calls this directly
no outgoing calls
no test coverage detected
searching dependent graphs…