From 76bf0b3f0c753c6d3bc2f19f3639c9bdd4184a4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:16:01 +0000 Subject: [PATCH] fix: replace NEXT_PUBLIC_API_URL with Next.js Route Handler proxy for runtime backend URL Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com> Agent-Logs-Url: https://github.com/christianlouis/pop_puller_to_gmail/sessions/59e2f604-9ad3-46ef-8670-fd6b3b7c14b2 --- CHANGELOG.md | 1 + docker-compose.new.yml | 2 +- docs/TODO.md | 1 + frontend/src/app/api/v1/[...path]/route.ts | 43 ++++++++++++++++++++++ frontend/src/lib/api.ts | 5 +-- 5 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 frontend/src/app/api/v1/[...path]/route.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d65a317..cdf54b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed +- Fixed frontend API calls being hardcoded to `http://localhost:8000` in production: `NEXT_PUBLIC_API_URL` is baked into the JavaScript bundle at Next.js build time, so it can never be overridden at container runtime. Replaced the `NEXT_PUBLIC_API_URL` mechanism with a Next.js Route Handler proxy at `/api/v1/[...path]` that reads `process.env.BACKEND_URL` at server startup and proxies all `/api/v1/*` requests to the real backend. The frontend Axios client now uses a relative base URL (`/api/v1`), which also eliminates the CORS issue since the browser only ever talks to the same-origin Next.js server. Update `BACKEND_URL=http://backend:8000` in `docker-compose.new.yml` (or your deployment env) to point the proxy at your backend. - Fixed infinite spinning wheel on the home page: `authStore` no longer initialises `isLoading` as `true` unconditionally — it is now `false` when no access token exists in `localStorage`, so unauthenticated users see the landing page immediately instead of an endless spinner - Home page now performs an auth check when a token is present in `localStorage`, redirecting authenticated users to the dashboard and clearing stale tokens on failure - Wrapped `useSearchParams()` in a `Suspense` boundary in `frontend/src/app/auth/callback/page.tsx` to fix the Next.js build error: "useSearchParams() should be wrapped in a suspense boundary at page /auth/callback" diff --git a/docker-compose.new.yml b/docker-compose.new.yml index 45d59d0..baf6605 100644 --- a/docker-compose.new.yml +++ b/docker-compose.new.yml @@ -96,7 +96,7 @@ services: ports: - "3000:3000" environment: - - NEXT_PUBLIC_API_URL=http://backend:8000 + - BACKEND_URL=http://backend:8000 depends_on: - backend restart: unless-stopped diff --git a/docs/TODO.md b/docs/TODO.md index 818ad17..4f26870 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -98,6 +98,7 @@ Comprehensive task breakdown for repository improvements and production readines - [x] Remove CodeQL checks from CI (was blocking builds) - [x] Upgrade SQLAlchemy to 2.0.48 to fix Python 3.14 test failures - [x] Fix Docker build failure: wrap `useSearchParams()` in Suspense boundary in `/auth/callback` page +- [x] Fix frontend API URL hardcoded to `localhost:8000` in production: replaced build-time `NEXT_PUBLIC_API_URL` with a runtime Next.js Route Handler proxy (`/api/v1/[...path]`) reading `BACKEND_URL` at server startup ### In Progress 🔨 - [ ] Configure branch protection rules diff --git a/frontend/src/app/api/v1/[...path]/route.ts b/frontend/src/app/api/v1/[...path]/route.ts new file mode 100644 index 0000000..41c7dae --- /dev/null +++ b/frontend/src/app/api/v1/[...path]/route.ts @@ -0,0 +1,43 @@ +import { NextRequest, NextResponse } from "next/server"; + +const BACKEND_URL = process.env.BACKEND_URL ?? "http://localhost:8000"; + +type RouteContext = { params: Promise<{ path: string[] }> }; + +async function handler(request: NextRequest, context: RouteContext) { + const { path } = await context.params; + const targetUrl = `${BACKEND_URL}/api/v1/${path.join("/")}${request.nextUrl.search}`; + + const headers = new Headers(request.headers); + headers.delete("host"); + // Request uncompressed response so the body can be forwarded as-is + headers.set("accept-encoding", "identity"); + + const hasBody = request.method !== "GET" && request.method !== "HEAD"; + const body = hasBody ? await request.arrayBuffer() : undefined; + + try { + const upstream = await fetch(targetUrl, { + method: request.method, + headers, + body, + }); + + return new NextResponse(upstream.body, { + status: upstream.status, + statusText: upstream.statusText, + headers: upstream.headers, + }); + } catch (error) { + console.error("Proxy error forwarding to backend:", error); + return NextResponse.json({ detail: "Backend unavailable" }, { status: 502 }); + } +} + +export const GET = handler; +export const POST = handler; +export const PUT = handler; +export const PATCH = handler; +export const DELETE = handler; +export const HEAD = handler; +export const OPTIONS = handler; diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index d115988..f96230d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -1,10 +1,7 @@ import axios from "axios"; -const API_BASE_URL = - process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; - const api = axios.create({ - baseURL: `${API_BASE_URL}/api/v1`, + baseURL: `/api/v1`, headers: { "Content-Type": "application/json", },