diff --git a/.github/workflows/gin.yml b/.github/workflows/gin.yml index 8ece7f1d..df774eab 100644 --- a/.github/workflows/gin.yml +++ b/.github/workflows/gin.yml @@ -26,14 +26,14 @@ jobs: - name: Setup golangci-lint uses: golangci/golangci-lint-action@v9 with: - version: v2.6 + version: v2.9 args: --verbose test: needs: lint strategy: matrix: os: [ubuntu-latest, macos-latest] - go: ["1.24", "1.25"] + go: ["1.24", "1.25", "1.26"] test-tags: [ "", diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 0098b952..ea933e7e 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -21,7 +21,7 @@ jobs: with: go-version: "^1" - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 + uses: goreleaser/goreleaser-action@v7 with: # either 'goreleaser' (default) or 'goreleaser-pro' distribution: goreleaser diff --git a/.github/workflows/trivy-scan.yml b/.github/workflows/trivy-scan.yml index b86aed7f..a4c62bf4 100644 --- a/.github/workflows/trivy-scan.yml +++ b/.github/workflows/trivy-scan.yml @@ -9,7 +9,7 @@ on: - master schedule: # Run daily at 00:00 UTC - - cron: '0 0 * * *' + - cron: "0 0 * * *" workflow_dispatch: # Allow manual trigger permissions: @@ -27,30 +27,30 @@ jobs: fetch-depth: 0 - name: Run Trivy vulnerability scanner (source code) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.1 with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'sarif' - output: 'trivy-results.sarif' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "sarif" + output: "trivy-results.sarif" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - name: Upload Trivy results to GitHub Security tab uses: github/codeql-action/upload-sarif@v4 if: always() with: - sarif_file: 'trivy-results.sarif' + sarif_file: "trivy-results.sarif" - name: Run Trivy scanner (table output for logs) - uses: aquasecurity/trivy-action@0.33.1 + uses: aquasecurity/trivy-action@0.34.1 if: always() with: - scan-type: 'fs' - scan-ref: '.' - scanners: 'vuln,secret,misconfig' - format: 'table' - severity: 'CRITICAL,HIGH,MEDIUM' + scan-type: "fs" + scan-ref: "." + scanners: "vuln,secret,misconfig" + format: "table" + severity: "CRITICAL,HIGH,MEDIUM" ignore-unfixed: true - exit-code: '1' + exit-code: "1" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9703d6b4..3b05a160 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,6 +36,6 @@ Please ensure your pull request meets the following requirements: - All tests pass in available continuous integration systems (e.g., GitHub Actions). - Add or modify tests to cover your code changes. - If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README. -- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1). +- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md). Thank you for contributing! diff --git a/binding/binding.go b/binding/binding.go index 702d0e82..eced0ae2 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -23,6 +23,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -86,6 +87,7 @@ var ( Header Binding = headerBinding{} Plain BindingBody = plainBinding{} TOML BindingBody = tomlBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -110,6 +112,8 @@ func Default(method, contentType string) Binding { return TOML case MIMEMultipartPOSTForm: return FormMultipart + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_nomsgpack.go b/binding/binding_nomsgpack.go index c8e61310..ae364d79 100644 --- a/binding/binding_nomsgpack.go +++ b/binding/binding_nomsgpack.go @@ -21,6 +21,7 @@ const ( MIMEYAML = "application/x-yaml" MIMEYAML2 = "application/yaml" MIMETOML = "application/toml" + MIMEBSON = "application/bson" ) // Binding describes the interface which needs to be implemented for binding the @@ -82,6 +83,7 @@ var ( Header = headerBinding{} TOML = tomlBinding{} Plain = plainBinding{} + BSON BindingBody = bsonBinding{} ) // Default returns the appropriate Binding instance based on the HTTP method @@ -104,6 +106,8 @@ func Default(method, contentType string) Binding { return FormMultipart case MIMETOML: return TOML + case MIMEBSON: + return BSON default: // case MIMEPOSTForm: return Form } diff --git a/binding/binding_test.go b/binding/binding_test.go index 07619ebf..f90488cd 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -21,6 +21,7 @@ import ( "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -172,6 +173,9 @@ func TestBindingDefault(t *testing.T) { assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML)) assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML)) + + assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON)) + assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON)) } func TestBindingJSONNilBody(t *testing.T) { @@ -731,6 +735,18 @@ func TestBindingProtoBufFail(t *testing.T) { string(data), string(data[1:])) } +func TestBindingBSON(t *testing.T) { + var obj FooStruct + obj.Foo = "bar" + data, _ := bson.Marshal(&obj) + testBodyBinding(t, + BSON, "bson", + "/", "/", + string(data), + // note: for badbody, we remove first byte to make it invalid + string(data[1:])) +} + func TestValidationFails(t *testing.T) { var obj FooStruct req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`) diff --git a/binding/bson.go b/binding/bson.go new file mode 100644 index 00000000..464890f0 --- /dev/null +++ b/binding/bson.go @@ -0,0 +1,30 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "io" + "net/http" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +type bsonBinding struct{} + +func (bsonBinding) Name() string { + return "bson" +} + +func (b bsonBinding) Bind(req *http.Request, obj any) error { + buf, err := io.ReadAll(req.Body) + if err == nil { + err = b.BindBody(buf, obj) + } + return err +} + +func (bsonBinding) BindBody(body []byte, obj any) error { + return bson.Unmarshal(body, obj) +} diff --git a/context.go b/context.go index c7bc61fe..a00d1e55 100644 --- a/context.go +++ b/context.go @@ -40,6 +40,7 @@ const ( MIMEYAML2 = binding.MIMEYAML2 MIMETOML = binding.MIMETOML MIMEPROTOBUF = binding.MIMEPROTOBUF + MIMEBSON = binding.MIMEBSON ) // BodyBytesKey indicates a default body bytes key. @@ -750,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid. func (c *Context) Bind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) @@ -831,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error { // "application/json" --> JSON binding // "application/xml" --> XML binding // -// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. -// It decodes the json payload into the struct specified as a pointer. +// It parses the request's body based on the Content-Type (e.g., JSON or XML). +// It decodes the payload into the struct specified as a pointer. // Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid. func (c *Context) ShouldBind(obj any) error { b := binding.Default(c.Request.Method, c.ContentType()) @@ -1057,7 +1058,7 @@ func (c *Context) requestHeader(key string) string { // bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function. func bodyAllowedForStatus(status int) bool { switch { - case status >= 100 && status <= 199: + case status >= http.StatusContinue && status < http.StatusOK: return false case status == http.StatusNoContent: return false @@ -1237,6 +1238,11 @@ func (c *Context) ProtoBuf(code int, obj any) { c.Render(code, render.ProtoBuf{Data: obj}) } +// BSON serializes the given struct as BSON into the response body. +func (c *Context) BSON(code int, obj any) { + c.Render(code, render.BSON{Data: obj}) +} + // String writes the given string into the response body. func (c *Context) String(code int, format string, values ...any) { c.Render(code, render.String{Format: format, Data: values}) @@ -1344,6 +1350,7 @@ type Negotiate struct { Data any TOMLData any PROTOBUFData any + BSONData any } // Negotiate calls different Render according to acceptable Accept format. @@ -1373,6 +1380,10 @@ func (c *Context) Negotiate(code int, config Negotiate) { data := chooseData(config.PROTOBUFData, config.Data) c.ProtoBuf(code, data) + case binding.MIMEBSON: + data := chooseData(config.BSONData, config.Data) + c.BSON(code, data) + default: c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck } diff --git a/context_test.go b/context_test.go index cb534884..41ec7bd5 100644 --- a/context_test.go +++ b/context_test.go @@ -32,6 +32,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -1701,6 +1702,23 @@ func TestContextNegotiationWithPROTOBUF(t *testing.T) { assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type")) } +func TestContextNegotiationWithBSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.Request, _ = http.NewRequest(http.MethodPost, "", nil) + + c.Negotiate(http.StatusOK, Negotiate{ + Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2}, + Data: H{"foo": "bar"}, + }) + + bData, _ := bson.Marshal(H{"foo": "bar"}) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, string(bData), w.Body.String()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestContextNegotiationNotSupport(t *testing.T) { w := httptest.NewRecorder() c, _ := CreateTestContext(w) diff --git a/docs/doc.md b/docs/doc.md index 449c8d02..7201df5c 100644 --- a/docs/doc.md +++ b/docs/doc.md @@ -22,6 +22,7 @@ - [How to write log file](#how-to-write-log-file) - [Custom Log Format](#custom-log-format) - [Controlling Log output coloring](#controlling-log-output-coloring) + - [Avoid logging query strings](#avoid-loging-query-strings) - [Model binding and validation](#model-binding-and-validation) - [Custom Validators](#custom-validators) - [Only Bind Query String](#only-bind-query-string) @@ -592,6 +593,20 @@ func main() { } ``` +### Avoid logging query strings + +```go +func main() { + router := gin.New() + + // SkipQueryString indicates that the logger should not log the query string. + // For example, /path?q=1 will be logged as /path + loggerConfig := gin.LoggerConfig{SkipQueryString: true} + + router.Use(gin.LoggerWithConfig(loggerConfig)) +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML, TOML and standard form values (foo=bar&boo=baz). diff --git a/errors.go b/errors.go index 829e9d2c..c0d907b9 100644 --- a/errors.go +++ b/errors.go @@ -26,8 +26,6 @@ const ( ErrorTypePublic ErrorType = 1 << 1 // ErrorTypeAny indicates any other error. ErrorTypeAny ErrorType = 1<<64 - 1 - // ErrorTypeNu indicates any other error. - ErrorTypeNu = 2 ) // Error represents a error's specification. diff --git a/go.mod b/go.mod index b755e40d..0f278049 100644 --- a/go.mod +++ b/go.mod @@ -2,41 +2,46 @@ module github.com/gin-gonic/gin go 1.24.0 +toolchain go1.24.7 + require ( - github.com/bytedance/sonic v1.14.2 + github.com/bytedance/sonic v1.15.0 github.com/gin-contrib/sse v1.1.0 - github.com/go-playground/validator/v10 v10.28.0 - github.com/goccy/go-json v0.10.2 - github.com/goccy/go-yaml v1.19.1 + github.com/go-playground/validator/v10 v10.30.1 + github.com/goccy/go-json v0.10.5 + github.com/goccy/go-yaml v1.19.2 github.com/json-iterator/go v1.1.12 github.com/mattn/go-isatty v0.0.20 github.com/modern-go/reflect2 v1.0.2 github.com/pelletier/go-toml/v2 v2.2.4 - github.com/quic-go/quic-go v0.57.1 + github.com/quic-go/quic-go v0.59.0 github.com/stretchr/testify v1.11.1 github.com/ugorji/go/codec v1.3.1 - golang.org/x/net v0.47.0 + go.mongodb.org/mongo-driver/v2 v2.5.0 + golang.org/x/net v0.50.0 google.golang.org/protobuf v1.36.10 ) +require gopkg.in/yaml.v3 v3.0.1 // indirect + require ( github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic/loader v0.4.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/gabriel-vasile/mimetype v1.4.10 // indirect + github.com/gabriel-vasile/mimetype v1.4.12 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect + go.uber.org/mock v0.6.0 // indirect + golang.org/x/arch v0.22.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect ) diff --git a/go.sum b/go.sum index 06442efb..71c07795 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,17 @@ github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= -github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= -github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= -github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0= -github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= +github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -20,12 +20,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= -github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE= -github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -41,8 +41,9 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -51,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10= -github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -70,21 +71,21 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= -go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI= +golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= -golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/logger.go b/logger.go index 6441f7ea..cf92553a 100644 --- a/logger.go +++ b/logger.go @@ -48,6 +48,11 @@ type LoggerConfig struct { // Optional. SkipPaths []string + // SkipQueryString indicates that query strings should not be written + // for cases such as when API keys are passed via query strings. + // Optional. Default value is false. + SkipQueryString bool + // Skip is a Skipper that indicates which logs should not be written. // Optional. Skip Skipper @@ -298,7 +303,7 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.BodySize = c.Writer.Size() - if raw != "" { + if raw != "" && !conf.SkipQueryString { path = path + "?" + raw } diff --git a/logger_test.go b/logger_test.go index 53d0df95..8099a894 100644 --- a/logger_test.go +++ b/logger_test.go @@ -471,3 +471,17 @@ func TestForceConsoleColor(t *testing.T) { // reset console color mode. consoleColorMode = autoColor } + +func TestLoggerWithConfigSkipQueryString(t *testing.T) { + buffer := new(strings.Builder) + router := New() + router.Use(LoggerWithConfig(LoggerConfig{ + Output: buffer, + SkipQueryString: true, + })) + router.GET("/logged", func(c *Context) { c.Status(http.StatusOK) }) + + PerformRequest(router, "GET", "/logged?a=21") + assert.Contains(t, buffer.String(), "200") + assert.NotContains(t, buffer.String(), "a=21") +} diff --git a/recovery_test.go b/recovery_test.go index 0faa3280..028c4ad6 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) { router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery", @@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) { router := New() router.Use(RecoveryWithWriter(buffer)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) { router.Use(RecoveryWithWriter(nil)) router.GET("/recovery", func(c *Context) { c.AbortWithStatus(http.StatusBadRequest) - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") @@ -162,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) { } router.Use(CustomRecoveryWithWriter(buffer, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -181,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -197,14 +197,14 @@ func TestCustomRecovery(t *testing.T) { } router.Use(CustomRecovery(handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -216,7 +216,7 @@ func TestCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } @@ -232,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { } router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery)) router.GET("/recovery", func(_ *Context) { - panic("Oupps, Houston, we have a problem") + panic("Oops, Houston, we have a problem") }) // RUN w := PerformRequest(router, http.MethodGet, "/recovery") // TEST assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "panic recovered") - assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem") assert.Contains(t, buffer.String(), t.Name()) assert.NotContains(t, buffer.String(), "GET /recovery") @@ -251,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) { assert.Equal(t, http.StatusBadRequest, w.Code) assert.Contains(t, buffer.String(), "GET /recovery") - assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String()) + assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String()) SetMode(TestMode) } diff --git a/render/bson.go b/render/bson.go new file mode 100644 index 00000000..07f02333 --- /dev/null +++ b/render/bson.go @@ -0,0 +1,34 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "net/http" + + "go.mongodb.org/mongo-driver/v2/bson" +) + +// BSON contains the given interface object. +type BSON struct { + Data any +} + +var bsonContentType = []string{"application/bson"} + +// Render (BSON) marshals the given interface object and writes data with custom ContentType. +func (r BSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + + bytes, err := bson.Marshal(&r.Data) + if err == nil { + _, err = w.Write(bytes) + } + return err +} + +// WriteContentType (BSONBuf) writes BSONBuf ContentType. +func (r BSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, bsonContentType) +} diff --git a/render/render_msgpack_test.go b/render/render_msgpack_test.go index 579897cc..48b23870 100644 --- a/render/render_msgpack_test.go +++ b/render/render_msgpack_test.go @@ -7,7 +7,7 @@ package render import ( - "bytes" + "errors" "net/http/httptest" "testing" @@ -16,9 +16,6 @@ import ( "github.com/ugorji/go/codec" ) -// TODO unit tests -// test errors - func TestRenderMsgPack(t *testing.T) { w := httptest.NewRecorder() data := map[string]any{ @@ -32,13 +29,52 @@ func TestRenderMsgPack(t *testing.T) { require.NoError(t, err) - h := new(codec.MsgpackHandle) - assert.NotNil(t, h) - buf := bytes.NewBuffer([]byte{}) - assert.NotNil(t, buf) - err = codec.NewEncoder(buf, h).Encode(data) - + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) require.NoError(t, err) - assert.Equal(t, w.Body.String(), buf.String()) + assert.Equal(t, data, decoded) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) } + +func TestWriteMsgPack(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + "num": 42, + } + + err := WriteMsgPack(w, data) + require.NoError(t, err) + + assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) + + var decoded map[string]any + var mh codec.MsgpackHandle + mh.RawToString = true + err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded) + require.NoError(t, err) + assert.Len(t, decoded, 2) + assert.Equal(t, "bar", decoded["foo"]) + assert.EqualValues(t, 42, decoded["num"]) +} + +type failWriter struct { + *httptest.ResponseRecorder +} + +func (w *failWriter) Write(data []byte) (int, error) { + return 0, errors.New("write error") +} + +func TestRenderMsgPackError(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]any{ + "foo": "bar", + } + + err := (MsgPack{data}).Render(&failWriter{w}) + require.Error(t, err) + assert.Contains(t, err.Error(), "write error") +} diff --git a/render/render_test.go b/render/render_test.go index 3db14c3e..9c3019eb 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -20,6 +20,7 @@ import ( testdata "github.com/gin-gonic/gin/testdata/protoexample" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/protobuf/proto" ) @@ -360,6 +361,31 @@ func TestRenderProtoBufFail(t *testing.T) { require.Error(t, err) } +func TestRenderBSON(t *testing.T) { + w := httptest.NewRecorder() + reps := []int64{int64(1), int64(2)} + type mystruct struct { + Label string + Reps []int64 + } + + data := &mystruct{ + Label: "test", + Reps: reps, + } + + (BSON{data}).WriteContentType(w) + bsonData, err := bson.Marshal(data) + require.NoError(t, err) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) + + err = (BSON{data}).Render(w) + + require.NoError(t, err) + assert.Equal(t, bsonData, w.Body.Bytes()) + assert.Equal(t, "application/bson", w.Header().Get("Content-Type")) +} + func TestRenderXML(t *testing.T) { w := httptest.NewRecorder() data := xmlmap{ diff --git a/routergroup.go b/routergroup.go index b2540ec1..c01b917e 100644 --- a/routergroup.go +++ b/routergroup.go @@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes { }) } -// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.. +// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead. // router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false}) // Gin by default uses: gin.Dir() func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {