Compare commits

...

4 Commits

Author SHA1 Message Date
Milad
9502ba0c77
Merge d02b7391f00946e0390ae5c3d684dd343f30a49b into b2b489dbf4826c2c630717a77fd5e42774625410 2026-01-18 17:57:33 -08:00
WeidiDeng
b2b489dbf4
chore(context): always trust xff headers from unix socket (#3359)
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-01-18 12:56:22 +08:00
OHZEKI Naoki
3ab698dc51
refactor(recovery): smart error comparison (#4142)
* refactor(recovery): rename var in CustomRecoveryWithWriter

* refactor(recovery): smart error comparison

* test(recovery): Directly reference the syscall error string
2026-01-17 16:40:43 +08:00
Miladev95
d02b7391f0 add plain binding tests 2025-12-23 16:09:33 +03:30
5 changed files with 145 additions and 35 deletions

93
binding/plain_test.go Normal file
View File

@ -0,0 +1,93 @@
// 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 (
"bytes"
"errors"
"io"
"net/http"
"strings"
"testing"
)
// errReadCloser simulates a ReadCloser whose Read returns a fixed error.
type errReadCloser struct{ err error }
func (e *errReadCloser) Read(p []byte) (int, error) { return 0, e.err }
func (e *errReadCloser) Close() error { return nil }
func TestDecodePlain_String_Success(t *testing.T) {
t.Parallel()
var s string
if err := (plainBinding{}).BindBody([]byte("hello world"), &s); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if s != "hello world" {
t.Fatalf("expected %q, got %q", "hello world", s)
}
}
func TestDecodePlain_ByteSlice_Success(t *testing.T) {
t.Parallel()
in := []byte{1, 2, 3, 4}
var b []byte
if err := (plainBinding{}).BindBody(in, &b); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !bytes.Equal(b, in) {
t.Fatalf("expected %v, got %v", in, b)
}
}
func TestPlainBind_UsesHTTPRequestBody(t *testing.T) {
t.Parallel()
var s string
req := &http.Request{Body: io.NopCloser(bytes.NewReader([]byte("reqbody")))}
if err := (plainBinding{}).Bind(req, &s); err != nil {
t.Fatalf("unexpected error: %v", err)
}
if s != "reqbody" {
t.Fatalf("expected %q, got %q", "reqbody", s)
}
}
func TestDecodePlain_NilObj_NoPanic(t *testing.T) {
// Passing nil obj should be a no-op and return nil error.
if err := (plainBinding{}).BindBody([]byte("x"), nil); err != nil {
t.Fatalf("expected nil error for nil obj, got %v", err)
}
// Passing a nil pointer (e.g., *string == nil) should also return nil error.
var ps *string = nil
if err := (plainBinding{}).BindBody([]byte("x"), ps); err != nil {
t.Fatalf("expected nil error for nil pointer obj, got %v", err)
}
}
func TestDecodePlain_UnsupportedType_Error(t *testing.T) {
var x int
err := (plainBinding{}).BindBody([]byte("x"), &x)
if err == nil {
t.Fatalf("expected error for unsupported type, got nil")
}
if !strings.Contains(err.Error(), "unknown type") {
t.Fatalf("expected error to contain 'unknown type', got %v", err)
}
}
func TestPlainBind_ReadError(t *testing.T) {
t.Parallel()
sentinel := errors.New("read fail")
req := &http.Request{Body: &errReadCloser{err: sentinel}}
var s string
err := (plainBinding{}).Bind(req, &s)
if err == nil {
t.Fatalf("expected read error, got nil")
}
if err != sentinel {
t.Fatalf("expected sentinel error %v, got %v", sentinel, err)
}
}

View File

@ -978,14 +978,27 @@ func (c *Context) ClientIP() string {
}
}
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined by Engine.SetTrustedProxies()
remoteIP := net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
var (
trusted bool
remoteIP net.IP
)
// If gin is listening a unix socket, always trust it.
localAddr, ok := c.Request.Context().Value(http.LocalAddrContextKey).(net.Addr)
if ok && strings.HasPrefix(localAddr.Network(), "unix") {
trusted = true
}
// Fallback
if !trusted {
// It also checks if the remoteIP is a trusted proxy or not.
// In order to perform this validation, it will see if the IP is contained within at least one of the CIDR blocks
// defined by Engine.SetTrustedProxies()
remoteIP = net.ParseIP(c.RemoteIP())
if remoteIP == nil {
return ""
}
trusted = c.engine.isTrustedProxy(remoteIP)
}
trusted := c.engine.isTrustedProxy(remoteIP)
if trusted && c.engine.ForwardedByClientIP && c.engine.RemoteIPHeaders != nil {
for _, headerName := range c.engine.RemoteIPHeaders {

View File

@ -1915,6 +1915,16 @@ func TestContextClientIP(t *testing.T) {
c.engine.trustedCIDRs, _ = c.engine.prepareTrustedCIDRs()
resetContextForClientIPTests(c)
// unix address
addr := &net.UnixAddr{Net: "unix", Name: "@"}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), http.LocalAddrContextKey, addr))
c.Request.RemoteAddr = addr.String()
assert.Equal(t, "20.20.20.20", c.ClientIP())
// reset
c.Request = c.Request.WithContext(context.Background())
resetContextForClientIPTests(c)
// Legacy tests (validating that the defaults don't break the
// (insecure!) old behaviour)
assert.Equal(t, "20.20.20.20", c.ClientIP())

