Compare commits

...

5 Commits

Author SHA1 Message Date
Arhant
7133c72c8f
Merge c678dc6d8753024a73083f93d0f51d51e1d8ed3d into dcaa4296d111981ffb31ac3eba90bb63e1eb5ab9 2026-08-22 20:14:43 -07:00
Amirhf
dcaa4296d1
docs(path): fix malformed comment in cleanPath (#4723) 2026-08-15 13:44:19 +08:00
Gaurav Patil
8dd20118ba
fix(deps): bump golang.org/x/net and golang.org/x/text to patched versions (#4807)
The scheduled Trivy scan has been failing on master with two HIGH
findings, and it fails every pull request along with it:

- CVE-2026-56852, golang.org/x/text, denial of service, fixed in 0.39.0
- CVE-2026-46600, golang.org/x/net/dns/dnsmessage, denial of service,
  fixed in 0.56.0

Bump both to the first patched release. golang.org/x/crypto and
golang.org/x/sys move with them as transitive requirements of x/net.

Verified with the same settings the workflow uses:

  trivy fs --scanners vuln --severity CRITICAL,HIGH,MEDIUM \
    --ignore-unfixed --exit-code 1 .

which now exits 0. go build ./... and go test ./... pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Bo-Yi Wu <appleboy.tw@gmail.com>
2026-08-15 13:43:07 +08:00
dependabot[bot]
00cfe5aac2
chore(deps): bump the actions group across 1 directory with 4 updates (#4787)
Bumps the actions group with 4 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [actions/setup-go](https://github.com/actions/setup-go), [actions/cache](https://github.com/actions/cache) and [codecov/codecov-action](https://github.com/codecov/codecov-action).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

Updates `actions/setup-go` from 6 to 7
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v6...v7)

Updates `actions/cache` from 5 to 6
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v5...v6)

Updates `codecov/codecov-action` from 6 to 7
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/setup-go
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: actions/cache
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
- dependency-name: codecov/codecov-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-08-15 13:42:11 +08:00
dumbprism19
c678dc6d87 feat:hot reload for gin 2026-05-20 16:39:06 +05:30
10 changed files with 291 additions and 23 deletions

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL

View File

@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Setup golangci-lint
@ -55,17 +55,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: ${{ matrix.go }}
cache: false
- name: Checkout Code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
ref: ${{ github.ref }}
- uses: actions/cache@v5
- uses: actions/cache@v6
with:
path: |
${{ matrix.go-build }}
@ -78,6 +78,6 @@ jobs:
run: make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v6
uses: codecov/codecov-action@v7
with:
flags: ${{ matrix.os }},go-${{ matrix.go }},${{ matrix.test-tags }}

View File

@ -13,11 +13,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v7
with:
go-version: "^1"
- name: Run GoReleaser

View File

@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 0

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.55.0
golang.org/x/net v0.56.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.52.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.39.0 // indirect
)

16
go.sum
View File

@ -79,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.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
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/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.39.0 h1:UbZz4pLOvn600D6Oh6GGEI6VAmndrEBLv8/6BEXzyus=
golang.org/x/text v0.39.0/go.mod h1:3UwRclnC2g0TU9x8PZiyfOajCd1zaUNHF9cvqcQZ+ZM=
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=

141
hot_reload.go Normal file
View File

@ -0,0 +1,141 @@
//go:build !windows
// 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"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"syscall"
"time"
)
const hotReloadListenerEnv = "GIN_LISTENER_FD"
// RunWithHotReload runs the engine and enables zero-downtime hot reload via
// SIGHUP. On SIGHUP, a child process inherits the listening socket and begins
// serving immediately while the parent drains in-flight requests (up to 30s)
// and exits. The child handles subsequent SIGHUPs the same way.
//
// Send SIGINT or SIGTERM for a clean shutdown without spawning a replacement.
//
// Note: hot reload re-executes the same binary. Rebuilding must be handled
// externally (e.g. with make or a file watcher) before sending SIGHUP.
func (engine *Engine) RunWithHotReload(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
if engine.isUnsafeTrustedProxies() {
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
}
engine.updateRouteTrees()
if fdStr := os.Getenv(hotReloadListenerEnv); fdStr != "" {
fd, parseErr := strconv.Atoi(fdStr)
if parseErr != nil {
return fmt.Errorf("gin: invalid %s=%q: %w", hotReloadListenerEnv, fdStr, parseErr)
}
return engine.runInherited(fd)
}
address := resolveAddress(addr)
ln, err := net.Listen("tcp", address)
if err != nil {
return err
}
debugPrint("Listening and serving HTTP on %s (hot reload enabled — send SIGHUP to reload)\n", address)
return engine.serveWithSignals(ln)
}
// runInherited is the child-process entry point: it reconstructs the listener
// from an fd inherited via ExtraFiles and hands off to serveWithSignals.
func (engine *Engine) runInherited(fd int) error {
f := os.NewFile(uintptr(fd), "gin-listener")
ln, err := net.FileListener(f)
f.Close() // net.FileListener dups the fd; our copy is no longer needed
if err != nil {
return fmt.Errorf("gin: could not create listener from fd %d: %w", fd, err)
}
defer ln.Close()
debugPrint("Listening and serving HTTP on inherited socket (hot reload enabled — send SIGHUP to reload)\n")
return engine.serveWithSignals(ln)
}
// serveWithSignals starts the HTTP server on ln and blocks until a signal
// arrives. SIGHUP forks a child then drains and exits; SIGINT/SIGTERM drain
// and exit without spawning a replacement.
func (engine *Engine) serveWithSignals(ln net.Listener) error {
srv := &http.Server{Handler: engine.Handler()}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM)
defer signal.Stop(sigCh)
go srv.Serve(ln) //nolint:errcheck
for sig := range sigCh {
switch sig {
case syscall.SIGHUP:
debugPrint("received SIGHUP — forking child for zero-downtime reload\n")
if err := spawnChild(ln); err != nil {
debugPrint("hot reload fork failed: %v\n", err)
continue
}
// Give the child a moment to call Accept before we stop.
time.Sleep(100 * time.Millisecond)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := srv.Shutdown(ctx)
cancel()
return err
case syscall.SIGINT, syscall.SIGTERM:
debugPrint("received %v — shutting down gracefully\n", sig)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
err := srv.Shutdown(ctx)
cancel()
return err
}
}
return nil
}
// spawnChild forks a new instance of the current binary, passing the listening
// socket as fd 3 via ExtraFiles and advertising it through GIN_LISTENER_FD.
func spawnChild(ln net.Listener) error {
tcpLn, ok := ln.(*net.TCPListener)
if !ok {
return fmt.Errorf("gin: hot reload requires a TCP listener, got %T", ln)
}
f, err := tcpLn.File()
if err != nil {
return fmt.Errorf("gin: could not duplicate listener fd: %w", err)
}
defer f.Close()
execPath, err := os.Executable()
if err != nil {
return fmt.Errorf("gin: could not resolve executable path: %w", err)
}
cmd := exec.Command(execPath, os.Args[1:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin
cmd.Env = append(os.Environ(), fmt.Sprintf("%s=3", hotReloadListenerEnv))
cmd.ExtraFiles = []*os.File{f} // ExtraFiles[0] becomes fd 3 in the child
if err := cmd.Start(); err != nil {
return fmt.Errorf("gin: failed to start child process: %w", err)
}
go cmd.Wait() //nolint:errcheck — best-effort zombie reap before parent exits
return nil
}

111
hot_reload_test.go Normal file
View File

@ -0,0 +1,111 @@
//go:build !windows
// 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 (
"fmt"
"net"
"os"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestRunWithHotReload_InvalidFD(t *testing.T) {
t.Setenv(hotReloadListenerEnv, "not-a-number")
err := New().RunWithHotReload()
assert.ErrorContains(t, err, "invalid")
}
// TestRunWithHotReload_GracefulShutdown starts the engine via RunWithHotReload
// and verifies it shuts down cleanly on SIGTERM. signal.Notify inside
// serveWithSignals captures SIGTERM before the default handler fires, so the
// test process is not terminated.
func TestRunWithHotReload_GracefulShutdown(t *testing.T) {
// Reserve a free port then release it; there is a small TOCTOU window.
ln0, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
addr := ln0.Addr().String()
ln0.Close()
engine := New()
engine.GET("/ping", func(c *Context) { c.String(200, "pong") })
errCh := make(chan error, 1)
go func() { errCh <- engine.RunWithHotReload(addr) }()
require.Eventually(t, func() bool {
conn, err := net.DialTimeout("tcp", addr, time.Second)
if err != nil {
return false
}
conn.Close()
return true
}, 5*time.Second, 10*time.Millisecond, "server never became reachable")
proc, err := os.FindProcess(os.Getpid())
require.NoError(t, err)
require.NoError(t, proc.Signal(syscall.SIGTERM))
select {
case err := <-errCh:
assert.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("server did not shut down within 10s")
}
}
// TestRunWithHotReload_InheritedListener exercises the child-process path by
// pre-opening a TCP socket, duplicating its fd, and advertising it via the
// environment variable that runInherited reads.
func TestRunWithHotReload_InheritedListener(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
defer ln.Close()
addr := ln.Addr().String()
// tcpLn.File() dups the underlying fd; we then dup again so that
// runInherited's f.Close() doesn't affect our reference.
tcpLn := ln.(*net.TCPListener)
f, err := tcpLn.File()
require.NoError(t, err)
defer f.Close()
dupFD, err := syscall.Dup(int(f.Fd()))
require.NoError(t, err)
// dupFD is now owned by RunWithHotReload; do not close it here.
t.Setenv(hotReloadListenerEnv, fmt.Sprintf("%d", dupFD))
engine := New()
engine.GET("/ping", func(c *Context) { c.String(200, "pong") })
errCh := make(chan error, 1)
go func() { errCh <- engine.RunWithHotReload() }()
require.Eventually(t, func() bool {
conn, err := net.DialTimeout("tcp", addr, time.Second)
if err != nil {
return false
}
conn.Close()
return true
}, 5*time.Second, 10*time.Millisecond, "inherited server never became reachable")
proc, _ := os.FindProcess(os.Getpid())
proc.Signal(syscall.SIGTERM)
select {
case err := <-errCh:
assert.NoError(t, err)
case <-time.After(10 * time.Second):
t.Fatal("inherited server did not shut down within 10s")
}
}

16
hot_reload_windows.go Normal file
View File

@ -0,0 +1,16 @@
//go:build windows
// 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 "errors"
// RunWithHotReload is not supported on Windows because SIGHUP and fd
// inheritance via ExtraFiles are Unix-only primitives. Use an external
// hot-reload tool such as Air (https://github.com/air-verse/air) instead.
func (engine *Engine) RunWithHotReload(addr ...string) error {
return errors.New("gin: RunWithHotReload is not supported on Windows")
}

View File

@ -55,7 +55,7 @@ func cleanPath(p string) string {
// A bit more clunky without a 'lazybuf' like the path package, but the loop
// gets completely inlined (bufApp calls).
// loop has no expensive function calls (except 1x make) // So in contrast to the path package this loop has no expensive function
// So in contrast to the path package this loop has no expensive function
// calls (except make, if needed).
for r < n {