|
1 | | -export const ServerError = ({ status, message }) => { |
2 | | - return <div className="error-container"> |
3 | | - <div className="error-status">{status}</div> |
4 | | - <div className="error-label">Server error</div> |
5 | | - <p className="error-desc">{String(message ?? 'The request failed.')}</p> |
6 | | - </div> |
| 1 | +import { ApiError } from '~/backendApi' |
| 2 | + |
| 3 | +type ServerErrorProps = { |
| 4 | + error?: unknown |
| 5 | + status?: number |
| 6 | + className?: string |
| 7 | +} |
| 8 | + |
| 9 | +const MAX_DESC_CHARS = 280 |
| 10 | + |
| 11 | +const labelForStatus = (status: number): string => { |
| 12 | + if (status === 0) return 'Connection failed' |
| 13 | + if (status === 400) return 'Bad request' |
| 14 | + if (status === 401) return 'Unauthorized' |
| 15 | + if (status === 403) return 'Forbidden' |
| 16 | + if (status === 404) return 'Not found' |
| 17 | + if (status === 408) return 'Request timeout' |
| 18 | + if (status === 409) return 'Conflict' |
| 19 | + if (status === 429) return 'Too many requests' |
| 20 | + if (status === 502) return 'Bad gateway' |
| 21 | + if (status === 503) return 'Service unavailable' |
| 22 | + if (status === 504) return 'Gateway timeout' |
| 23 | + if (status >= 500) return 'Server error' |
| 24 | + if (status >= 400) return 'Request failed' |
| 25 | + return 'Error' |
| 26 | +} |
| 27 | + |
| 28 | +const extractMessage = (error: unknown): string | undefined => { |
| 29 | + if (error == null) return undefined |
| 30 | + if (typeof error === 'string') return error.trim() || undefined |
| 31 | + if (error instanceof ApiError) { |
| 32 | + const body = error.body as unknown |
| 33 | + if (body && typeof body === 'object') { |
| 34 | + const b = body as Record<string, unknown> |
| 35 | + if (typeof b.error === 'string' && b.error) return b.error |
| 36 | + if (typeof b.message === 'string' && b.message) return b.message |
| 37 | + } |
| 38 | + if (typeof body === 'string' && body) return body |
| 39 | + if (error.message) return error.message |
| 40 | + return error.statusText || undefined |
| 41 | + } |
| 42 | + if (error instanceof Error) return error.message || undefined |
| 43 | + try { return String(error) } catch { return undefined } |
| 44 | +} |
| 45 | + |
| 46 | +const truncate = (text: string): string => |
| 47 | + text.length <= MAX_DESC_CHARS ? text : text.slice(0, MAX_DESC_CHARS - 1).trimEnd() + '…' |
| 48 | + |
| 49 | +const derive = (error: unknown, statusOverride?: number) => { |
| 50 | + let status = statusOverride |
| 51 | + if (status == null && error instanceof ApiError) status = error.status |
| 52 | + |
| 53 | + const message = extractMessage(error) |
| 54 | + const hasStatus = status != null && status > 0 |
| 55 | + |
| 56 | + return { |
| 57 | + statusDisplay: hasStatus ? String(status) : '—', |
| 58 | + label: hasStatus ? labelForStatus(status!) : 'Connection failed', |
| 59 | + description: message |
| 60 | + ? truncate(message) |
| 61 | + : hasStatus |
| 62 | + ? 'The request failed.' |
| 63 | + : 'Could not reach the server. Check your connection and try again.', |
| 64 | + } |
| 65 | +} |
| 66 | + |
| 67 | +export const ServerError = ({ error, status, className }: ServerErrorProps) => { |
| 68 | + const { statusDisplay, label, description } = derive(error, status) |
| 69 | + const containerClass = className ? `error-container ${className}` : 'error-container' |
| 70 | + return ( |
| 71 | + <div className={containerClass}> |
| 72 | + <div className="error-status">{statusDisplay}</div> |
| 73 | + <div className="error-label">{label}</div> |
| 74 | + <p className="error-desc">{description}</p> |
| 75 | + </div> |
| 76 | + ) |
7 | 77 | } |
8 | 78 |
|
9 | 79 | export default ServerError |
0 commit comments