fix(ui): proxy WebSocket signaling upgrade so local web calls work (#425) (#454)

* fix(ui): proxy WebSocket signaling upgrade so local web calls work (#425)

1.34.0 replaced the next.config `/api/* -> BACKEND_URL` rewrite with the
Route Handler `api/v1/[...path]/route.ts`. Route Handlers proxy HTTP fine
(so `/api/v1/*` still 200s) but cannot upgrade WebSocket connections. The
removed *rewrite* used to carry the upgrade, so without it the signaling
socket (`/api/v1/ws/signaling/...`) has no proxy path and every local web
call dies before WebRTC negotiation — the symptom reported in #425. nginx
would proxy the upgrade but only runs in the `remote` compose profile, so
local OSS deployments have nothing to carry it.

Re-add a `beforeFiles` rewrite scoped to `/api/v1/ws/:path*` so the upgrade
is proxied to the backend *before* the `[...path]` Route Handler can swallow
it. HTTP `/api/v1/*` is untouched and still flows through the Route Handler
(auth/cookie handling intact).

Verified on a 1.34.0-derived source build: signaling WS now reports
`[accepted]` / `connection open` server-side and `WebSocket connected` +
`ICE connection state: connected` client-side; WebRTC negotiates end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ui): use direct localhost WebSocket signaling

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Abhishek Kumar <abhishek@a6k.me>
This commit is contained in:
yogi6969 2026-06-19 17:20:07 +05:30 committed by GitHub
parent 7d053320df
commit ae2023e315
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -36,6 +36,34 @@ const HANDLED_SERVICE_ERROR_TYPES = new Set([
'quota_check_failed',
]);
const LOCALHOST_API_BASE_URL = 'http://localhost:8000';
const LOCALHOST_API_HEALTH_URL = `${LOCALHOST_API_BASE_URL}/api/v1/health`;
const LOCALHOST_API_PROBE_TIMEOUT_MS = 1500;
function isLocalhostUi() {
if (typeof window === 'undefined') return false;
return ['localhost', '127.0.0.1', '::1'].includes(window.location.hostname);
}
async function probeLocalhostApi() {
const controller = new AbortController();
const timeout = window.setTimeout(() => controller.abort(), LOCALHOST_API_PROBE_TIMEOUT_MS);
try {
const response = await fetch(LOCALHOST_API_HEALTH_URL, {
cache: 'no-store',
signal: controller.signal,
});
return response.ok;
} catch {
return false;
} finally {
window.clearTimeout(timeout);
}
}
export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initialContextVariables, onNodeTransition }: UseWebSocketRTCProps) => {
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('idle');
const [connectionActive, setConnectionActive] = useState(false);
@ -108,10 +136,26 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
const currentAllowInterruptRef = useRef<boolean | undefined>(undefined);
const interruptWarningShownRef = useRef(false);
// Get WebSocket URL from client configuration
const getWebSocketUrl = useCallback(() => {
// Get base URL from client configuration
const baseUrl = client.getConfig().baseUrl || 'http://127.0.0.1:8000';
const getWebSocketUrl = useCallback(async () => {
let baseUrl = client.getConfig().baseUrl || 'http://127.0.0.1:8000';
if (isLocalhostUi()) {
// Local Docker exposes the API on localhost:8000 while the UI runs
// on localhost:3010. WebSocket upgrades cannot pass through the
// Next.js route-handler HTTP proxy, so local browser calls should
// connect to the API directly when that port is available. A
// Next.js rewrite/proxy for the upgrade was considered, but we
// keep the WebRTC signaling path direct so signaling and the API's
// ICE/WebRTC handling terminate at the same local endpoint.
const localhostApiReachable = await probeLocalhostApi();
if (!localhostApiReachable) {
throw new Error('Dograh API is not reachable at http://localhost:8000. Ensure the api container is running and port 8000 is published.');
}
baseUrl = LOCALHOST_API_BASE_URL;
}
// Convert HTTP to WS protocol
const wsUrl = baseUrl.replace(/^http/, 'ws');
return `${wsUrl}/api/v1/ws/signaling/${workflowId}/${workflowRunId}?token=${accessToken}`;
@ -292,9 +336,10 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
return pc;
};
const connectWebSocket = useCallback(() => {
const connectWebSocket = useCallback(async () => {
const wsUrl = await getWebSocketUrl();
return new Promise<void>((resolve, reject) => {
const wsUrl = getWebSocketUrl();
logger.info(`Connecting to WebSocket: ${wsUrl}`);
const ws = new WebSocket(wsUrl);
@ -307,7 +352,7 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
ws.onerror = (error) => {
logger.error('WebSocket error:', error);
reject(error);
reject(new Error(`WebSocket connection failed at ${wsUrl}`));
};
ws.onclose = (event) => {
@ -774,6 +819,9 @@ export const useWebSocketRTC = ({ workflowId, workflowRunId, accessToken, initia
}
} catch (error) {
logger.error('Failed to start connection:', error);
if (error instanceof Error) {
setPermissionError(error.message);
}
setConnectionStatus('failed');
} finally {
setIsStarting(false);