fix: stream SSE responses end-to-end through Next.js proxy#770
Open
hyeonho-park wants to merge 2 commits intolfnovo:mainfrom
Open
fix: stream SSE responses end-to-end through Next.js proxy#770hyeonho-park wants to merge 2 commits intolfnovo:mainfrom
hyeonho-park wants to merge 2 commits intolfnovo:mainfrom
Conversation
The /api/search/ask and /api/sources/{id}/chat/sessions/{id}/messages endpoints emit Server-Sent Events (data: ...\n\n) but previously declared media_type=text/plain, causing Next.js rewrites() proxy and other intermediaries to buffer the response until the upstream connection closed.
Switch to text/event-stream and add Cache-Control: no-cache, Connection: keep-alive, and X-Accel-Buffering: no headers so proxies forward chunks as they arrive.
Next.js 16 rewrites() proxy buffers text/event-stream responses, so the UI stays at 'Processing...' until the full response is received at connection close.
Route handlers under src/app/api/{search/ask,sources/[sourceId]/chat/sessions/[sessionId]/messages} take precedence over rewrites() and return the upstream ReadableStream directly, which Next.js streams to the client without buffering. INTERNAL_API_URL env var reuses the existing configuration.
Contributor
There was a problem hiding this comment.
2 issues found across 5 files
Confidence score: 3/5
- There is some concrete regression risk:
frontend/src/app/api/_sse-proxy.tsdoes not handle network-levelfetchfailures, which can surface upstream outages as unhandled 500s instead of controlled proxy errors. api/routers/search.pysetsConnection: keep-aliveon SSE responses, which is invalid as a hop-by-hop header for HTTP/2+ and can cause interoperability issues through proxies/clients.- Given both issues are medium severity (6/10) with high confidence (9/10), this looks mergeable with caution rather than low-risk.
- Pay close attention to
frontend/src/app/api/_sse-proxy.tsandapi/routers/search.py- error handling for upstream failures and HTTP-compliant SSE headers need tightening.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="api/routers/search.py">
<violation number="1" location="api/routers/search.py:153">
P2: `Connection: keep-alive` is a hop-by-hop header and is invalid for HTTP/2+ responses; remove it from SSE response headers.</violation>
</file>
<file name="frontend/src/app/api/_sse-proxy.ts">
<violation number="1" location="frontend/src/app/api/_sse-proxy.ts:9">
P2: `sseProxy` does not catch network-level `fetch` failures, so upstream outages can bubble as unhandled 500s instead of controlled proxy errors.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| media_type="text/event-stream", | ||
| headers={ | ||
| "Cache-Control": "no-cache", | ||
| "Connection": "keep-alive", |
Contributor
There was a problem hiding this comment.
P2: Connection: keep-alive is a hop-by-hop header and is invalid for HTTP/2+ responses; remove it from SSE response headers.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/routers/search.py, line 153:
<comment>`Connection: keep-alive` is a hop-by-hop header and is invalid for HTTP/2+ responses; remove it from SSE response headers.</comment>
<file context>
@@ -147,7 +147,12 @@ async def ask_knowledge_base(ask_request: AskRequest):
+ media_type="text/event-stream",
+ headers={
+ "Cache-Control": "no-cache",
+ "Connection": "keep-alive",
+ "X-Accel-Buffering": "no",
+ },
</file context>
| const body = await req.text() | ||
| const auth = req.headers.get('authorization') | ||
|
|
||
| const upstream = await fetch(`${INTERNAL_API_URL}${upstreamPath}`, { |
Contributor
There was a problem hiding this comment.
P2: sseProxy does not catch network-level fetch failures, so upstream outages can bubble as unhandled 500s instead of controlled proxy errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At frontend/src/app/api/_sse-proxy.ts, line 9:
<comment>`sseProxy` does not catch network-level `fetch` failures, so upstream outages can bubble as unhandled 500s instead of controlled proxy errors.</comment>
<file context>
@@ -0,0 +1,36 @@
+ const body = await req.text()
+ const auth = req.headers.get('authorization')
+
+ const upstream = await fetch(`${INTERNAL_API_URL}${upstreamPath}`, {
+ method: 'POST',
+ headers: {
</file context>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The
/api/search/askand/api/sources/{source_id}/chat/sessions/{session_id}/messagesendpoints emit Server-Sent Events (data: ...\n\n) but declaremedia_type="text/plain". Combined with Next.js 16rewrites()proxy buffering non-SSE content types, the frontend'sReadableStreamnever receives chunks progressively. The UI sits atProcessing...indefinitely even though the backend logs200 OKright away.Root cause
Two layers need to change for streaming to actually reach the browser:
StreamingResponse(..., media_type="text/plain")makes intermediaries assume the body is plain text and buffer it.rewrites()proxy buffers text/event-stream too. Even after fixing (1), Next.js' built-in rewrites handler in standalone mode reads the upstream body to completion before forwarding. There is no stable config flag in Next 15/16 to disable this for SSE.Reproducible with
curl -Nfrom the host:curl -N :5055/api/search/askstreams chunks in real time after fix 1.curl -N :8502/api/search/askstill buffers after fix 1; needs fix 2.Fix
Backend (
api/routers/search.py,api/routers/source_chat.py): switch tomedia_type="text/event-stream"and addCache-Control: no-cache,Connection: keep-alive,X-Accel-Buffering: no.Frontend: add
src/app/api/_sse-proxy.tshelper and two App Router route handlers (/api/search/ask,/api/sources/[sourceId]/chat/sessions/[sessionId]/messages). They returnupstream.bodyas aReadableStream, which Next.js streams without buffering. Reuses the existingINTERNAL_API_URLenv var so all deployment topologies keep working.Verification
curl -Ni -X POST http://localhost:8502/api/search/ask ...now returnsContent-Type: text/event-streamwith chunks at distinct timestamps.fetch('/api/search/ask')receivesstrategy/answer/final_answer/completeevents progressively; the Ask UI renders each stage as it arrives.GET /api/search/askreturns405 Method Not Allowed(confirms the new route handler is routed, not the rewrite).next.config.tsrewrites unchanged.Scope
runtime = 'nodejs'+dynamic = 'force-dynamic'foroutput: 'standalone'compatibility.