From 2f109e74caee1a33248744b05a700a65f03bec5c Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 5 Aug 2026 16:28:16 +0800 Subject: [PATCH] fix: emit response.failed when compact keepalive commits headers but no SSE payload When OpenAI Responses streaming has compact keepalive commit 200 headers but no semantic SSE event has been written, upstream 4xx errors were silently swallowed, causing clients to hang until timeout. The fix distinguishes between 'headers committed' and 'meaningful SSE output written'. Compact keepalive may commit headers for heartbeat purposes without writing actual SSE events. In that case, the Responses stream still needs its protocol-correct terminal response.failed event. Preserves existing behavior when a complete upstream response or meaningful SSE payload was already communicated, and preserves image keepalive semantics. Fixes #5289 --- .../handler/openai_gateway_handler.go | 6 +++++- .../handler/openai_gateway_handler_test.go | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/backend/internal/handler/openai_gateway_handler.go b/backend/internal/handler/openai_gateway_handler.go index b787d0f976..ea60b26692 100644 --- a/backend/internal/handler/openai_gateway_handler.go +++ b/backend/internal/handler/openai_gateway_handler.go @@ -2696,7 +2696,11 @@ func (h *OpenAIGatewayHandler) ensureForwardErrorResponse(c *gin.Context, stream imageKeepalivePaddingOnly = adjustedSize < 0 imageKeepaliveResponseWritten = adjustedSize >= 0 } - if service.IsResponseCommitted(c) || (!compactKeepaliveCommitted && imageKeepaliveResponseWritten) { + compactKeepaliveHasMeaningfulOutput := compactKeepaliveCommitted && service.OpenAICompactKeepaliveAdjustedWrittenSize(c) > 0 + // Compact keepalive may have committed 200 headers without writing a + // semantic SSE event. In that case the Responses stream still needs its + // protocol-correct terminal response.failed event. + if (service.IsResponseCommitted(c) && (!compactKeepaliveCommitted || compactKeepaliveHasMeaningfulOutput)) || (!compactKeepaliveCommitted && imageKeepaliveResponseWritten) { return false } if c.Writer.Written() && !imageKeepalivePaddingOnly { diff --git a/backend/internal/handler/openai_gateway_handler_test.go b/backend/internal/handler/openai_gateway_handler_test.go index 543cda1e55..82e530cbe4 100644 --- a/backend/internal/handler/openai_gateway_handler_test.go +++ b/backend/internal/handler/openai_gateway_handler_test.go @@ -334,6 +334,26 @@ func TestOpenAIEnsureForwardErrorResponse_AfterDeltaAppendsSingleValidResponseFa require.Equal(t, 1, errorEvents) } +func TestOpenAIEnsureForwardErrorResponse_CompactKeepaliveOnlyWritesResponseFailed(t *testing.T) { + gin.SetMode(gin.TestMode) + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest(http.MethodPost, EndpointResponses, nil) + service.MarkOpenAICompactClientStream(c) + + stop := service.StartOpenAICompactSSEKeepalive(c, 5*time.Millisecond) + defer stop() + before := service.OpenAICompactKeepaliveAdjustedWrittenSize(c) + require.Eventually(t, c.Writer.Written, time.Second, time.Millisecond) + require.Equal(t, before, service.OpenAICompactKeepaliveAdjustedWrittenSize(c)) + + h := &OpenAIGatewayHandler{} + require.True(t, h.ensureForwardErrorResponse(c, false)) + require.Equal(t, http.StatusOK, w.Code) + require.Contains(t, w.Body.String(), "event: response.failed\n") + require.NotContains(t, w.Body.String(), "event: error\n") +} + func TestOpenAIEnsureForwardErrorResponse_ImageJSONKeepaliveWritesSingleJSONFallback(t *testing.T) { gin.SetMode(gin.TestMode) w := httptest.NewRecorder()