Compare commits

...

9 Commits

Author SHA1 Message Date
ljluestc
111f368377
Merge 0cebb2da0a9921d5080255df5c18c2699eb58590 into d75fcd4c9ab260e5225de590f1f0f8c0e0e12d11 2026-06-16 11:43:15 +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
0cebb2da0a fix context copy 2025-03-23 22:40:36 -07:00
ljluestc
e5d837948a fix context error 2025-03-23 22:10:34 -07:00
ljluestc
043b245931 fix context error 2025-03-23 20:49:53 -07:00
7 changed files with 169 additions and 25 deletions

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

@ -141,6 +141,15 @@ func (c *Context) Copy() *Context {
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
cErrors := c.Errors
cp.Errors = make(errorMsgs, len(cErrors))
for i, e := range cErrors {
cp.Errors[i] = &Error{
Err: e.Err,
Type: e.Type,
Meta: e.Meta,
}
}
return &cp
}
@ -619,8 +628,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 +1056,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"
@ -688,6 +689,41 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.fullPath, c.fullPath)
}
func TestContextCopyErrors(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
// Add errors to the original context
c.Error(fmt.Errorf("first error")).SetType(ErrorTypePublic).SetMeta("meta1") // nolint: errcheck
c.Error(fmt.Errorf("second error")).SetType(ErrorTypePrivate).SetMeta(42) // nolint: errcheck
// Copy the context
cp := c.Copy()
// Verify the copied context has the same number of errors
assert.Equal(t, len(c.Errors), len(cp.Errors), "Copied context should have the same number of errors")
// Verify that the slices are distinct (deep copy) by checking contents and ensuring independence
assert.True(t, reflect.DeepEqual(c.Errors, cp.Errors), "Copied errors should have the same content initially")
// Since we cant compare slices with ==, we rely on content equality and test isolation below
// Check each error in the copied context matches the original
for i, origErr := range c.Errors {
copiedErr := cp.Errors[i]
assert.Equal(t, origErr.Err, copiedErr.Err, "Error message should match")
assert.Equal(t, origErr.Type, copiedErr.Type, "Error type should match")
assert.Equal(t, origErr.Meta, copiedErr.Meta, "Error metadata should match")
// Ensure pointers are different (deep copy)
assert.NotSame(t, origErr, copiedErr, "Each error should be a distinct instance")
}
// Modify original context errors and ensure copy remains unchanged
c.Error(fmt.Errorf("third error")) // nolint: errcheck
assert.Equal(t, 2, len(cp.Errors), "Copied context errors should not reflect changes to original")
assert.Equal(t, 3, len(c.Errors), "Original context should have new error")
assert.False(t, reflect.DeepEqual(c.Errors, cp.Errors), "Copied errors should differ after modification")
}
func TestContextHandlerName(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
@ -2955,6 +2991,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)

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()
}