Compare commits

..

11 Commits

Author SHA1 Message Date
Shirshendu Bhowmick
d74ff178e2
Merge 16857146c8961f809f1a6735e84556d5835eaec9 into d9307dbcbbe796a64d9e0ef23452da888dd7f904 2026-06-23 10:34:29 +05:30
Muhammad Ardy Junata
d9307dbcbb
fix(context): skip chmod on pre-existing dirs in SaveUploadedFile (#4702)
* fix: skip chmod on pre-existing dirs in SaveUploadedFile

Fixes #4622

Prior to this change, SaveUploadedFile unconditionally called
os.Chmod(dir, mode) after os.MkdirAll, even when the target
directory already existed. This caused 'operation not permitted'
errors when saving files into system-owned directories like /tmp
that the current process does not own.

The fix uses os.Stat before MkdirAll to detect whether the
directory already exists, and only calls os.Chmod when the
directory was freshly created by MkdirAll.

This restores the behaviour from v1.10.1 where only os.MkdirAll
was used (which correctly skips permission changes on existing dirs)
while preserving the custom permission feature added in v1.12.0.

Regression test added: TestSaveUploadedFileToExistingDir saves
a file into os.TempDir() (a pre-existing system directory) and
asserts no error is returned.

* test: make SaveUploadedFile #4622 regression test platform-independent

The regression test relied on os.Chmod failing on a pre-existing directory,
which only happens for a non-owner, non-root process. When tests run as root
(common in containers/CI) or with a user-owned $TMPDIR, the buggy chmod
succeeds, so the test passed even against the unfixed code.

Assert the actual contract instead: a pre-existing directory's permissions
are left unchanged. This catches the regression deterministically on every
platform.

Also tighten the SaveUploadedFile doc/comments: the requested perm is enforced
only on the newly created destination directory, not on every directory in the
path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:34:41 +08:00
Pierre F.
da1e108614
test(context): use t.TempDir() for SaveUploadedFile permission test on WSL (#4709)
* fix: change file creation to use c.Temp instead to work on wsl

* test(context): use t.TempDir() in SaveUploadedFile failure test for WSL

Mirror the WSL-portability fix applied to TestSaveUploadedFileWithPermission:
write under t.TempDir() instead of a relative path so the test does not depend
on the working directory's filesystem (drvfs reports 0o777 on WSL) and is
cleaned up automatically.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 22:43:44 +08:00
MuaazTasawar
074b669a95
test(response_writer): add tests for Flush() with and without http.Flusher (#4699)
* test(response_writer): add tests for Flush() with and without http.Flusher

* test(response_writer): drop stray comment and clarify Flush regression note

- Remove orphaned doc comment left at the end of the file
- Reword the issue #4460 reference to reflect the no-panic guard

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-06-22 21:18:24 +08:00
DadaVinqi
4a3eb31fb1
fix(recovery): record recovered panics in c.Errors (#4698)
* fix: record recovered panic errors

* chore(deps): bump quic-go to v0.59.1

* refactor(recovery): simplify panic error recording

- Collapse the if/else into a single c.Error call in defaultHandleRecovery
- Assert the recorded panic error is ErrorTypePrivate in the test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: m0_66095053 <2876430886@qq.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:56:28 +08:00
Amirhf
293ad7edeb
fix(context): Copy() copies Errors and Accepted fields (#4695)
* Previously, Copy() only copied Keys and Params, leaving Errors and
Accepted as nil even when set on the original context. Goroutines
receiving a copied context could not observe errors attached before
the copy, and content-negotiation state set by middleware was silently
lost.

* Replace assert.Equal(t, len(c.Errors), len(cp.Errors)) with assert.Len(t, cp.Errors, 2) to satisfy the testifylint linter rule

---------

Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-06-22 19:09:26 +08:00
Bo-Yi Wu
2e4d4f3896
chore(deps): bump github.com/quic-go/quic-go to v0.60.0 (#4713)
- Upgrade quic-go from v0.59.0 to v0.60.0
2026-06-22 19:00:06 +08: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
8 changed files with 322 additions and 41 deletions

View File

@ -142,6 +142,16 @@ func (c *Context) Copy() *Context {
cp.Params = make([]Param, len(cParams))
copy(cp.Params, cParams)
if c.Errors != nil {
cp.Errors = make(errorMsgs, len(c.Errors))
copy(cp.Errors, c.Errors)
}
if c.Accepted != nil {
cp.Accepted = make([]string, len(c.Accepted))
copy(cp.Accepted, c.Accepted)
}
return &cp
}
@ -620,8 +630,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
@ -717,6 +727,11 @@ func (c *Context) MultipartForm() (*multipart.Form, error) {
}
// SaveUploadedFile uploads the form file to specific dst.
// An optional perm argument specifies the permission bits used when creating
// the destination directory. If not provided, the default is 0750. The exact
// permission is enforced only on the destination directory and only when it is
// newly created by this call; pre-existing directories (e.g. /tmp) are not
// modified.
func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm ...fs.FileMode) error {
src, err := file.Open()
if err != nil {
@ -729,11 +744,19 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
mode = perm[0]
}
dir := filepath.Dir(dst)
// Record whether the destination directory exists before MkdirAll, so we
// only chmod a directory we just created. Chmod'ing a pre-existing directory
// the process does not own (e.g. /tmp) fails with "operation not permitted"
// (#4622). A non-ErrNotExist stat error also skips chmod and lets MkdirAll
// surface the underlying failure.
_, statErr := os.Stat(dir)
if err = os.MkdirAll(dir, mode); err != nil {
return err
}
if err = os.Chmod(dir, mode); err != nil {
return err
if errors.Is(statErr, os.ErrNotExist) {
if err = os.Chmod(dir, mode); err != nil {
return err
}
}
out, err := os.Create(dst)
@ -1052,6 +1075,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"
@ -248,13 +249,11 @@ func TestSaveUploadedFileWithPermission(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "permission_test", f.Filename)
var mode fs.FileMode = 0o755
require.NoError(t, c.SaveUploadedFile(f, "permission_test", mode))
t.Cleanup(func() {
assert.NoError(t, os.Remove("permission_test"))
})
info, err := os.Stat(filepath.Dir("permission_test"))
dst := filepath.Join(t.TempDir(), "subdir", "permission_test")
require.NoError(t, c.SaveUploadedFile(f, dst, mode))
info, err := os.Stat(filepath.Dir(dst))
require.NoError(t, err)
assert.Equal(t, info.Mode().Perm(), mode)
assert.Equal(t, mode, info.Mode().Perm())
}
func TestSaveUploadedFileWithPermissionFailed(t *testing.T) {
@ -272,7 +271,52 @@ func TestSaveUploadedFileWithPermissionFailed(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, "permission_test", f.Filename)
var mode fs.FileMode = 0o644
require.Error(t, c.SaveUploadedFile(f, "test/permission_test", mode))
dst := filepath.Join(t.TempDir(), "test", "permission_test")
require.Error(t, c.SaveUploadedFile(f, dst, mode))
}
// TestSaveUploadedFileToExistingDir is a regression test for issue #4622.
// SaveUploadedFile must not call os.Chmod on a directory that already exists,
// because the process may not own it (e.g. /tmp on Linux/macOS), where chmod
// fails with "operation not permitted". This asserts the behavioral contract
// directly — a pre-existing directory's permissions are left unchanged — so it
// catches the regression on every platform, including environments (root/CI,
// user-owned $TMPDIR) where chmod on the temp dir would otherwise succeed.
func TestSaveUploadedFileToExistingDir(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "existing_dir_test")
require.NoError(t, err)
_, err = w.Write([]byte("existing_dir_test"))
require.NoError(t, err)
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodPost, "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.FormFile("file")
require.NoError(t, err)
// A pre-existing directory owned by this process, set to a known mode.
dir := t.TempDir()
require.NoError(t, os.Chmod(dir, 0o700))
// Pass a perm that differs from the directory's current mode. The fix must
// not apply it to the pre-existing directory; the old code chmod'd it
// unconditionally, which also failed outright on unowned dirs like /tmp.
dst := filepath.Join(dir, "existing_dir_test.txt")
require.NoError(t, c.SaveUploadedFile(f, dst, 0o755))
// The pre-existing directory's permissions must be unchanged.
info, err := os.Stat(dir)
require.NoError(t, err)
assert.Equal(t, os.FileMode(0o700), info.Mode().Perm(),
"permissions of a pre-existing directory must not be modified")
// The file must still be written with the correct content.
content, err := os.ReadFile(dst)
require.NoError(t, err)
assert.Equal(t, "existing_dir_test", string(content))
}
func TestContextReset(t *testing.T) {
@ -689,6 +733,50 @@ func TestContextCopy(t *testing.T) {
assert.Equal(t, cp.fullPath, c.fullPath)
}
func TestContextCopyCopiesErrors(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
_ = c.Error(errors.New("first error"))
_ = c.Error(errors.New("second error"))
cp := c.Copy()
// copied context has the same errors
assert.Len(t, cp.Errors, 2)
assert.Equal(t, c.Errors[0].Error(), cp.Errors[0].Error())
assert.Equal(t, c.Errors[1].Error(), cp.Errors[1].Error())
// mutations on the copy do not affect the original
_ = cp.Error(errors.New("third error"))
assert.Len(t, c.Errors, 2)
assert.Len(t, cp.Errors, 3)
}
func TestContextCopyCopiesAccepted(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
c.SetAccepted("application/json", "text/html")
cp := c.Copy()
assert.Equal(t, c.Accepted, cp.Accepted)
// mutations on the copy do not affect the original
cp.SetAccepted("text/plain")
assert.Equal(t, []string{"application/json", "text/html"}, c.Accepted)
assert.Equal(t, []string{"text/plain"}, cp.Accepted)
}
func TestContextCopyNilErrorsAndAccepted(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest(http.MethodGet, "/", nil)
cp := c.Copy()
assert.Nil(t, cp.Errors)
assert.Nil(t, cp.Accepted)
}
func TestContextHandlerName(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
@ -2962,6 +3050,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)

10
go.mod
View File

@ -12,11 +12,11 @@ require (
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.59.0
github.com/quic-go/quic-go v0.60.0
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
)

22
go.sum
View File

@ -50,10 +50,12 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0=
github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk=
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.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=
github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0=
github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk=
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=
@ -77,15 +79,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

@ -106,7 +106,12 @@ func secureRequestDump(r *http.Request) string {
return strings.Join(lines, "\r\n")
}
func defaultHandleRecovery(c *Context, _ any) {
func defaultHandleRecovery(c *Context, err any) {
e, ok := err.(error)
if !ok {
e = fmt.Errorf("%v", err)
}
c.Error(e) //nolint: errcheck
c.AbortWithStatus(http.StatusInternalServerError)
}

View File

@ -5,6 +5,7 @@
package gin
import (
"errors"
"net"
"net/http"
"os"
@ -152,6 +153,49 @@ func TestPanicWithAbortHandler(t *testing.T) {
assert.NotContains(t, out, "panic recovered")
}
func TestPanicInHandlerRecordsError(t *testing.T) {
tests := []struct {
name string
recoveredErr any
expectedErr string
}{
{
name: "string panic",
recoveredErr: "Oops, Houston, we have a problem",
expectedErr: "Oops, Houston, we have a problem",
},
{
name: "error panic",
recoveredErr: errors.New("recovered error"),
expectedErr: "recovered error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
router := New()
var recoveredErrors errorMsgs
router.Use(func(c *Context) {
c.Next()
recoveredErrors = c.Errors
})
router.Use(RecoveryWithWriter(nil))
router.GET("/recovery", func(_ *Context) {
panic(tt.recoveredErr)
})
w := PerformRequest(router, http.MethodGet, "/recovery")
assert.Equal(t, http.StatusInternalServerError, w.Code)
if assert.Len(t, recoveredErrors, 1) {
assert.EqualError(t, recoveredErrors[0], tt.expectedErr)
assert.Equal(t, ErrorTypePrivate, recoveredErrors[0].Type)
}
})
}
}
func TestCustomRecoveryWithWriter(t *testing.T) {
errBuffer := new(strings.Builder)
buffer := new(strings.Builder)

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

@ -15,10 +15,33 @@ import (
"github.com/stretchr/testify/require"
)
// TODO
// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
// func (w *responseWriter) CloseNotify() <-chan bool {
// func (w *responseWriter) Flush() {
// TestResponseWriterFlushWithFlusher verifies Flush() calls the underlying Flusher.
func TestResponseWriterFlushWithFlusher(t *testing.T) {
testWriter := httptest.NewRecorder()
writer := &responseWriter{ResponseWriter: testWriter}
writer.Flush()
assert.True(t, testWriter.Flushed)
}
// TestResponseWriterFlushWithNonFlusher verifies Flush() is a no-op
// when the underlying ResponseWriter does not implement http.Flusher.
// Guards against the panic reported in https://github.com/gin-gonic/gin/issues/4460
func TestResponseWriterFlushWithNonFlusher(t *testing.T) {
nonFlusher := &nonFlusherWriter{header: http.Header{}}
writer := &responseWriter{ResponseWriter: nonFlusher}
require.NotPanics(t, func() {
writer.Flush()
})
}
// nonFlusherWriter is a minimal http.ResponseWriter that does NOT implement http.Flusher.
type nonFlusherWriter struct {
header http.Header
}
func (w *nonFlusherWriter) Header() http.Header { return w.header }
func (w *nonFlusherWriter) Write(b []byte) (int, error) { return len(b), nil }
func (w *nonFlusherWriter) WriteHeader(code int) {}
var (
_ ResponseWriter = &responseWriter{}
@ -113,15 +136,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()
}