diff --git a/render/json.go b/render/json.go index 2f98676c..d5ae54d0 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,14 +161,22 @@ 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 12 bytes: worst case is a \uXXXX\uXXXX surrogate pair for _, r := range bytesconv.BytesToString(ret) { - if r > unicode.MaxASCII { + switch { + case r <= unicode.MaxASCII: + buffer.WriteByte(byte(r)) + case r <= 0xFFFF: escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf buffer.Write(escapeBuf) - } else { - buffer.WriteByte(byte(r)) + default: + // Code points outside the Basic Multilingual Plane don't fit in a + // single 4-hex-digit \u escape; RFC 8259 ยง7 requires encoding them + // as a UTF-16 surrogate pair instead. + r1, r2 := utf16.EncodeRune(r) + escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", r1, r2) + buffer.Write(escapeBuf) } } diff --git a/render/render_test.go b/render/render_test.go index f63878b9..4cfe3925 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -5,6 +5,7 @@ package render import ( + "encoding/json" "encoding/xml" "errors" "html/template" @@ -261,6 +262,23 @@ func TestRenderAsciiJSON(t *testing.T) { assert.Equal(t, "3.1415926", w2.Body.String()) } +func TestRenderAsciiJSONNonBMP(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{"msg": "๐Ÿ˜€"} + + err := (AsciiJSON{data}).Render(w) + require.NoError(t, err) + + // U+1F600 is outside the Basic Multilingual Plane and must be encoded as + // a UTF-16 surrogate pair (RFC 8259 ยง7), not a single \u escape with 5+ + // hex digits. + assert.JSONEq(t, "{\"msg\":\"\\ud83d\\ude00\"}", w.Body.String()) + + var decoded map[string]string + require.NoError(t, json.Unmarshal(w.Body.Bytes(), &decoded)) + assert.Equal(t, "๐Ÿ˜€", decoded["msg"]) +} + func TestRenderAsciiJSONFail(t *testing.T) { w := httptest.NewRecorder() data := make(chan int)