Oliver-ai-bot_2.0/frontend/lib/api-client.ts
Vadym Samoilenko 778b63506b fix: SharePoint download, tool_call_id=None, 204 JSON parse
- sharepoint_browse.py: replace @microsoft.graph.downloadUrl in $select
  with direct /content endpoint download (MS Graph OData annotation not
  returned when $select is used — use /drives/{id}/items/{id}/content
  with follow_redirects=True instead)
- llm.py: fix tool_call_id=None crash — streaming delta chunks have
  id=None in subsequent chunks; use `or` instead of .get(default) which
  doesn't handle explicit None values; deduplicate tool calls by id to
  avoid duplicate execution; skip incomplete delta entries with empty name
- api-client.ts: handle 204 No Content in handleResponse — return null
  instead of calling response.json() on empty body (was crashing on
  DELETE region/department endpoints)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 21:37:43 +00:00

141 lines
4.7 KiB
TypeScript

const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:1222/api/v1';
class ApiClient {
private baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
private getHeaders(): Record<string, string> {
const headers: Record<string, string> = {
'Content-Type': 'application/json',
};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('access_token');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
}
return headers;
}
private async handleResponse<T>(response: Response, retry?: () => Promise<Response>): Promise<T> {
if (response.status === 401) {
// Try refresh
const refreshed = await this.refreshToken();
if (!refreshed) {
if (typeof window !== 'undefined') {
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
// Clear Zustand persisted auth state so ProtectedRoute sees logout
localStorage.removeItem('auth-storage');
window.location.href = '/login';
}
throw new Error('Session expired');
}
if (retry) {
return this.handleResponse<T>(await retry());
}
throw new Error('Token refreshed, please retry');
}
if (!response.ok) {
const body = await response.json().catch(() => ({}));
throw new Error(body.detail || `Request failed: ${response.status}`);
}
// 204 No Content — nothing to parse
if (response.status === 204) return null as unknown as T;
return response.json();
}
private async refreshToken(): Promise<boolean> {
const refreshToken = typeof window !== 'undefined' ? localStorage.getItem('refresh_token') : null;
if (!refreshToken) return false;
try {
const res = await fetch(`${this.baseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh_token: refreshToken }),
});
if (!res.ok) return false;
const data = await res.json();
if (typeof window !== 'undefined') {
localStorage.setItem('access_token', data.access_token);
}
return true;
} catch {
return false;
}
}
async get<T>(path: string): Promise<T> {
const url = `${this.baseUrl}${path}`;
const res = await fetch(url, { headers: this.getHeaders() });
return this.handleResponse<T>(res, () => fetch(url, { headers: this.getHeaders() }));
}
async post<T>(path: string, body?: unknown): Promise<T> {
const url = `${this.baseUrl}${path}`;
const getOpts = () => ({
method: 'POST',
headers: this.getHeaders(),
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const res = await fetch(url, getOpts());
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
}
async put<T>(path: string, body?: unknown): Promise<T> {
const url = `${this.baseUrl}${path}`;
const getOpts = () => ({
method: 'PUT',
headers: this.getHeaders(),
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const res = await fetch(url, getOpts());
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
}
async patch<T>(path: string, body?: unknown): Promise<T> {
const url = `${this.baseUrl}${path}`;
const getOpts = () => ({
method: 'PATCH',
headers: this.getHeaders(),
body: body !== undefined ? JSON.stringify(body) : undefined,
});
const res = await fetch(url, getOpts());
return this.handleResponse<T>(res, () => fetch(url, getOpts()));
}
async delete<T>(path: string): Promise<T> {
const url = `${this.baseUrl}${path}`;
const res = await fetch(url, { method: 'DELETE', headers: this.getHeaders() });
return this.handleResponse<T>(res, () => fetch(url, { method: 'DELETE', headers: this.getHeaders() }));
}
async upload<T>(path: string, formData: FormData): Promise<T> {
const url = `${this.baseUrl}${path}`;
const getHeaders = () => {
const headers: Record<string, string> = {};
if (typeof window !== 'undefined') {
const token = localStorage.getItem('access_token');
if (token) headers['Authorization'] = `Bearer ${token}`;
}
return headers;
};
const res = await fetch(url, { method: 'POST', headers: getHeaders(), body: formData });
return this.handleResponse<T>(res, () => fetch(url, { method: 'POST', headers: getHeaders(), body: formData }));
}
getStreamUrl(path: string): string {
return `${this.baseUrl}${path}`;
}
getAuthHeaders(): Record<string, string> {
return this.getHeaders();
}
}
const apiClient = new ApiClient(API_URL);
export default apiClient;