diff --git a/.travis.yml b/.travis.yml index 2eeb0b3d..be196feb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,7 @@ go: - 1.9.x - 1.10.x - 1.11.x + - 1.12.x - master matrix: @@ -14,6 +15,8 @@ matrix: include: - go: 1.11.x env: GO111MODULE=on + - go: 1.12.x + env: GO111MODULE=on git: depth: 10 diff --git a/Makefile b/Makefile index 7211144a..7ab57e27 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,10 @@ test: exit 1; \ elif grep -q "build failed" tmp.out; then \ rm tmp.out; \ - exit; \ + exit 1; \ + elif grep -q "setup failed" tmp.out; then \ + rm tmp.out; \ + exit 1; \ fi; \ if [ -f profile.out ]; then \ cat profile.out | grep -v "mode:" >> coverage.out; \ diff --git a/README.md b/README.md index 3da8785b..058eee2a 100644 --- a/README.md +++ b/README.md @@ -215,9 +215,6 @@ $ go build -tags=jsoniter . ```go func main() { - // Disable Console Color - // gin.DisableConsoleColor() - // Creates a gin router with default middleware: // logger and recovery (crash-free) middleware router := gin.Default() @@ -570,6 +567,48 @@ func main() { ::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " ``` +### Controlling Log output coloring + +By default, logs output on console should be colorized depending on the detected TTY. + +Never colorize logs: + +```go +func main() { + // Disable log's color + gin.DisableConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +Always colorize logs: + +```go +func main() { + // Force log's color + gin.ForceConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + ### Model binding and validation To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). @@ -1673,6 +1712,11 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } log.Println("Server exiting") } ``` diff --git a/binding/binding_test.go b/binding/binding_test.go index 1044e6c2..c9dea347 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -61,6 +61,10 @@ type FooStructForMapType struct { MapFoo map[string]interface{} `form:"map_foo"` } +type FooStructForIgnoreFormTag struct { + Foo *string `form:"-"` +} + type InvalidNameType struct { 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) { testFormBindingInvalidName(t, "POST", "/", "/", @@ -860,6 +870,21 @@ func testFormBindingForTimeFailLocation(t *testing.T, method, path, badPath, bod 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) { b := Form assert.Equal(t, "form", b.Name()) diff --git a/binding/form_mapping.go b/binding/form_mapping.go index 8900ab70..8eb5c0d1 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -41,6 +41,9 @@ func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { defaultValue = defaultList[1] } } + if inputFieldName == "-" { + continue + } if inputFieldName == "" { inputFieldName = typeField.Name diff --git a/context.go b/context.go index f04c506b..df07da64 100644 --- a/context.go +++ b/context.go @@ -82,6 +82,10 @@ func (c *Context) Copy() *Context { cp.Writer = &cp.writermem cp.index = abortIndex cp.handlers = nil + cp.Keys = map[string]interface{}{} + for k, v := range c.Keys { + cp.Keys[k] = v + } return &cp } @@ -91,6 +95,16 @@ func (c *Context) HandlerName() string { 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. func (c *Context) Handler() HandlerFunc { return c.handlers.Last() diff --git a/context_test.go b/context_test.go index b974827b..92747166 100644 --- a/context_test.go +++ b/context_test.go @@ -331,6 +331,8 @@ func TestContextCopy(t *testing.T) { assert.Equal(t, cp.Keys, c.Keys) assert.Equal(t, cp.engine, c.engine) 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) { @@ -340,10 +342,26 @@ func TestContextHandlerName(t *testing.T) { 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 handlerNameTest2(c *Context) { + +} + var handlerTest HandlerFunc = func(c *Context) { } diff --git a/examples/graceful-shutdown/graceful-shutdown/server.go b/examples/graceful-shutdown/graceful-shutdown/server.go index 33be0c8f..999a209e 100644 --- a/examples/graceful-shutdown/graceful-shutdown/server.go +++ b/examples/graceful-shutdown/graceful-shutdown/server.go @@ -48,5 +48,10 @@ func main() { if err := srv.Shutdown(ctx); err != nil { log.Fatal("Server Shutdown:", err) } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } log.Println("Server exiting") } diff --git a/gin.go b/gin.go index 6e5ea6d7..e28e9579 100644 --- a/gin.go +++ b/gin.go @@ -10,6 +10,7 @@ import ( "net" "net/http" "os" + "path" "sync" "github.com/gin-gonic/gin/render" @@ -318,6 +319,7 @@ func (engine *Engine) RunUnix(file string) (err error) { return } defer listener.Close() + os.Chmod(file, 0777) err = http.Serve(listener, engine) return } @@ -437,17 +439,20 @@ func serveError(c *Context, code int, defaultMessage []byte) { func redirectTrailingSlash(c *Context) { 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 if req.Method != "GET" { code = http.StatusTemporaryRedirect } - req.URL.Path = path + "/" - if length := len(path); length > 1 && path[length-1] == '/' { - req.URL.Path = path[:length-1] + req.URL.Path = p + "/" + if length := len(p); length > 1 && p[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) c.writermem.WriteHeaderNow() } diff --git a/githubapi_test.go b/githubapi_test.go index 29aa1584..fb74d659 100644 --- a/githubapi_test.go +++ b/githubapi_test.go @@ -346,6 +346,29 @@ func TestBindUriError(t *testing.T) { 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) { for _, route := range githubAPI { router.Handle(route.method, route.path, func(c *Context) { diff --git a/logger.go b/logger.go index d2869f51..dc639975 100644 --- a/logger.go +++ b/logger.go @@ -24,6 +24,7 @@ var ( cyan = string([]byte{27, 91, 57, 55, 59, 52, 54, 109}) reset = string([]byte{27, 91, 48, 109}) disableColor = false + forceColor = false ) // LoggerConfig defines the config for Logger middleware. @@ -63,15 +64,62 @@ type LogFormatterParams struct { ErrorMessage string // IsTerm shows whether does gin's output descriptor refers to a terminal. 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. var defaultLogFormatter = func(param LogFormatterParams) string { var statusColor, methodColor, resetColor string if param.IsTerm { - statusColor = colorForStatus(param.StatusCode) - methodColor = colorForMethod(param.Method) - resetColor = reset + statusColor = param.StatusCodeColor() + methodColor = param.MethodColor() + resetColor = param.ResetColor() } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", @@ -90,6 +138,11 @@ func DisableConsoleColor() { disableColor = true } +// ForceConsoleColor force color output in the console. +func ForceConsoleColor() { + forceColor = true +} + // ErrorLogger returns a handlerfunc for any error type. func ErrorLogger() HandlerFunc { return ErrorLoggerT(ErrorTypeAny) @@ -144,9 +197,9 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { isTerm := true - if w, ok := out.(*os.File); !ok || + if w, ok := out.(*os.File); (!ok || (os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) || - disableColor { + disableColor) && !forceColor { isTerm = false } @@ -185,6 +238,8 @@ func LoggerWithConfig(conf LoggerConfig) HandlerFunc { param.StatusCode = c.Writer.Status() param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String() + param.BodySize = c.Writer.Size() + if raw != "" { path = path + "?" + raw } @@ -195,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 - } -} diff --git a/logger_test.go b/logger_test.go index d0169251..c551677a 100644 --- a/logger_test.go +++ b/logger_test.go @@ -257,6 +257,13 @@ func TestDefaultLogFormatter(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, 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") @@ -268,12 +275,24 @@ func TestColorForMethod(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, 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, 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) { router := New() router.Use(ErrorLogger()) @@ -340,3 +359,10 @@ func TestDisableConsoleColor(t *testing.T) { DisableConsoleColor() assert.True(t, disableColor) } + +func TestForceConsoleColor(t *testing.T) { + New() + assert.False(t, forceColor) + ForceConsoleColor() + assert.True(t, forceColor) +} diff --git a/mode.go b/mode.go index f787b5ca..8aa84aa8 100644 --- a/mode.go +++ b/mode.go @@ -11,8 +11,8 @@ import ( "github.com/gin-gonic/gin/binding" ) -// ENV_GIN_MODE indicates environment name for gin mode. -const ENV_GIN_MODE = "GIN_MODE" +// EnvGinMode indicates environment name for gin mode. +const EnvGinMode = "GIN_MODE" const ( // DebugMode indicates gin mode is debug. @@ -44,7 +44,7 @@ var ginMode = debugCode var modeName = DebugMode func init() { - mode := os.Getenv(ENV_GIN_MODE) + mode := os.Getenv(EnvGinMode) SetMode(mode) } diff --git a/mode_test.go b/mode_test.go index cf27acd8..3dba5150 100644 --- a/mode_test.go +++ b/mode_test.go @@ -13,13 +13,13 @@ import ( ) func init() { - os.Setenv(ENV_GIN_MODE, TestMode) + os.Setenv(EnvGinMode, TestMode) } func TestSetMode(t *testing.T) { assert.Equal(t, testCode, ginMode) assert.Equal(t, TestMode, Mode()) - os.Unsetenv(ENV_GIN_MODE) + os.Unsetenv(EnvGinMode) SetMode("") assert.Equal(t, debugCode, ginMode) diff --git a/render/reader.go b/render/reader.go index ab60e53a..312af741 100644 --- a/render/reader.go +++ b/render/reader.go @@ -36,8 +36,8 @@ func (r Reader) WriteContentType(w http.ResponseWriter) { func (r Reader) writeHeaders(w http.ResponseWriter, headers map[string]string) { header := w.Header() for k, v := range headers { - if val := header[k]; len(val) == 0 { - header[k] = []string{v} + if header.Get(k) == "" { + header.Set(k, v) } } } diff --git a/render/render_test.go b/render/render_test.go index 3df04a17..76e29eeb 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -470,6 +470,7 @@ func TestRenderReader(t *testing.T) { body := "#!PNG some raw data" headers := make(map[string]string) headers["Content-Disposition"] = `attachment; filename="filename.png"` + headers["x-request-id"] = "requestId" err := (Reader{ 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, strconv.Itoa(len(body)), w.Header().Get("Content-Length")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) + assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) } diff --git a/routes_test.go b/routes_test.go index 8d50292d..a842704f 100644 --- a/routes_test.go +++ b/routes_test.go @@ -16,8 +16,16 @@ import ( "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) + for _, h := range headers { + req.Header.Add(h.Key, h.Value) + } w := httptest.NewRecorder() r.ServeHTTP(w, req) return w @@ -170,6 +178,13 @@ func TestRouteRedirectTrailingSlash(t *testing.T) { w = performRequest(router, "PUT", "/path4/") 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 w = performRequest(router, "GET", "/path/")