Compare commits

...

11 Commits

Author SHA1 Message Date
ljluestc
db3958dd1e
Merge 92006a551229eced677ce6dc07e450d4ad64e7ef into d75fcd4c9ab260e5225de590f1f0f8c0e0e12d11 2026-06-16 11:29:58 +02:00
Sai Asish Y
d75fcd4c9a
fix(response): panic on Hijack/CloseNotify when wrapper unsupported (#4645)
* response_writer: don't panic on Hijack/CloseNotify when wrapper unsupported

Closes #4638. http.TimeoutHandler's writer doesn't implement http.Hijacker/CloseNotifier; mirror Flush's graceful degradation.

* response_writer: keep Written() false when Hijack is unsupported

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>

---------

Signed-off-by: Sai Asish Y <say.apm35@gmail.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-06-02 22:02:14 +08:00
Tero Saarni
8d0468f728
chore(deps): bump golang.org/x/net to v0.55.0 (#4678)
Signed-off-by: Tero Saarni <tero.saarni@est.tech>
2026-06-02 21:50:57 +08:00
Leehainuo
88c4263538
docs(context): align inline comments in GetPostForm example (#4675)
Standardize comment alignment in GetPostForm documentation example
to improve readability and maintain consistency with code formatting
conventions.
2026-06-02 21:49:36 +08:00
Raju Ahmed
96ece6a141
feat(context): add Scheme() with proper reverse proxy support (#4655)
* feat(context): add Scheme method to determine HTTP scheme from request

* test(context): add tests for Scheme method
2026-06-02 21:48:53 +08:00
wanghaolong613
c79f5d466e
refactor: optimize error message concatenation in default_validator (#4685) 2026-06-02 21:45:58 +08:00
ljluestc
92006a5512 fix CI 2026-01-24 09:40:30 -08:00
ljluestc
8ca6e308c6 docs: remove emojis from PR description 2026-01-24 09:37:32 -08:00
ljluestc
7ae74c5272 docs: update PR description 2026-01-24 09:36:23 -08:00
ljluestc
9a76687101 feat: improve json-iterator example and fix tests 2026-01-24 09:27:47 -08:00
ljluestc
e0109c70d9 docs: add json-iterator example 2026-01-24 00:45:32 -08:00
12 changed files with 274 additions and 31 deletions

View File

@ -830,7 +830,11 @@ func TestUriBinding(t *testing.T) {
}
var not NotSupportStruct
require.Error(t, b.BindUri(m, &not))
assert.Equal(t, map[string]any(nil), not.Name)
require.Error(t, b.BindUri(m, &not))
// Check that if the map is not nil, it is empty (json-iterator may allocate map before error)
if not.Name != nil {
assert.Empty(t, not.Name)
}
}
func TestUriInnerBinding(t *testing.T) {

View File

@ -32,7 +32,10 @@ func (err SliceValidationError) Error() string {
if b.Len() > 0 {
b.WriteString("\n")
}
b.WriteString("[" + strconv.Itoa(i) + "]: " + err[i].Error())
b.WriteString("[")
b.WriteString(strconv.Itoa(i))
b.WriteString("]: ")
b.WriteString(err[i].Error())
}
}
return b.String()

View File

@ -619,8 +619,8 @@ func (c *Context) DefaultPostForm(key, defaultValue string) string {
// For example, during a PATCH request to update the user's email:
//
// email=mail@example.com --> ("mail@example.com", true) := GetPostForm("email") // set email to "mail@example.com"
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
// email= --> ("", true) := GetPostForm("email") // set email to ""
// --> ("", false) := GetPostForm("email") // do nothing with email
func (c *Context) GetPostForm(key string) (string, bool) {
if values, ok := c.GetPostFormArray(key); ok {
return values[0], ok
@ -1047,6 +1047,33 @@ func (c *Context) IsWebsocket() bool {
return false
}
// Scheme returns the HTTP scheme of the request ("http" or "https").
// When running behind reverse proxies or load balancers `Request.URL.Scheme` is usually empty.
// the original scheme is commonly forwarded via headers such as X-Forwarded-Proto.
// Reference:
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/X-Forwarded-Proto
func (c *Context) Scheme() string {
if c.Request.TLS != nil {
return "https"
}
if scheme := c.requestHeader("X-Forwarded-Proto"); scheme != "" {
return scheme
}
if scheme := c.requestHeader("X-Forwarded-Protocol"); scheme != "" {
return scheme
}
if ssl := c.requestHeader("X-Forwarded-Ssl"); ssl == "on" {
return "https"
}
if scheme := c.requestHeader("X-Url-Scheme"); scheme != "" {
return scheme
}
if scheme := c.Request.URL.Scheme; scheme != "" {
return scheme
}
return "http"
}
func (c *Context) requestHeader(key string) string {
return c.Request.Header.Get(key)
}

View File

@ -7,6 +7,7 @@ package gin
import (
"bytes"
"context"
"crypto/tls"
"errors"
"fmt"
"html/template"
@ -2138,7 +2139,7 @@ func TestContextBindRequestTooLarge(t *testing.T) {
// https://github.com/goccy/go-json/issues/485
var expectedCode int
switch json.Package {
case "github.com/goccy/go-json":
case "github.com/goccy/go-json", "github.com/json-iterator/go":
expectedCode = http.StatusBadRequest
default:
expectedCode = http.StatusRequestEntityTooLarge
@ -2955,6 +2956,65 @@ func TestWebsocketsRequired(t *testing.T) {
assert.False(t, c.IsWebsocket())
}
func TestContextScheme(t *testing.T) {
// TLS connection takes highest priority.
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.TLS = &tls.ConnectionState{}
assert.Equal(t, "https", c.Scheme())
// X-Forwarded-Proto header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Proto", "https")
assert.Equal(t, "https", c.Scheme())
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Proto", "http")
assert.Equal(t, "http", c.Scheme())
// X-Forwarded-Protocol header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Protocol", "https")
assert.Equal(t, "https", c.Scheme())
// X-Forwarded-Ssl: on header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Ssl", "on")
assert.Equal(t, "https", c.Scheme())
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Forwarded-Ssl", "off")
assert.Equal(t, "http", c.Scheme())
// X-Url-Scheme header.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.Header.Set("X-Url-Scheme", "https")
assert.Equal(t, "https", c.Scheme())
// Request.URL.Scheme fallback.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "https://example.com/", nil)
assert.Equal(t, "https", c.Scheme())
// Default fallback: plain http.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
assert.Equal(t, "http", c.Scheme())
// TLS takes priority over X-Forwarded-Proto.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.Request.TLS = &tls.ConnectionState{}
c.Request.Header.Set("X-Forwarded-Proto", "http")
assert.Equal(t, "https", c.Scheme())
}
func TestGetRequestHeaderValue(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/chat", nil)

View File

@ -0,0 +1,26 @@
# JSON Iterator Example
This example demonstrates how to integrate [json-iterator/go](https://github.com/json-iterator/go) with Gin to replace the default encoding/json for better performance.
## How it works
Gin supports custom JSON serialization and deserialization logic via the `json.API` variable. By implementing the `json.Core` interface (which includes `Marshal`, `Unmarshal`, `NewEncoder`, `NewDecoder` etc.), we can swap out the underlying JSON engine.
## Usage
1. Define your custom configuration using `jsoniter.Config`.
2. Implement the `json.Core` interface wrappers.
3. Assign your custom implementation to `json.API` before creating the Gin engine.
## Run the example
```bash
go run main.go
```
Test it:
```bash
curl http://localhost:8080/ping
# Output: {"message":"pong"}
```

View File

@ -0,0 +1,45 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
ginjson "github.com/gin-gonic/gin/codec/json"
"github.com/stretchr/testify/assert"
)
func TestJsonIterator(t *testing.T) {
// Restore default json api after test
originalAPI := ginjson.API
defer func() {
ginjson.API = originalAPI
}()
// Use custom json api
ginjson.API = customJsonApi{}
gin.SetMode(gin.TestMode)
r := gin.New()
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"hello": "world",
"foo": "bar",
})
})
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/test", nil)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
// Verify JSON response
var response map[string]string
err := json.Unmarshal(w.Body.Bytes(), &response)
assert.NoError(t, err)
assert.Equal(t, "world", response["hello"])
assert.Equal(t, "bar", response["foo"])
}

View File

@ -0,0 +1,55 @@
package main
import (
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/codec/json"
jsoniter "github.com/json-iterator/go"
)
var customConfig = jsoniter.Config{
EscapeHTML: true,
SortMapKeys: true,
ValidateJsonRawMessage: true,
}.Froze()
// customJsonApi implement api.JsonApi
type customJsonApi struct {
}
func (j customJsonApi) Marshal(v any) ([]byte, error) {
return customConfig.Marshal(v)
}
func (j customJsonApi) Unmarshal(data []byte, v any) error {
return customConfig.Unmarshal(data, v)
}
func (j customJsonApi) MarshalIndent(v any, prefix, indent string) ([]byte, error) {
return customConfig.MarshalIndent(v, prefix, indent)
}
func (j customJsonApi) NewEncoder(writer io.Writer) json.Encoder {
return customConfig.NewEncoder(writer)
}
func (j customJsonApi) NewDecoder(reader io.Reader) json.Decoder {
return customConfig.NewDecoder(reader)
}
func main() {
// Replace the default json api with json-iterator
json.API = customJsonApi{}
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"message": "pong",
})
})
r.Run(":8080")
}

View File

@ -64,7 +64,20 @@ func testRequest(t *testing.T, params ...string) {
}
func TestRunEmpty(t *testing.T) {
os.Setenv("PORT", "")
// Listen on a random available port to avoid conflicts
l, err := net.Listen("tcp", "localhost:0")
require.NoError(t, err)
defer l.Close()
addr := l.Addr().String()
_, port, err := net.SplitHostPort(addr)
require.NoError(t, err)
// Close the listener so router.Run() can bind to it (there's a small race here, but better than hardcoded 8080)
// Actually, router.Run() calls http.ListenAndServe which creates its own listener.
// If we close 'l', 'router.Run' can pick it up.
l.Close()
os.Setenv("PORT", port)
router := New()
go func() {
router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
@ -72,11 +85,11 @@ func TestRunEmpty(t *testing.T) {
}()
// Wait for server to be ready with exponential backoff
err := waitForServerReady("http://localhost:8080/example", 10)
err = waitForServerReady("http://"+addr+"/example", 10)
require.NoError(t, err, "server should start successfully")
require.Error(t, router.Run(":8080"))
testRequest(t, "http://localhost:8080/example")
require.Error(t, router.Run(":"+port))
testRequest(t, "http://"+addr+"/example")
}
func TestBadTrustedCIDRs(t *testing.T) {

8
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver/v2 v2.5.0
golang.org/x/net v0.52.0
golang.org/x/net v0.55.0
google.golang.org/protobuf v1.36.11
)
@ -39,7 +39,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.25.0 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/crypto v0.51.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
)

16
go.sum
View File

@ -77,15 +77,15 @@ 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.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE=
golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@ -114,15 +114,22 @@ func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
if w.size > 0 {
return nil, nil, errHijackAlreadyWritten
}
hijacker, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
return nil, nil, http.ErrNotSupported
}
if w.size < 0 {
w.size = 0
}
return w.ResponseWriter.(http.Hijacker).Hijack()
return hijacker.Hijack()
}
// CloseNotify implements the http.CloseNotifier interface.
func (w *responseWriter) CloseNotify() <-chan bool {
return w.ResponseWriter.(http.CloseNotifier).CloseNotify()
if cn, ok := w.ResponseWriter.(http.CloseNotifier); ok {
return cn.CloseNotify()
}
return nil
}
// Flush implements the http.Flusher interface.

View File

@ -113,15 +113,18 @@ func TestResponseWriterHijack(t *testing.T) {
writer.reset(testWriter)
w := ResponseWriter(writer)
assert.Panics(t, func() {
_, _, err := w.Hijack()
require.NoError(t, err)
})
assert.True(t, w.Written())
// httptest.ResponseRecorder doesn't implement http.Hijacker; return
// http.ErrNotSupported instead of panicking (#4638). On unsupported the
// writer state stays untouched so the handler can still emit a normal
// HTTP response as a fallback.
conn, buf, err := w.Hijack()
assert.Nil(t, conn)
assert.Nil(t, buf)
require.ErrorIs(t, err, http.ErrNotSupported)
assert.False(t, w.Written())
assert.Panics(t, func() {
w.CloseNotify()
})
// CloseNotify on a non-CloseNotifier returns nil instead of panicking.
assert.Nil(t, w.CloseNotify())
w.Flush()
}