File size: 3,515 Bytes
eff6797
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import axios from 'axios';

const API_BASE = import.meta.env.VITE_API_URL ?? '';

export const apiClient = axios.create({
  baseURL: API_BASE,
  headers: { 'Content-Type': 'application/json' },
  timeout: 30000,
});

apiClient.interceptors.request.use((config) => {
  if (typeof window !== 'undefined') {
    const token = localStorage.getItem('cfi_token') || sessionStorage.getItem('cfi_token');
    if (token && !config.headers.Authorization) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    const tenantId = localStorage.getItem('cfi_tenant_id') || sessionStorage.getItem('cfi_tenant_id');
    if (tenantId && !config.headers['X-Tenant-ID']) {
      config.headers['X-Tenant-ID'] = tenantId;
    }
  }
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    if (axios.isCancel(error)) {
      // Normal React / TanStack Query query cancellation on unmount or re-render
      return Promise.reject(error);
    }
    const isTest =
      (typeof process !== 'undefined' && process.env?.NODE_ENV === 'test') ||
      (typeof import.meta !== 'undefined' && import.meta.env?.MODE === 'test');
    if (!isTest) {
      if (error.code === 'ECONNABORTED' || error.message?.includes('timeout')) {
        console.warn('[API Warning] Request timed out, using cached/fallback state:', error.config?.url);
      } else {
        console.warn('[API Warning]', error.response?.data ?? error.message);
      }
    }
    return Promise.reject(error);
  },
);

export function getActiveTenantId(): string {
  if (typeof window !== 'undefined') {
    return localStorage.getItem('cfi_tenant_id') || sessionStorage.getItem('cfi_tenant_id') || 'default';
  }
  return 'default';
}

export function setClientTenant(tenantId: string | null): void {
  if (typeof window !== 'undefined') {
    if (tenantId) {
      localStorage.setItem('cfi_tenant_id', tenantId);
    } else {
      localStorage.removeItem('cfi_tenant_id');
      sessionStorage.removeItem('cfi_tenant_id');
    }
  }
}

export function switchActiveTenant(queryClient: { clear: () => void }, newTenantId: string): void {
  setClientTenant(newTenantId);
  queryClient.clear();
}

/**
 * Safely extract and format an API error message into a displayable string.
 * Prevents React Minified Error #31 caused by rendering raw error objects or
 * FastAPI/Pydantic validation error detail arrays: [{ loc, msg, type }].
 */
export function formatApiError(err: unknown, defaultMessage = 'An unexpected error occurred'): string {
  if (!err) return defaultMessage;
  const anyErr = err as any;
  const detail = anyErr?.response?.data?.detail ?? anyErr?.data?.detail;

  if (typeof detail === 'string') {
    return detail.trim() || defaultMessage;
  }

  if (Array.isArray(detail)) {
    const formatted = detail
      .map((item) => {
        if (typeof item === 'string') return item;
        if (item && typeof item === 'object') {
          const locStr = Array.isArray(item.loc) ? item.loc.join('.') : '';
          const msg = item.msg || item.message || JSON.stringify(item);
          return locStr ? `${locStr}: ${msg}` : msg;
        }
        return String(item);
      })
      .filter(Boolean)
      .join('; ');
    return formatted || defaultMessage;
  }

  if (detail && typeof detail === 'object') {
    return (detail as any).msg || (detail as any).message || JSON.stringify(detail);
  }

  if (typeof anyErr?.message === 'string') {
    return anyErr.message;
  }

  return defaultMessage;
}