mirror of
https://github.com/gin-gonic/gin.git
synced 2026-09-04 22:53:34 +08:00
Merge b05d45dbb144a322ebb6384950a20570858946c6 into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9
This commit is contained in:
commit
5848c2ff12
5
gin.go
5
gin.go
@ -743,6 +743,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
|
|||||||
if tree.method == httpMethod {
|
if tree.method == httpMethod {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Reset skippedNodes before each getValue call so residue from the
|
||||||
|
// previous tree does not accumulate. The slice is allocated once per
|
||||||
|
// pooled Context with a fixed capacity (engine.maxSections); getValue
|
||||||
|
// extends it with a raw reslice that panics if len exceeds cap.
|
||||||
|
*c.skippedNodes = (*c.skippedNodes)[:0]
|
||||||
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
|
if value := tree.root.getValue(rPath, nil, c.skippedNodes, unescape); value.handlers != nil {
|
||||||
allowed = append(allowed, tree.method)
|
allowed = append(allowed, tree.method)
|
||||||
}
|
}
|
||||||
|
|||||||
32
gin_test.go
32
gin_test.go
@ -1058,6 +1058,38 @@ func TestMethodNotAllowedNoRoute(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusNotFound, resp.Code)
|
assert.Equal(t, http.StatusNotFound, resp.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMethodNotAllowedSkippedNodesPanic is a regression test for #4818.
|
||||||
|
// With HandleMethodNotAllowed enabled, handleHTTPRequest calls getValue once
|
||||||
|
// per method tree to build the Allow header. Each call may push onto
|
||||||
|
// c.skippedNodes, which has a fixed capacity (engine.maxSections). Without a
|
||||||
|
// reset between calls the slice fills up and a subsequent reslice panics with
|
||||||
|
// "slice bounds out of range". Because the panic occurs before any handler
|
||||||
|
// runs, gin.Recovery() cannot catch it and the client receives an empty reply.
|
||||||
|
func TestMethodNotAllowedSkippedNodesPanic(t *testing.T) {
|
||||||
|
SetMode(ReleaseMode)
|
||||||
|
router := New()
|
||||||
|
router.HandleMethodNotAllowed = true
|
||||||
|
|
||||||
|
h := func(c *Context) {}
|
||||||
|
router.OPTIONS("/:p0/:p1/a/:p2", h)
|
||||||
|
router.GET("/:p0/:p1/a/:p2", h)
|
||||||
|
router.PATCH("/b/:p0/:p1/c", h)
|
||||||
|
router.DELETE("/b/:p0/:p1/d/:p3", h)
|
||||||
|
router.GET("/b/:p0/:p1/e/f", h)
|
||||||
|
router.POST("/b/:p0/:p1/g/:p4/h", h)
|
||||||
|
router.OPTIONS("/b/:p0/:p1/g/:p4/h", h)
|
||||||
|
router.DELETE("/b/cache", h)
|
||||||
|
router.GET("/b/clients/:p1/g", h)
|
||||||
|
router.POST("/b/clients/:p1/g", h)
|
||||||
|
router.PATCH("/b/clients/:p1/g/:p4", h)
|
||||||
|
router.OPTIONS("/b/clients/:p1/g/:p4", h)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/b/clients/42", nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
assert.NotPanics(t, func() { router.ServeHTTP(w, req) })
|
||||||
|
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
|
// Test the fix for https://github.com/gin-gonic/gin/pull/4415
|
||||||
func TestLiteralColonWithRun(t *testing.T) {
|
func TestLiteralColonWithRun(t *testing.T) {
|
||||||
SetMode(TestMode)
|
SetMode(TestMode)
|
||||||
|
|||||||
@ -160,11 +160,21 @@ func (r AsciiJSON) Render(w http.ResponseWriter) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
escapeBuf := make([]byte, 0, 6) // Preallocate 6 bytes for Unicode escape sequences
|
escapeBuf := make([]byte, 0, 12) // 12 bytes for a UTF-16 surrogate pair (\uHHHH\uLLLL)
|
||||||
|
|
||||||
for _, r := range bytesconv.BytesToString(ret) {
|
for _, r := range bytesconv.BytesToString(ret) {
|
||||||
if r > unicode.MaxASCII {
|
if r > unicode.MaxASCII {
|
||||||
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r) // Reuse escapeBuf
|
if r > 0xFFFF {
|
||||||
|
// Non-BMP character: encode as a UTF-16 surrogate pair per RFC 8259 §7.
|
||||||
|
// \u alone cannot represent code points above U+FFFF (it is always 4 hex digits),
|
||||||
|
// so we split the rune into a high surrogate and a low surrogate.
|
||||||
|
r -= 0x10000
|
||||||
|
high := 0xD800 + (r>>10)&0x3FF
|
||||||
|
low := 0xDC00 + r&0x3FF
|
||||||
|
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x\\u%04x", high, low)
|
||||||
|
} else {
|
||||||
|
escapeBuf = fmt.Appendf(escapeBuf[:0], "\\u%04x", r)
|
||||||
|
}
|
||||||
buffer.Write(escapeBuf)
|
buffer.Write(escapeBuf)
|
||||||
} else {
|
} else {
|
||||||
buffer.WriteByte(byte(r))
|
buffer.WriteByte(byte(r))
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
package render
|
package render
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
@ -261,6 +262,48 @@ func TestRenderAsciiJSON(t *testing.T) {
|
|||||||
assert.Equal(t, "3.1415926", w2.Body.String())
|
assert.Equal(t, "3.1415926", w2.Body.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderAsciiJSONNonBMP(t *testing.T) {
|
||||||
|
// Non-BMP code points (> U+FFFF) must be encoded as UTF-16 surrogate pairs.
|
||||||
|
// Previously, fmt.Appendf(buf, "\\u%04x", r) emitted 5+ hex digits for such
|
||||||
|
// runes, which is invalid JSON — decoders misread the first 4 digits as a
|
||||||
|
// different character and left the remaining digit(s) as literal text.
|
||||||
|
// RFC 8259 §7: \u escapes are exactly 4 hex digits; non-BMP values use pairs.
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
want string // exact \uHHHH\uLLLL literal
|
||||||
|
decoded string // value after json.Unmarshal round-trip
|
||||||
|
}{
|
||||||
|
// 😀 U+1F600: high=\uD83D low=\uDE00
|
||||||
|
{"grinning face", "😀", `\ud83d\ude00`, "😀"},
|
||||||
|
// 𝄞 U+1D11E: high=\uD834 low=\uDD1E
|
||||||
|
{"musical symbol G clef", "𝄞", `\ud834\udd1e`, "𝄞"},
|
||||||
|
// 𠀀 U+20000 (first CJK Extension B): high=\uD840 low=\uDC00
|
||||||
|
{"CJK Extension B first", "𠀀", `\ud840\udc00`, "𠀀"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
err := (AsciiJSON{map[string]string{"v": tc.input}}).Render(w)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body := w.Body.String()
|
||||||
|
|
||||||
|
// The surrogate pair must appear verbatim in the raw output.
|
||||||
|
assert.Contains(t, body, tc.want,
|
||||||
|
"expected surrogate pair %q in raw output %q", tc.want, body)
|
||||||
|
|
||||||
|
// The round-trip via json.Unmarshal must recover the original rune.
|
||||||
|
var decoded map[string]string
|
||||||
|
require.NoError(t, json.Unmarshal([]byte(body), &decoded))
|
||||||
|
assert.Equal(t, tc.decoded, decoded["v"],
|
||||||
|
"round-trip mismatch: got %q want %q", decoded["v"], tc.decoded)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRenderAsciiJSONFail(t *testing.T) {
|
func TestRenderAsciiJSONFail(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
data := make(chan int)
|
data := make(chan int)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user