diff --git a/CHANGELOG.md b/CHANGELOG.md index 592c2abc..3ac51ad3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/README.md b/README.md index 771b577f..fc1d01cf 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) -[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc) +[![GoDoc](https://pkg.go.dev/badge/github.com/gin-gonic/gin?status.svg)](https://pkg.go.dev/github.com/gin-gonic/gin?tab=doc) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](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. diff --git a/binding/form_mapping.go b/binding/form_mapping.go index b81ad195..f0913ea5 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -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 } diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go index 2a560371..2675d46b 100644 --- a/binding/form_mapping_test.go +++ b/binding/form_mapping_test.go @@ -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 } diff --git a/context.go b/context.go index a9458833..95b1807d 100644 --- a/context.go +++ b/context.go @@ -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{} + } } } diff --git a/context_test.go b/context_test.go index ce077bc6..1a5a3c5f 100644 --- a/context_test.go +++ b/context_test.go @@ -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() diff --git a/recovery.go b/recovery.go index 8cf0932a..d02b829b 100644 --- a/recovery.go +++ b/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 diff --git a/recovery_test.go b/recovery_test.go index 21a0a480..6cc2a47a 100644 --- a/recovery_test.go +++ b/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) +}