Some checks failed
CI / build-and-test (pull_request) Has been cancelled
- CORS_ORIGIN: несколько origin через запятую; комментарии в .env.example - Версия бэкенда: APP_VERSION, безопасное чтение package.json, футер при пустой версии - Сообщения API: unknown_error и ответы 401/403/404 без JSON; отладочный лог при !ok - Статус «внесите результат» для прошедшей даты + блок на карточке старта и стили
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import { ApiError, toApiError } from "./errors";
|
||
|
||
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() || "http://localhost:3001";
|
||
|
||
function buildUrl(path: string): string {
|
||
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
||
return `${API_BASE_URL}${normalizedPath}`;
|
||
}
|
||
|
||
async function parseResponseBody(response: Response): Promise<unknown> {
|
||
const contentType = response.headers.get("content-type") ?? "";
|
||
if (!contentType.includes("application/json")) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
return await response.json();
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);
|
||
|
||
function delay(ms: number): Promise<void> {
|
||
return new Promise((resolve) => {
|
||
setTimeout(resolve, ms);
|
||
});
|
||
}
|
||
|
||
export async function requestJson<T>(path: string, init?: RequestInit): Promise<T> {
|
||
const method = (init?.method ?? "GET").toUpperCase();
|
||
const idempotent = method === "GET" || method === "HEAD";
|
||
const maxAttempts = idempotent ? 3 : 1;
|
||
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||
try {
|
||
const response = await fetch(buildUrl(path), {
|
||
...init,
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
...(init?.headers ?? {}),
|
||
},
|
||
});
|
||
|
||
if (response.status === 204) {
|
||
return undefined as T;
|
||
}
|
||
|
||
const payload = await parseResponseBody(response);
|
||
|
||
if (!response.ok) {
|
||
const retryable = idempotent && GATEWAY_RETRY_STATUSES.has(response.status) && attempt < maxAttempts;
|
||
if (retryable) {
|
||
await delay(80 * attempt);
|
||
continue;
|
||
}
|
||
// #region agent log
|
||
fetch("http://127.0.0.1:7488/ingest/a18f912f-72c6-4a58-866b-17810a6b89d2", {
|
||
method: "POST",
|
||
headers: { "Content-Type": "application/json", "X-Debug-Session-Id": "587ee5" },
|
||
body: JSON.stringify({
|
||
sessionId: "587ee5",
|
||
hypothesisId: "H-http-not-ok",
|
||
location: "http.ts:requestJson",
|
||
message: "HTTP error response",
|
||
data: {
|
||
path,
|
||
status: response.status,
|
||
contentType: response.headers.get("content-type"),
|
||
payloadIsObject: payload !== null && typeof payload === "object",
|
||
},
|
||
timestamp: Date.now(),
|
||
}),
|
||
}).catch(() => {});
|
||
// #endregion
|
||
throw toApiError(response.status, payload);
|
||
}
|
||
|
||
return payload as T;
|
||
} catch (error) {
|
||
if (error instanceof ApiError) {
|
||
throw error;
|
||
}
|
||
if (error instanceof DOMException && error.name === "AbortError") {
|
||
throw error;
|
||
}
|
||
const retryable = idempotent && attempt < maxAttempts;
|
||
if (retryable) {
|
||
await delay(80 * attempt);
|
||
continue;
|
||
}
|
||
throw new ApiError({
|
||
code: "network_error",
|
||
status: null,
|
||
message: "Не удалось связаться с сервером.",
|
||
});
|
||
}
|
||
}
|
||
|
||
throw new ApiError({
|
||
code: "network_error",
|
||
status: null,
|
||
message: "Не удалось связаться с сервером.",
|
||
});
|
||
}
|