From 1519fdeb03b2a2cde6c8c8b831a2600c450d5e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 4 Aug 2026 21:26:41 +0200 Subject: [PATCH 1/4] fix: don't mislabel flush errors as "not a flusher" go_sapi_flush logged every ResponseController.Flush() error with the same "not a flusher" message, even when the writer clearly supports Flush but the call failed for another reason (e.g. a client disconnect). Only use that message for http.ErrNotSupported and log the real error otherwise. --- frankenphp.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index ad2dedc42a..55e66680ac 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -627,8 +627,12 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool { if err := fc.responseController.Flush(); err != nil { ctx := thread.context() - if globalLogger.Enabled(ctx, slog.LevelWarn) { - globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err)) + if errors.Is(err, http.ErrNotSupported) { + if globalLogger.Enabled(ctx, slog.LevelWarn) { + globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err)) + } + } else if globalLogger.Enabled(ctx, slog.LevelWarn) { + globalLogger.LogAttrs(ctx, slog.LevelWarn, "flush error", slog.Any("error", err)) } } From 43e691974727990e92bb0f5c254db23d11a85008 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 4 Aug 2026 21:31:08 +0200 Subject: [PATCH 2/4] fix: log expected flush errors at debug, not warn A client disconnecting mid-flush is routine and not actionable, unlike the "not a flusher" misconfiguration case. --- frankenphp.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 55e66680ac..7705282ab0 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -631,8 +631,9 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool { if globalLogger.Enabled(ctx, slog.LevelWarn) { globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err)) } - } else if globalLogger.Enabled(ctx, slog.LevelWarn) { - globalLogger.LogAttrs(ctx, slog.LevelWarn, "flush error", slog.Any("error", err)) + } else if globalLogger.Enabled(ctx, slog.LevelDebug) { + // e.g. a client disconnecting mid-flush: expected, not actionable + globalLogger.LogAttrs(ctx, slog.LevelDebug, "flush error", slog.Any("error", err)) } } From f83f78486b9e3a0f31bdb6bca2945bb3e8dd725d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 4 Aug 2026 21:33:31 +0200 Subject: [PATCH 3/4] fix: match existing warn level and flatten go_sapi_flush Same event as go_ub_write's "write error" (client disconnect), so log it at the same level for consistency. Flatten the nested if/else into early returns. --- frankenphp.go | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 7705282ab0..7eed465de9 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -624,17 +624,20 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool { if fc.responseController == nil { fc.responseController = http.NewResponseController(fc.responseWriter) } - if err := fc.responseController.Flush(); err != nil { - ctx := thread.context() - if errors.Is(err, http.ErrNotSupported) { - if globalLogger.Enabled(ctx, slog.LevelWarn) { - globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err)) - } - } else if globalLogger.Enabled(ctx, slog.LevelDebug) { - // e.g. a client disconnecting mid-flush: expected, not actionable - globalLogger.LogAttrs(ctx, slog.LevelDebug, "flush error", slog.Any("error", err)) - } + err := fc.responseController.Flush() + if err == nil { + return false + } + + msg := "flush error" + if errors.Is(err, http.ErrNotSupported) { + msg = "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue" + } + + ctx := thread.context() + if globalLogger.Enabled(ctx, slog.LevelWarn) { + globalLogger.LogAttrs(ctx, slog.LevelWarn, msg, slog.Any("error", err)) } return false From 9b6e3560105d023824a0f9c348544f63f2c08f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Tue, 4 Aug 2026 21:37:57 +0200 Subject: [PATCH 4/4] fix: log routine write/flush failures at debug, not warn Caddy (streaming.go: "backConn write failed", "streaming error") and Mercure (subscribe.go: "Failed to write comment", "Connection closed by the client") both log repeated per-write streaming failures at debug, reserving warn for one-off actionable problems. Match that: a client disconnecting mid-response is routine and can repeat on every write of a request, so it doesn't belong at warn. Only the "not a flusher" misconfiguration case stays at warn. Also avoid computing the message/level before knowing whether the logger is even enabled for it. --- frankenphp.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/frankenphp.go b/frankenphp.go index 7eed465de9..f302b1bf11 100644 --- a/frankenphp.go +++ b/frankenphp.go @@ -451,8 +451,8 @@ func go_ub_write(threadIndex C.uintptr_t, cBuf *C.char, length C.size_t) (C.size if e != nil { ctx = thread.context() - if fc.logger.Enabled(ctx, slog.LevelWarn) { - fc.logger.LogAttrs(ctx, slog.LevelWarn, "write error", slog.Any("error", e)) + if fc.logger.Enabled(ctx, slog.LevelDebug) { + fc.logger.LogAttrs(ctx, slog.LevelDebug, "write error", slog.Any("error", e)) } } @@ -630,14 +630,14 @@ func go_sapi_flush(threadIndex C.uintptr_t) bool { return false } - msg := "flush error" - if errors.Is(err, http.ErrNotSupported) { - msg = "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue" - } - ctx := thread.context() - if globalLogger.Enabled(ctx, slog.LevelWarn) { - globalLogger.LogAttrs(ctx, slog.LevelWarn, msg, slog.Any("error", err)) + + if errors.Is(err, http.ErrNotSupported) { + if globalLogger.Enabled(ctx, slog.LevelWarn) { + globalLogger.LogAttrs(ctx, slog.LevelWarn, "the current responseWriter is not a flusher, if you are not using a custom build, please report this issue", slog.Any("error", err)) + } + } else if globalLogger.Enabled(ctx, slog.LevelDebug) { + globalLogger.LogAttrs(ctx, slog.LevelDebug, "flush error", slog.Any("error", err)) } return false