7b6cd4d965
- Add validation rules for form fields with XSS protection - Create API error handler to parse and manage server validation errors - Integrate validation rules into contact and inquiries forms - Enhance error handling with user feedback for form submissions - Introduce unit tests for validation and error handling functionalities
185 lines
5.8 KiB
TypeScript
185 lines
5.8 KiB
TypeScript
/**
|
|
* 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<string, string | string[]>;
|
|
error?: string;
|
|
detail?: string | Record<string, string[]>;
|
|
}
|
|
|
|
/**
|
|
* Parsed error result
|
|
*/
|
|
export interface ParsedApiError {
|
|
generalMessage: string;
|
|
fieldErrors: Record<string, string>;
|
|
}
|
|
|
|
/**
|
|
* Field name mapping for common API field names to form field names
|
|
*/
|
|
const FIELD_NAME_MAP: Record<string, string> = {
|
|
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<string, string> = {
|
|
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<ApiErrorResponse>;
|
|
|
|
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<T extends FieldValues>(
|
|
error: unknown,
|
|
setError: UseFormSetError<T>,
|
|
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<T>, {
|
|
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);
|
|
}
|
|
}
|