Files
runners-calendar/frontend/src/api/http.ts
Vaka.pro e0ed0b6435
Some checks failed
CI / build-and-test (pull_request) Has been cancelled
fix: прод — CORS, версия API, ошибки клиента и подсказка по прошедшим стартам
- CORS_ORIGIN: несколько origin через запятую; комментарии в .env.example
- Версия бэкенда: APP_VERSION, безопасное чтение package.json, футер при пустой версии
- Сообщения API: unknown_error и ответы 401/403/404 без JSON; отладочный лог при !ok
- Статус «внесите результат» для прошедшей даты + блок на карточке старта и стили
2026-04-08 01:21:11 +03:00

107 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "Не удалось связаться с сервером.",
});
}