Skip to content

Commit 3d59f09

Browse files
fix: add WebSocket proxy support for WAF mode
The frontend proxy only handled HTTP requests but MACAE uses WebSocket connections for real-time agent streaming. Add a WebSocket proxy route that forwards wss:// connections to the private backend Container App over the VNet using the websockets library. Also fix Dockerfile BuildKit --mount issue for ACR Task builds. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent e92c2d0 commit 3d59f09

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

src/App/Dockerfile

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ WORKDIR /app
3434
COPY pyproject.toml requirements.txt* uv.lock* ./
3535

3636
# Install Python dependencies using UV
37-
RUN --mount=type=cache,target=/root/.cache/uv \
38-
if [ -f "requirements.txt" ]; then \
37+
RUN if [ -f "requirements.txt" ]; then \
3938
uv pip install --system -r requirements.txt && uv pip install --system "uvicorn[standard]"; \
4039
else \
4140
uv pip install --system pyproject.toml && uv pip install --system "uvicorn[standard]"; \

src/App/frontend_server.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1+
import asyncio
12
import os
23

34
import httpx
45
import uvicorn
6+
import websockets
57
from dotenv import load_dotenv
6-
from fastapi import FastAPI, Request
8+
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
79
from fastapi.middleware.cors import CORSMiddleware
810
from fastapi.responses import FileResponse, StreamingResponse
911
from fastapi.staticfiles import StaticFiles
@@ -93,6 +95,43 @@ async def proxy_api(request: Request, path: str):
9395
headers=dict(response.headers),
9496
)
9597

98+
@app.websocket("/api/{path:path}")
99+
async def proxy_websocket(websocket: WebSocket, path: str):
100+
"""Proxy WebSocket connections to the private backend over VNet."""
101+
await websocket.accept()
102+
103+
# Build the backend WebSocket URL
104+
backend_ws_url = BACKEND_API_URL.replace("https://", "wss://").replace("http://", "ws://")
105+
query_string = str(websocket.query_params)
106+
target_url = f"{backend_ws_url}/api/{path}"
107+
if query_string:
108+
target_url = f"{target_url}?{query_string}"
109+
110+
try:
111+
async with websockets.connect(target_url) as backend_ws:
112+
113+
async def forward_to_backend():
114+
try:
115+
while True:
116+
data = await websocket.receive_text()
117+
await backend_ws.send(data)
118+
except WebSocketDisconnect:
119+
await backend_ws.close()
120+
121+
async def forward_to_client():
122+
try:
123+
async for message in backend_ws:
124+
await websocket.send_text(message)
125+
except websockets.exceptions.ConnectionClosed:
126+
await websocket.close()
127+
128+
await asyncio.gather(forward_to_backend(), forward_to_client())
129+
except Exception:
130+
try:
131+
await websocket.close()
132+
except Exception:
133+
pass
134+
96135

97136
@app.get("/{full_path:path}")
98137
async def serve_app(full_path: str):

src/App/requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ jinja2
55
azure-identity
66
python-dotenv
77
python-multipart
8-
httpx
8+
httpx
9+
websockets

0 commit comments

Comments
 (0)