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 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-03-01 20:43:58 +00:00
parent e360983249
commit 1f5a0c27da
3 changed files with 132 additions and 0 deletions

View file

@ -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 });
}
}

View file

@ -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 });
}
}

View file

@ -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 });
}
}