mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-17 22:32:26 +08:00
Merge branch 'master' into utree
This commit is contained in:
commit
b4789d32c1
@ -15,6 +15,10 @@ matrix:
|
||||
- go: 1.14.x
|
||||
env:
|
||||
- TESTTAGS=nomsgpack
|
||||
- go: 1.15.x
|
||||
- go: 1.15.x
|
||||
env:
|
||||
- TESTTAGS=nomsgpack
|
||||
- go: master
|
||||
|
||||
git:
|
||||
|
@ -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()
|
||||
|
@ -7,6 +7,7 @@ package gin
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"log"
|
||||
@ -64,6 +65,18 @@ func TestDebugPrintRoutes(t *testing.T) {
|
||||
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||
}
|
||||
|
||||
func TestDebugPrintRouteFunc(t *testing.T) {
|
||||
DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) {
|
||||
fmt.Fprintf(DefaultWriter, "[GIN-debug] %-6s %-40s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers)
|
||||
}
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
debugPrintRoute("GET", "/path/to/route/:param1/:param2", HandlersChain{func(c *Context) {}, handlerNameTest})
|
||||
SetMode(TestMode)
|
||||
})
|
||||
assert.Regexp(t, `^\[GIN-debug\] GET /path/to/route/:param1/:param2 --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, re)
|
||||
}
|
||||
|
||||
func TestDebugPrintLoadTemplate(t *testing.T) {
|
||||
re := captureOutput(t, func() {
|
||||
SetMode(DebugMode)
|
||||
|
20
errors.go
20
errors.go
@ -55,7 +55,7 @@ func (msg *Error) SetMeta(data interface{}) *Error {
|
||||
|
||||
// JSON creates a properly formatted JSON
|
||||
func (msg *Error) JSON() interface{} {
|
||||
json := H{}
|
||||
jsonData := H{}
|
||||
if msg.Meta != nil {
|
||||
value := reflect.ValueOf(msg.Meta)
|
||||
switch value.Kind() {
|
||||
@ -63,16 +63,16 @@ func (msg *Error) JSON() interface{} {
|
||||
return msg.Meta
|
||||
case reflect.Map:
|
||||
for _, key := range value.MapKeys() {
|
||||
json[key.String()] = value.MapIndex(key).Interface()
|
||||
jsonData[key.String()] = value.MapIndex(key).Interface()
|
||||
}
|
||||
default:
|
||||
json["meta"] = msg.Meta
|
||||
jsonData["meta"] = msg.Meta
|
||||
}
|
||||
}
|
||||
if _, ok := json["error"]; !ok {
|
||||
json["error"] = msg.Error()
|
||||
if _, ok := jsonData["error"]; !ok {
|
||||
jsonData["error"] = msg.Error()
|
||||
}
|
||||
return json
|
||||
return jsonData
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaller interface.
|
||||
@ -135,17 +135,17 @@ func (a errorMsgs) Errors() []string {
|
||||
}
|
||||
|
||||
func (a errorMsgs) JSON() interface{} {
|
||||
switch len(a) {
|
||||
switch length := len(a); length {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return a.Last().JSON()
|
||||
default:
|
||||
json := make([]interface{}, len(a))
|
||||
jsonData := make([]interface{}, length)
|
||||
for i, err := range a {
|
||||
json[i] = err.JSON()
|
||||
jsonData[i] = err.JSON()
|
||||
}
|
||||
return json
|
||||
return jsonData
|
||||
}
|
||||
}
|
||||
|
||||
|
6
fs.go
6
fs.go
@ -9,7 +9,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
|
||||
type onlyfilesFS struct {
|
||||
type onlyFilesFS struct {
|
||||
fs http.FileSystem
|
||||
}
|
||||
|
||||
@ -26,11 +26,11 @@ func Dir(root string, listDirectory bool) http.FileSystem {
|
||||
if listDirectory {
|
||||
return fs
|
||||
}
|
||||
return &onlyfilesFS{fs}
|
||||
return &onlyFilesFS{fs}
|
||||
}
|
||||
|
||||
// Open conforms to http.Filesystem.
|
||||
func (fs onlyfilesFS) Open(name string) (http.File, error) {
|
||||
func (fs onlyFilesFS) Open(name string) (http.File, error) {
|
||||
f, err := fs.fs.Open(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
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)
|
||||
}
|
||||
|
@ -187,7 +187,7 @@ func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileS
|
||||
fileServer := http.StripPrefix(absolutePath, http.FileServer(fs))
|
||||
|
||||
return func(c *Context) {
|
||||
if _, nolisting := fs.(*onlyfilesFS); nolisting {
|
||||
if _, noListing := fs.(*onlyFilesFS); noListing {
|
||||
c.Writer.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user