From 852dbfe7e1fa1f206576333429b68de8119406dd Mon Sep 17 00:00:00 2001 From: Stanislaw <56738034+anvme@users.noreply.github.com> Date: Mon, 14 Sep 2026 15:02:59 +0200 Subject: [PATCH 1/2] fix: truncate CSAT feedback by runes to preserve valid UTF-8 Feedback was sliced by bytes at maxCsatFeedbackLength, which could split a multibyte character and produce invalid UTF-8. Postgres then rejects the row and the entire submission fails, losing the rating too. Truncate by runes. --- cmd/csat.go | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/cmd/csat.go b/cmd/csat.go index 5efe3fab..ac434034 100644 --- a/cmd/csat.go +++ b/cmd/csat.go @@ -152,10 +152,9 @@ func handleSubmitCSATResponse(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid UUID", nil, envelope.InputError) } - // Trim feedback if it exceeds max length. - if len(req.Feedback) > maxCsatFeedbackLength { - req.Feedback = req.Feedback[:maxCsatFeedbackLength] - } + // Trim feedback if it exceeds max length. Truncate by runes so a + // multibyte UTF-8 character is never split into invalid bytes. + req.Feedback = truncateFeedback(req.Feedback) // Update CSAT response if err := app.csat.UpdateResponse(uuid, req.Rating, req.Feedback, nil); err != nil { @@ -187,9 +186,7 @@ func validateCSATForm(r *fastglue.Request) (int, string, json.RawMessage, string return 0, "", nil, "csat.pleaseFillRequired" } - if len(feedback) > maxCsatFeedbackLength { - feedback = feedback[:maxCsatFeedbackLength] - } + feedback = truncateFeedback(feedback) // Collect extra form fields into meta, skipping the known fields. meta := make(map[string]string) @@ -220,3 +217,18 @@ func validateCSATForm(r *fastglue.Request) (int, string, json.RawMessage, string return rating, feedback, metaJSON, "" } + +// truncateFeedback caps feedback at maxCsatFeedbackLength runes. Truncating by +// runes rather than bytes avoids splitting a multibyte UTF-8 character, which +// would otherwise produce invalid UTF-8 that the database rejects, failing the +// whole submission (including the rating). +func truncateFeedback(s string) string { + if len(s) <= maxCsatFeedbackLength { + return s + } + r := []rune(s) + if len(r) <= maxCsatFeedbackLength { + return s + } + return string(r[:maxCsatFeedbackLength]) +} From 6487b6b2429b3d7268a9b79fd3d14ad57d2a506b Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Mon, 14 Sep 2026 18:47:46 +0530 Subject: [PATCH 2/2] remove over-explanatory CSAT comments --- cmd/csat.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/cmd/csat.go b/cmd/csat.go index ac434034..9655ad2a 100644 --- a/cmd/csat.go +++ b/cmd/csat.go @@ -152,8 +152,6 @@ func handleSubmitCSATResponse(r *fastglue.Request) error { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, "Invalid UUID", nil, envelope.InputError) } - // Trim feedback if it exceeds max length. Truncate by runes so a - // multibyte UTF-8 character is never split into invalid bytes. req.Feedback = truncateFeedback(req.Feedback) // Update CSAT response @@ -218,10 +216,6 @@ func validateCSATForm(r *fastglue.Request) (int, string, json.RawMessage, string return rating, feedback, metaJSON, "" } -// truncateFeedback caps feedback at maxCsatFeedbackLength runes. Truncating by -// runes rather than bytes avoids splitting a multibyte UTF-8 character, which -// would otherwise produce invalid UTF-8 that the database rejects, failing the -// whole submission (including the rating). func truncateFeedback(s string) string { if len(s) <= maxCsatFeedbackLength { return s