diff --git a/context.go b/context.go index d3e4c6d7..3ebb3ee4 100644 --- a/context.go +++ b/context.go @@ -769,8 +769,19 @@ func (c *Context) BindUri(obj any) error { // It will abort the request with HTTP 400 if any error occurs. // See the binding package. func (c *Context) MustBindWith(obj any, b binding.Binding) error { - if err := c.ShouldBindWith(obj, b); err != nil { - c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck + err := c.ShouldBindWith(obj, b) + if err != nil { + var maxBytesErr *http.MaxBytesError + + // Note: When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error + // https://github.com/goccy/go-json/issues/485 + // https://github.com/bytedance/sonic/issues/800 + switch { + case errors.As(err, &maxBytesErr): + c.AbortWithError(http.StatusRequestEntityTooLarge, err).SetType(ErrorTypeBind) //nolint: errcheck + default: + c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck + } return err } return nil diff --git a/context_test.go b/context_test.go index 084bae5d..b64afba3 100644 --- a/context_test.go +++ b/context_test.go @@ -28,6 +28,7 @@ import ( "github.com/gin-contrib/sse" "github.com/gin-gonic/gin/binding" + "github.com/gin-gonic/gin/internal/json" testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -671,7 +672,7 @@ func TestContextDefaultQueryOnEmptyRequest(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") + body := strings.NewReader("foo=bar&page=11&both=&foo=second") c.Request, _ = http.NewRequest(http.MethodPost, "/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) @@ -946,7 +947,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) { c.JSONP(http.StatusCreated, H{"foo": "bar"}) assert.Equal(t, http.StatusCreated, w.Code) - assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -972,7 +973,7 @@ func TestContextRenderAPIJSON(t *testing.T) { c.JSON(http.StatusCreated, H{"foo": "bar"}) assert.Equal(t, http.StatusCreated, w.Code) - assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String()) assert.Equal(t, "application/vnd.api+json", w.Header().Get("Content-Type")) } @@ -1432,7 +1433,7 @@ func TestContextNegotiationWithJSON(t *testing.T) { }) assert.Equal(t, http.StatusOK, w.Code) - assert.JSONEq(t, "{\"foo\":\"bar\"}", w.Body.String()) + assert.JSONEq(t, `{"foo":"bar"}`, w.Body.String()) assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) } @@ -1848,9 +1849,41 @@ func TestContextContentType(t *testing.T) { assert.Equal(t, "application/json", c.ContentType()) } +func TestContextBindRequestTooLarge(t *testing.T) { + // When using sonic or go-json as JSON encoder, they do not propagate the http.MaxBytesError error + // The response will fail with a generic 400 instead of 413 + // https://github.com/goccy/go-json/issues/485 + // https://github.com/bytedance/sonic/issues/800 + var expectedCode int + switch json.Package { + case "github.com/goccy/go-json", "github.com/bytedance/sonic": + expectedCode = http.StatusBadRequest + default: + expectedCode = http.StatusRequestEntityTooLarge + } + + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) + c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 10) + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + require.Error(t, c.BindJSON(&obj)) + c.Writer.WriteHeaderNow() + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.Equal(t, expectedCode, w.Code) + assert.True(t, c.IsAborted()) +} + func TestContextAutoBindJSON(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { @@ -1867,7 +1900,7 @@ func TestContextBindWithJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type var obj struct { @@ -1884,7 +1917,7 @@ func TestContextBindWithXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(` + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(` FOO BAR @@ -1951,7 +1984,7 @@ func TestContextBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", bytes.NewBufferString("foo=unused")) + c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo", strings.NewReader("foo=unused")) var obj struct { Foo string `form:"foo"` @@ -1967,7 +2000,7 @@ func TestContextBindWithYAML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo")) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type var obj struct { @@ -1984,7 +2017,7 @@ func TestContextBindWithTOML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo = 'bar'\nbar = 'foo'")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'bar'\nbar = 'foo'")) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type var obj struct { @@ -2001,7 +2034,7 @@ func TestContextBadAutoBind(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader("\"foo\":\"bar\", \"bar\":\"foo\"}")) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { Foo string `json:"foo"` @@ -2020,7 +2053,7 @@ func TestContextBadAutoBind(t *testing.T) { func TestContextAutoShouldBindJSON(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { @@ -2037,7 +2070,7 @@ func TestContextShouldBindWithJSON(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type var obj struct { @@ -2054,7 +2087,7 @@ func TestContextShouldBindWithXML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(` + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(` FOO BAR @@ -2121,7 +2154,7 @@ func TestContextShouldBindWithQuery(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", bytes.NewBufferString("foo=unused")) + c.Request, _ = http.NewRequest(http.MethodPost, "/?foo=bar&bar=foo&Foo=bar1&Bar=foo1", strings.NewReader("foo=unused")) var obj struct { Foo string `form:"foo"` @@ -2141,7 +2174,7 @@ func TestContextShouldBindWithYAML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo: bar\nbar: foo")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo")) c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type var obj struct { @@ -2158,7 +2191,7 @@ func TestContextShouldBindWithTOML(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("foo='bar'\nbar= 'foo'")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo='bar'\nbar= 'foo'")) c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type var obj struct { @@ -2175,7 +2208,7 @@ func TestContextBadAutoShouldBind(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) - c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "http://example.com", strings.NewReader(`"foo":"bar", "bar":"foo"}`)) c.Request.Header.Add("Content-Type", MIMEJSON) var obj struct { Foo string `json:"foo"` @@ -2239,7 +2272,7 @@ func TestContextShouldBindBodyWith(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest( - http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyA), + http.MethodPost, "http://example.com", strings.NewReader(tt.bodyA), ) // When it binds to typeA and typeB, it finds the body is // not typeB but typeA. @@ -2257,7 +2290,7 @@ func TestContextShouldBindBodyWith(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) c.Request, _ = http.NewRequest( - http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyB), + http.MethodPost, "http://example.com", strings.NewReader(tt.bodyB), ) objA := typeA{} require.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA)) @@ -2603,7 +2636,7 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) { func TestContextGolangContext(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`{"foo":"bar", "bar":"foo"}`)) require.NoError(t, c.Err()) assert.Nil(t, c.Done()) ti, ok := c.Deadline() @@ -2651,7 +2684,7 @@ func TestGetRequestHeaderValue(t *testing.T) { func TestContextGetRawData(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) - body := bytes.NewBufferString("Fetch binary post data") + body := strings.NewReader("Fetch binary post data") c.Request, _ = http.NewRequest(http.MethodPost, "/", body) c.Request.Header.Add("Content-Type", MIMEPOSTForm) diff --git a/internal/json/go_json.go b/internal/json/go_json.go index 47c35598..dee09dec 100644 --- a/internal/json/go_json.go +++ b/internal/json/go_json.go @@ -20,3 +20,6 @@ var ( // NewEncoder is exported by gin/json package. NewEncoder = json.NewEncoder ) + +// Package indicates what library is being used for JSON encoding. +const Package = "github.com/goccy/go-json" diff --git a/internal/json/json.go b/internal/json/json.go index 26817786..539daa78 100644 --- a/internal/json/json.go +++ b/internal/json/json.go @@ -20,3 +20,6 @@ var ( // NewEncoder is exported by gin/json package. NewEncoder = json.NewEncoder ) + +// Package indicates what library is being used for JSON encoding. +const Package = "encoding/json" diff --git a/internal/json/jsoniter.go b/internal/json/jsoniter.go index 45ed16ba..287ebf70 100644 --- a/internal/json/jsoniter.go +++ b/internal/json/jsoniter.go @@ -21,3 +21,6 @@ var ( // NewEncoder is exported by gin/json package. NewEncoder = json.NewEncoder ) + +// Package indicates what library is being used for JSON encoding. +const Package = "github.com/json-iterator/go" diff --git a/internal/json/sonic.go b/internal/json/sonic.go index 8cd88049..b3f72424 100644 --- a/internal/json/sonic.go +++ b/internal/json/sonic.go @@ -21,3 +21,6 @@ var ( // NewEncoder is exported by gin/json package. NewEncoder = json.NewEncoder ) + +// Package indicates what library is being used for JSON encoding. +const Package = "github.com/bytedance/sonic"