chore(bind): return 413 status code when error is http.MaxBytesError (#4227)

* Bind: return 413 status code when error is `http.MaxBytesError`

The Go standard library includes a method `http.MaxBytesReader` that allows limiting the request body. For example, users can create a middleware like:

```go
func MiddlewareMaxBodySize(c *gin.Context) {
	// Limit request body to 100 bytes
	c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 100)
	c.Next()
}
```

When the body exceeds the limit, reading from the request body returns an error of type `http.MaxBytesError`.

This PR makes sure that when the error is of kind `http.MaxBytesError`, Gin returns the correct status code 413 (Request Entity Too Large) instead of a generic 400 (Bad Request).

* Disable test when using sonic

* Fix

* Disable for go-json too

* Add references to GitHub issues

* Test that the response is 400 for sonic and go-json
This commit is contained in:
Alessandro (Ale) Segala 2025-05-25 05:36:33 -07:00 committed by GitHub
parent c4287b1300
commit 40725d85ba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 80 additions and 24 deletions

View File

@ -769,8 +769,19 @@ func (c *Context) BindUri(obj any) error {
// It will abort the request with HTTP 400 if any error occurs. // It will abort the request with HTTP 400 if any error occurs.
// See the binding package. // See the binding package.
func (c *Context) MustBindWith(obj any, b binding.Binding) error { func (c *Context) MustBindWith(obj any, b binding.Binding) error {
if err := c.ShouldBindWith(obj, b); err != nil { err := c.ShouldBindWith(obj, b)
c.AbortWithError(http.StatusBadRequest, err).SetType(ErrorTypeBind) //nolint: errcheck 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 err
} }
return nil return nil

View File

@ -28,6 +28,7 @@ import (
"github.com/gin-contrib/sse" "github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/internal/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -671,7 +672,7 @@ func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
func TestContextQueryAndPostForm(t *testing.T) { func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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, c.Request, _ = http.NewRequest(http.MethodPost,
"/?both=GET&id=main&id=omit&array[]=first&array[]=second&ids[a]=hi&ids[b]=3.14", body) "/?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) c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@ -946,7 +947,7 @@ func TestContextRenderJSONPWithoutCallback(t *testing.T) {
c.JSONP(http.StatusCreated, H{"foo": "bar"}) c.JSONP(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) 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")) 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"}) c.JSON(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code) 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")) 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.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")) 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()) 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) { func TestContextAutoBindJSON(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
@ -1867,7 +1900,7 @@ func TestContextBindWithJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct { var obj struct {
@ -1884,7 +1917,7 @@ func TestContextBindWithXML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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> <root>
<foo>FOO</foo> <foo>FOO</foo>
<bar>BAR</bar> <bar>BAR</bar>
@ -1951,7 +1984,7 @@ func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`
@ -1967,7 +2000,7 @@ func TestContextBindWithYAML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct { var obj struct {
@ -1984,7 +2017,7 @@ func TestContextBindWithTOML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct { var obj struct {
@ -2001,7 +2034,7 @@ func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
Foo string `json:"foo"` Foo string `json:"foo"`
@ -2020,7 +2053,7 @@ func TestContextBadAutoBind(t *testing.T) {
func TestContextAutoShouldBindJSON(t *testing.T) { func TestContextAutoShouldBindJSON(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
@ -2037,7 +2070,7 @@ func TestContextShouldBindWithJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct { var obj struct {
@ -2054,7 +2087,7 @@ func TestContextShouldBindWithXML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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> <root>
<foo>FOO</foo> <foo>FOO</foo>
<bar>BAR</bar> <bar>BAR</bar>
@ -2121,7 +2154,7 @@ func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 { var obj struct {
Foo string `form:"foo"` Foo string `form:"foo"`
@ -2141,7 +2174,7 @@ func TestContextShouldBindWithYAML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct { var obj struct {
@ -2158,7 +2191,7 @@ func TestContextShouldBindWithTOML(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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 c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type
var obj struct { var obj struct {
@ -2175,7 +2208,7 @@ func TestContextBadAutoShouldBind(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) 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) c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct { var obj struct {
Foo string `json:"foo"` Foo string `json:"foo"`
@ -2239,7 +2272,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest( 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 // When it binds to typeA and typeB, it finds the body is
// not typeB but typeA. // not typeB but typeA.
@ -2257,7 +2290,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest( c.Request, _ = http.NewRequest(
http.MethodPost, "http://example.com", bytes.NewBufferString(tt.bodyB), http.MethodPost, "http://example.com", strings.NewReader(tt.bodyB),
) )
objA := typeA{} objA := typeA{}
require.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA)) require.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
@ -2603,7 +2636,7 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) {
func TestContextGolangContext(t *testing.T) { func TestContextGolangContext(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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()) require.NoError(t, c.Err())
assert.Nil(t, c.Done()) assert.Nil(t, c.Done())
ti, ok := c.Deadline() ti, ok := c.Deadline()
@ -2651,7 +2684,7 @@ func TestGetRequestHeaderValue(t *testing.T) {
func TestContextGetRawData(t *testing.T) { func TestContextGetRawData(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) 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, _ = http.NewRequest(http.MethodPost, "/", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm) c.Request.Header.Add("Content-Type", MIMEPOSTForm)

View File

@ -20,3 +20,6 @@ var (
// NewEncoder is exported by gin/json package. // NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewEncoder = json.NewEncoder
) )
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/goccy/go-json"

View File

@ -20,3 +20,6 @@ var (
// NewEncoder is exported by gin/json package. // NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewEncoder = json.NewEncoder
) )
// Package indicates what library is being used for JSON encoding.
const Package = "encoding/json"

View File

@ -21,3 +21,6 @@ var (
// NewEncoder is exported by gin/json package. // NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewEncoder = json.NewEncoder
) )
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/json-iterator/go"

View File

@ -21,3 +21,6 @@ var (
// NewEncoder is exported by gin/json package. // NewEncoder is exported by gin/json package.
NewEncoder = json.NewEncoder NewEncoder = json.NewEncoder
) )
// Package indicates what library is being used for JSON encoding.
const Package = "github.com/bytedance/sonic"