-
Notifications
You must be signed in to change notification settings - Fork 633
Expand file tree
/
Copy pathapi-client.ts
More file actions
187 lines (169 loc) · 6.04 KB
/
Copy pathapi-client.ts
File metadata and controls
187 lines (169 loc) · 6.04 KB
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
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
import * as Sentry from '@sentry/nextjs';
const API_URL = process.env.NEXT_PUBLIC_API_URL;
export class ApiError extends Error {
status: number;
responseBody: any;
endpoint: string;
constructor(message: string, status: number, responseBody?: any, endpoint?: string) {
super(message);
this.name = 'ApiError';
this.status = status;
this.responseBody = responseBody;
this.endpoint = endpoint || 'unknown';
if (Error.captureStackTrace) {
Error.captureStackTrace(this, ApiError);
}
}
}
/**
* Creates a user-friendly error message based on the error type and status
*/
function getUserFriendlyErrorMessage(error: any, endpoint: string): string {
if (error instanceof ApiError) {
switch (error.status) {
case 401:
return 'Your session has expired. Please sign in again.';
case 403:
return "You don't have permission to access this resource.";
case 404:
return 'The requested resource was not found.';
case 429:
return 'Too many requests. Please wait a moment and try again.';
case 500:
return 'A server error occurred. Our team has been notified.';
case 502:
case 503:
case 504:
return 'The service is temporarily unavailable. Please try again in a few moments.';
default:
return 'An unexpected error occurred. Please try again.';
}
}
// Network or other errors
if (error.message?.includes('fetch')) {
return 'Unable to connect to the server. Please check your internet connection and try again.';
}
return 'An unexpected error occurred. Please try again.';
}
/**
* Reports error to Sentry with additional context
*/
function reportErrorToSentry(error: any, endpoint: string, context: Record<string, any> = {}) {
Sentry.withScope((scope: any) => {
scope.setTag('error_source', 'api_client');
scope.setTag('endpoint', endpoint);
scope.setContext('api_request', {
endpoint,
url: `${API_URL}${endpoint}`,
...context,
});
if (error instanceof ApiError) {
scope.setTag('api_status', error.status);
scope.setLevel('error');
scope.setContext('response_body', error.responseBody);
} else {
scope.setTag('error_type', 'network_error');
scope.setLevel('error');
}
Sentry.captureException(error);
});
}
/**
* Performs an authenticated fetch request to the backend API using a session cookie.
* Handles retrieving the session_id cookie and adding the Authorization header.
*
* @param endpoint - The API endpoint path (e.g., '/opsboard/projects').
* @param options - Optional fetch options (method, body, etc.). Defaults to GET.
* @returns The JSON response data.
* @throws {ApiError} If the API returns an error status or fetch fails.
* @throws {Error} If session_id cookie is not found or API URL is missing.
*/
export async function fetchAuthenticatedApi<T = any>(
endpoint: string,
options: RequestInit = {},
): Promise<T> {
if (!API_URL) {
throw new Error('NEXT_PUBLIC_API_URL environment variable is not set.');
}
const headers = new Headers(options.headers || {});
if (!headers.has('Content-Type') && options.body) {
headers.set('Content-Type', 'application/json');
}
const fetchOptions: RequestInit = {
...options,
headers,
credentials: 'include',
};
const targetUrl = `${API_URL}${endpoint}`;
try {
const response = await fetch(targetUrl, fetchOptions);
if (!response.ok) {
let errorBody;
try {
errorBody = await response.json();
} catch (e) {
try {
errorBody = await response.text();
} catch (readErr) {
errorBody = 'Failed to read error response body';
}
}
// In development, don't log 401 errors as they're expected when not authenticated
const isDevelopment = process.env.NODE_ENV === 'development';
if (response.status !== 401 || !isDevelopment) {
console.error(`[API Client] API Error ${response.status} for ${endpoint}:`, errorBody);
}
throw new ApiError(
`API request failed with status ${response.status}`,
response.status,
errorBody,
endpoint,
);
}
if (response.status === 204) {
return undefined as T;
}
const data: T = await response.json();
return data;
} catch (error) {
if (error instanceof ApiError) {
// Report API errors to Sentry with context
reportErrorToSentry(error, endpoint, {
status: error.status,
responseBody: error.responseBody,
});
// Check for 401 Unauthorized specifically
if (error.status === 401) {
// Ensure this runs only on the client side
if (typeof window !== 'undefined') {
const isDevelopment = process.env.NODE_ENV === 'development';
if (!isDevelopment) {
console.warn(`[API Client] Received 401 Unauthorized for ${endpoint}.`);
}
// Prevent infinite loops if signin page itself triggers a 401 somehow
if (window.location.pathname !== '/signin') {
window.location.href = '/signin';
}
// Throw a specific error to signal the call failed due to auth and redirection occurred.
throw new Error('Unauthorized. Redirecting to signin.');
} else {
// If not client-side (SSR/build), just re-throw the original error.
// The caller might handle server-side redirection if needed.
throw error;
}
}
// Re-throw other ApiErrors with user-friendly message
const userMessage = getUserFriendlyErrorMessage(error, endpoint);
throw new Error(userMessage);
} else {
// Report network/unexpected errors to Sentry
reportErrorToSentry(error, endpoint, {
error_type: 'network_or_unexpected',
original_message: (error as Error).message,
});
console.error(`[API Client] Network or unexpected error for ${endpoint}:`, error);
const userMessage = getUserFriendlyErrorMessage(error, endpoint);
throw new Error(userMessage);
}
}
}