Fixed API calls on governance page
Enforced multitenancy
This commit is contained in:
@@ -15,64 +15,23 @@ export interface FriendlyError {
|
||||
title: string;
|
||||
message: string;
|
||||
statusCode?: number;
|
||||
hasReturnObject: boolean;
|
||||
isProblemDetails: boolean;
|
||||
category: ErrorCategory;
|
||||
categoryLabel?: string;
|
||||
fieldErrors?: Array<{ field: string; message: string }>;
|
||||
errorsList?: string[];
|
||||
fieldErrors?: Array<{ field: string; message: string }>;
|
||||
technicalDetails?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts camelCase or PascalCase field names to human-readable titles.
|
||||
* e.g. "HashId" -> "Resource Identifier", "startDate" -> "Start Date"
|
||||
*/
|
||||
function humanizeFieldName(field: string): string {
|
||||
if (!field) return 'Field';
|
||||
|
||||
const customMappings: Record<string, string> = {
|
||||
hashid: 'Resource Identifier (HashId)',
|
||||
id: 'Record ID',
|
||||
sub: 'User Identity',
|
||||
orgid: 'Organisation',
|
||||
organisationid: 'Organisation',
|
||||
portfolioid: 'Portfolio',
|
||||
programmeid: 'Programme',
|
||||
projectid: 'Project',
|
||||
grantid: 'Award',
|
||||
awardid: 'Award',
|
||||
contractid: 'Contract',
|
||||
budgetid: 'Budget',
|
||||
disbursementid: 'Disbursement',
|
||||
indicatorid: 'Indicator',
|
||||
riskid: 'Risk',
|
||||
email: 'Email Address',
|
||||
name: 'Name',
|
||||
title: 'Title',
|
||||
code: 'Reference Code',
|
||||
startdate: 'Start Date',
|
||||
enddate: 'End Date',
|
||||
targetdate: 'Target Date',
|
||||
actualdate: 'Actual Date',
|
||||
amount: 'Amount',
|
||||
budgetamount: 'Budget Amount',
|
||||
targetvalue: 'Target Value',
|
||||
actualvalue: 'Actual Value',
|
||||
baselinevalue: 'Baseline Value',
|
||||
status: 'Status',
|
||||
governancestatus: 'Governance Status',
|
||||
contractstatus: 'Contract Status',
|
||||
disbursementstatus: 'Disbursement Status',
|
||||
};
|
||||
|
||||
const lower = field.toLowerCase().replace(/[^a-z0-9]/g, '');
|
||||
if (customMappings[lower]) {
|
||||
return customMappings[lower];
|
||||
}
|
||||
|
||||
// Insert space before uppercase letters
|
||||
const spaced = field.replace(/([a-z])([A-Z])/g, '$1 $2');
|
||||
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
|
||||
export interface ProblemDetailsResponse {
|
||||
type?: string | null;
|
||||
title?: string | null;
|
||||
status?: number | null;
|
||||
detail?: string | null;
|
||||
instance?: string | null;
|
||||
errors?: Record<string, string[] | string> | any;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,7 +39,6 @@ function humanizeFieldName(field: string): string {
|
||||
*/
|
||||
export function isRawHttpOutput(text: string): boolean {
|
||||
if (!text || typeof text !== 'string') return false;
|
||||
// If it has multiple header-like lines or standard HTTP request/response signatures
|
||||
const headerPatterns = [
|
||||
/^(?:accept|authorization|content-type|content-length|origin|referer|user-agent|sec-ch-ua|sec-fetch|x-forwarded|x-real-ip|host):/im,
|
||||
/(?:authorization:\s*bearer|content-type:\s*application\/)/i,
|
||||
@@ -114,10 +72,9 @@ export function sanitizeTechnicalDetails(text: string): string {
|
||||
/**
|
||||
* Cleans technical jargon, stack traces, and framework namespaces from text strings.
|
||||
*/
|
||||
function cleanTechnicalJargon(text: string): string {
|
||||
export function cleanTechnicalJargon(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
// If text is a raw HTTP dump, do not return it as clean user text
|
||||
if (isRawHttpOutput(text)) {
|
||||
return '';
|
||||
}
|
||||
@@ -132,7 +89,7 @@ function cleanTechnicalJargon(text: string): string {
|
||||
.replace(/at\s+[a-zA-Z0-9_.]+\s*\(.*?\)/g, '')
|
||||
.trim();
|
||||
|
||||
// In this system, ProblemDetails title is always "Error" — ignore trivial or generic "Error" strings
|
||||
// In this system, ProblemDetails title is typically "Error" — ignore trivial or generic "Error" strings
|
||||
if (cleaned.length < 2 || cleaned === '[object Object]' || cleaned.toLowerCase() === 'error') {
|
||||
return '';
|
||||
}
|
||||
@@ -141,7 +98,42 @@ function cleanTechnicalJargon(text: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a response object conforms to RFC 7807 / RFC 9457 ProblemDetails.
|
||||
* Safely inspects whether an error contains a valid response body object.
|
||||
* Note: not all error responses will have a return object, so this method explicitly checks.
|
||||
*/
|
||||
export function inspectErrorResponseObject(error: unknown): ProblemDetailsResponse | null {
|
||||
if (!error) return null;
|
||||
|
||||
// 1. Inspect Axios error or error with response structure
|
||||
const anyErr = error as any;
|
||||
const data = anyErr?.response?.data;
|
||||
if (data && typeof data === 'object') {
|
||||
return data as ProblemDetailsResponse;
|
||||
}
|
||||
// Check if response body is a JSON string
|
||||
if (typeof data === 'string' && data.trim().startsWith('{')) {
|
||||
try {
|
||||
const parsed = JSON.parse(data.trim());
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
return parsed as ProblemDetailsResponse;
|
||||
}
|
||||
} catch {
|
||||
// Not a JSON object
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Direct object inspection (e.g. if response.data was passed directly)
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
if ('detail' in anyErr || 'status' in anyErr || 'title' in anyErr || 'type' in anyErr) {
|
||||
return anyErr as ProblemDetailsResponse;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a response object conforms to RFC 7807 / RFC 9110 ProblemDetails.
|
||||
*/
|
||||
export function isProblemDetailsObject(data: any): data is ProblemDetails {
|
||||
if (!data || typeof data !== 'object') return false;
|
||||
@@ -151,11 +143,35 @@ export function isProblemDetailsObject(data: any): data is ProblemDetails {
|
||||
'status' in data ||
|
||||
'type' in data ||
|
||||
'errors' in data ||
|
||||
'Errors' in data ||
|
||||
'errorList' in data
|
||||
'Errors' in data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a comma-delimited error list from the `detail` property.
|
||||
* e.g. "Failed to update organisation" -> ["Failed to update organisation"]
|
||||
* e.g. "Invalid organisation name, Budget exceeds ceiling, Target date cannot be in past"
|
||||
* -> ["Invalid organisation name", "Budget exceeds ceiling", "Target date cannot be in past"]
|
||||
*/
|
||||
export function parseCommaDelimitedErrors(detail?: string | null): string[] {
|
||||
if (!detail || typeof detail !== 'string') return [];
|
||||
const trimmed = detail.trim();
|
||||
if (!trimmed) return [];
|
||||
|
||||
// Split by comma
|
||||
const rawParts = trimmed.split(',');
|
||||
const errors: string[] = [];
|
||||
|
||||
for (const part of rawParts) {
|
||||
const cleaned = cleanTechnicalJargon(part.trim());
|
||||
if (cleaned && cleaned.length > 0) {
|
||||
errors.push(cleaned);
|
||||
}
|
||||
}
|
||||
|
||||
return errors.length > 0 ? errors : [trimmed];
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects whether the user action context was an entity creation or an update.
|
||||
*/
|
||||
@@ -204,81 +220,46 @@ function detectOperationIntent(
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts and formats human-friendly error guidance from any error,
|
||||
* accessing the ProblemDetails `errors` array directly to display the error.
|
||||
* In this system the title field is always "Error", so no generic error testing is performed.
|
||||
* Comprehensive common error handling function across the application.
|
||||
*
|
||||
* Handles standard RFC 9110 / RFC 7807 error responses:
|
||||
* {
|
||||
* "type": "https://tools.ietf.org/html/rfc9110#section-15.5.1",
|
||||
* "title": "Error",
|
||||
* "status": 400,
|
||||
* "detail": "Failed to update organisation"
|
||||
* }
|
||||
*
|
||||
* - Parses comma-delimited error list in the `detail` property.
|
||||
* - Handles authentication (401) and forbidden (403) specifically.
|
||||
* - Explicitly inspects whether a return object exists, handling responses without return objects gracefully.
|
||||
*/
|
||||
export function parseFriendlyError(
|
||||
error: unknown,
|
||||
fallbackActionTitle = 'Action Failed'
|
||||
): FriendlyError {
|
||||
let statusCode: number | undefined;
|
||||
let rawTitle: string | undefined;
|
||||
let rawDetail: string | undefined;
|
||||
let rawErrors: any = undefined;
|
||||
let isProblemDetails = false;
|
||||
let rawResponseData: any = null;
|
||||
let technicalSummary = '';
|
||||
let httpMethod = '';
|
||||
let requestUrl = '';
|
||||
|
||||
// 1. Inspect Axios error structure
|
||||
// 1. Inspect error metadata if available
|
||||
const anyErr = error as any;
|
||||
if (axios.isAxiosError(error)) {
|
||||
const axiosErr = error as AxiosError<any>;
|
||||
statusCode = axiosErr.response?.status;
|
||||
rawResponseData = axiosErr.response?.data;
|
||||
httpMethod = axiosErr.config?.method?.toUpperCase() || '';
|
||||
requestUrl = axiosErr.config?.url || '';
|
||||
technicalSummary = `${httpMethod || 'HTTP'} ${requestUrl} -> ${statusCode || axiosErr.code || 'ERR'}`;
|
||||
|
||||
if (rawResponseData && typeof rawResponseData === 'object') {
|
||||
const dataObj = rawResponseData as any;
|
||||
if (isProblemDetailsObject(dataObj)) {
|
||||
isProblemDetails = true;
|
||||
rawTitle = dataObj.title || dataObj.Title || undefined;
|
||||
rawDetail = dataObj.detail || dataObj.Detail || undefined;
|
||||
statusCode = dataObj.status || dataObj.Status || statusCode;
|
||||
rawErrors = dataObj.errors ?? dataObj.Errors ?? dataObj.errorList ?? dataObj.ErrorList;
|
||||
} else if (dataObj.message || dataObj.error || dataObj.errors || dataObj.Errors) {
|
||||
rawDetail = dataObj.message || dataObj.error;
|
||||
rawErrors = dataObj.errors ?? dataObj.Errors;
|
||||
} else if (Array.isArray(dataObj)) {
|
||||
rawErrors = dataObj;
|
||||
}
|
||||
} else if (typeof rawResponseData === 'string') {
|
||||
if (isRawHttpOutput(rawResponseData)) {
|
||||
technicalSummary = sanitizeTechnicalDetails(rawResponseData);
|
||||
rawDetail = undefined;
|
||||
} else {
|
||||
// Check if response data is stringified JSON containing ProblemDetails
|
||||
try {
|
||||
const parsed = JSON.parse(rawResponseData) as any;
|
||||
if (isProblemDetailsObject(parsed)) {
|
||||
isProblemDetails = true;
|
||||
rawTitle = parsed.title || parsed.Title || undefined;
|
||||
rawDetail = parsed.detail || parsed.Detail || undefined;
|
||||
statusCode = parsed.status || parsed.Status || statusCode;
|
||||
rawErrors = parsed.errors ?? parsed.Errors ?? parsed.errorList ?? parsed.ErrorList;
|
||||
} else if (Array.isArray(parsed)) {
|
||||
rawErrors = parsed;
|
||||
} else {
|
||||
rawDetail = cleanTechnicalJargon(rawResponseData);
|
||||
}
|
||||
} catch {
|
||||
if (!rawResponseData.includes('<html')) {
|
||||
rawDetail = cleanTechnicalJargon(rawResponseData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Network disconnection or request timeout (no response received)
|
||||
// Network disconnection or request timeout (no response received at all)
|
||||
if (!axiosErr.response) {
|
||||
if (axiosErr.code === 'ECONNABORTED' || axiosErr.message.includes('timeout')) {
|
||||
return {
|
||||
title: 'Request Timed Out',
|
||||
message: 'The server took too long to respond. Please check your network and try again.',
|
||||
message: 'The server took too long to respond. Please check your network connection and try again.',
|
||||
statusCode: 408,
|
||||
hasReturnObject: false,
|
||||
isProblemDetails: false,
|
||||
category: 'server',
|
||||
categoryLabel: 'Timeout',
|
||||
@@ -289,244 +270,247 @@ export function parseFriendlyError(
|
||||
title: 'Connection Unavailable',
|
||||
message: 'Unable to reach the system service. Please verify your internet connection or try again in a few moments.',
|
||||
statusCode: 0,
|
||||
hasReturnObject: false,
|
||||
isProblemDetails: false,
|
||||
category: 'server',
|
||||
categoryLabel: 'Network',
|
||||
technicalDetails: technicalSummary || axiosErr.message,
|
||||
};
|
||||
}
|
||||
} else if (error && typeof error === 'object') {
|
||||
// 2. Direct ProblemDetails object or custom error object
|
||||
const obj = error as any;
|
||||
if (isProblemDetailsObject(obj)) {
|
||||
isProblemDetails = true;
|
||||
rawTitle = obj.title || obj.Title || undefined;
|
||||
rawDetail = obj.detail || obj.Detail || undefined;
|
||||
statusCode = obj.status || obj.Status || undefined;
|
||||
rawErrors = obj.errors ?? obj.Errors ?? obj.errorList ?? obj.ErrorList;
|
||||
} else if (obj.response?.data && isProblemDetailsObject(obj.response.data)) {
|
||||
isProblemDetails = true;
|
||||
const respData = obj.response.data as any;
|
||||
rawTitle = respData.title || respData.Title || undefined;
|
||||
rawDetail = respData.detail || respData.Detail || undefined;
|
||||
statusCode = obj.response.status || respData.status || respData.Status;
|
||||
rawErrors = respData.errors ?? respData.Errors ?? respData.errorList ?? respData.ErrorList;
|
||||
} else if (Array.isArray(obj)) {
|
||||
rawErrors = obj;
|
||||
} else if (obj.message) {
|
||||
rawDetail = obj.message;
|
||||
const statusMatch = obj.message.match(/status code (\d{3})/i);
|
||||
if (statusMatch) {
|
||||
statusCode = parseInt(statusMatch[1], 10);
|
||||
}
|
||||
} else if (anyErr && typeof anyErr === 'object') {
|
||||
statusCode = anyErr.response?.status ?? anyErr.status;
|
||||
httpMethod = anyErr.config?.method?.toUpperCase() || '';
|
||||
requestUrl = anyErr.config?.url || '';
|
||||
if (statusCode) {
|
||||
technicalSummary = `HTTP ${statusCode}`;
|
||||
}
|
||||
} else if (typeof error === 'string') {
|
||||
// 3. String error
|
||||
const statusMatch = error.match(/status code (\d{3})/i);
|
||||
if (statusMatch) {
|
||||
statusCode = parseInt(statusMatch[1], 10);
|
||||
}
|
||||
rawDetail = cleanTechnicalJargon(error);
|
||||
}
|
||||
|
||||
// Extract errors list directly from ProblemDetails errors array / dictionary
|
||||
const fieldErrors: Array<{ field: string; message: string }> = [];
|
||||
const errorsList: string[] = [];
|
||||
// 2. Inspect for returned response object
|
||||
const returnObject = inspectErrorResponseObject(error);
|
||||
const hasReturnObject = returnObject !== null;
|
||||
|
||||
if (rawErrors) {
|
||||
if (Array.isArray(rawErrors)) {
|
||||
rawErrors.forEach((item) => {
|
||||
if (typeof item === 'string' && item.trim()) {
|
||||
const cleaned = cleanTechnicalJargon(item.trim());
|
||||
if (cleaned) errorsList.push(cleaned);
|
||||
} else if (item && typeof item === 'object') {
|
||||
const msg =
|
||||
item.message ||
|
||||
item.errorMessage ||
|
||||
item.detail ||
|
||||
item.error ||
|
||||
item.description ||
|
||||
JSON.stringify(item);
|
||||
const cleaned = cleanTechnicalJargon(String(msg));
|
||||
if (cleaned) errorsList.push(cleaned);
|
||||
}
|
||||
});
|
||||
} else if (typeof rawErrors === 'object') {
|
||||
for (const [key, val] of Object.entries(rawErrors)) {
|
||||
const isNumericKey = /^\d+$/.test(key);
|
||||
const fieldTitle = isNumericKey ? '' : humanizeFieldName(key);
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((msg) => {
|
||||
if (msg) {
|
||||
const cleaned = cleanTechnicalJargon(String(msg));
|
||||
if (cleaned) {
|
||||
if (fieldTitle && !cleaned.toLowerCase().includes(fieldTitle.toLowerCase())) {
|
||||
fieldErrors.push({ field: fieldTitle, message: cleaned });
|
||||
errorsList.push(`${fieldTitle}: ${cleaned}`);
|
||||
} else {
|
||||
errorsList.push(cleaned);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
} else if (typeof val === 'string' && val.trim()) {
|
||||
const cleaned = cleanTechnicalJargon(val.trim());
|
||||
if (cleaned) {
|
||||
if (fieldTitle && !cleaned.toLowerCase().includes(fieldTitle.toLowerCase())) {
|
||||
fieldErrors.push({ field: fieldTitle, message: cleaned });
|
||||
errorsList.push(`${fieldTitle}: ${cleaned}`);
|
||||
} else {
|
||||
errorsList.push(cleaned);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof rawErrors === 'string' && rawErrors.trim()) {
|
||||
const cleaned = cleanTechnicalJargon(rawErrors.trim());
|
||||
if (cleaned) errorsList.push(cleaned);
|
||||
if (returnObject) {
|
||||
if (returnObject.status && typeof returnObject.status === 'number') {
|
||||
statusCode = returnObject.status;
|
||||
}
|
||||
}
|
||||
|
||||
// Detect operation intent (creation, update, deletion, general)
|
||||
const operationIntent = detectOperationIntent(httpMethod, fallbackActionTitle, requestUrl);
|
||||
|
||||
let friendlyTitle = fallbackActionTitle;
|
||||
let friendlyMessage = '';
|
||||
let category: ErrorCategory = 'general';
|
||||
let categoryLabel: string | undefined = undefined;
|
||||
// 3. Special handling for Auth (401) and Forbid (403)
|
||||
if (statusCode === 401) {
|
||||
const rawDetail = returnObject?.detail || (error as any)?.message;
|
||||
const cleanDetail = cleanTechnicalJargon(rawDetail || '');
|
||||
return {
|
||||
title: 'Authentication Required',
|
||||
message: cleanDetail || 'Your authentication session has expired or is unauthorized (HTTP 401). Please sign in again.',
|
||||
statusCode: 401,
|
||||
hasReturnObject,
|
||||
isProblemDetails: hasReturnObject,
|
||||
category: 'security',
|
||||
categoryLabel: 'Unauthorized',
|
||||
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : 'HTTP 401 Unauthorized',
|
||||
};
|
||||
}
|
||||
|
||||
const cleanDetail = cleanTechnicalJargon(rawDetail || '');
|
||||
if (statusCode === 403) {
|
||||
const rawDetail = returnObject?.detail || (error as any)?.message;
|
||||
const detailErrors = parseCommaDelimitedErrors(rawDetail);
|
||||
const friendlyMessage =
|
||||
detailErrors.length > 0
|
||||
? detailErrors.length === 1
|
||||
? detailErrors[0]
|
||||
: detailErrors.map((err) => `• ${err}`).join('\n')
|
||||
: 'You do not have permission to access this resource or perform this action (HTTP 403).';
|
||||
|
||||
// In this system, ProblemDetails allows access to the errors array to display the error directly if available
|
||||
if (errorsList.length > 0) {
|
||||
category = 'validation';
|
||||
categoryLabel =
|
||||
operationIntent === 'creation'
|
||||
? 'Validation'
|
||||
: operationIntent === 'update'
|
||||
? 'Validation'
|
||||
: 'Validation';
|
||||
return {
|
||||
title: fallbackActionTitle !== 'Action Failed' ? fallbackActionTitle : 'Access Forbidden',
|
||||
message: friendlyMessage,
|
||||
statusCode: 403,
|
||||
hasReturnObject,
|
||||
isProblemDetails: hasReturnObject,
|
||||
category: 'security',
|
||||
categoryLabel: 'Forbidden',
|
||||
errorsList: detailErrors.length > 0 ? detailErrors : undefined,
|
||||
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : 'HTTP 403 Forbidden',
|
||||
};
|
||||
}
|
||||
|
||||
// Display the error directly
|
||||
if (errorsList.length === 1) {
|
||||
friendlyMessage = errorsList[0];
|
||||
} else {
|
||||
friendlyMessage = errorsList.map((err) => `• ${err}`).join('\n');
|
||||
// 4. Case A: Response has a valid return object (e.g. RFC 9110 ProblemDetails)
|
||||
if (hasReturnObject && returnObject) {
|
||||
const rawDetail =
|
||||
returnObject.detail ??
|
||||
returnObject.Detail ??
|
||||
returnObject.message ??
|
||||
returnObject.error;
|
||||
|
||||
// Parse comma-delimited errors from detail property
|
||||
const errorsList = parseCommaDelimitedErrors(rawDetail);
|
||||
|
||||
// If detail was empty, check if errors array/dictionary exists
|
||||
if (errorsList.length === 0 && returnObject.errors) {
|
||||
if (Array.isArray(returnObject.errors)) {
|
||||
returnObject.errors.forEach((e: any) => {
|
||||
const cleaned = cleanTechnicalJargon(typeof e === 'string' ? e : e?.message || JSON.stringify(e));
|
||||
if (cleaned) errorsList.push(cleaned);
|
||||
});
|
||||
} else if (typeof returnObject.errors === 'object') {
|
||||
for (const [key, val] of Object.entries(returnObject.errors)) {
|
||||
if (Array.isArray(val)) {
|
||||
val.forEach((m) => {
|
||||
const cleaned = cleanTechnicalJargon(String(m));
|
||||
if (cleaned) errorsList.push(`${key}: ${cleaned}`);
|
||||
});
|
||||
} else if (typeof val === 'string' && val.trim()) {
|
||||
const cleaned = cleanTechnicalJargon(val);
|
||||
if (cleaned) errorsList.push(`${key}: ${cleaned}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If detail provides extra non-duplicate context, include it
|
||||
if (cleanDetail && !errorsList.some((e) => e.toLowerCase() === cleanDetail.toLowerCase())) {
|
||||
friendlyMessage = `${cleanDetail}\n\n${friendlyMessage}`;
|
||||
}
|
||||
} else if (cleanDetail) {
|
||||
// If no errors array, but detail is available, display detail directly
|
||||
friendlyMessage = cleanDetail;
|
||||
if (statusCode === 403) {
|
||||
category = 'security';
|
||||
categoryLabel = 'Access Denied';
|
||||
let category: ErrorCategory = 'general';
|
||||
let categoryLabel: string = 'Error';
|
||||
|
||||
if (statusCode === 400) {
|
||||
if (operationIntent === 'creation') {
|
||||
category = 'creation';
|
||||
categoryLabel = 'Creation Error';
|
||||
} else if (operationIntent === 'update') {
|
||||
category = 'update';
|
||||
categoryLabel = 'Update Error';
|
||||
} else {
|
||||
category = 'validation';
|
||||
categoryLabel = 'Validation Error';
|
||||
}
|
||||
} else if (statusCode === 404) {
|
||||
category = 'notFound';
|
||||
categoryLabel = 'Not Found';
|
||||
} else if (statusCode === 409) {
|
||||
category = 'conflict';
|
||||
categoryLabel = 'Conflict';
|
||||
} else if (statusCode === 422) {
|
||||
category = 'validation';
|
||||
categoryLabel = 'Validation Error';
|
||||
} else if (statusCode && statusCode >= 500) {
|
||||
category = 'server';
|
||||
categoryLabel = 'Service Issue';
|
||||
categoryLabel = 'Server Error';
|
||||
}
|
||||
} else {
|
||||
// Fallback status-specific messaging when neither errors array nor detail is available
|
||||
switch (statusCode) {
|
||||
case 403:
|
||||
category = 'security';
|
||||
categoryLabel = 'Invalid Identifier / Forbid';
|
||||
friendlyTitle = 'Access Not Permitted or Invalid Identifier';
|
||||
friendlyMessage =
|
||||
'The reference identifier (hash ID) is invalid, expired, or you do not have permission to access this item. Please verify your selection or contact your system administrator.';
|
||||
break;
|
||||
|
||||
case 404:
|
||||
category = 'notFound';
|
||||
categoryLabel = 'Record Not Found';
|
||||
friendlyTitle = 'Record Not Found';
|
||||
friendlyMessage =
|
||||
'The requested record could not be found. It may have been archived, removed, or the link is outdated.';
|
||||
break;
|
||||
|
||||
case 400:
|
||||
if (operationIntent === 'creation') {
|
||||
category = 'creation';
|
||||
categoryLabel = 'Creation Error';
|
||||
friendlyMessage =
|
||||
'The new record could not be created. Please review the input parameters and prerequisites.';
|
||||
} else if (operationIntent === 'update') {
|
||||
category = 'update';
|
||||
categoryLabel = 'Update Error';
|
||||
friendlyMessage =
|
||||
'The record could not be updated. It may be locked or the requested status transition is not permitted.';
|
||||
} else {
|
||||
category = 'general';
|
||||
categoryLabel = 'Request Error';
|
||||
friendlyMessage =
|
||||
'The request could not be processed due to invalid parameters or business rule requirements.';
|
||||
}
|
||||
break;
|
||||
|
||||
case 409:
|
||||
category = 'conflict';
|
||||
categoryLabel = 'Conflict';
|
||||
friendlyTitle = 'Update Conflict';
|
||||
friendlyMessage =
|
||||
'This action conflicts with the current status of the record. Please refresh the page and try again.';
|
||||
break;
|
||||
|
||||
case 422:
|
||||
category = 'validation';
|
||||
categoryLabel = 'Incomplete Data';
|
||||
friendlyMessage =
|
||||
'The submitted information could not be processed. Please verify that all required fields are complete.';
|
||||
break;
|
||||
|
||||
case 429:
|
||||
category = 'server';
|
||||
categoryLabel = 'Rate Limit';
|
||||
friendlyTitle = 'System Busy';
|
||||
friendlyMessage =
|
||||
'The system received too many requests in a short time. Please pause for a moment before trying again.';
|
||||
break;
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
category = 'server';
|
||||
categoryLabel = 'Service Issue';
|
||||
friendlyTitle = 'Service Temporarily Unavailable';
|
||||
friendlyMessage =
|
||||
'The central service encountered an unexpected issue while processing your request. Please try again in a few moments.';
|
||||
break;
|
||||
|
||||
default:
|
||||
friendlyMessage =
|
||||
'An unexpected issue occurred while processing this request. Please try again or contact support.';
|
||||
break;
|
||||
// Format display message from errorsList
|
||||
let friendlyMessage = '';
|
||||
if (errorsList.length > 1) {
|
||||
friendlyMessage = errorsList.map((err) => `• ${err}`).join('\n');
|
||||
} else if (errorsList.length === 1) {
|
||||
friendlyMessage = errorsList[0];
|
||||
} else {
|
||||
// Fallback message if detail was empty string
|
||||
friendlyMessage =
|
||||
operationIntent === 'creation'
|
||||
? 'Failed to create record. Please verify the submitted parameters.'
|
||||
: operationIntent === 'update'
|
||||
? 'Failed to update record. Please verify the submitted parameters.'
|
||||
: 'Request could not be processed due to invalid parameters.';
|
||||
}
|
||||
|
||||
// Use contextual action title (or returnObject.title if not "Error")
|
||||
let displayTitle = fallbackActionTitle;
|
||||
if (displayTitle === 'Action Failed' && returnObject.title && returnObject.title.toLowerCase() !== 'error') {
|
||||
displayTitle = returnObject.title;
|
||||
}
|
||||
|
||||
return {
|
||||
title: displayTitle,
|
||||
message: friendlyMessage,
|
||||
statusCode,
|
||||
hasReturnObject: true,
|
||||
isProblemDetails: true,
|
||||
category,
|
||||
categoryLabel,
|
||||
errorsList: errorsList.length > 0 ? errorsList : undefined,
|
||||
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : (statusCode ? `HTTP Status ${statusCode}` : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
// Ensure title is friendly and not an uncaught technical string
|
||||
if (friendlyTitle.includes('AxiosError') || friendlyTitle.includes('status code')) {
|
||||
friendlyTitle = fallbackActionTitle;
|
||||
// 5. Case B: Error response DOES NOT have a return object (e.g. empty response, network, non-JSON 500/502/503)
|
||||
let title = fallbackActionTitle || 'Action Failed';
|
||||
let message = '';
|
||||
let category: ErrorCategory = 'general';
|
||||
let categoryLabel: string = 'Error';
|
||||
|
||||
switch (statusCode) {
|
||||
case 400:
|
||||
category = operationIntent === 'creation' ? 'creation' : operationIntent === 'update' ? 'update' : 'validation';
|
||||
categoryLabel = 'Bad Request';
|
||||
message =
|
||||
operationIntent === 'creation'
|
||||
? 'The new record could not be created. Please review the submitted parameters.'
|
||||
: operationIntent === 'update'
|
||||
? 'The record could not be updated. Please verify the submitted parameters.'
|
||||
: 'The request could not be processed due to invalid parameters.';
|
||||
break;
|
||||
|
||||
case 404:
|
||||
title = 'Record Not Found';
|
||||
category = 'notFound';
|
||||
categoryLabel = 'Not Found';
|
||||
message = 'The requested resource could not be found (HTTP 404). It may have been archived, removed, or the link is outdated.';
|
||||
break;
|
||||
|
||||
case 409:
|
||||
title = 'Resource Conflict';
|
||||
category = 'conflict';
|
||||
categoryLabel = 'Conflict';
|
||||
message = 'This operation conflicts with the current status of the resource (HTTP 409). Please refresh the page and try again.';
|
||||
break;
|
||||
|
||||
case 422:
|
||||
category = 'validation';
|
||||
categoryLabel = 'Incomplete Data';
|
||||
message = 'The submitted information could not be processed by the server (HTTP 422). Please verify all required fields.';
|
||||
break;
|
||||
|
||||
case 429:
|
||||
title = 'Rate Limit Exceeded';
|
||||
category = 'server';
|
||||
categoryLabel = 'Rate Limit';
|
||||
message = 'The system received too many requests in a short time (HTTP 429). Please pause for a moment before trying again.';
|
||||
break;
|
||||
|
||||
case 500:
|
||||
case 502:
|
||||
case 503:
|
||||
case 504:
|
||||
title = 'Service Temporarily Unavailable';
|
||||
category = 'server';
|
||||
categoryLabel = 'Service Issue';
|
||||
message = `The server encountered an error while processing your request (HTTP ${statusCode}). Please try again in a few moments.`;
|
||||
break;
|
||||
|
||||
default:
|
||||
if (typeof error === 'string' && error.trim()) {
|
||||
message = cleanTechnicalJargon(error);
|
||||
} else if (error instanceof Error && error.message) {
|
||||
message = cleanTechnicalJargon(error.message);
|
||||
}
|
||||
if (!message) {
|
||||
message = statusCode
|
||||
? `An unexpected server issue occurred (HTTP ${statusCode}). Please try again or contact support.`
|
||||
: 'An unexpected issue occurred while processing this request. Please try again.';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
title: friendlyTitle,
|
||||
message: friendlyMessage,
|
||||
title,
|
||||
message,
|
||||
statusCode,
|
||||
isProblemDetails,
|
||||
hasReturnObject: false,
|
||||
isProblemDetails: false,
|
||||
category,
|
||||
categoryLabel,
|
||||
fieldErrors: fieldErrors.length > 0 ? fieldErrors : undefined,
|
||||
errorsList: errorsList.length > 0 ? errorsList : undefined,
|
||||
technicalDetails: technicalSummary ? sanitizeTechnicalDetails(technicalSummary) : (statusCode ? `HTTP Status ${statusCode}` : undefined),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user