mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-22 01:12:16 +08:00
Merge remote-tracking branch 'upstream/master' into split-examples
This commit is contained in:
commit
db90057613
@ -7,6 +7,7 @@ go:
|
|||||||
- 1.9.x
|
- 1.9.x
|
||||||
- 1.10.x
|
- 1.10.x
|
||||||
- 1.11.x
|
- 1.11.x
|
||||||
|
- 1.12.x
|
||||||
- master
|
- master
|
||||||
|
|
||||||
matrix:
|
matrix:
|
||||||
@ -14,6 +15,8 @@ matrix:
|
|||||||
include:
|
include:
|
||||||
- go: 1.11.x
|
- go: 1.11.x
|
||||||
env: GO111MODULE=on
|
env: GO111MODULE=on
|
||||||
|
- go: 1.12.x
|
||||||
|
env: GO111MODULE=on
|
||||||
|
|
||||||
git:
|
git:
|
||||||
depth: 10
|
depth: 10
|
||||||
|
@ -61,6 +61,10 @@ type FooStructForMapType struct {
|
|||||||
MapFoo map[string]interface{} `form:"map_foo"`
|
MapFoo map[string]interface{} `form:"map_foo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FooStructForIgnoreFormTag struct {
|
||||||
|
Foo *string `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
type InvalidNameType struct {
|
type InvalidNameType struct {
|
||||||
TestName string `invalid_name:"test_name"`
|
TestName string `invalid_name:"test_name"`
|
||||||
}
|
}
|
||||||
@ -278,6 +282,12 @@ func TestBindingFormForTime2(t *testing.T) {
|
|||||||
"", "")
|
"", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFormBindingIgnoreField(t *testing.T) {
|
||||||
|
testFormBindingIgnoreField(t, "POST",
|
||||||
|
"/", "/",
|
||||||
|
"-=bar", "")
|
||||||
|
}
|
||||||
|
|
||||||
func TestBindingFormInvalidName(t *testing.T) {
|
func TestBindingFormInvalidName(t *testing.T) {
|
||||||
testFormBindingInvalidName(t, "POST",
|
testFormBindingInvalidName(t, "POST",
|
||||||
"/", "/",
|
"/", "/",
|
||||||
@ -860,6 +870,21 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testFormBindingIgnoreField(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
|
b := Form
|
||||||
|
assert.Equal(t, "form", b.Name())
|
||||||
|
|
||||||
|
obj := FooStructForIgnoreFormTag{}
|
||||||
|
req := requestWithBody(method, path, body)
|
||||||
|
if method == "POST" {
|
||||||
|
req.Header.Add("Content-Type", MIMEPOSTForm)
|
||||||
|
}
|
||||||
|
err := b.Bind(req, &obj)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Nil(t, obj.Foo)
|
||||||
|
}
|
||||||
|
|
||||||
func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) {
|
func testFormBindingInvalidName(t *testing.T, method, path, badPath, body, badBody string) {
|
||||||
b := Form
|
b := Form
|
||||||
assert.Equal(t, "form", b.Name())
|
assert.Equal(t, "form", b.Name())
|
||||||
|
@ -41,6 +41,9 @@ func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error {
|
|||||||
defaultValue = defaultList[1]
|
defaultValue = defaultList[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if inputFieldName == "-" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if inputFieldName == "" {
|
if inputFieldName == "" {
|
||||||
inputFieldName = typeField.Name
|
inputFieldName = typeField.Name
|
||||||
|
|
||||||
|
35
context.go
35
context.go
@ -6,6 +6,7 @@ package gin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"math"
|
"math"
|
||||||
@ -82,6 +83,10 @@ func (c *Context) Copy() *Context {
|
|||||||
cp.Writer = &cp.writermem
|
cp.Writer = &cp.writermem
|
||||||
cp.index = abortIndex
|
cp.index = abortIndex
|
||||||
cp.handlers = nil
|
cp.handlers = nil
|
||||||
|
cp.Keys = map[string]interface{}{}
|
||||||
|
for k, v := range c.Keys {
|
||||||
|
cp.Keys[k] = v
|
||||||
|
}
|
||||||
return &cp
|
return &cp
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,6 +96,16 @@ func (c *Context) HandlerName() string {
|
|||||||
return nameOfFunction(c.handlers.Last())
|
return nameOfFunction(c.handlers.Last())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HandlerNames returns a list of all registered handlers for this context in descending order,
|
||||||
|
// following the semantics of HandlerName()
|
||||||
|
func (c *Context) HandlerNames() []string {
|
||||||
|
hn := make([]string, 0, len(c.handlers))
|
||||||
|
for _, val := range c.handlers {
|
||||||
|
hn = append(hn, nameOfFunction(val))
|
||||||
|
}
|
||||||
|
return hn
|
||||||
|
}
|
||||||
|
|
||||||
// Handler returns the main handler.
|
// Handler returns the main handler.
|
||||||
func (c *Context) Handler() HandlerFunc {
|
func (c *Context) Handler() HandlerFunc {
|
||||||
return c.handlers.Last()
|
return c.handlers.Last()
|
||||||
@ -866,6 +881,13 @@ func (c *Context) File(filepath string) {
|
|||||||
http.ServeFile(c.Writer, c.Request, filepath)
|
http.ServeFile(c.Writer, c.Request, filepath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func (c *Context) FileAttachment(filepath, filename string) {
|
||||||
|
c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename))
|
||||||
|
http.ServeFile(c.Writer, c.Request, filepath)
|
||||||
|
}
|
||||||
|
|
||||||
// SSEvent writes a Server-Sent Event into the body stream.
|
// SSEvent writes a Server-Sent Event into the body stream.
|
||||||
func (c *Context) SSEvent(name string, message interface{}) {
|
func (c *Context) SSEvent(name string, message interface{}) {
|
||||||
c.Render(-1, sse.Event{
|
c.Render(-1, sse.Event{
|
||||||
@ -938,7 +960,18 @@ func (c *Context) NegotiateFormat(offered ...string) string {
|
|||||||
}
|
}
|
||||||
for _, accepted := range c.Accepted {
|
for _, accepted := range c.Accepted {
|
||||||
for _, offert := range offered {
|
for _, offert := range offered {
|
||||||
if accepted == offert {
|
// According to RFC 2616 and RFC 2396, non-ASCII characters are not allowed in headers,
|
||||||
|
// therefore we can just iterate over the string without casting it into []rune
|
||||||
|
i := 0
|
||||||
|
for ; i < len(accepted); i++ {
|
||||||
|
if accepted[i] == '*' || offert[i] == '*' {
|
||||||
|
return offert
|
||||||
|
}
|
||||||
|
if accepted[i] != offert[i] {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if i == len(accepted) {
|
||||||
return offert
|
return offert
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -331,6 +331,8 @@ func TestContextCopy(t *testing.T) {
|
|||||||
assert.Equal(t, cp.Keys, c.Keys)
|
assert.Equal(t, cp.Keys, c.Keys)
|
||||||
assert.Equal(t, cp.engine, c.engine)
|
assert.Equal(t, cp.engine, c.engine)
|
||||||
assert.Equal(t, cp.Params, c.Params)
|
assert.Equal(t, cp.Params, c.Params)
|
||||||
|
cp.Set("foo", "notBar")
|
||||||
|
assert.False(t, cp.Keys["foo"] == c.Keys["foo"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContextHandlerName(t *testing.T) {
|
func TestContextHandlerName(t *testing.T) {
|
||||||
@ -340,10 +342,26 @@ func TestContextHandlerName(t *testing.T) {
|
|||||||
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
|
assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextHandlerNames(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest, func(c *Context) {}, handlerNameTest2}
|
||||||
|
|
||||||
|
names := c.HandlerNames()
|
||||||
|
|
||||||
|
assert.True(t, len(names) == 4)
|
||||||
|
for _, name := range names {
|
||||||
|
assert.Regexp(t, `^(.*/vendor/)?(github\.com/gin-gonic/gin\.){1}(TestContextHandlerNames\.func.*){0,1}(handlerNameTest.*){0,1}`, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func handlerNameTest(c *Context) {
|
func handlerNameTest(c *Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handlerNameTest2(c *Context) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
var handlerTest HandlerFunc = func(c *Context) {
|
var handlerTest HandlerFunc = func(c *Context) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -961,6 +979,19 @@ func TestContextRenderFile(t *testing.T) {
|
|||||||
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextRenderAttachment(t *testing.T) {
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
c, _ := CreateTestContext(w)
|
||||||
|
newFilename := "new_filename.go"
|
||||||
|
|
||||||
|
c.Request, _ = http.NewRequest("GET", "/", nil)
|
||||||
|
c.FileAttachment("./gin.go", newFilename)
|
||||||
|
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "func New() *Engine {")
|
||||||
|
assert.Equal(t, fmt.Sprintf("attachment; filename=\"%s\"", newFilename), w.HeaderMap.Get("Content-Disposition"))
|
||||||
|
}
|
||||||
|
|
||||||
// TestContextRenderYAML tests that the response is serialized as YAML
|
// TestContextRenderYAML tests that the response is serialized as YAML
|
||||||
// and Content-Type is set to application/x-yaml
|
// and Content-Type is set to application/x-yaml
|
||||||
func TestContextRenderYAML(t *testing.T) {
|
func TestContextRenderYAML(t *testing.T) {
|
||||||
@ -1140,17 +1171,41 @@ func TestContextNegotiationFormat(t *testing.T) {
|
|||||||
func TestContextNegotiationFormatWithAccept(t *testing.T) {
|
func TestContextNegotiationFormatWithAccept(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
|
||||||
|
|
||||||
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML))
|
assert.Equal(t, MIMEXML, c.NegotiateFormat(MIMEJSON, MIMEXML))
|
||||||
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML))
|
assert.Equal(t, MIMEHTML, c.NegotiateFormat(MIMEXML, MIMEHTML))
|
||||||
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
|
assert.Empty(t, c.NegotiateFormat(MIMEJSON))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextNegotiationFormatWithWildcardAccept(t *testing.T) {
|
||||||
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("Accept", "*/*")
|
||||||
|
|
||||||
|
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
|
||||||
|
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
|
||||||
|
assert.Equal(t, c.NegotiateFormat("application/*"), "application/*")
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON)
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEXML), MIMEXML)
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
|
||||||
|
|
||||||
|
c, _ = CreateTestContext(httptest.NewRecorder())
|
||||||
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
|
c.Request.Header.Add("Accept", "text/*")
|
||||||
|
|
||||||
|
assert.Equal(t, c.NegotiateFormat("*/*"), "*/*")
|
||||||
|
assert.Equal(t, c.NegotiateFormat("text/*"), "text/*")
|
||||||
|
assert.Equal(t, c.NegotiateFormat("application/*"), "")
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEJSON), "")
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEXML), "")
|
||||||
|
assert.Equal(t, c.NegotiateFormat(MIMEHTML), MIMEHTML)
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextNegotiationFormatCustom(t *testing.T) {
|
func TestContextNegotiationFormatCustom(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
c.Request, _ = http.NewRequest("POST", "/", nil)
|
c.Request, _ = http.NewRequest("POST", "/", nil)
|
||||||
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
|
c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9;q=0.8")
|
||||||
|
|
||||||
c.Accepted = nil
|
c.Accepted = nil
|
||||||
c.SetAccepted(MIMEJSON, MIMEXML)
|
c.SetAccepted(MIMEJSON, MIMEXML)
|
||||||
@ -1224,22 +1279,24 @@ func TestContextError(t *testing.T) {
|
|||||||
c, _ := CreateTestContext(httptest.NewRecorder())
|
c, _ := CreateTestContext(httptest.NewRecorder())
|
||||||
assert.Empty(t, c.Errors)
|
assert.Empty(t, c.Errors)
|
||||||
|
|
||||||
c.Error(errors.New("first error")) // nolint: errcheck
|
firstErr := errors.New("first error")
|
||||||
|
c.Error(firstErr) // nolint: errcheck
|
||||||
assert.Len(t, c.Errors, 1)
|
assert.Len(t, c.Errors, 1)
|
||||||
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
|
assert.Equal(t, "Error #01: first error\n", c.Errors.String())
|
||||||
|
|
||||||
|
secondErr := errors.New("second error")
|
||||||
c.Error(&Error{ // nolint: errcheck
|
c.Error(&Error{ // nolint: errcheck
|
||||||
Err: errors.New("second error"),
|
Err: secondErr,
|
||||||
Meta: "some data 2",
|
Meta: "some data 2",
|
||||||
Type: ErrorTypePublic,
|
Type: ErrorTypePublic,
|
||||||
})
|
})
|
||||||
assert.Len(t, c.Errors, 2)
|
assert.Len(t, c.Errors, 2)
|
||||||
|
|
||||||
assert.Equal(t, errors.New("first error"), c.Errors[0].Err)
|
assert.Equal(t, firstErr, c.Errors[0].Err)
|
||||||
assert.Nil(t, c.Errors[0].Meta)
|
assert.Nil(t, c.Errors[0].Meta)
|
||||||
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
|
assert.Equal(t, ErrorTypePrivate, c.Errors[0].Type)
|
||||||
|
|
||||||
assert.Equal(t, errors.New("second error"), c.Errors[1].Err)
|
assert.Equal(t, secondErr, c.Errors[1].Err)
|
||||||
assert.Equal(t, "some data 2", c.Errors[1].Meta)
|
assert.Equal(t, "some data 2", c.Errors[1].Meta)
|
||||||
assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)
|
assert.Equal(t, ErrorTypePublic, c.Errors[1].Type)
|
||||||
|
|
||||||
|
@ -1,137 +0,0 @@
|
|||||||
# How to build one effective middleware?
|
|
||||||
|
|
||||||
## Consitituent part
|
|
||||||
|
|
||||||
The middleware has two parts:
|
|
||||||
|
|
||||||
- part one is what is executed once, when you initialize your middleware. That's where you set up all the global objects, logicals etc. Everything that happens one per application lifetime.
|
|
||||||
|
|
||||||
- part two is what executes on every request. For example, a database middleware you simply inject your "global" database object into the context. Once it's inside the context, you can retrieve it from within other middlewares and your handler function.
|
|
||||||
|
|
||||||
```go
|
|
||||||
func funcName(params string) gin.HandlerFunc {
|
|
||||||
// <---
|
|
||||||
// This is part one
|
|
||||||
// --->
|
|
||||||
// The follow code is an example
|
|
||||||
if err := check(params); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
// <---
|
|
||||||
// This is part two
|
|
||||||
// --->
|
|
||||||
// The follow code is an example
|
|
||||||
c.Set("TestVar", params)
|
|
||||||
c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Execution process
|
|
||||||
|
|
||||||
Firstly, we have the follow example code:
|
|
||||||
|
|
||||||
```go
|
|
||||||
func main() {
|
|
||||||
router := gin.Default()
|
|
||||||
|
|
||||||
router.Use(globalMiddleware())
|
|
||||||
|
|
||||||
router.GET("/rest/n/api/*some", mid1(), mid2(), handler)
|
|
||||||
|
|
||||||
router.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func globalMiddleware() gin.HandlerFunc {
|
|
||||||
fmt.Println("globalMiddleware...1")
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
fmt.Println("globalMiddleware...2")
|
|
||||||
c.Next()
|
|
||||||
fmt.Println("globalMiddleware...3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func handler(c *gin.Context) {
|
|
||||||
fmt.Println("exec handler.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func mid1() gin.HandlerFunc {
|
|
||||||
fmt.Println("mid1...1")
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
|
|
||||||
fmt.Println("mid1...2")
|
|
||||||
c.Next()
|
|
||||||
fmt.Println("mid1...3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func mid2() gin.HandlerFunc {
|
|
||||||
fmt.Println("mid2...1")
|
|
||||||
|
|
||||||
return func(c *gin.Context) {
|
|
||||||
fmt.Println("mid2...2")
|
|
||||||
c.Next()
|
|
||||||
fmt.Println("mid2...3")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
According to [Consitituent part](#consitituent-part) said, when we run the gin process, **part one** will execute firstly and will print the follow information:
|
|
||||||
|
|
||||||
```go
|
|
||||||
globalMiddleware...1
|
|
||||||
mid1...1
|
|
||||||
mid2...1
|
|
||||||
```
|
|
||||||
|
|
||||||
And init order are:
|
|
||||||
|
|
||||||
```go
|
|
||||||
globalMiddleware...1
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid1...1
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid2...1
|
|
||||||
```
|
|
||||||
|
|
||||||
When we curl one request `curl -v localhost:8080/rest/n/api/some`, **part two** will execute their middleware and output the following information:
|
|
||||||
|
|
||||||
```go
|
|
||||||
globalMiddleware...2
|
|
||||||
mid1...2
|
|
||||||
mid2...2
|
|
||||||
exec handler.
|
|
||||||
mid2...3
|
|
||||||
mid1...3
|
|
||||||
globalMiddleware...3
|
|
||||||
```
|
|
||||||
|
|
||||||
In other words, run order are:
|
|
||||||
|
|
||||||
```go
|
|
||||||
globalMiddleware...2
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid1...2
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid2...2
|
|
||||||
|
|
|
||||||
v
|
|
||||||
exec handler.
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid2...3
|
|
||||||
|
|
|
||||||
v
|
|
||||||
mid1...3
|
|
||||||
|
|
|
||||||
v
|
|
||||||
globalMiddleware...3
|
|
||||||
```
|
|
15
gin.go
15
gin.go
@ -10,6 +10,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin/render"
|
"github.com/gin-gonic/gin/render"
|
||||||
@ -318,6 +319,7 @@ func (engine *Engine) RunUnix(file string) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer listener.Close()
|
defer listener.Close()
|
||||||
|
os.Chmod(file, 0777)
|
||||||
err = http.Serve(listener, engine)
|
err = http.Serve(listener, engine)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -437,17 +439,20 @@ func serveError(c *Context, code int, defaultMessage []byte) {
|
|||||||
|
|
||||||
func redirectTrailingSlash(c *Context) {
|
func redirectTrailingSlash(c *Context) {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
path := req.URL.Path
|
p := req.URL.Path
|
||||||
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
|
p = prefix + "/" + req.URL.Path
|
||||||
|
}
|
||||||
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
|
||||||
if req.Method != "GET" {
|
if req.Method != "GET" {
|
||||||
code = http.StatusTemporaryRedirect
|
code = http.StatusTemporaryRedirect
|
||||||
}
|
}
|
||||||
|
|
||||||
req.URL.Path = path + "/"
|
req.URL.Path = p + "/"
|
||||||
if length := len(path); length > 1 && path[length-1] == '/' {
|
if length := len(p); length > 1 && p[length-1] == '/' {
|
||||||
req.URL.Path = path[:length-1]
|
req.URL.Path = p[:length-1]
|
||||||
}
|
}
|
||||||
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
|
debugPrint("redirecting request %d: %s --> %s", code, p, req.URL.String())
|
||||||
http.Redirect(c.Writer, req, req.URL.String(), code)
|
http.Redirect(c.Writer, req, req.URL.String(), code)
|
||||||
c.writermem.WriteHeaderNow()
|
c.writermem.WriteHeaderNow()
|
||||||
}
|
}
|
||||||
|
@ -346,6 +346,29 @@ func TestBindUriError(t *testing.T) {
|
|||||||
assert.Equal(t, http.StatusBadRequest, w1.Code)
|
assert.Equal(t, http.StatusBadRequest, w1.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRaceContextCopy(t *testing.T) {
|
||||||
|
DefaultWriter = os.Stdout
|
||||||
|
router := Default()
|
||||||
|
router.GET("/test/copy/race", func(c *Context) {
|
||||||
|
c.Set("1", 0)
|
||||||
|
c.Set("2", 0)
|
||||||
|
|
||||||
|
// Sending a copy of the Context to two separate routines
|
||||||
|
go readWriteKeys(c.Copy())
|
||||||
|
go readWriteKeys(c.Copy())
|
||||||
|
c.String(http.StatusOK, "run OK, no panics")
|
||||||
|
})
|
||||||
|
w := performRequest(router, "GET", "/test/copy/race")
|
||||||
|
assert.Equal(t, "run OK, no panics", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func readWriteKeys(c *Context) {
|
||||||
|
for {
|
||||||
|
c.Set("1", rand.Int())
|
||||||
|
c.Set("2", c.Value("1"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func githubConfigRouter(router *Engine) {
|
func githubConfigRouter(router *Engine) {
|
||||||
for _, route := range githubAPI {
|
for _, route := range githubAPI {
|
||||||
router.Handle(route.method, route.path, func(c *Context) {
|
router.Handle(route.method, route.path, func(c *Context) {
|
||||||
|
89
logger.go
89
logger.go
@ -64,15 +64,62 @@ type LogFormatterParams struct {
|
|||||||
ErrorMessage string
|
ErrorMessage string
|
||||||
// IsTerm shows whether does gin's output descriptor refers to a terminal.
|
// IsTerm shows whether does gin's output descriptor refers to a terminal.
|
||||||
IsTerm bool
|
IsTerm bool
|
||||||
|
// BodySize is the size of the Response Body
|
||||||
|
BodySize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// StatusCodeColor is the ANSI color for appropriately logging http status code to a terminal.
|
||||||
|
func (p *LogFormatterParams) StatusCodeColor() string {
|
||||||
|
code := p.StatusCode
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
||||||
|
return green
|
||||||
|
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
||||||
|
return white
|
||||||
|
case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
|
||||||
|
return yellow
|
||||||
|
default:
|
||||||
|
return red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MethodColor is the ANSI color for appropriately logging http method to a terminal.
|
||||||
|
func (p *LogFormatterParams) MethodColor() string {
|
||||||
|
method := p.Method
|
||||||
|
|
||||||
|
switch method {
|
||||||
|
case "GET":
|
||||||
|
return blue
|
||||||
|
case "POST":
|
||||||
|
return cyan
|
||||||
|
case "PUT":
|
||||||
|
return yellow
|
||||||
|
case "DELETE":
|
||||||
|
return red
|
||||||
|
case "PATCH":
|
||||||
|
return green
|
||||||
|
case "HEAD":
|
||||||
|
return magenta
|
||||||
|
case "OPTIONS":
|
||||||
|
return white
|
||||||
|
default:
|
||||||
|
return reset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetColor resets all escape attributes.
|
||||||
|
func (p *LogFormatterParams) ResetColor() string {
|
||||||
|
return reset
|
||||||
}
|
}
|
||||||
|
|
||||||
// defaultLogFormatter is the default log format function Logger middleware uses.
|
// defaultLogFormatter is the default log format function Logger middleware uses.
|
||||||
var defaultLogFormatter = func(param LogFormatterParams) string {
|
var defaultLogFormatter = func(param LogFormatterParams) string {
|
||||||
var statusColor, methodColor, resetColor string
|
var statusColor, methodColor, resetColor string
|
||||||
if param.IsTerm {
|
if param.IsTerm {
|
||||||
statusColor = colorForStatus(param.StatusCode)
|
statusColor = param.StatusCodeColor()
|
||||||
methodColor = colorForMethod(param.Method)
|
methodColor = param.MethodColor()
|
||||||
resetColor = reset
|
resetColor = param.ResetColor()
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s",
|
||||||
@ -191,6 +238,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
param.StatusCode = c.Writer.Status()
|
param.StatusCode = c.Writer.Status()
|
||||||
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
|
param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()
|
||||||
|
|
||||||
|
param.BodySize = c.Writer.Size()
|
||||||
|
|
||||||
if raw != "" {
|
if raw != "" {
|
||||||
path = path + "?" + raw
|
path = path + "?" + raw
|
||||||
}
|
}
|
||||||
@ -201,37 +250,3 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func colorForStatus(code int) string {
|
|
||||||
switch {
|
|
||||||
case code >= http.StatusOK && code < http.StatusMultipleChoices:
|
|
||||||
return green
|
|
||||||
case code >= http.StatusMultipleChoices && code < http.StatusBadRequest:
|
|
||||||
return white
|
|
||||||
case code >= http.StatusBadRequest && code < http.StatusInternalServerError:
|
|
||||||
return yellow
|
|
||||||
default:
|
|
||||||
return red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func colorForMethod(method string) string {
|
|
||||||
switch method {
|
|
||||||
case "GET":
|
|
||||||
return blue
|
|
||||||
case "POST":
|
|
||||||
return cyan
|
|
||||||
case "PUT":
|
|
||||||
return yellow
|
|
||||||
case "DELETE":
|
|
||||||
return red
|
|
||||||
case "PATCH":
|
|
||||||
return green
|
|
||||||
case "HEAD":
|
|
||||||
return magenta
|
|
||||||
case "OPTIONS":
|
|
||||||
return white
|
|
||||||
default:
|
|
||||||
return reset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -257,6 +257,13 @@ func TestDefaultLogFormatter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForMethod(t *testing.T) {
|
func TestColorForMethod(t *testing.T) {
|
||||||
|
colorForMethod := func(method string) string {
|
||||||
|
p := LogFormatterParams{
|
||||||
|
Method: method,
|
||||||
|
}
|
||||||
|
return p.MethodColor()
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
|
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), colorForMethod("GET"), "get should be blue")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
|
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), colorForMethod("POST"), "post should be cyan")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
|
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForMethod("PUT"), "put should be yellow")
|
||||||
@ -268,12 +275,24 @@ func TestColorForMethod(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorForStatus(t *testing.T) {
|
func TestColorForStatus(t *testing.T) {
|
||||||
|
colorForStatus := func(code int) string {
|
||||||
|
p := LogFormatterParams{
|
||||||
|
StatusCode: code,
|
||||||
|
}
|
||||||
|
return p.StatusCodeColor()
|
||||||
|
}
|
||||||
|
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
|
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), colorForStatus(http.StatusOK), "2xx should be green")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), colorForStatus(http.StatusMovedPermanently), "3xx should be white")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
assert.Equal(t, string([]byte{27, 91, 57, 48, 59, 52, 51, 109}), colorForStatus(http.StatusNotFound), "4xx should be yellow")
|
||||||
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
|
assert.Equal(t, string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), colorForStatus(2), "other things should be red")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResetColor(t *testing.T) {
|
||||||
|
p := LogFormatterParams{}
|
||||||
|
assert.Equal(t, string([]byte{27, 91, 48, 109}), p.ResetColor())
|
||||||
|
}
|
||||||
|
|
||||||
func TestErrorLogger(t *testing.T) {
|
func TestErrorLogger(t *testing.T) {
|
||||||
router := New()
|
router := New()
|
||||||
router.Use(ErrorLogger())
|
router.Use(ErrorLogger())
|
||||||
|
6
mode.go
6
mode.go
@ -11,8 +11,8 @@ import (
|
|||||||
"github.com/gin-gonic/gin/binding"
|
"github.com/gin-gonic/gin/binding"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ENV_GIN_MODE indicates environment name for gin mode.
|
// EnvGinMode indicates environment name for gin mode.
|
||||||
const ENV_GIN_MODE = "GIN_MODE"
|
const EnvGinMode = "GIN_MODE"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// DebugMode indicates gin mode is debug.
|
// DebugMode indicates gin mode is debug.
|
||||||
@ -44,7 +44,7 @@ var ginMode = debugCode
|
|||||||
var modeName = DebugMode
|
var modeName = DebugMode
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
mode := os.Getenv(ENV_GIN_MODE)
|
mode := os.Getenv(EnvGinMode)
|
||||||
SetMode(mode)
|
SetMode(mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,13 +13,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
os.Setenv(ENV_GIN_MODE, TestMode)
|
os.Setenv(EnvGinMode, TestMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetMode(t *testing.T) {
|
func TestSetMode(t *testing.T) {
|
||||||
assert.Equal(t, testCode, ginMode)
|
assert.Equal(t, testCode, ginMode)
|
||||||
assert.Equal(t, TestMode, Mode())
|
assert.Equal(t, TestMode, Mode())
|
||||||
os.Unsetenv(ENV_GIN_MODE)
|
os.Unsetenv(EnvGinMode)
|
||||||
|
|
||||||
SetMode("")
|
SetMode("")
|
||||||
assert.Equal(t, debugCode, ginMode)
|
assert.Equal(t, debugCode, ginMode)
|
||||||
|
@ -36,8 +36,8 @@ func (r Reader) WriteContentType(w http.ResponseWriter) {
|
|||||||
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
|
func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) {
|
||||||
header := w.Header()
|
header := w.Header()
|
||||||
for k, v := range headers {
|
for k, v := range headers {
|
||||||
if val := header[k]; len(val) == 0 {
|
if header.Get(k) == "" {
|
||||||
header[k] = []string{v}
|
header.Set(k, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -470,6 +470,7 @@ func TestRenderReader(t *testing.T) {
|
|||||||
body := "#!PNG some raw data"
|
body := "#!PNG some raw data"
|
||||||
headers := make(map[string]string)
|
headers := make(map[string]string)
|
||||||
headers["Content-Disposition"] = `attachment; filename="filename.png"`
|
headers["Content-Disposition"] = `attachment; filename="filename.png"`
|
||||||
|
headers["x-request-id"] = "requestId"
|
||||||
|
|
||||||
err := (Reader{
|
err := (Reader{
|
||||||
ContentLength: int64(len(body)),
|
ContentLength: int64(len(body)),
|
||||||
@ -483,4 +484,5 @@ func TestRenderReader(t *testing.T) {
|
|||||||
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
|
||||||
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
|
assert.Equal(t, strconv.Itoa(len(body)), w.Header().Get("Content-Length"))
|
||||||
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
|
||||||
|
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
|
||||||
}
|
}
|
||||||
|
@ -16,8 +16,16 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder {
|
type header struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func performRequest(r http.Handler, method, path string, headers ...header) *httptest.ResponseRecorder {
|
||||||
req, _ := http.NewRequest(method, path, nil)
|
req, _ := http.NewRequest(method, path, nil)
|
||||||
|
for _, h := range headers {
|
||||||
|
req.Header.Add(h.Key, h.Value)
|
||||||
|
}
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
return w
|
return w
|
||||||
@ -170,6 +178,13 @@ func TestRouteRedirectTrailingSlash(t *testing.T) {
|
|||||||
w = performRequest(router, "PUT", "/path4/")
|
w = performRequest(router, "PUT", "/path4/")
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
|
||||||
|
w = performRequest(router, "GET", "/path2", header{Key: "X-Forwarded-Prefix", Value: "/api"})
|
||||||
|
assert.Equal(t, "/api/path2/", w.Header().Get("Location"))
|
||||||
|
assert.Equal(t, 301, w.Code)
|
||||||
|
|
||||||
|
w = performRequest(router, "GET", "/path2/", header{Key: "X-Forwarded-Prefix", Value: "/api/"})
|
||||||
|
assert.Equal(t, 200, w.Code)
|
||||||
|
|
||||||
router.RedirectTrailingSlash = false
|
router.RedirectTrailingSlash = false
|
||||||
|
|
||||||
w = performRequest(router, "GET", "/path/")
|
w = performRequest(router, "GET", "/path/")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user