fix: better fix to the backend connectivity problem using the react backend for guessing the API URL

This commit is contained in:
LUIS NOVO 2025-10-19 10:16:58 -03:00
parent 0a759b121c
commit a73ce8e094
7 changed files with 93 additions and 48 deletions

View file

@ -6,17 +6,24 @@ Starting from version 1.0.0-alpha, Open Notebook uses a simplified API connectio
### How It Works ### How It Works
The frontend now automatically discovers the API location at runtime, eliminating the need for complex network configurations. This works for both Docker deployment modes: The frontend automatically discovers the API location at runtime by analyzing the incoming HTTP request. This eliminates the need for complex network configurations and works for both Docker deployment modes:
- Multi-container (docker-compose with separate SurrealDB) - Multi-container (docker-compose with separate SurrealDB)
- Single-container (all services in one container) - Single-container (all services in one container)
### Default Configuration **Auto-detection logic:**
1. If `API_URL` environment variable is set → use it (explicit override)
2. Otherwise, detect from the HTTP request:
- Uses the same hostname you're accessing the frontend from
- Automatically changes port to 5055 (API port)
- Respects `X-Forwarded-Proto` header for reverse proxy setups
3. Falls back to `http://localhost:5055` if detection fails
By default, the API is accessible at `http://localhost:5055`. This works for most local Docker deployments where: **Examples:**
- You access the frontend at `http://localhost:8502` - Access frontend at `http://localhost:8502` → API at `http://localhost:5055`
- Your browser can directly reach `http://localhost:5055` - Access frontend at `http://10.20.30.20:8502` → API at `http://10.20.30.20:5055`
- Access frontend at `http://my-server:8502` → API at `http://my-server:5055`
**No configuration needed** for standard localhost deployments! **No configuration needed** for most deployments!
### Custom Configuration ### Custom Configuration

View file

@ -99,12 +99,15 @@ curl http://your-server-ip:5055/api/config
Expected response: Expected response:
```json ```json
{ {
"apiUrl": "http://localhost:5055",
"version": "1.0.0", "version": "1.0.0",
"dbStatus": "connected" "latestVersion": "1.0.0",
"hasUpdate": false,
"dbStatus": "online"
} }
``` ```
Note: The API URL is now auto-detected by the frontend from the hostname you're accessing, so `/api/config` no longer returns `apiUrl`.
### Troubleshooting ### Troubleshooting
**Problem:** Frontend shows "Cannot connect to API" error **Problem:** Frontend shows "Cannot connect to API" error

View file

