Files
dictia-public/static/js/utils/apiClient.js

120 lines
3.5 KiB
JavaScript

/**
* API client utilities for making HTTP requests
*/
class APIError extends Error {
constructor(message, status, data) {
super(message);
this.name = 'APIError';
this.status = status;
this.data = data;
}
}
/**
* Safely parse JSON response, handling HTML error pages gracefully
*/
async function safeJsonParse(response) {
const contentType = response.headers.get('content-type') || '';
// If response is not JSON, extract useful error from HTML
if (!contentType.includes('application/json')) {
const text = await response.text();
// Try to extract error message from HTML title or h1
const titleMatch = text.match(/<title>([^<]+)<\/title>/i);
const h1Match = text.match(/<h1>([^<]+)<\/h1>/i);
const errorMsg = titleMatch?.[1] || h1Match?.[1] ||
`Server returned non-JSON response (status ${response.status})`;
throw new APIError(errorMsg, response.status, { htmlResponse: true });
}
return response.json();
}
export async function apiRequest(url, options = {}) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
...(csrfToken && { 'X-CSRFToken': csrfToken })
}
};
const mergedOptions = {
...defaultOptions,
...options,
headers: {
...defaultOptions.headers,
...options.headers
}
};
try {
const response = await fetch(url, mergedOptions);
const data = await safeJsonParse(response);
if (!response.ok) {
throw new APIError(
data.error || 'Request failed',
response.status,
data
);
}
return data;
} catch (error) {
if (error instanceof APIError) {
throw error;
}
throw new APIError(error.message, 0, null);
}
}
export async function uploadFile(url, file, onProgress = null) {
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content');
const formData = new FormData();
formData.append('audio_file', file);
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
if (onProgress) {
xhr.upload.addEventListener('progress', (e) => {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
onProgress(percentComplete);
}
});
}
xhr.addEventListener('load', () => {
if (xhr.status >= 200 && xhr.status < 300) {
try {
const data = JSON.parse(xhr.responseText);
resolve(data);
} catch (e) {
reject(new Error('Invalid response format'));
}
} else {
try {
const error = JSON.parse(xhr.responseText);
reject(new APIError(error.error || 'Upload failed', xhr.status, error));
} catch (e) {
reject(new APIError('Upload failed', xhr.status, null));
}
}
});
xhr.addEventListener('error', () => {
reject(new Error('Network error'));
});
xhr.open('POST', url);
if (csrfToken) {
xhr.setRequestHeader('X-CSRFToken', csrfToken);
}
xhr.send(formData);
});
}