From 3ddf111092f85bd37ec0a6bf0e94798705407334 Mon Sep 17 00:00:00 2001 From: Md Mushfiqur Rahim <20mahin2020@gmail.com> Date: Sat, 13 Jun 2026 09:38:33 +0000 Subject: [PATCH 1/2] fix: encode non-BMP characters as UTF-16 surrogate pairs in AsciiJSON The AsciiJSON Render method used fmt.Appendf(buf, "\u%04x", r) for all non-ASCII runes. For non-BMP characters (code points > 0xFFFF, e.g. emoji), this produced invalid JSON with raw 5+ hex digit sequences like \u1f389. Fix: use utf16.Encode([]rune{r}) to convert non-BMP runes into UTF-16 surrogate pairs, producing valid JSON like \ud83c\udf89. Fixes #4688 Signed-off-by: Md Mushfiqur Rahim <20mahin2020@gmail.com> --- render/json.go | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/render/json.go b/render/json.go index 2f98676c..6902c94d 100644 --- a/render/json.go +++ b/render/json.go @@ -10,6 +10,7 @@ import ( "html/template" "net/http" "unicode" + "unicode/utf16" "github.com/gin-gonic/gin/codec/json" "github.com/gin-gonic/gin/internal/bytesconv" @@ -160,11 +161,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) error { } var buffer bytes.Buffer - escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences + escapeBuf := make([]byte, 0, 12) // Preallocate for up to two \uXXXX sequences for _, r := range bytesconv.BytesToString(ret) { if r > unicode.MaxASCII { - escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf + if r > 0xFFFF { + surrogates := utf16.Encode([]rune{r}) + escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", surrogates[0], surrogates[1]) + } else { + escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) + } buffer.Write(escapeBuf) } else { buffer.WriteByte(byte(r)) From d0c2793becbd2f065b1a4942fb346b09fb855a37 Mon Sep 17 00:00:00 2001 From: Md Mushfiqur Rahim <20mahin2020@gmail.com> Date: Thu, 25 Jun 2026 00:32:47 +0000 Subject: [PATCH 2/2] test: add test for non-BMP characters in AsciiJSON --- context_test.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/context_test.go b/context_test.go index 2dfdd392..11ca9ede 100644 --- a/context_test.go +++ b/context_test.go @@ -1263,6 +1263,18 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) { assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } +func TestContextAsciiJSONNonBMP(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + // Non-BMP characters (emoji) should be encoded as UTF-16 surrogate pairs + c.AsciiJSON(http.StatusOK, H{"emoji": "😀"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "{\"emoji\":\"\\ud83d\\ude00\"}", w.Body.String()) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) +} + func TestContextRenderNoContentAsciiJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w)