From ffc36edb97035a574852750576c8a4e01bd5ee13 Mon Sep 17 00:00:00 2001 From: MarkLo127 Date: Thu, 12 Mar 2026 11:03:05 +0800 Subject: [PATCH] Fix Railway ECONNREFUSED error by unifying backend URL resolution Problem: Next.js frontend in production mode falls back to http://backend:8000 (Docker Compose internal hostname) when BACKEND_URL env var is not set. In Railway's distributed environment, this hostname doesn't exist, causing ECONNREFUSED errors. Solution: Create unified getBackendUrl() function with consistent fallback priority across all server-side proxying files: 1. Explicit BACKEND_URL (for Railway / custom deployment) 2. Development mode -> http://localhost:8000 3. NEXT_PUBLIC_API_URL (may be set in Railway) 4. Docker Compose default -> http://backend:8000 Changes: - New: frontend/lib/backend-url.ts - centralized URL resolution - Updated: frontend/next.config.ts - use getBackendUrl() - Updated: frontend/app/api/chat/route.ts - use getBackendUrl() - Updated: frontend/app/api/auth/google/token/route.ts - use getBackendUrl() Railway users must set BACKEND_URL=https://.up.railway.app Co-Authored-By: Claude Haiku 4.5 --- frontend/app/api/auth/google/token/route.ts | 9 +++---- frontend/app/api/chat/route.ts | 6 ++--- frontend/lib/backend-url.ts | 27 +++++++++++++++++++++ frontend/next.config.ts | 9 ++----- 4 files changed, 34 insertions(+), 17 deletions(-) create mode 100644 frontend/lib/backend-url.ts diff --git a/frontend/app/api/auth/google/token/route.ts b/frontend/app/api/auth/google/token/route.ts index cf8b66c1..fd39c0ca 100644 --- a/frontend/app/api/auth/google/token/route.ts +++ b/frontend/app/api/auth/google/token/route.ts @@ -3,14 +3,11 @@ * This proxies the token exchange to the backend */ import { NextRequest, NextResponse } from "next/server"; - -// Use backend internal URL if available (Railway), fallback to public URL -const BACKEND_URL = process.env.BACKEND_URL || - process.env.NEXT_PUBLIC_API_URL || - "http://localhost:8000"; +import { getBackendUrl } from "@/lib/backend-url"; export async function POST(request: NextRequest) { try { + const backendUrl = getBackendUrl(); const body = await request.json(); const { code, redirect_uri } = body; @@ -22,7 +19,7 @@ export async function POST(request: NextRequest) { } // Forward to backend - const backendResponse = await fetch(`${BACKEND_URL}/api/auth/google/token`, { + const backendResponse = await fetch(`${backendUrl}/api/auth/google/token`, { method: "POST", headers: { "Content-Type": "application/json", diff --git a/frontend/app/api/chat/route.ts b/frontend/app/api/chat/route.ts index 9f6b1794..f55b469c 100644 --- a/frontend/app/api/chat/route.ts +++ b/frontend/app/api/chat/route.ts @@ -1,11 +1,9 @@ import { NextResponse } from "next/server"; +import { getBackendUrl } from "@/lib/backend-url"; export async function POST(req: Request) { try { - const isDev = process.env.NODE_ENV === "development"; - const backendUrl = - process.env.BACKEND_URL || - (isDev ? "http://localhost:8000" : "http://backend:8000"); + const backendUrl = getBackendUrl(); // Read the complete body from the request const bodyText = await req.text(); diff --git a/frontend/lib/backend-url.ts b/frontend/lib/backend-url.ts new file mode 100644 index 00000000..e0478007 --- /dev/null +++ b/frontend/lib/backend-url.ts @@ -0,0 +1,27 @@ +/** + * Unified backend URL resolution for server-side proxying. + * + * Priority: + * 1. BACKEND_URL – explicit override (Railway / custom deployment) + * 2. Development mode – http://localhost:8000 + * 3. NEXT_PUBLIC_API_URL – fallback (often set in Railway for client-side too) + * 4. Docker Compose default – http://backend:8000 + * + * Used by next.config.ts rewrites and Next.js API route handlers. + */ +export function getBackendUrl(): string { + if (process.env.BACKEND_URL) { + return process.env.BACKEND_URL; + } + + if (process.env.NODE_ENV === "development") { + return "http://localhost:8000"; + } + + if (process.env.NEXT_PUBLIC_API_URL) { + return process.env.NEXT_PUBLIC_API_URL; + } + + // Docker Compose internal hostname (only works when both services share a Docker network) + return "http://backend:8000"; +} diff --git a/frontend/next.config.ts b/frontend/next.config.ts index 97e29448..34a20338 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,4 +1,5 @@ import type { NextConfig } from "next"; +import { getBackendUrl } from "./lib/backend-url"; const nextConfig: NextConfig = { output: 'standalone', @@ -56,13 +57,7 @@ const nextConfig: NextConfig = { }, async rewrites() { - // In development: use localhost backend - // In production (Railway): use BACKEND_URL env var or fallback to Railway URL - const isDev = process.env.NODE_ENV === 'development'; - // Default to http://backend:8000 in production (Docker) if not set - const backendUrl = process.env.BACKEND_URL || - (isDev ? "http://localhost:8000" : "http://backend:8000"); - + const backendUrl = getBackendUrl(); console.log(`[Next.js] Rewriting API requests to: ${backendUrl}`); return [