Merge pull request #69 from christianlouis/copilot/fix-cors-policy-error
fix: replace build-time NEXT_PUBLIC_API_URL with runtime proxy for backend API calls
This commit is contained in:
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- 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
|
- 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"
|
- 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"
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
- NEXT_PUBLIC_API_URL=http://backend:8000
|
- BACKEND_URL=http://backend:8000
|
||||||
depends_on:
|
depends_on:
|
||||||
- backend
|
- backend
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ Comprehensive task breakdown for repository improvements and production readines
|
|||||||
- [x] Remove CodeQL checks from CI (was blocking builds)
|
- [x] Remove CodeQL checks from CI (was blocking builds)
|
||||||
- [x] Upgrade SQLAlchemy to 2.0.48 to fix Python 3.14 test failures
|
- [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 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 🔨
|
### In Progress 🔨
|
||||||
- [ ] Configure branch protection rules
|
- [ ] Configure branch protection rules
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -1,10 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
|
|
||||||
const API_BASE_URL =
|
|
||||||
process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
|
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: `${API_BASE_URL}/api/v1`,
|
baseURL: `/api/v1`,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user