Compare commits

...

5 Commits

Author SHA1 Message Date
NARITA
e771fb6bae
Merge f894f4c7ed9557787a64b128bb179b8487384379 into 5260de6a83283abb87e827130accd495ad543cf3 2026-02-18 22:17:18 -08:00
dependabot[bot]
5260de6a83
chore(deps): bump golang.org/x/net from 0.49.0 to 0.50.0 (#4538)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.49.0 to 0.50.0.
- [Commits](https://github.com/golang/net/compare/v0.49.0...v0.50.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.50.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:40:02 +08:00
dependabot[bot]
5f424ff6f6
chore(deps): bump github.com/bytedance/sonic from 1.14.2 to 1.15.0 (#4539)
Bumps [github.com/bytedance/sonic](https://github.com/bytedance/sonic) from 1.14.2 to 1.15.0.
- [Release notes](https://github.com/bytedance/sonic/releases)
- [Commits](https://github.com/bytedance/sonic/compare/v1.14.2...v1.15.0)

---
updated-dependencies:
- dependency-name: github.com/bytedance/sonic
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-18 00:39:40 +08:00
Amirhf
216a4a7c28
test(render): add comprehensive tests for MsgPack render (#4537)
* test(render): add comprehensive tests for MsgPack render

* test(render): make msgpack tests deterministic

Decode the rendered msgpack output and assert values instead of comparing raw bytes (which can vary with map iteration order).
Enable MsgpackHandle.RawToString so msgpack strings decode as Go strings.

---------

Co-authored-by: AmirHossein Fallah <amirhossein.fallah@arvancloud.ir>
2026-02-18 00:38:36 +08:00
Rikiya Narita
f894f4c7ed feat(engine): add Shutdown method for graceful shutdown support
- Add server and serverLock fields to Engine struct
- Add Shutdown() method that calls http.Server.Shutdown()
- Modify Run/RunTLS/RunUnix/RunListener to store server reference
- Add RunWithShutdown convenience method with signal handling
- Add comprehensive tests for graceful shutdown (8 test cases)
- Fix lint errors (errorlint, testifylint, errcheck)
2026-01-19 11:29:52 +09:00
6 changed files with 451 additions and 29 deletions

50
gin.go
View File

@ -5,6 +5,7 @@
package gin
import (
"context"
"fmt"
"html/template"
"net"
@ -186,6 +187,11 @@ type Engine struct {
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
// server holds a reference to the HTTP server for graceful shutdown.
// This is set when one of the Run* methods is called.
server *http.Server
serverLock sync.Mutex
}
var _ IRouter = (*Engine)(nil)
@ -534,6 +540,30 @@ func parseIP(ip string) net.IP {
return parsedIP
}
// Shutdown gracefully shuts down the server without interrupting any active connections.
// Shutdown works by first closing all open listeners, then closing all idle connections,
// and then waiting indefinitely for connections to return to idle and then shut down.
// If the provided context expires before the shutdown is complete, Shutdown returns the
// context's error, otherwise it returns any error returned from closing the Server's
// underlying Listener(s).
//
// When Shutdown is called, Serve, ListenAndServe, and ListenAndServeTLS immediately
// return ErrServerClosed. Make sure the program doesn't exit and waits instead for
// Shutdown to return.
//
// This method returns nil if the server has not been started.
func (engine *Engine) Shutdown(ctx context.Context) error {
engine.serverLock.Lock()
srv := engine.server
engine.serverLock.Unlock()
if srv == nil {
return nil
}
return srv.Shutdown(ctx)
}
// Run attaches the router to a http.Server and starts listening and serving HTTP requests.
// It is a shortcut for http.ListenAndServe(addr, router)
// Note: this method will block the calling goroutine indefinitely unless an error happens.
@ -551,6 +581,11 @@ func (engine *Engine) Run(addr ...string) (err error) {
Addr: address,
Handler: engine.Handler(),
}
engine.serverLock.Lock()
engine.server = server
engine.serverLock.Unlock()
err = server.ListenAndServe()
return
}
@ -571,6 +606,11 @@ func (engine *Engine) RunTLS(addr, certFile, keyFile string) (err error) {
Addr: addr,
Handler: engine.Handler(),
}
engine.serverLock.Lock()
engine.server = server
engine.serverLock.Unlock()
err = server.ListenAndServeTLS(certFile, keyFile)
return
}
@ -597,6 +637,11 @@ func (engine *Engine) RunUnix(file string) (err error) {
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
engine.serverLock.Lock()
engine.server = server
engine.serverLock.Unlock()
err = server.Serve(listener)
return
}
@ -654,6 +699,11 @@ func (engine *Engine) RunListener(listener net.Listener) (err error) {
server := &http.Server{ // #nosec G112
Handler: engine.Handler(),
}
engine.serverLock.Lock()
engine.server = server
engine.serverLock.Unlock()
err = server.Serve(listener)
return
}

12
go.mod
View File

@ -5,7 +5,7 @@ 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.5
@ -18,7 +18,7 @@ require (
github.com/stretchr/testify v1.11.1
github.com/ugorji/go/codec v1.3.1
go.mongodb.org/mongo-driver v1.17.9
golang.org/x/net v0.49.0
golang.org/x/net v0.50.0
google.golang.org/protobuf v1.36.10
)
@ -26,7 +26,7 @@ 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
@ -41,7 +41,7 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.6.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.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
)

24
go.sum
View File

@ -1,9 +1,9 @@
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=
@ -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.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
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=

69
graceful.go Normal file
View File

@ -0,0 +1,69 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"context"
"errors"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
// ShutdownConfig holds configuration for graceful shutdown.
type ShutdownConfig struct {
// Timeout is the maximum duration to wait for active connections to finish.
// Default: 10 seconds
Timeout time.Duration
// Signals are the OS signals that will trigger shutdown.
// Default: SIGINT, SIGTERM
Signals []os.Signal
}
// RunWithShutdown starts the HTTP server and handles graceful shutdown on SIGINT/SIGTERM.
// It blocks until the server is shut down.
// The timeout parameter specifies the maximum duration to wait for active connections to finish.
func (engine *Engine) RunWithShutdown(addr string, timeout time.Duration) error {
return engine.RunWithShutdownConfig(addr, ShutdownConfig{
Timeout: timeout,
Signals: []os.Signal{syscall.SIGINT, syscall.SIGTERM},
})
}
// RunWithShutdownConfig starts the HTTP server with custom shutdown configuration.
// It blocks until the server is shut down.
func (engine *Engine) RunWithShutdownConfig(addr string, config ShutdownConfig) error {
if config.Timeout == 0 {
config.Timeout = 10 * time.Second
}
if len(config.Signals) == 0 {
config.Signals = []os.Signal{syscall.SIGINT, syscall.SIGTERM}
}
ctx, stop := signal.NotifyContext(context.Background(), config.Signals...)
defer stop()
errCh := make(chan error, 1)
go func() {
if err := engine.Run(addr); err != nil && !errors.Is(err, http.ErrServerClosed) {
errCh <- err
}
close(errCh)
}()
select {
case err := <-errCh:
return err
case <-ctx.Done():
}
shutdownCtx, cancel := context.WithTimeout(context.Background(), config.Timeout)
defer cancel()
return engine.Shutdown(shutdownCtx)
}

267
graceful_test.go Normal file
View File

@ -0,0 +1,267 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"context"
"net"
"net/http"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEngineShutdown(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Start server in goroutine
go func() {
err := router.Run(":18080")
assert.ErrorIs(t, err, http.ErrServerClosed)
}()
time.Sleep(100 * time.Millisecond) // Wait for server start
// Verify server is running
resp, err := http.Get("http://localhost:18080/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err = router.Shutdown(ctx)
require.NoError(t, err)
// Wait a moment for server to fully stop
time.Sleep(50 * time.Millisecond)
// Verify server is stopped
_, err = http.Get("http://localhost:18080/")
require.Error(t, err)
}
func TestEngineShutdownBeforeStart(t *testing.T) {
router := New()
// Shutdown before starting should not error
err := router.Shutdown(context.Background())
require.NoError(t, err)
}
func TestEngineShutdownTLS(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Start TLS server in goroutine
go func() {
err := router.RunTLS(":18443", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")
assert.ErrorIs(t, err, http.ErrServerClosed)
}()
time.Sleep(100 * time.Millisecond) // Wait for server start
// Shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := router.Shutdown(ctx)
require.NoError(t, err)
}
func TestEngineShutdownWithActiveRequest(t *testing.T) {
router := New()
requestStarted := make(chan struct{})
requestDone := make(chan struct{})
router.GET("/slow", func(c *Context) {
close(requestStarted)
time.Sleep(500 * time.Millisecond) // Simulate slow request
c.String(http.StatusOK, "done")
close(requestDone)
})
// Start server
go func() {
_ = router.Run(":18081")
}()
time.Sleep(100 * time.Millisecond)
// Start slow request
go func() {
resp, err := http.Get("http://localhost:18081/slow")
if err == nil {
resp.Body.Close()
}
}()
// Wait for request to start
<-requestStarted
// Initiate shutdown while request is in progress
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
shutdownDone := make(chan error, 1)
go func() {
shutdownDone <- router.Shutdown(ctx)
}()
// Verify request completes before shutdown finishes
select {
case <-requestDone:
// Request completed - this is expected
case err := <-shutdownDone:
t.Errorf("Shutdown completed before request finished: %v", err)
}
// Wait for shutdown to complete
err := <-shutdownDone
require.NoError(t, err)
}
func TestRunWithShutdown(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdown(":18082", 5*time.Second)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18082/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send shutdown signal to self
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGINT)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownConfig(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
config := ShutdownConfig{
Timeout: 5 * time.Second,
Signals: []os.Signal{syscall.SIGUSR1},
}
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdownConfig(":18083", config)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18083/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send custom signal
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGUSR1)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownConfigDefaults(t *testing.T) {
router := New()
router.GET("/", func(c *Context) {
c.String(http.StatusOK, "ok")
})
// Test with zero values to check defaults are applied
config := ShutdownConfig{}
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdownConfig(":18084", config)
}()
// Wait for server to start
time.Sleep(100 * time.Millisecond)
// Verify server is running
resp, err := http.Get("http://localhost:18084/")
require.NoError(t, err)
resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Send SIGINT (default signal)
p, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
err = p.Signal(syscall.SIGINT)
require.NoError(t, err)
// Wait for shutdown to complete
select {
case err := <-errCh:
require.NoError(t, err)
case <-time.After(15 * time.Second):
t.Fatal("Shutdown timed out")
}
}
func TestRunWithShutdownServerError(t *testing.T) {
router := New()
// Start a server on the same port first
listener, err := net.Listen("tcp", ":18085")
require.NoError(t, err)
defer listener.Close()
// Try to run on the same port - should fail
errCh := make(chan error, 1)
go func() {
errCh <- router.RunWithShutdown(":18085", 5*time.Second)
}()
// Should get an error because port is already in use
select {
case err := <-errCh:
require.Error(t, err)
case <-time.After(2 * time.Second):
t.Fatal("Expected error but timed out")
}
}

View File

@ -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")
}