Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions apps/web/src/app/api/transcribe/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,10 @@ function getGemini() {
return _gemini;
}

const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000';
// Backend URL with validation - skip if not a valid URL
const rawBackendUrl = process.env.BACKEND_URL || '';
const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000';
const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http');
Comment on lines +17 to +20
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The new BACKEND_AVAILABLE flag is false when process.env.BACKEND_URL is unset, which disables the documented localhost fallback and breaks local development workflows.
Severity: MEDIUM

Suggested Fix

The logic should differentiate between an unset environment variable and an invalid one. A potential fix is to set BACKEND_AVAILABLE to true if rawBackendUrl is empty, allowing the localhost fallback to be used. For example: const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http') || rawBackendUrl === '';. This preserves the original fallback behavior for local development while still guarding against invalid template strings from deployment environments.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: apps/web/src/app/api/transcribe/route.ts#L17-L20

Potential issue: The logic for setting `BACKEND_URL` was changed to handle cases where
the variable might be an invalid template string. However, this introduced a bug. When
`process.env.BACKEND_URL` is not set, `rawBackendUrl` becomes an empty string, causing
`BACKEND_AVAILABLE` to be `false`. Consequently, any code guarded by `if
(BACKEND_AVAILABLE)` will not execute, effectively disabling the documented localhost
fallback to `'http://localhost:8000'`. This breaks local development for users who rely
on the default behavior of running the backend on localhost without explicitly setting
the environment variable. This also creates an inconsistency, as some API routes were
not updated with this new logic.

Did we get this right? 👍 / 👎 to inform future reviews.


Comment on lines +19 to 21
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in /api/video: rawBackendUrl.startsWith('http') is too permissive to be considered URL validation (e.g., httpx://... or https://${...} would be treated as available and can still trigger URL parse errors). Consider using new URL(rawBackendUrl) + protocol check (and optionally rejecting ${...} tokens) to decide BACKEND_AVAILABLE.

Suggested change
const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000';
const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http');
let BACKEND_URL = 'http://localhost:8000';
let BACKEND_AVAILABLE = false;
if (rawBackendUrl) {
try {
const parsed = new URL(rawBackendUrl);
const protocolOk = parsed.protocol === 'http:' || parsed.protocol === 'https:';
const hasTemplateTokens = rawBackendUrl.includes('${');
if (protocolOk && !hasTemplateTokens) {
BACKEND_URL = rawBackendUrl;
BACKEND_AVAILABLE = true;
}
} catch {
// Invalid BACKEND_URL; keep defaults
}
}

Copilot uses AI. Check for mistakes.
/**
* POST /api/transcribe
Expand All @@ -37,7 +40,7 @@ export async function POST(request: Request) {
}

// Strategy 1: Try YouTube transcript API via backend (fast + free)
if (url && !audioUrl) {
if (url && !audioUrl && BACKEND_AVAILABLE) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8_000);
Expand Down
75 changes: 43 additions & 32 deletions apps/web/src/app/api/video/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { NextResponse } from 'next/server';

const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000';
// Backend URL with validation - skip if not a valid URL
const rawBackendUrl = process.env.BACKEND_URL || '';
const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000';
const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http');

Comment on lines +5 to 7
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rawBackendUrl.startsWith('http') is not actually validating that BACKEND_URL is a parseable http(s) URL (e.g., it would treat httpx://... or https://${...} as "available" and still hit fetch URL-parse errors). Consider validating with new URL(rawBackendUrl) (and checking protocol is http:/https:) and/or explicitly rejecting placeholder tokens like ${...} before setting BACKEND_AVAILABLE.

Suggested change
const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000';
const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http');
let BACKEND_URL = 'http://localhost:8000';
let BACKEND_AVAILABLE = false;
if (rawBackendUrl) {
try {
const parsed = new URL(rawBackendUrl);
const isHttpProtocol = parsed.protocol === 'http:' || parsed.protocol === 'https:';
const hasPlaceholder = rawBackendUrl.includes('${');
if (isHttpProtocol && !hasPlaceholder) {
BACKEND_URL = rawBackendUrl;
BACKEND_AVAILABLE = true;
}
} catch {
// Invalid URL in BACKEND_URL; fall back to defaults
}
}

Copilot uses AI. Check for mistakes.
/**
* Get the absolute base URL for the current request.
Expand Down Expand Up @@ -28,10 +31,11 @@ export async function POST(request: Request) {
return NextResponse.json({ error: 'Video URL is required' }, { status: 400 });
}

// ── Strategy 1: Full backend pipeline ──
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);
// ── Strategy 1: Full backend pipeline (skip if no backend configured) ──
if (BACKEND_AVAILABLE) {
try {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 15_000);

let response: Response;
try {
Expand Down Expand Up @@ -95,9 +99,10 @@ export async function POST(request: Request) {
},
});
}
console.warn(`Backend returned ${response.status}, falling back to frontend-only pipeline`);
} catch {
console.log('Backend unavailable — using frontend-only pipeline');
console.warn(`Backend returned ${response.status}, falling back to frontend-only pipeline`);
} catch {
console.log('Backend unavailable — using frontend-only pipeline');
}
}

// ── Strategy 2: Frontend-only pipeline ──
Expand Down Expand Up @@ -179,29 +184,35 @@ export async function POST(request: Request) {
}

export async function GET() {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/health`);
const health = await response.json();

return NextResponse.json({
name: 'UVAI Video Analysis API',
version: '2.0.0',
backend_status: health.status,
backend_components: health.components,
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
health: 'GET /api/video - Check API status',
},
});
} catch {
return NextResponse.json({
name: 'UVAI Video Analysis API',
version: '2.0.0',
backend_status: 'unavailable',
frontend_pipeline: 'active',
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
},
});
// If backend URL is configured and valid, check its health
if (BACKEND_AVAILABLE) {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/health`);
const health = await response.json();

return NextResponse.json({
name: 'UVAI Video Analysis API',
version: '2.0.0',
backend_status: health.status,
backend_components: health.components,
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
health: 'GET /api/video - Check API status',
},
});
} catch {
// Backend configured but unreachable
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In GET, if BACKEND_AVAILABLE is true but the backend health check fails, the handler currently falls through to the frontend-only response with backend_status: 'not-configured', which misrepresents the state (it is configured but unreachable). Consider returning backend_status: 'unavailable' (or similar) when the fetch throws, and reserving 'not-configured' strictly for when BACKEND_AVAILABLE is false.

Suggested change
// Backend configured but unreachable
// Backend configured but unreachable
return NextResponse.json({
name: 'UVAI Video Analysis API',
version: '2.0.0',
backend_status: 'unavailable',
frontend_pipeline: 'active',
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
health: 'GET /api/video - Check API status',
},
});

Copilot uses AI. Check for mistakes.
}
}

// Frontend-only mode
return NextResponse.json({
name: 'UVAI Video Analysis API',
version: '2.0.0',
backend_status: 'not-configured',
frontend_pipeline: 'active',
endpoints: {
analyze: 'POST /api/video - Analyze a video URL',
},
});
}
Loading