From 9141edfaa99d48ab8a1e0d81f1d6096f4db9c4ce Mon Sep 17 00:00:00 2001 From: Froidefond Julien Date: Fri, 6 Mar 2026 22:40:57 +0100 Subject: [PATCH] fix: handle SSE controller errors gracefully - Add isActive checks before writing to SSE controller - Wrap controller operations in try/catch to prevent 'already closed' errors - Fix race condition when client disconnects during SSE streaming --- .../app/api/jobs/[id]/stream/route.ts | 26 ++++++++++++++----- apps/backoffice/app/api/jobs/stream/route.ts | 19 +++++++++----- 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/apps/backoffice/app/api/jobs/[id]/stream/route.ts b/apps/backoffice/app/api/jobs/[id]/stream/route.ts index d665350..45cd77a 100644 --- a/apps/backoffice/app/api/jobs/[id]/stream/route.ts +++ b/apps/backoffice/app/api/jobs/[id]/stream/route.ts @@ -33,26 +33,38 @@ export async function GET( }, }); - if (response.ok) { + if (response.ok && isActive) { const data = await response.json(); const dataStr = JSON.stringify(data); // Only send if data changed - if (dataStr !== lastData) { + if (dataStr !== lastData && isActive) { lastData = dataStr; - controller.enqueue( - new TextEncoder().encode(`data: ${dataStr}\n\n`) - ); + try { + controller.enqueue( + new TextEncoder().encode(`data: ${dataStr}\n\n`) + ); + } catch (err) { + // Controller closed, ignore + isActive = false; + return; + } // Stop polling if job is complete if (data.status === "success" || data.status === "failed" || data.status === "cancelled") { isActive = false; - controller.close(); + try { + controller.close(); + } catch (err) { + // Already closed, ignore + } } } } } catch (error) { - console.error("SSE fetch error:", error); + if (isActive) { + console.error("SSE fetch error:", error); + } } }; diff --git a/apps/backoffice/app/api/jobs/stream/route.ts b/apps/backoffice/app/api/jobs/stream/route.ts index ddf74b8..857f10b 100644 --- a/apps/backoffice/app/api/jobs/stream/route.ts +++ b/apps/backoffice/app/api/jobs/stream/route.ts @@ -28,20 +28,27 @@ export async function GET(request: NextRequest) { }, }); - if (response.ok) { + if (response.ok && isActive) { const data = await response.json(); const dataStr = JSON.stringify(data); // Send if data changed - if (dataStr !== lastData) { + if (dataStr !== lastData && isActive) { lastData = dataStr; - controller.enqueue( - new TextEncoder().encode(`data: ${dataStr}\n\n`) - ); + try { + controller.enqueue( + new TextEncoder().encode(`data: ${dataStr}\n\n`) + ); + } catch (err) { + // Controller closed, ignore + isActive = false; + } } } } catch (error) { - console.error("SSE fetch error:", error); + if (isActive) { + console.error("SSE fetch error:", error); + } } };