/** * API Error Handler Utility * Parses server error responses and extracts field-level validation errors */ import { AxiosError } from "axios"; import { FieldValues, Path, UseFormSetError } from "react-hook-form"; /** * Standard API error response structure */ export interface ApiErrorResponse { message?: string; errors?: Record; error?: string; detail?: string | Record; } /** * Parsed error result */ export interface ParsedApiError { generalMessage: string; fieldErrors: Record; } /** * Field name mapping for common API field names to form field names */ const FIELD_NAME_MAP: Record = { fullName: "full_name", full_name: "full_name", email: "email", phone: "phone", phoneNumber: "phone_number", phone_number: "phone_number", message: "message", companyName: "company_name", company_name: "company_name", workEmail: "work_email", work_email: "work_email", }; /** * Common error messages for validation types */ const ERROR_MESSAGES: Record = { invalid_input: "Invalid input detected. Please remove any special characters or scripts.", xss_detected: "Invalid content detected. Please remove any HTML tags or scripts.", validation_failed: "Validation failed. Please check your input.", required: "This field is required.", invalid_email: "Please enter a valid email address.", invalid_phone: "Please enter a valid phone number.", too_short: "Input is too short.", too_long: "Input is too long.", }; /** * Normalizes field name from API to form field name */ function normalizeFieldName(fieldName: string): string { return FIELD_NAME_MAP[fieldName] || fieldName; } /** * Extracts a human-readable error message from various error formats */ function extractErrorMessage(error: string | string[] | unknown): string { if (typeof error === "string") { return ERROR_MESSAGES[error] || error; } if (Array.isArray(error)) { return error.map((e) => ERROR_MESSAGES[e] || e).join(". "); } return "Invalid input"; } /** * Parses API error response and extracts field-level errors * @param error - The axios error object * @returns Parsed error with general message and field-specific errors */ export function parseApiError(error: unknown): ParsedApiError { const result: ParsedApiError = { generalMessage: "Something went wrong. Please try again.", fieldErrors: {}, }; if (!(error instanceof Error)) { return result; } const axiosError = error as AxiosError; if (!axiosError.response) { // Network error result.generalMessage = "Unable to connect to server. Please check your connection."; return result; } const { status, data } = axiosError.response; // Handle different HTTP status codes if (status === 400) { result.generalMessage = "Validation error. Please check the highlighted fields."; // Extract field errors from different response formats if (data?.errors) { // Format: { errors: { field1: "error", field2: ["error1", "error2"] } } Object.entries(data.errors).forEach(([field, error]) => { const normalizedField = normalizeFieldName(field); result.fieldErrors[normalizedField] = extractErrorMessage(error); }); } else if (data?.detail && typeof data.detail === "object") { // Django REST framework format: { detail: { field1: ["error"] } } Object.entries(data.detail).forEach(([field, errors]) => { const normalizedField = normalizeFieldName(field); result.fieldErrors[normalizedField] = extractErrorMessage(errors); }); } else if (data?.message) { result.generalMessage = data.message; } else if (data?.error) { result.generalMessage = data.error; } } else if (status === 401) { result.generalMessage = "Authentication required. Please log in and try again."; } else if (status === 403) { result.generalMessage = "You don't have permission to perform this action."; } else if (status === 404) { result.generalMessage = "The requested resource was not found."; } else if (status === 422) { // Unprocessable Entity - validation error result.generalMessage = "Validation error. Please check the highlighted fields."; if (data?.errors) { Object.entries(data.errors).forEach(([field, error]) => { const normalizedField = normalizeFieldName(field); result.fieldErrors[normalizedField] = extractErrorMessage(error); }); } } else if (status === 429) { result.generalMessage = "Too many requests. Please wait a moment and try again."; } else if (status >= 500) { result.generalMessage = "Server error. Please try again later."; } return result; } /** * Handles API error and sets form field errors using react-hook-form * @param error - The error from API call * @param setError - react-hook-form setError function * @param showToast - Function to show toast notification * @param formFields - Optional list of form field names to validate against */ export function handleApiError( error: unknown, setError: UseFormSetError, showToast: (message: string) => void, formFields?: string[] ): void { const parsedError = parseApiError(error); // Set field-specific errors let hasFieldErrors = false; Object.entries(parsedError.fieldErrors).forEach(([field, message]) => { // Only set error if field exists in form (or if formFields not specified) if (!formFields || formFields.includes(field)) { setError(field as Path, { type: "server", message, }); hasFieldErrors = true; } }); // Show toast with general message // If we have field errors, use a more specific message if (hasFieldErrors) { showToast("Please correct the errors in the highlighted fields."); } else { showToast(parsedError.generalMessage); } }