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:
parent
e360983249
commit
1f5a0c27da
3 changed files with 132 additions and 0 deletions
44
frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts
Normal file
44
frontend/app/api/v1/ppt/jobs/[job_id]/stream/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
44
frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts
Normal file
44
frontend/app/api/v1/ppt/outlines/stream/[id]/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
44
frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts
Normal file
44
frontend/app/api/v1/ppt/presentation/stream/[id]/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue