refactor(api): unify /api contract across frontend, nginx, and backend
Some checks failed
CI / build-and-test (pull_request) Has been cancelled

This commit is contained in:
Anton
2026-04-08 11:59:46 +03:00
parent 9f63b190f1
commit 8eaf006906
17 changed files with 103 additions and 81 deletions

View File

@@ -47,7 +47,7 @@ function isGatewayStatus(status: number): boolean {
return status === 502 || status === 503 || status === 504;
}
function hasStructuredApiError(payload: unknown): payload is ApiErrorPayload {
export function isStructuredApiErrorPayload(payload: unknown): payload is ApiErrorPayload {
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
return false;
}
@@ -55,7 +55,7 @@ function hasStructuredApiError(payload: unknown): payload is ApiErrorPayload {
}
export function toApiError(status: number, payload: unknown): ApiError {
if (isGatewayStatus(status) && !hasStructuredApiError(payload)) {
if (isGatewayStatus(status) && !isStructuredApiErrorPayload(payload)) {
return new ApiError({
code: "network_error",
status,
@@ -63,7 +63,7 @@ export function toApiError(status: number, payload: unknown): ApiError {
});
}
if (!hasStructuredApiError(payload) && (status === 401 || status === 403 || status === 404)) {
if (!isStructuredApiErrorPayload(payload) && (status === 401 || status === 403 || status === 404)) {
return new ApiError({
code: "network_error",
status,

View File

@@ -1,10 +1,10 @@
import { ApiError, toApiError } from "./errors";
import { ApiError, isStructuredApiErrorPayload, toApiError } from "./errors";
const API_BASE_URL = (import.meta.env.VITE_API_BASE_URL as string | undefined)?.trim() || "http://localhost:3001";
const API_ROOT = "/api";
function buildUrl(path: string): string {
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
return `${API_BASE_URL}${normalizedPath}`;
return `${API_ROOT}${normalizedPath}`;
}
async function parseResponseBody(response: Response): Promise<unknown> {
@@ -36,6 +36,14 @@ async function parseResponseBody(response: Response): Promise<unknown> {
const GATEWAY_RETRY_STATUSES = new Set([502, 503, 504]);
/** Повтор при «пустом» 404: часто бывает при нескольких инстансах/прокси до полного деплоя. */
function shouldRetryIdempotentError(status: number, payload: unknown): boolean {
if (GATEWAY_RETRY_STATUSES.has(status)) {
return true;
}
return status === 404 && !isStructuredApiErrorPayload(payload);
}
function delay(ms: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, ms);
@@ -69,7 +77,7 @@ export async function requestJson<T>(path: string, init?: RequestInit): Promise<
const payload = await parseResponseBody(response);
if (!response.ok) {
const retryable = idempotent && GATEWAY_RETRY_STATUSES.has(response.status) && attempt < maxAttempts;
const retryable = idempotent && attempt < maxAttempts && shouldRetryIdempotentError(response.status, payload);
if (retryable) {
await delay(80 * attempt);
continue;