You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
difyctl SSE read failures become unhandled rejections while the consumer processes an event #43942
I confirm that I am using English to submit this report, otherwise it will be closed.
【中文用户 & Non English User】请使用英语提交,否则会被关闭 :)
Please do not modify this template :) and fill in all the required fields.
Dify version
Source main 0838e2511da0ef0953b59d90d5407e0b441e2b32, difyctl 1.17.1.
Cloud or Self Hosted
Self Hosted (Source). Isolated CLI stream reproduction on Windows, Node.js 24.18.0; no deployed server or model call.
Steps to reproduce
cli/src/call/sse.ts starts a background read pump and only awaits its Promise when the consumer drains the queue. If the response body fails while the async generator is suspended at a yielded event, the pump rejects before a handler is attached. Node's default unhandled-rejection policy terminates the process before the consumer can handle the stream error.
With the CLI dependencies installed, save this as cli/repro.mjs and run node --unhandled-rejections=strict cli/repro.mjs from the repository root:
import{parseSSE}from'./src/call/sse.ts'letcontrollerconstbody=newReadableStream({start(c){controller=c}})constevents=parseSSE(body)controller.enqueue(newTextEncoder().encode('data: hello\n\n'))console.log('first event:',awaitevents.next())controller.error(newError('network disconnected'))// Represents time spent writing/processing the previously yielded event.awaitnewPromise(resolve=>setTimeout(resolve,30))try{awaitevents.next()}catch(error){console.log('caught by consumer:',error.message)}
The reproduction used an unchanged snapshot of sse.ts with the repository-pinned eventsource-parser@4.1.1. It exits with status 1 before printing caught by consumer.
Proposed focused fix: attach a rejection observer to the pump immediately, retaining the existing await that propagates the original error to the consumer. Add a regression that yields one event, fails the response body while the consumer is suspended, and verifies that resuming consumption rejects with that error without an unhandled rejection. This does not add retries or change SSE event ordering.
✔️ Expected Behavior
A failed response body is reported through the iterator and can reach the CLI's normal error handling, including when the consumer is temporarily processing an event.
❌ Actual Behavior
first event:
Error: network disconnected
at .../repro.mjs:...
Node.js v24.18.0
The process exits before the surrounding consumer's catch runs.
Your diagnosis matches the code at that commit. The pump async IIFE starts immediately, and its only consumer is await pump, which runs only after the queue drains and done is true. If reader.read() rejects while the generator is suspended at yield ev, pump rejects with no handler attached, and Node treats that as an unhandled rejection.
Your proposed fix should work. Add a no-op observer right after the pump is created:
pump.catch() returns a new promise, so the original pump stays rejected and the existing await pump in the done branch still rethrows the original error to the consumer.
The same observer covers a second case: if the consumer exits early through return() or break and pump rejects afterwards, the rejection is also unhandled, because the finally block never awaits pump.
For the regression test, enqueue one event, call controller.error(...), wait a few ms, then assert that the next events.next() rejects with that error. The test also needs to fail on an unhandled rejection. Vitest does this by default, or you can register a process.on('unhandledRejection') listener in the test and assert it was never called.
Activity
dosubot commentedon Oct 12, 2026
Hi @huyengiang101086-spec! I'm Dosu and I'm helping the Dify team.
Your diagnosis matches the code at that commit. The
pumpasync IIFE starts immediately, and its only consumer isawait pump, which runs only after the queue drains anddoneis true. Ifreader.read()rejects while the generator is suspended atyield ev,pumprejects with no handler attached, and Node treats that as an unhandled rejection.Your proposed fix should work. Add a no-op observer right after the pump is created:
pump.catch()returns a new promise, so the originalpumpstays rejected and the existingawait pumpin thedonebranch still rethrows the original error to the consumer.The same observer covers a second case: if the consumer exits early through
return()orbreakandpumprejects afterwards, the rejection is also unhandled, because thefinallyblock never awaitspump.For the regression test, enqueue one event, call
controller.error(...), wait a few ms, then assert that the nextevents.next()rejects with that error. The test also needs to fail on an unhandled rejection. Vitest does this by default, or you can register aprocess.on('unhandledRejection')listener in the test and assert it was never called.To reply, just mention @dosu.
Share context across your team and agents. Try Dosu.