View File

@ -12,12 +12,12 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"os"
"runtime"
"strings"
"syscall"
"time"
"github.com/gin-gonic/gin/internal/bytesconv"
@ -57,40 +57,33 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
}
return func(c *Context) {
defer func() {
if err := recover(); err != nil {
if rec := recover(); rec != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {
var se *os.SyscallError
if errors.As(ne, &se) {
seStr := strings.ToLower(se.Error())
if strings.Contains(seStr, "broken pipe") ||
strings.Contains(seStr, "connection reset by peer") {
brokenPipe = true
}
}
}
if e, ok := err.(error); ok && errors.Is(e, http.ErrAbortHandler) {
brokenPipe = true
var isBrokenPipe bool
err, ok := rec.(error)
if ok {
isBrokenPipe = errors.Is(err, syscall.EPIPE) ||
errors.Is(err, syscall.ECONNRESET) ||
errors.Is(err, http.ErrAbortHandler)
}
if logger != nil {
if brokenPipe {
logger.Printf("%s\n%s%s", err, secureRequestDump(c.Request), reset)
if isBrokenPipe {
logger.Printf("%s\n%s%s", rec, secureRequestDump(c.Request), reset)
} else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), secureRequestDump(c.Request), err, stack(stackSkip), reset)
timeFormat(time.Now()), secureRequestDump(c.Request), rec, stack(stackSkip), reset)
} else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack(stackSkip), reset)
timeFormat(time.Now()), rec, stack(stackSkip), reset)
}
}
if brokenPipe {
if isBrokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) //nolint: errcheck
c.Error(err) //nolint: errcheck
c.Abort()
} else {
handle(c, err)
handle(c, rec)
}
}
}()

View File

@ -98,13 +98,13 @@ func TestFunction(t *testing.T) {
func TestPanicWithBrokenPipe(t *testing.T) {
const expectCode = 204
expectMsgs := map[syscall.Errno]string{
syscall.EPIPE: "broken pipe",
syscall.ECONNRESET: "connection reset by peer",
expectErrnos := []syscall.Errno{
syscall.EPIPE,
syscall.ECONNRESET,
}
for errno, expectMsg := range expectMsgs {
t.Run(expectMsg, func(t *testing.T) {
for _, errno := range expectErrnos {
t.Run("Recovery from "+errno.Error(), func(t *testing.T) {
var buf strings.Builder
router := New()
@ -122,7 +122,8 @@ func TestPanicWithBrokenPipe(t *testing.T) {
w := PerformRequest(router, http.MethodGet, "/recovery")
// TEST
assert.Equal(t, expectCode, w.Code)
assert.Contains(t, strings.ToLower(buf.String()), expectMsg)
assert.Contains(t, strings.ToLower(buf.String()), errno.Error())
assert.NotContains(t, strings.ToLower(buf.String()), "[Recovery]")
})
}
}