mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-17 22:32:26 +08:00
commit
30d395aae0
@ -8,14 +8,14 @@
|
||||
|
||||
## Gin v1.6.2
|
||||
|
||||
### BUFIXES
|
||||
### BUGFIXES
|
||||
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
|
||||
### ENHANCEMENTS
|
||||
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
|
||||
|
||||
## Gin v1.6.1
|
||||
|
||||
### BUFIXES
|
||||
### BUGFIXES
|
||||
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
|
||||
|
||||
## Gin v1.6.0
|
||||
@ -25,7 +25,7 @@
|
||||
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148)
|
||||
* Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
|
||||
### FEATURES
|
||||
* add yaml negotitation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
||||
* add yaml negotiation [#2220](https://github.com/gin-gonic/gin/pull/2220)
|
||||
* FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
|
||||
### BUGFIXES
|
||||
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)
|
||||
|
46
README.md
46
README.md
@ -5,7 +5,7 @@
|
||||
[](https://travis-ci.org/gin-gonic/gin)
|
||||
[](https://codecov.io/gh/gin-gonic/gin)
|
||||
[](https://goreportcard.com/report/github.com/gin-gonic/gin)
|
||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||
[](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc)
|
||||
[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
|
||||
[](https://www.codetriage.com/gin-gonic/gin)
|
||||
@ -496,6 +496,39 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Custom Recovery behavior
|
||||
```go
|
||||
func main() {
|
||||
// Creates a router without any middleware by default
|
||||
r := gin.New()
|
||||
|
||||
// Global middleware
|
||||
// Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release.
|
||||
// By default gin.DefaultWriter = os.Stdout
|
||||
r.Use(gin.Logger())
|
||||
|
||||
// Recovery middleware recovers from any panics and writes a 500 if there was one.
|
||||
r.Use(gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
||||
if err, ok := recovered.(string); ok {
|
||||
c.String(http.StatusInternalServerError, fmt.Sprintf("error: %s", err))
|
||||
}
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}))
|
||||
|
||||
r.GET("/panic", func(c *gin.Context) {
|
||||
// panic with a string -- the custom middleware could save this to a database or report it to the user
|
||||
panic("foo")
|
||||
})
|
||||
|
||||
r.GET("/", func(c *gin.Context) {
|
||||
c.String(http.StatusOK, "ohai")
|
||||
})
|
||||
|
||||
// Listen and serve on 0.0.0.0:8080
|
||||
r.Run(":8080")
|
||||
}
|
||||
```
|
||||
|
||||
### How to write log file
|
||||
```go
|
||||
func main() {
|
||||
@ -725,12 +758,12 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"gopkg.in/go-playground/validator.v10"
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// Booking contains binded and validated data.
|
||||
type Booking struct {
|
||||
CheckIn time.Time `form:"check_in" binding:"required" time_format:"2006-01-02"`
|
||||
CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"`
|
||||
CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"`
|
||||
}
|
||||
|
||||
@ -767,11 +800,14 @@ func getBookable(c *gin.Context) {
|
||||
```
|
||||
|
||||
```console
|
||||
$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17"
|
||||
$ curl "localhost:8085/bookable?check_in=2030-04-16&check_out=2030-04-17"
|
||||
{"message":"Booking dates are valid!"}
|
||||
|
||||
$ curl "localhost:8085/bookable?check_in=2018-03-10&check_out=2018-03-09"
|
||||
$ curl "localhost:8085/bookable?check_in=2030-03-10&check_out=2030-03-09"
|
||||
{"error":"Key: 'Booking.CheckOut' Error:Field validation for 'CheckOut' failed on the 'gtfield' tag"}
|
||||
|
||||
$ curl "localhost:8085/bookable?check_in=2000-03-09&check_out=2000-03-10"
|
||||
{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"}%
|
||||
```
|
||||
|
||||
[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
|
||||
|
@ -270,7 +270,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
|
||||
|
||||
switch tf := strings.ToLower(timeFormat); tf {
|
||||
case "unix", "unixnano":
|
||||
tv, err := strconv.ParseInt(val, 10, 0)
|
||||
tv, err := strconv.ParseInt(val, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ func TestMappingTime(t *testing.T) {
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMapiingTimeDuration(t *testing.T) {
|
||||
func TestMappingTimeDuration(t *testing.T) {
|
||||
var s struct {
|
||||
D time.Duration
|
||||
}
|
||||
|
@ -416,7 +416,11 @@ func (c *Context) QueryArray(key string) []string {
|
||||
|
||||
func (c *Context) initQueryCache() {
|
||||
if c.queryCache == nil {
|
||||
c.queryCache = c.Request.URL.Query()
|
||||
if c.Request != nil {
|
||||
c.queryCache = c.Request.URL.Query()
|
||||
} else {
|
||||
c.queryCache = url.Values{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -410,6 +410,21 @@ func TestContextQuery(t *testing.T) {
|
||||
assert.Empty(t, c.PostForm("foo"))
|
||||
}
|
||||
|
||||
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
|
||||
assert.NotPanics(t, func() {
|
||||
value, ok := c.GetQuery("NoKey")
|
||||
assert.False(t, ok)
|
||||
assert.Empty(t, value)
|
||||
})
|
||||
assert.NotPanics(t, func() {
|
||||
assert.Equal(t, "nada", c.DefaultQuery("NoKey", "nada"))
|
||||
})
|
||||
assert.NotPanics(t, func() {
|
||||
assert.Empty(t, c.Query("NoKey"))
|
||||
})
|
||||
}
|
||||
|
||||
func TestContextQueryAndPostForm(t *testing.T) {
|
||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
|
||||
@ -940,7 +955,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
|
||||
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
|
||||
}
|
||||
|
||||
// TestContextData tests that the response can be written from `bytesting`
|
||||
// TestContextData tests that the response can be written from `bytestring`
|
||||
// with specified MIME type
|
||||
func TestContextRenderData(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
|
27
recovery.go
27
recovery.go
@ -26,13 +26,29 @@ var (
|
||||
slash = []byte("/")
|
||||
)
|
||||
|
||||
// RecoveryFunc defines the function passable to CustomRecovery.
|
||||
type RecoveryFunc func(c *Context, err interface{})
|
||||
|
||||
// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
|
||||
func Recovery() HandlerFunc {
|
||||
return RecoveryWithWriter(DefaultErrorWriter)
|
||||
}
|
||||
|
||||
//CustomRecovery returns a middleware that recovers from any panics and calls the provided handle func to handle it.
|
||||
func CustomRecovery(handle RecoveryFunc) HandlerFunc {
|
||||
return RecoveryWithWriter(DefaultErrorWriter, handle)
|
||||
}
|
||||
|
||||
// RecoveryWithWriter returns a middleware for a given writer that recovers from any panics and writes a 500 if there was one.
|
||||
func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
func RecoveryWithWriter(out io.Writer, recovery ...RecoveryFunc) HandlerFunc {
|
||||
if len(recovery) > 0 {
|
||||
return CustomRecoveryWithWriter(out, recovery[0])
|
||||
}
|
||||
return CustomRecoveryWithWriter(out, defaultHandleRecovery)
|
||||
}
|
||||
|
||||
// CustomRecoveryWithWriter returns a middleware for a given writer that recovers from any panics and calls the provided handle func to handle it.
|
||||
func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc {
|
||||
var logger *log.Logger
|
||||
if out != nil {
|
||||
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
|
||||
@ -70,13 +86,12 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
timeFormat(time.Now()), err, stack, reset)
|
||||
}
|
||||
}
|
||||
|
||||
// If the connection is dead, we can't write a status to it.
|
||||
if brokenPipe {
|
||||
// If the connection is dead, we can't write a status to it.
|
||||
c.Error(err.(error)) // nolint: errcheck
|
||||
c.Abort()
|
||||
} else {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
handle(c, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@ -84,6 +99,10 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
func defaultHandleRecovery(c *Context, err interface{}) {
|
||||
c.AbortWithStatus(http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
// stack returns a nicely formatted stack frame, skipping skip frames.
|
||||
func stack(skip int) []byte {
|
||||
buf := new(bytes.Buffer) // the returned data
|
||||
|
106
recovery_test.go
106
recovery_test.go
@ -62,7 +62,7 @@ func TestPanicInHandler(t *testing.T) {
|
||||
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(), "TestPanicInHandler")
|
||||
assert.Contains(t, buffer.String(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
@ -144,3 +144,107 @@ func TestPanicWithBrokenPipe(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomRecoveryWithWriter(t *testing.T) {
|
||||
errBuffer := new(bytes.Buffer)
|
||||
buffer := new(bytes.Buffer)
|
||||
router := New()
|
||||
handleRecovery := func(c *Context, err interface{}) {
|
||||
errBuffer.WriteString(err.(string))
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
router.Use(CustomRecoveryWithWriter(buffer, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/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(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
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())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
func TestCustomRecovery(t *testing.T) {
|
||||
errBuffer := new(bytes.Buffer)
|
||||
buffer := new(bytes.Buffer)
|
||||
router := New()
|
||||
DefaultErrorWriter = buffer
|
||||
handleRecovery := func(c *Context, err interface{}) {
|
||||
errBuffer.WriteString(err.(string))
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
router.Use(CustomRecovery(handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/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(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
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())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
||||
func TestRecoveryWithWriterWithCustomRecovery(t *testing.T) {
|
||||
errBuffer := new(bytes.Buffer)
|
||||
buffer := new(bytes.Buffer)
|
||||
router := New()
|
||||
DefaultErrorWriter = buffer
|
||||
handleRecovery := func(c *Context, err interface{}) {
|
||||
errBuffer.WriteString(err.(string))
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
}
|
||||
router.Use(RecoveryWithWriter(DefaultErrorWriter, handleRecovery))
|
||||
router.GET("/recovery", func(_ *Context) {
|
||||
panic("Oupps, Houston, we have a problem")
|
||||
})
|
||||
// RUN
|
||||
w := performRequest(router, "GET", "/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(), t.Name())
|
||||
assert.NotContains(t, buffer.String(), "GET /recovery")
|
||||
|
||||
// Debug mode prints the request
|
||||
SetMode(DebugMode)
|
||||
// RUN
|
||||
w = performRequest(router, "GET", "/recovery")
|
||||
// TEST
|
||||
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())
|
||||
|
||||
SetMode(TestMode)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user