diff --git a/frontend/src/api/frontApis.test.ts b/frontend/src/api/frontApis.test.ts new file mode 100644 index 0000000..ab462ce --- /dev/null +++ b/frontend/src/api/frontApis.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { joinApiUrl } from "./frontApis"; + +describe("joinApiUrl", () => { + it("keeps same-origin paths when base url is slash", () => { + expect(joinApiUrl("/", "/v1/pipelines")).toBe("/v1/pipelines"); + }); + + it("joins absolute host and trims trailing slash", () => { + expect(joinApiUrl("http://127.0.0.1:8500/", "/v1/pipelines")).toBe( + "http://127.0.0.1:8500/v1/pipelines" + ); + }); + + it("accepts path without leading slash", () => { + expect(joinApiUrl("http://127.0.0.1:8500", "v1/pipelines")).toBe( + "http://127.0.0.1:8500/v1/pipelines" + ); + }); +}); + diff --git a/frontend/src/api/frontApis.ts b/frontend/src/api/frontApis.ts index bdc9528..ad60428 100644 --- a/frontend/src/api/frontApis.ts +++ b/frontend/src/api/frontApis.ts @@ -22,6 +22,18 @@ import type { const API_BASE_URL = import.meta.env.VITE_FRONT_API_BASE_URL?.trim() || "http://127.0.0.1:8500"; +export function joinApiUrl(baseUrl: string, path: string): string { + const normalizedPath = path.startsWith("/") ? path : `/${path}`; + const normalizedBase = baseUrl.trim(); + + // "/" is commonly used in Docker+nginx builds and should resolve as same-origin. + if (!normalizedBase || normalizedBase === "/") { + return normalizedPath; + } + + return `${normalizedBase.replace(/\/+$/, "")}${normalizedPath}`; +} + // Log which backend the frontend is targeting on startup, with file + line hint. // This runs once when the module is loaded. // eslint-disable-next-line no-console @@ -30,7 +42,8 @@ console.info( ); async function fetchJson(path: string, init?: RequestInit): Promise { - const response = await fetch(`${API_BASE_URL}${path}`, { + const url = joinApiUrl(API_BASE_URL, path); + const response = await fetch(url, { headers: { "Content-Type": "application/json", ...(init?.headers || {}), @@ -49,7 +62,24 @@ async function fetchJson(path: string, init?: RequestInit): Promise { } throw new Error(message); } - return (await response.json()) as T; + + if (response.status === 204) { + return undefined as T; + } + + const bodyText = await response.text(); + if (!bodyText.trim()) { + return undefined as T; + } + + try { + return JSON.parse(bodyText) as T; + } catch { + const preview = bodyText.slice(0, 160).replace(/\s+/g, " ").trim(); + throw new Error( + `Expected JSON response from ${url}, but received non-JSON content: ${preview || ""}` + ); + } } export function listAvailableGraphs(): Promise { @@ -189,7 +219,10 @@ export async function streamAgentChatResponse( ): Promise { const { appId, sessionId, apiKey, message, onText, signal } = options; const response = await fetch( - `${API_BASE_URL}/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses`, + joinApiUrl( + API_BASE_URL, + `/v1/apps/${encodeURIComponent(appId)}/sessions/${encodeURIComponent(sessionId)}/responses` + ), { method: "POST", headers: { diff --git a/frontend/vite.config.js b/frontend/vite.config.js index a3db408..9af995d 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -4,5 +4,15 @@ export default defineConfig({ plugins: [react()], server: { port: 5173, + proxy: { + "/v1": { + target: "http://127.0.0.1:8500", + changeOrigin: true, + }, + "/apps": { + target: "http://127.0.0.1:8500", + changeOrigin: true, + }, + }, }, }); diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 907927b..645a518 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -5,6 +5,16 @@ export default defineConfig({ plugins: [react()], server: { port: 5173, + proxy: { + "/v1": { + target: "http://127.0.0.1:8500", + changeOrigin: true, + }, + "/apps": { + target: "http://127.0.0.1:8500", + changeOrigin: true, + }, + }, }, });