From 1f5a0c27da7e791685312010f04ae8d0591c0fec Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Sun, 1 Mar 2026 20:43:58 +0000 Subject: [PATCH] Fix SSE proxy: create Next.js route handlers for all streaming endpoints Next.js rewrites() buffer HTTP responses and drop long-lived connections, making SSE (text/event-stream) impossible. The backend never even received the request (no log entry in API, ECONNRESET in web proxy logs). Create dedicated route.ts files for all 3 SSE endpoints: - /api/v1/ppt/outlines/stream/[id] - /api/v1/ppt/presentation/stream/[id] - /api/v1/ppt/jobs/[job_id]/stream Each route forwards cookies for auth and returns backend's ReadableStream directly as a Response, preventing any buffering. Sets X-Accel-Buffering: no to also disable nginx buffering. Co-Authored-By: Claude Sonnet 4.6 --- .../api/v1/ppt/jobs/[job_id]/stream/route.ts | 44 +++++++++++++++++++ .../api/v1/ppt/outlines/stream/[id]/route.ts | 44 +++++++++++++++++++ .../v1/ppt/presentation/stream/[id]/route.ts | 44 +++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts create mode 100644 frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts create mode 100644 frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts diff --git a/frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts b/frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts new file mode 100644 index 0000000..7f42ffc --- /dev/null +++ b/frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts @@ -0,0 +1,44 @@ +/** + * SSE Proxy for job progress stream. + * Next.js rewrites() don't handle SSE (text/event-stream) — they buffer responses + * and drop long-lived connections. This route handler streams directly without buffering. + */ +import { NextRequest } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +const API_URL = process.env.API_INTERNAL_URL || 'http://api:8000'; + +export async function GET( + request: NextRequest, + { params }: { params: { job_id: string } } +) { + try { + const backendResponse = await fetch( + `${API_URL}/api/v1/ppt/jobs/${params.job_id}/stream`, + { + headers: { + Cookie: request.headers.get('cookie') || '', + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + } + ); + + if (!backendResponse.ok || !backendResponse.body) { + return new Response(null, { status: backendResponse.status }); + } + + return new Response(backendResponse.body, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'X-Accel-Buffering': 'no', + 'Connection': 'keep-alive', + }, + }); + } catch (error) { + console.error('[SSE] jobs/stream error:', error); + return new Response(null, { status: 502 }); + } +} diff --git a/frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts b/frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts new file mode 100644 index 0000000..f156be1 --- /dev/null +++ b/frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts @@ -0,0 +1,44 @@ +/** + * SSE Proxy for outline generation stream. + * Next.js rewrites() don't handle SSE (text/event-stream) — they buffer responses + * and drop long-lived connections. This route handler streams directly without buffering. + */ +import { NextRequest } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +const API_URL = process.env.API_INTERNAL_URL || 'http://api:8000'; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const backendResponse = await fetch( + `${API_URL}/api/v1/ppt/outlines/stream/${params.id}`, + { + headers: { + Cookie: request.headers.get('cookie') || '', + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + } + ); + + if (!backendResponse.ok || !backendResponse.body) { + return new Response(null, { status: backendResponse.status }); + } + + return new Response(backendResponse.body, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'X-Accel-Buffering': 'no', + 'Connection': 'keep-alive', + }, + }); + } catch (error) { + console.error('[SSE] outlines/stream error:', error); + return new Response(null, { status: 502 }); + } +} diff --git a/frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts b/frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts new file mode 100644 index 0000000..a5e662b --- /dev/null +++ b/frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts @@ -0,0 +1,44 @@ +/** + * SSE Proxy for presentation generation stream. + * Next.js rewrites() don't handle SSE (text/event-stream) — they buffer responses + * and drop long-lived connections. This route handler streams directly without buffering. + */ +import { NextRequest } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +const API_URL = process.env.API_INTERNAL_URL || 'http://api:8000'; + +export async function GET( + request: NextRequest, + { params }: { params: { id: string } } +) { + try { + const backendResponse = await fetch( + `${API_URL}/api/v1/ppt/presentation/stream/${params.id}`, + { + headers: { + Cookie: request.headers.get('cookie') || '', + Accept: 'text/event-stream', + 'Cache-Control': 'no-cache', + }, + } + ); + + if (!backendResponse.ok || !backendResponse.body) { + return new Response(null, { status: backendResponse.status }); + } + + return new Response(backendResponse.body, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache, no-transform', + 'X-Accel-Buffering': 'no', + 'Connection': 'keep-alive', + }, + }); + } catch (error) { + console.error('[SSE] presentation/stream error:', error); + return new Response(null, { status: 502 }); + } +}