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://<backend-service>.up.railway.app

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
MarkLo127 2026-03-12 11:03:05 +08:00
parent 69eba15bbb
commit ffc36edb97
4 changed files with 34 additions and 17 deletions

View File

@ -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",

View File

@ -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();

View File

@ -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";
}

View File

@ -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 [