@ -118,34 +118,13 @@ async def check_database_health() -> dict:
async def get_config(request: Request): async def get_config(request: Request):
""" """
Get frontend configuration. Get frontend configuration.
This endpoint provides runtime configuration to the frontend,
allowing the same Docker image to work in different environments.
Auto-detection logic: Returns version information and health status.
1. If API_URL env var is set, use it (explicit override) Note: The frontend determines the API URL via its own runtime-config endpoint,
2. Otherwise, detect from incoming HTTP request (zero-config) so this endpoint no longer returns apiUrl.
Also checks for version updates from GitHub (with caching and error handling). Also checks for version updates from GitHub (with caching and error handling).
""" """
# Check if API_URL is explicitly set
env_api_url = os.getenv("API_URL")
if env_api_url:
logger.debug(f"Using API_URL from environment: {env_api_url}")
api_url = env_api_url
else:
# Auto-detect from request
# Get the protocol (http or https)
# Check X-Forwarded-Proto first (for reverse proxies), then fallback to request scheme
proto = request.headers.get("x-forwarded-proto", request.url.scheme)
# Get the host (includes port if non-standard)
host = request.headers.get("host", f"{request.client.host}:5055")
# Construct the API URL
api_url = f"{proto}://{host}"
logger.info(f"Auto-detected API URL from request: {api_url} (proto={proto}, host={host})")
# Get current version # Get current version
current_version = get_version() current_version = get_version()
@ -168,7 +147,6 @@ async def get_config(request: Request):
logger.warning(f"Database offline: {db_health.get('error', 'Unknown error')}") logger.warning(f"Database offline: {db_health.get('error', 'Unknown error')}")
return { return {
"apiUrl": api_url,
"version": current_version, "version": current_version,
"latestVersion": latest_version, "latestVersion": latest_version,
"hasUpdate": has_update, "hasUpdate": has_update,

View file

@ -12,7 +12,14 @@ The frontend uses a three-tier priority system to determine the API URL:
1. **Runtime Configuration** (Highest Priority): `API_URL` environment variable set at container runtime 1. **Runtime Configuration** (Highest Priority): `API_URL` environment variable set at container runtime
2. **Build-time Configuration**: `NEXT_PUBLIC_API_URL` baked into the Docker image 2. **Build-time Configuration**: `NEXT_PUBLIC_API_URL` baked into the Docker image
3. **Auto-detection** (Fallback): Infers from the incoming HTTP request 3. **Auto-detection** (Fallback): Infers from the incoming HTTP request headers
**Auto-detection details:**
- The Next.js frontend analyzes the incoming HTTP request
- Extracts the hostname from the `host` header
- Respects the `X-Forwarded-Proto` header (for HTTPS behind reverse proxies)
- Constructs the API URL as `{protocol}://{hostname}:5055`
- Example: Request to `http://10.20.30.20:8502` → API URL becomes `http://10.20.30.20:5055`
## Common Scenarios ## Common Scenarios

View file

@ -1,4 +1,4 @@
import { NextResponse } from 'next/server' import { NextRequest, NextResponse } from 'next/server'
/** /**
* Runtime Configuration Endpoint * Runtime Configuration Endpoint
@ -6,17 +6,54 @@ import { NextResponse } from 'next/server'
* This endpoint provides server-side environment variables to the client at runtime. * This endpoint provides server-side environment variables to the client at runtime.
* This solves the NEXT_PUBLIC_* limitation where variables are baked into the build. * This solves the NEXT_PUBLIC_* limitation where variables are baked into the build.
* *
* Users can now set API_URL in their docker.env and it will be picked up at runtime, * Auto-detection logic:
* allowing the same Docker image to work in different deployment scenarios. * 1. If API_URL env var is set, use it (explicit override)
* 2. Otherwise, detect from incoming HTTP request headers (zero-config)
* 3. Fallback to localhost:5055 if detection fails
*
* This allows the same Docker image to work in different deployment scenarios.
*/ */
export async function GET() { export async function GET(request: NextRequest) {
// Priority: // Priority 1: Check if API_URL is explicitly set
// 1. API_URL from environment (set by user at runtime) const envApiUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL
// 2. NEXT_PUBLIC_API_URL from build time (fallback)
// 3. Default to localhost:5055
const apiUrl = process.env.API_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5055'
if (envApiUrl) {
return NextResponse.json({
apiUrl: envApiUrl,
})
}
// Priority 2: Auto-detect from request headers
try {
// Get the protocol (http or https)
// Check X-Forwarded-Proto first (for reverse proxies), then fallback to request scheme
const proto = request.headers.get('x-forwarded-proto') ||
request.nextUrl.protocol.replace(':', '') ||
'http'
// Get the host header (includes port if non-standard)
const hostHeader = request.headers.get('host')
if (hostHeader) {
// Extract just the hostname (remove port if present)
const hostname = hostHeader.split(':')[0]
// Construct the API URL with port 5055
const apiUrl = `${proto}://${hostname}:5055`
console.log(`[runtime-config] Auto-detected API URL: ${apiUrl} (proto=${proto}, host=${hostHeader})`)
return NextResponse.json({
apiUrl,
})
}
} catch (error) {
console.error('[runtime-config] Auto-detection failed:', error)
}
// Priority 3: Fallback to localhost
console.log('[runtime-config] Using fallback: http://localhost:5055')
return NextResponse.json({ return NextResponse.json({
apiUrl, apiUrl: 'http://localhost:5055',
}) })
} }

View file

@ -3,7 +3,7 @@
* This allows the same Docker image to work in different environments. * This allows the same Docker image to work in different environments.
*/ */
import { AppConfig } from '@/lib/types/config' import { AppConfig, BackendConfigResponse } from '@/lib/types/config'
// Build timestamp for debugging - set at build time // Build timestamp for debugging - set at build time
const BUILD_TIME = new Date().toISOString() const BUILD_TIME = new Date().toISOString()
@ -116,9 +116,9 @@ async function fetchConfig(): Promise<AppConfig> {
}) })
if (response.ok) { if (response.ok) {
const data = await response.json() const data: BackendConfigResponse = await response.json()
config = { config = {
apiUrl: data.apiUrl || baseUrl, apiUrl: baseUrl, // Use baseUrl from runtime-config (Python no longer returns this)
version: data.version || 'unknown', version: data.version || 'unknown',
buildTime: BUILD_TIME, buildTime: BUILD_TIME,
latestVersion: data.latestVersion || null, latestVersion: data.latestVersion || null,

View file

@ -1,5 +1,18 @@
/** /**
* Application configuration received from backend /api/config * Backend configuration response from Python API /api/config endpoint.
* Note: apiUrl is determined by the Next.js runtime-config endpoint,
* not returned by the Python backend.
*/
export interface BackendConfigResponse {
version: string
latestVersion?: string | null
hasUpdate?: boolean
dbStatus?: "online" | "offline"
}
/**
* Complete application configuration used by the frontend.
* This is constructed from the backend response + runtime-config.
*/ */
export interface AppConfig { export interface AppConfig {
apiUrl: string apiUrl: string