Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -769,8 +769,19 @@
// 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

Check warning on line 781 in context.go

View check run for this annotation

Codecov / codecov/patch

context.go#L780-L781

Added lines #L780 - L781 were not covered by tests
default:
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck
}
return err
}
return nil
Expand Down
77 changes: 55 additions & 22 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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"))
}

Expand All @@ -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"))
}

Expand Down Expand Up @@ -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"))
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -1884,7 +1917,7 @@ func TestContextBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)

c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
<bar>BAR</bar>
Expand Down Expand Up @@ -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"`
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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"`
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -2054,7 +2087,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)

c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(`<?xml version="1.0" encoding="UTF-8"?>
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader(`<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
<bar>BAR</bar>
Expand Down Expand Up @@ -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"`
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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"`
Expand Down Expand Up @@ -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.
Expand All @@ -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))
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down
3 changes: 3 additions & 0 deletions internal/json/go_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions internal/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions internal/json/jsoniter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
3 changes: 3 additions & 0 deletions internal/json/sonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"