Fixed API calls on governance page

Enforced multitenancy
This commit is contained in:
2026-09-26 10:28:43 +02:00
parent b2688867f3
commit f3a8f78144
16 changed files with 3552 additions and 503 deletions
@@ -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),
};
}