fix(api): дублировать маршруты под /api и убрать Content-Type у GET #16

Merged
admin merged 1 commits from fix/api-prefix-routing into main 2026-04-08 07:21:04 +00:00
5 changed files with 21 additions and 22 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "calendar-run-backend", "name": "calendar-run-backend",
"version": "1.0.0", "version": "1.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",

View File

@@ -12,6 +12,9 @@ export function createApp(): express.Express {
app.use(healthRouter); app.use(healthRouter);
app.use(racesRouter); app.use(racesRouter);
// Тот же API под /api/* — если прокси не снимает префикс или запрос идёт напрямую на порт бэкенда с /api.
app.use("/api", healthRouter);
app.use("/api", racesRouter);
app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => { app.use((err: unknown, _req: Request, res: Response, _next: NextFunction) => {
if (err instanceof SyntaxError && "body" in err) { if (err instanceof SyntaxError && "body" in err) {

View File

@@ -12,6 +12,11 @@ test("GET /health returns ok", async () => {
assert.ok(res.body.version.length > 0); assert.ok(res.body.version.length > 0);
}); });
test("GET /api/health returns ok (prefix without proxy strip)", async () => {
const res = await request(app).get("/api/health").expect(200);
assert.equal(res.body.status, "ok");
});
test("GET /ready succeeds with mock database", async () => { test("GET /ready succeeds with mock database", async () => {
const res = await request(app).get("/ready").expect(200); const res = await request(app).get("/ready").expect(200);
assert.equal(res.body.status, "ready"); assert.equal(res.body.status, "ready");
@@ -34,6 +39,11 @@ test("GET /races accepts year and month", async () => {
assert.ok(Array.isArray(res.body)); assert.ok(Array.isArray(res.body));
}); });
test("GET /api/races mirrors GET /races", async () => {
const res = await request(app).get("/api/races?year=2026&month=5").expect(200);
assert.ok(Array.isArray(res.body));
});
test("GET /races/:id returns not_found", async () => { test("GET /races/:id returns not_found", async () => {
const res = await request(app).get("/races/does-not-exist").expect(404); const res = await request(app).get("/races/does-not-exist").expect(404);
assert.equal(res.body.error, "not_found"); assert.equal(res.body.error, "not_found");

View File

@@ -1,7 +1,7 @@
{ {
"name": "calendar-run-frontend", "name": "calendar-run-frontend",
"private": true, "private": true,
"version": "0.1.0", "version": "0.1.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View File

@@ -35,10 +35,15 @@ export async function requestJson<T>(path: string, init?: RequestInit): Promise<
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
try { try {
const defaultHeaders: Record<string, string> = {};
if (method !== "GET" && method !== "HEAD") {
defaultHeaders["Content-Type"] = "application/json";
}
const response = await fetch(buildUrl(path), { const response = await fetch(buildUrl(path), {
...init, ...init,
headers: { headers: {
"Content-Type": "application/json", ...defaultHeaders,
...(init?.headers ?? {}), ...(init?.headers ?? {}),
}, },
}); });
@@ -55,25 +60,6 @@ export async function requestJson<T>(path: string, init?: RequestInit): Promise<
await delay(80 * attempt); await delay(80 * attempt);
continue; 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); throw toApiError(response.status, payload);
} }