mirror of
https://github.com/gin-gonic/gin.git
synced 2026-07-13 06:31:18 +08:00
Compare commits
9 Commits
44f918cc14
...
dd4e12c328
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd4e12c328 | ||
|
|
f5c267d2f8 | ||
|
|
bf52b077c8 | ||
|
|
6e3ac82fa7 | ||
|
|
71cefce08e | ||
|
|
882f42b0ed | ||
|
|
488f8c3ffa | ||
|
|
8e07d37c63 | ||
|
|
f894f4c7ed |
4
.github/workflows/trivy-scan.yml
vendored
4
.github/workflows/trivy-scan.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Run Trivy vulnerability scanner (source code)
|
||||
uses: aquasecurity/trivy-action@0.33.1
|
||||
uses: aquasecurity/trivy-action@0.34.0
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
scan-ref: '.'
|
||||
@ -44,7 +44,7 @@ jobs:
|
||||
sarif_file: 'trivy-results.sarif'
|
||||
|
||||
- name: Run Trivy scanner (table output for logs)
|
||||
uses: aquasecurity/trivy-action@0.33.1
|
||||
uses: aquasecurity/trivy-action@0.34.0
|
||||
if: always()
|
||||
with:
|
||||
scan-type: 'fs'
|
||||
|
||||
@ -36,6 +36,6 @@ Please ensure your pull request meets the following requirements:
|
||||
- All tests pass in available continuous integration systems (e.g., GitHub Actions).
|
||||
- Add or modify tests to cover your code changes.
|
||||
- If your pull request introduces a new feature, document it in [`docs/doc.md`](docs/doc.md), not in the README.
|
||||
- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md:1).
|
||||
- Follow the checklist in the [Pull Request Template](.github/PULL_REQUEST_TEMPLATE.md).
|
||||
|
||||
Thank you for contributing!
|
||||
|
||||
10
context.go
10
context.go
@ -751,8 +751,8 @@ func (c *Context) SaveUploadedFile(file *multipart.FileHeader, dst string, perm
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
//
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||
// It decodes the payload into the struct specified as a pointer.
|
||||
// It writes a 400 error and sets Content-Type header "text/plain" in the response if input is not valid.
|
||||
func (c *Context) Bind(obj any) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
@ -832,8 +832,8 @@ func (c *Context) MustBindWith(obj any, b binding.Binding) error {
|
||||
// "application/json" --> JSON binding
|
||||
// "application/xml" --> XML binding
|
||||
//
|
||||
// It parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input.
|
||||
// It decodes the json payload into the struct specified as a pointer.
|
||||
// It parses the request's body based on the Content-Type (e.g., JSON or XML).
|
||||
// It decodes the payload into the struct specified as a pointer.
|
||||
// Like c.Bind() but this method does not set the response status code to 400 or abort if input is not valid.
|
||||
func (c *Context) ShouldBind(obj any) error {
|
||||
b := binding.Default(c.Request.Method, c.ContentType())
|
||||
@ -1058,7 +1058,7 @@ func (c *Context) requestHeader(key string) string {
|
||||
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function.
|
||||
func bodyAllowedForStatus(status int) bool {
|
||||
switch {
|
||||
case status >= 100 && status <= 199:
|
||||
case status >= http.StatusContinue && status < http.StatusOK:
|
||||
return false
|
||||
case status == http.StatusNoContent:
|
||||
return false
|
||||
|
||||
@ -26,8 +26,6 @@ const (
|
||||
ErrorTypePublic ErrorType = 1 << 1
|
||||
// ErrorTypeAny indicates any other error.
|
||||
ErrorTypeAny ErrorType = 1<<64 - 1
|
||||
// ErrorTypeNu indicates any other error.
|
||||
ErrorTypeNu = 2
|
||||
)
|
||||
|
||||
// Error represents a error's specification.
|
||||
|
||||
50
gin.go
50
gin.go
@ -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
|
||||
}
|
||||
|
||||
14
go.mod
14
go.mod
@ -9,16 +9,16 @@ require (
|
||||
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
|
||||
github.com/goccy/go-yaml v1.19.1
|
||||
github.com/goccy/go-yaml v1.19.2
|
||||
github.com/json-iterator/go v1.1.12
|
||||
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.57.1
|
||||
github.com/quic-go/quic-go v0.59.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/ugorji/go/codec v1.3.1
|
||||
go.mongodb.org/mongo-driver v1.17.7
|
||||
golang.org/x/net v0.47.0
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/net v0.49.0
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
@ -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.45.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.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
|
||||
)
|
||||
|
||||
30
go.sum
30
go.sum
@ -24,8 +24,8 @@ github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.19.1 h1:3rG3+v8pkhRqoQ/88NYNMHYVGYztCOCIZ7UQhu7H+NE=
|
||||
github.com/goccy/go-yaml v1.19.1/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
@ -52,8 +52,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
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/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=
|
||||
@ -71,23 +71,21 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
go.mongodb.org/mongo-driver v1.17.7 h1:a9w+U3Vt67eYzcfq3k/OAv284/uUUkL0uP75VE5rCOU=
|
||||
go.mongodb.org/mongo-driver v1.17.7/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ=
|
||||
go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU=
|
||||
go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ=
|
||||
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.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
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=
|
||||
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
69
graceful.go
Normal 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
267
graceful_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,7 @@ func TestPanicClean(t *testing.T) {
|
||||
router.Use(RecoveryWithWriter(buffer))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery",
|
||||
@ -52,14 +52,14 @@ func TestPanicInHandler(t *testing.T) {
|
||||
router := New()
|
||||
router.Use(RecoveryWithWriter(buffer))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
@ -80,7 +80,7 @@ func TestPanicWithAbort(t *testing.T) {
|
||||
router.Use(RecoveryWithWriter(nil))
|
||||
router.GET("/recovery", func(c *Context) {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
@ -162,14 +162,14 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
}
|
||||
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
@ -181,7 +181,7 @@ func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -197,14 +197,14 @@ func TestCustomRecovery(t *testing.T) {
|
||||
}
|
||||
router.Use(CustomRecovery(handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
@ -216,7 +216,7 @@ func TestCustomRecovery(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
@ -232,14 +232,14 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||
}
|
||||
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
panic("Oops, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := PerformRequest(router, http.MethodGet, "/recovery")
|
||||
// TEST
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "panic recovered")
|
||||
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), "Oops, Houston, we have a problem")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
@ -251,7 +251,7 @@ func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||
assert.Contains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
assert.Equal(t, strings.Repeat("Oupps, Houston, we have a problem", 2), errBuffer.String())
|
||||
assert.Equal(t, strings.Repeat("Oops, Houston, we have a problem", 2), errBuffer.String())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
@ -169,7 +169,7 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
|
||||
})
|
||||
}
|
||||
|
||||
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead..
|
||||
// StaticFileFS works just like `StaticFile` but a custom `http.FileSystem` can be used instead.
|
||||
// router.StaticFileFS("favicon.ico", "./resources/favicon.ico", Dir{".", false})
|
||||
// Gin by default uses: gin.Dir()
|
||||
func (group *RouterGroup) StaticFileFS(relativePath, filepath string, fs http.FileSystem) IRoutes {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user