Merge branch 'master' into feature/array_support_for_json_bind

This commit is contained in:
thinkerou 2020-10-27 10:15:29 +08:00 committed by GitHub
commit c078ab8293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 299 additions and 33 deletions

View File

@ -3,8 +3,6 @@ language: go
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.11.x
env: GO111MODULE=on
- go: 1.12.x - go: 1.12.x
env: GO111MODULE=on env: GO111MODULE=on
- go: 1.13.x - go: 1.13.x
@ -15,6 +13,10 @@ matrix:
- go: 1.14.x - go: 1.14.x
env: env:
- TESTTAGS=nomsgpack - TESTTAGS=nomsgpack
- go: 1.15.x
- go: 1.15.x
env:
- TESTTAGS=nomsgpack
- go: master - go: master
git: git:

View File

@ -8,14 +8,14 @@
## Gin v1.6.2 ## Gin v1.6.2
### BUFIXES ### BUGFIXES
* fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305) * fix missing initial sync.RWMutex [#2305](https://github.com/gin-gonic/gin/pull/2305)
### ENHANCEMENTS ### ENHANCEMENTS
* Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306) * Add set samesite in cookie. [#2306](https://github.com/gin-gonic/gin/pull/2306)
## Gin v1.6.1 ## Gin v1.6.1
### BUFIXES ### BUGFIXES
* Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294) * Revert "fix accept incoming network connections" [#2294](https://github.com/gin-gonic/gin/pull/2294)
## Gin v1.6.0 ## Gin v1.6.0
@ -25,7 +25,7 @@
* drop support govendor [#2148](https://github.com/gin-gonic/gin/pull/2148) * 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) * Added support for SameSite cookie flag [#1615](https://github.com/gin-gonic/gin/pull/1615)
### FEATURES ### 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) * FileFromFS [#2112](https://github.com/gin-gonic/gin/pull/2112)
### BUGFIXES ### BUGFIXES
* Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280) * Unix Socket Handling [#2280](https://github.com/gin-gonic/gin/pull/2280)

View File

@ -5,7 +5,7 @@
[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![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) [![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) [![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) [![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) [![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) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](https://www.codetriage.com/gin-gonic/gin)
@ -84,7 +84,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first. To install Gin package, you need to install Go and set your Go workspace first.
1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin. 1. The first need [Go](https://golang.org/) installed (**version 1.12+ is required**), then you can use the below Go command to install Gin.
```sh ```sh
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
@ -178,8 +178,8 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr
- [x] Zero allocation router. - [x] Zero allocation router.
- [x] Still the fastest http router and framework. From routing to writing. - [x] Still the fastest http router and framework. From routing to writing.
- [x] Complete suite of unit tests - [x] Complete suite of unit tests.
- [x] Battle tested - [x] Battle tested.
- [x] API frozen, new releases will not break your code. - [x] API frozen, new releases will not break your code.
## Build with [jsoniter](https://github.com/json-iterator/go) ## Build with [jsoniter](https://github.com/json-iterator/go)
@ -340,7 +340,7 @@ func main() {
``` ```
``` ```
ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou]
``` ```
### Upload files ### Upload files
@ -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 ### How to write log file
```go ```go
func main() { func main() {
@ -725,12 +758,12 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"gopkg.in/go-playground/validator.v10" "github.com/go-playground/validator/v10"
) )
// Booking contains binded and validated data. // Booking contains binded and validated data.
type Booking struct { 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"` 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 ```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!"} {"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"} {"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. [Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way.
@ -1219,6 +1255,7 @@ func main() {
} }
reader := response.Body reader := response.Body
defer reader.Close()
contentLength := response.ContentLength contentLength := response.ContentLength
contentType := response.Header.Get("Content-Type") contentType := response.Header.Get("Content-Type")

View File

@ -270,7 +270,7 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
switch tf := strings.ToLower(timeFormat); tf { switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano": case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 0) tv, err := strconv.ParseInt(val, 10, 64)
if err != nil { if err != nil {
return err return err
} }

View File

@ -190,7 +190,7 @@ func TestMappingTime(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestMapiingTimeDuration(t *testing.T) { func TestMappingTimeDuration(t *testing.T) {
var s struct { var s struct {
D time.Duration D time.Duration
} }

View File

@ -295,6 +295,22 @@ func (c *Context) GetInt64(key string) (i64 int64) {
return return
} }
// GetUint returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint(key string) (ui uint) {
if val, ok := c.Get(key); ok && val != nil {
ui, _ = val.(uint)
}
return
}
// GetUint64 returns the value associated with the key as an unsigned integer.
func (c *Context) GetUint64(key string) (ui64 uint64) {
if val, ok := c.Get(key); ok && val != nil {
ui64, _ = val.(uint64)
}
return
}
// GetFloat64 returns the value associated with the key as a float64. // GetFloat64 returns the value associated with the key as a float64.
func (c *Context) GetFloat64(key string) (f64 float64) { func (c *Context) GetFloat64(key string) (f64 float64) {
if val, ok := c.Get(key); ok && val != nil { if val, ok := c.Get(key); ok && val != nil {
@ -416,7 +432,11 @@ func (c *Context) QueryArray(key string) []string {
func (c *Context) initQueryCache() { func (c *Context) initQueryCache() {
if c.queryCache == nil { 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{}
}
} }
} }
@ -953,7 +973,7 @@ func (c *Context) File(filepath string) {
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }
// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way. // FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way.
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
defer func(old string) { defer func(old string) {
c.Request.URL.Path = old c.Request.URL.Path = old
@ -967,7 +987,7 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {
// FileAttachment writes the specified file into the body stream in an efficient way // FileAttachment writes the specified file into the body stream in an efficient way
// On the client side, the file will typically be downloaded with the given filename // On the client side, the file will typically be downloaded with the given filename
func (c *Context) FileAttachment(filepath, filename string) { func (c *Context) FileAttachment(filepath, filename string) {
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
http.ServeFile(c.Writer, c.Request, filepath) http.ServeFile(c.Writer, c.Request, filepath)
} }

View File

@ -261,6 +261,18 @@ func TestContextGetInt64(t *testing.T) {
assert.Equal(t, int64(42424242424242), c.GetInt64("int64")) assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
} }
func TestContextGetUint(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint", uint(1))
assert.Equal(t, uint(1), c.GetUint("uint"))
}
func TestContextGetUint64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("uint64", uint64(18446744073709551615))
assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64"))
}
func TestContextGetFloat64(t *testing.T) { func TestContextGetFloat64(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
c.Set("float64", 4.2) c.Set("float64", 4.2)
@ -410,6 +422,21 @@ func TestContextQuery(t *testing.T) {
assert.Empty(t, c.PostForm("foo")) 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) { func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder()) c, _ := CreateTestContext(httptest.NewRecorder())
body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second") body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
@ -940,7 +967,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type")) 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 // with specified MIME type
func TestContextRenderData(t *testing.T) { func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
@ -1255,7 +1282,7 @@ func TestContextIsAborted(t *testing.T) {
assert.True(t, c.IsAborted()) assert.True(t, c.IsAborted())
} }
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytestring`
// with specified MIME type // with specified MIME type
func TestContextAbortWithStatus(t *testing.T) { func TestContextAbortWithStatus(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()

View File

@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool {
return (msg.Type & flags) > 0 return (msg.Type & flags) > 0
} }
// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap()
func (msg *Error) Unwrap() error {
return msg.Err
}
// ByType returns a readonly copy filtered the byte. // ByType returns a readonly copy filtered the byte.
// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic.
func (a errorMsgs) ByType(typ ErrorType) errorMsgs { func (a errorMsgs) ByType(typ ErrorType) errorMsgs {

33
errors_1.13_test.go Normal file
View File

@ -0,0 +1,33 @@
// +build go1.13
package gin
import (
"errors"
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
type TestErr string
func (e TestErr) Error() string { return string(e) }
// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()".
// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13,
// hence the "// +build go1.13" directive at the beginning of this file.
func TestErrorUnwrap(t *testing.T) {
innerErr := TestErr("somme error")
// 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr
err := fmt.Errorf("wrapped: %w", &Error{
Err: innerErr,
Type: ErrorTypeAny,
})
// check that 'errors.Is()' and 'errors.As()' behave as expected :
assert.True(t, errors.Is(err, innerErr))
var testErr TestErr
assert.True(t, errors.As(err, &testErr))
}

2
go.mod
View File

@ -4,7 +4,7 @@ go 1.13
require ( require (
github.com/gin-contrib/sse v0.1.0 github.com/gin-contrib/sse v0.1.0
github.com/go-playground/validator/v10 v10.2.0 github.com/go-playground/validator/v10 v10.4.1
github.com/golang/protobuf v1.3.3 github.com/golang/protobuf v1.3.3
github.com/json-iterator/go v1.1.9 github.com/json-iterator/go v1.1.9
github.com/mattn/go-isatty v0.0.12 github.com/mattn/go-isatty v0.0.12

11
go.sum
View File

@ -9,8 +9,8 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -34,8 +34,15 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -26,13 +26,29 @@ var (
slash = []byte("/") 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. // Recovery returns a middleware that recovers from any panics and writes a 500 if there was one.
func Recovery() HandlerFunc { func Recovery() HandlerFunc {
return RecoveryWithWriter(DefaultErrorWriter) 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. // 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 var logger *log.Logger
if out != nil { if out != nil {
logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags) logger = log.New(out, "\n\n\x1b[31m", log.LstdFlags)
@ -60,23 +76,23 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc {
headers[idx] = current[0] + ": *" headers[idx] = current[0] + ": *"
} }
} }
headersToStr := strings.Join(headers, "\r\n")
if brokenPipe { if brokenPipe {
logger.Printf("%s\n%s%s", err, string(httpRequest), reset) logger.Printf("%s\n%s%s", err, headersToStr, reset)
} else if IsDebugging() { } else if IsDebugging() {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) timeFormat(time.Now()), headersToStr, err, stack, reset)
} else { } else {
logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
timeFormat(time.Now()), err, stack, reset) timeFormat(time.Now()), err, stack, reset)
} }
} }
// If the connection is dead, we can't write a status to it.
if brokenPipe { if brokenPipe {
// If the connection is dead, we can't write a status to it.
c.Error(err.(error)) // nolint: errcheck c.Error(err.(error)) // nolint: errcheck
c.Abort() c.Abort()
} else { } else {
c.AbortWithStatus(http.StatusInternalServerError) handle(c, err)
} }
} }
}() }()
@ -84,6 +100,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. // stack returns a nicely formatted stack frame, skipping skip frames.
func stack(skip int) []byte { func stack(skip int) []byte {
buf := new(bytes.Buffer) // the returned data buf := new(bytes.Buffer) // the returned data

View File

@ -62,7 +62,7 @@ func TestPanicInHandler(t *testing.T) {
assert.Equal(t, http.StatusInternalServerError, w.Code) assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, buffer.String(), "panic recovered") assert.Contains(t, buffer.String(), "panic recovered")
assert.Contains(t, buffer.String(), "Oupps, Houston, we have a problem") 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") assert.NotContains(t, buffer.String(), "GET /recovery")
// Debug mode prints the request // 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)
}

View File

@ -7,6 +7,8 @@ package render
import ( import (
"fmt" "fmt"
"net/http" "net/http"
"github.com/gin-gonic/gin/internal/bytesconv"
) )
// String contains the given interface object slice and its format. // String contains the given interface object slice and its format.
@ -34,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err
_, err = fmt.Fprintf(w, format, data...) _, err = fmt.Fprintf(w, format, data...)
return return
} }
_, err = w.Write([]byte(format)) _, err = w.Write(bytesconv.StringToBytes(format))
return return
} }

View File

@ -103,7 +103,10 @@ func parseAccept(acceptHeader string) []string {
parts := strings.Split(acceptHeader, ",") parts := strings.Split(acceptHeader, ",")
out := make([]string, 0, len(parts)) out := make([]string, 0, len(parts))
for _, part := range parts { for _, part := range parts {
if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { if i := strings.IndexByte(part, ';'); i > 0 {
part = part[:i]
}
if part = strings.TrimSpace(part); part != "" {
out = append(out, part) out = append(out, part)
} }
} }

View File

@ -18,6 +18,12 @@ func init() {
SetMode(TestMode) SetMode(TestMode)
} }
func BenchmarkParseAccept(b *testing.B) {
for i := 0; i < b.N; i++ {
parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8")
}
}
type testStruct struct { type testStruct struct {
T *testing.T T *testing.T
} }