Skip to content

Commit 1246077

Browse files
jsell-rhclaude
andcommitted
feat(frontend): add catch-all proxy route for ambient API v1
Add /api/ambient/v1/[...path] catch-all route that proxies requests to the ambient-api-server backend. Supports all HTTP methods, SSE streaming, and request body forwarding with auth header injection. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e5af6bc commit 1246077

4 files changed

Lines changed: 95 additions & 0 deletions

File tree

components/frontend/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ GITHUB_APP_SLUG=ambient-code-vteam
1212
# Default local backend URL
1313
BACKEND_URL=http://localhost:8080/api
1414

15+
# Ambient API Server URL (ambient-api-server microservice)
16+
# Proxied via /api/ambient/v1/... catch-all route
17+
API_SERVER_URL=http://localhost:8000
18+
1519
# Optional: OpenShift identity details for local development
1620
# If you login with 'oc login', you can set these to forward identity headers
1721
OC_TOKEN=
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { buildForwardHeadersAsync } from '@/lib/auth';
3+
import { API_SERVER_URL } from '@/lib/config';
4+
5+
export const runtime = 'nodejs';
6+
export const dynamic = 'force-dynamic';
7+
8+
async function proxyRequest(
9+
request: NextRequest,
10+
{ params }: { params: Promise<{ path: string[] }> },
11+
): Promise<Response> {
12+
const { path } = await params;
13+
const pathStr = path.map(s => encodeURIComponent(s)).join('/');
14+
const url = new URL(`/api/ambient/v1/${pathStr}`, API_SERVER_URL);
15+
url.search = request.nextUrl.search;
16+
17+
const headers = await buildForwardHeadersAsync(request);
18+
19+
// Forward content-type for requests with bodies
20+
const contentType = request.headers.get('content-type');
21+
if (contentType) {
22+
headers['content-type'] = contentType;
23+
}
24+
25+
let upstream: Response;
26+
try {
27+
upstream = await fetch(url.toString(), {
28+
method: request.method,
29+
headers,
30+
body: request.body,
31+
// @ts-expect-error -- Node.js fetch supports duplex for streaming request bodies
32+
duplex: 'half',
33+
});
34+
} catch (error: unknown) {
35+
const message = error instanceof Error ? error.message : 'Unknown error';
36+
return NextResponse.json(
37+
{ error: 'upstream_unavailable', message },
38+
{ status: 502 },
39+
);
40+
}
41+
42+
// SSE/streaming: pipe through without buffering
43+
const upstreamContentType = upstream.headers.get('content-type') || '';
44+
if (
45+
upstreamContentType.includes('text/event-stream') ||
46+
upstreamContentType.includes('application/x-ndjson')
47+
) {
48+
const { readable, writable } = new TransformStream();
49+
50+
if (upstream.body) {
51+
upstream.body.pipeTo(writable).catch((err: unknown) => {
52+
// AbortError is normal when client disconnects
53+
if (err instanceof Error && err.name !== 'AbortError') {
54+
console.error('Ambient API proxy pipe error:', err);
55+
}
56+
});
57+
}
58+
59+
return new Response(readable, {
60+
status: upstream.status,
61+
headers: {
62+
'Content-Type': upstreamContentType,
63+
'Cache-Control': 'no-cache, no-store, must-revalidate',
64+
Connection: 'keep-alive',
65+
'X-Accel-Buffering': 'no',
66+
},
67+
});
68+
}
69+
70+
// Non-streaming: buffer and forward
71+
const body = await upstream.arrayBuffer();
72+
return new Response(body, {
73+
status: upstream.status,
74+
headers: {
75+
'Content-Type': upstreamContentType || 'application/json',
76+
},
77+
});
78+
}
79+
80+
export const GET = proxyRequest;
81+
export const POST = proxyRequest;
82+
export const PUT = proxyRequest;
83+
export const PATCH = proxyRequest;
84+
export const DELETE = proxyRequest;

components/frontend/src/lib/config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
// API configuration for frontend
22
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8080/api'
33

4+
// Ambient API Server URL (ambient-api-server microservice)
5+
export const API_SERVER_URL = process.env.API_SERVER_URL || 'http://localhost:8000'
6+
47
/**
58
* Get the API base URL for frontend requests
69
*/

components/frontend/src/lib/env.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ type EnvConfig = {
1212
// Backend API URL (server-side only)
1313
BACKEND_URL: string;
1414

15+
// Ambient API Server URL (server-side only)
16+
API_SERVER_URL: string;
17+
1518
// GitHub configuration (public)
1619
GITHUB_APP_SLUG: string;
1720

@@ -64,6 +67,7 @@ function getBooleanEnv(key: string, defaultValue = false): boolean {
6467
export const env: EnvConfig = {
6568
NODE_ENV: (process.env.NODE_ENV || 'development') as Environment,
6669
BACKEND_URL: getEnv('BACKEND_URL', 'http://localhost:8080/api'),
70+
API_SERVER_URL: getEnv('API_SERVER_URL', 'http://localhost:8000'),
6771
GITHUB_APP_SLUG: getEnv('GITHUB_APP_SLUG', 'ambient-code-vteam'),
6872
FEEDBACK_URL: getOptionalEnv('FEEDBACK_URL'),
6973
OC_TOKEN: getOptionalEnv('OC_TOKEN'),

0 commit comments

Comments
 (0)