Просмотр исходного кода

Merge pull request #871 from gin-gonic/develop

v1.2
Javier Provecho Fernandez 8 лет назад
Родитель
Сommit
e878cc1b58
66 измененных файлов с 2197 добавлено и 328 удалено
  1. 2 2
      .gitignore
  2. 15 6
      .travis.yml
  3. 2 4
      AUTHORS.md
  4. 1 1
      BENCHMARKS.md
  5. 50 9
      CHANGELOG.md
  6. 0 36
      Godeps/Godeps.json
  7. 61 0
      Makefile
  8. 291 48
      README.md
  9. 4 0
      benchmarks_test.go
  10. 16 11
      binding/binding.go
  11. 43 3
      binding/binding_test.go
  12. 4 0
      binding/default_validator.go
  13. 32 0
      binding/form_mapping.go
  14. 0 1
      binding/json.go
  15. 28 0
      binding/msgpack.go
  16. 2 2
      binding/protobuf.go
  17. 201 36
      context.go
  18. 11 0
      context_appengine.go
  19. 509 56
      context_test.go
  20. 20 0
      debug_test.go
  21. 14 1
      deprecated.go
  22. 2 2
      errors.go
  23. 7 0
      errors_test.go
  24. 2 1
      examples/app-engine/hello.go
  25. 19 0
      examples/auto-tls/example1.go
  26. 26 0
      examples/auto-tls/example2.go
  27. 2 0
      examples/basic/main.go
  28. 45 0
      examples/graceful-shutdown/close/server.go
  29. 48 0
      examples/graceful-shutdown/graceful-shutdown/server.go
  30. 10 0
      examples/realtime-advanced/Makefile
  31. 0 1
      examples/realtime-advanced/routes.go
  32. 7 5
      examples/realtime-advanced/stats.go
  33. 9 0
      examples/realtime-chat/Makefile
  34. 39 0
      examples/upload-file/multiple/main.go
  35. 17 0
      examples/upload-file/multiple/public/index.html
  36. 34 0
      examples/upload-file/single/main.go
  37. 16 0
      examples/upload-file/single/public/index.html
  38. 1 0
      fixtures/basic/hello.tmpl
  39. 1 0
      fixtures/basic/raw.tmpl
  40. 5 1
      fs.go
  41. 60 12
      gin.go
  42. 1 1
      ginS/README.md
  43. 18 14
      gin_integration_test.go
  44. 111 1
      gin_test.go
  45. 0 14
      helpers_test.go
  46. 18 11
      logger.go
  47. 21 7
      logger_test.go
  48. 3 4
      middleware_test.go
  49. 3 3
      mode.go
  50. 1 1
      path.go
  51. 1 1
      path_test.go
  52. 9 6
      render/data.go
  53. 21 5
      render/html.go
  54. 21 5
      render/json.go
  55. 31 0
      render/msgpack.go
  56. 2 0
      render/redirect.go
  57. 3 0
      render/render.go
  58. 25 2
      render/render_test.go
  59. 4 1
      render/text.go
  60. 5 1
      render/xml.go
  61. 5 1
      render/yaml.go
  62. 39 0
      routes_test.go
  63. 17 0
      test_helpers.go
  64. 22 5
      tree.go
  65. 51 7
      tree_test.go
  66. 109 0
      vendor/vendor.json

+ 2 - 2
.gitignore

@@ -1,4 +1,4 @@
-Godeps/*
-!Godeps/Godeps.json
+vendor/*
+!vendor/vendor.json
 coverage.out
 coverage.out
 count.out
 count.out

+ 15 - 6
.travis.yml

@@ -1,14 +1,23 @@
 language: go
 language: go
 sudo: false
 sudo: false
 go:
 go:
-  - 1.4
-  - 1.5.4
-  - 1.6.4
-  - 1.7.4
-  - tip
+  - 1.6.x
+  - 1.7.x
+  - 1.8.x
+  - master
+
+git:
+  depth: 3
+
+install:
+  - make install
 
 
 script:
 script:
-  - go test -v -covermode=count -coverprofile=coverage.out
+  - make vet
+  - make fmt-check
+  - make embedmd
+  - make misspell-check
+  - make test
 
 
 after_success:
 after_success:
   - bash <(curl -s https://codecov.io/bash)
   - bash <(curl -s https://codecov.io/bash)

+ 2 - 4
AUTHORS.md

@@ -1,8 +1,6 @@
 List of all the awesome people working to make Gin the best Web Framework in Go.
 List of all the awesome people working to make Gin the best Web Framework in Go.
 
 
-
-
-##gin 0.x series authors
+## gin 0.x series authors
 
 
 **Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
 **Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho)
 
 
@@ -226,4 +224,4 @@ People and companies, who have contributed, in alphabetical order.
 
 
 
 
 **@yuyabee**
 **@yuyabee**
-- Fixed README
+- Fixed README

+ 1 - 1
BENCHMARKS.md

@@ -295,4 +295,4 @@ BenchmarkPossum_GPlusAll      100000         19685 ns/op        6240 B/op
 BenchmarkR2router_GPlusAll    100000         16251 ns/op        5040 B/op         76 allocs/op
 BenchmarkR2router_GPlusAll    100000         16251 ns/op        5040 B/op         76 allocs/op
 BenchmarkRevel_GPlusAll    20000         93489 ns/op       21656 B/op        368 allocs/op
 BenchmarkRevel_GPlusAll    20000         93489 ns/op       21656 B/op        368 allocs/op
 BenchmarkRivet_GPlusAll   100000         16907 ns/op        5408 B/op         64 allocs/op
 BenchmarkRivet_GPlusAll   100000         16907 ns/op        5408 B/op         64 allocs/op
-```
+```

+ 50 - 9
CHANGELOG.md

@@ -1,6 +1,47 @@
-#CHANGELOG
-
-###Gin 1.0rc2 (...)
+# CHANGELOG
+
+### Gin 1.2
+
+- [NEW] Switch from godeps to govendor
+- [NEW] Add support for Let's Encrypt via gin-gonic/autotls
+- [NEW] Improve README examples and add extra at examples folder
+- [NEW] Improved support with App Engine
+- [NEW] Add custom template delimiters, see #860
+- [NEW] Add Template Func Maps, see #962
+- [NEW] Add \*context.Handler(), see #928
+- [NEW] Add \*context.GetRawData()
+- [NEW] Add \*context.GetHeader() (request)
+- [NEW] Add \*context.AbortWithStatusJSON() (JSON content type)
+- [NEW] Add \*context.Keys type cast helpers
+- [NEW] Add \*context.ShouldBindWith()
+- [NEW] Add \*context.MustBindWith()
+- [NEW] Add \*engine.SetFuncMap()
+- [DEPRECATE] On next release: \*context.BindWith(), see #855
+- [FIX] Refactor render
+- [FIX] Reworked tests
+- [FIX] logger now supports cygwin
+- [FIX] Use X-Forwarded-For before X-Real-Ip
+- [FIX] time.Time binding (#904)
+
+### Gin 1.1.4
+
+- [NEW] Support google appengine for IsTerminal func
+
+### Gin 1.1.3
+
+- [FIX] Reverted Logger: skip ANSI color commands
+
+### Gin 1.1
+
+- [NEW] Implement QueryArray and PostArray methods 
+- [NEW] Refactor GetQuery and GetPostForm 
+- [NEW] Add contribution guide 
+- [FIX] Corrected typos in README
+- [FIX] Removed additional Iota  
+- [FIX] Changed imports to gopkg instead of github in README (#733) 
+- [FIX] Logger: skip ANSI color commands if output is not a tty
+
+### Gin 1.0rc2 (...)
 
 
 - [PERFORMANCE] Fast path for writing Content-Type.
 - [PERFORMANCE] Fast path for writing Content-Type.
 - [PERFORMANCE] Much faster 404 routing
 - [PERFORMANCE] Much faster 404 routing
@@ -35,7 +76,7 @@
 - [FIX] MIT license in every file
 - [FIX] MIT license in every file
 
 
 
 
-###Gin 1.0rc1 (May 22, 2015)
+### Gin 1.0rc1 (May 22, 2015)
 
 
 - [PERFORMANCE] Zero allocation router
 - [PERFORMANCE] Zero allocation router
 - [PERFORMANCE] Faster JSON, XML and text rendering
 - [PERFORMANCE] Faster JSON, XML and text rendering
@@ -79,7 +120,7 @@
 - [FIX] Better support for Google App Engine (using log instead of fmt)
 - [FIX] Better support for Google App Engine (using log instead of fmt)
 
 
 
 
-###Gin 0.6 (Mar 9, 2015)
+### Gin 0.6 (Mar 9, 2015)
 
 
 - [NEW] Support multipart/form-data
 - [NEW] Support multipart/form-data
 - [NEW] NoMethod handler
 - [NEW] NoMethod handler
@@ -89,14 +130,14 @@
 - [FIX] Improve color logger
 - [FIX] Improve color logger
 
 
 
 
-###Gin 0.5 (Feb 7, 2015)
+### Gin 0.5 (Feb 7, 2015)
 
 
 - [NEW] Content Negotiation
 - [NEW] Content Negotiation
 - [FIX] Solved security bug that allow a client to spoof ip
 - [FIX] Solved security bug that allow a client to spoof ip
 - [FIX] Fix unexported/ignored fields in binding
 - [FIX] Fix unexported/ignored fields in binding
 
 
 
 
-###Gin 0.4 (Aug 21, 2014)
+### Gin 0.4 (Aug 21, 2014)
 
 
 - [NEW] Development mode
 - [NEW] Development mode
 - [NEW] Unit tests
 - [NEW] Unit tests
@@ -105,7 +146,7 @@
 - [FIX] Improved documentation for model binding
 - [FIX] Improved documentation for model binding
 
 
 
 
-###Gin 0.3 (Jul 18, 2014)
+### Gin 0.3 (Jul 18, 2014)
 
 
 - [PERFORMANCE] Normal log and error log are printed in the same call.
 - [PERFORMANCE] Normal log and error log are printed in the same call.
 - [PERFORMANCE] Improve performance of NoRouter()
 - [PERFORMANCE] Improve performance of NoRouter()
@@ -123,7 +164,7 @@
 - [FIX] Check application/x-www-form-urlencoded when parsing form
 - [FIX] Check application/x-www-form-urlencoded when parsing form
 
 
 
 
-###Gin 0.2b (Jul 08, 2014)
+### Gin 0.2b (Jul 08, 2014)
 - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
 - [PERFORMANCE] Using sync.Pool to allocatio/gc overhead
 - [NEW] Travis CI integration
 - [NEW] Travis CI integration
 - [NEW] Completely new logger
 - [NEW] Completely new logger

+ 0 - 36
Godeps/Godeps.json

@@ -1,36 +0,0 @@
-{
-	"ImportPath": "github.com/gin-gonic/gin",
-	"GoVersion": "go1.5.1",
-	"Deps": [
-		{
-			"ImportPath": "github.com/davecgh/go-spew/spew",
-			"Rev": "5215b55f46b2b919f50a1df0eaa5886afe4e3b3d"
-		},
-		{
-			"ImportPath": "github.com/golang/protobuf/proto",
-			"Rev": "2402d76f3d41f928c7902a765dfc872356dd3aad"
-		},
-		{
-			"ImportPath": "github.com/manucorporat/sse",
-			"Rev": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d"
-		},
-		{
-			"ImportPath": "github.com/pmezard/go-difflib/difflib",
-			"Rev": "792786c7400a136282c1664665ae0a8db921c6c2"
-		},
-		{
-			"ImportPath": "github.com/stretchr/testify/assert",
-			"Comment": "v1.1.3",
-			"Rev": "f390dcf405f7b83c997eac1b06768bb9f44dec18"
-		},
-		{
-			"ImportPath": "golang.org/x/net/context",
-			"Rev": "f315505cf3349909cdf013ea56690da34e96a451"
-		},
-		{
-			"ImportPath": "gopkg.in/go-playground/validator.v8",
-			"Comment": "v8.15.1",
-			"Rev": "c193cecd124b5cc722d7ee5538e945bdb3348435"
-		}
-	]
-}

+ 61 - 0
Makefile

@@ -0,0 +1,61 @@
+GOFMT ?= gofmt "-s"
+PACKAGES ?= $(shell go list ./... | grep -v /vendor/)
+GOFILES := $(shell find . -name "*.go" -type f -not -path "./vendor/*")
+
+all: build
+
+install: deps
+	govendor sync
+
+.PHONY: test
+test:
+	go test -v -covermode=count -coverprofile=coverage.out
+
+.PHONY: fmt
+fmt:
+	$(GOFMT) -w $(GOFILES)
+
+.PHONY: fmt-check
+fmt-check:
+	# get all go files and run go fmt on them
+	@diff=$$($(GOFMT) -d $(GOFILES)); \
+	if [ -n "$$diff" ]; then \
+		echo "Please run 'make fmt' and commit the result:"; \
+		echo "$${diff}"; \
+		exit 1; \
+	fi;
+
+vet:
+	go vet $(PACKAGES)
+
+deps:
+	@hash govendor > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
+		go get -u github.com/kardianos/govendor; \
+	fi
+	@hash embedmd > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
+		go get -u github.com/campoy/embedmd; \
+	fi
+
+embedmd:
+	embedmd -d *.md
+
+.PHONY: lint
+lint:
+	@hash golint > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
+		go get -u github.com/golang/lint/golint; \
+	fi
+	for PKG in $(PACKAGES); do golint -set_exit_status $$PKG || exit 1; done;
+
+.PHONY: misspell-check
+misspell-check:
+	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
+		go get -u github.com/client9/misspell/cmd/misspell; \
+	fi
+	misspell -error $(GOFILES)
+
+.PHONY: misspell
+misspell:
+	@hash misspell > /dev/null 2>&1; if [ $$? -ne 0 ]; then \
+		go get -u github.com/client9/misspell/cmd/misspell; \
+	fi
+	misspell -w $(GOFILES)

+ 291 - 48
README.md

@@ -1,4 +1,3 @@
-
 # Gin Web Framework
 # Gin Web Framework
 
 
 <img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.jpg">
 <img align="right" src="https://raw.githubusercontent.com/gin-gonic/gin/master/logo.jpg">
@@ -16,10 +15,11 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
 ```sh
 ```sh
 $ cat test.go
 $ cat test.go
 ```
 ```
+
 ```go
 ```go
 package main
 package main
 
 
-import "gopkg.in/gin-gonic/gin.v1"
+import "github.com/gin-gonic/gin"
 
 
 func main() {
 func main() {
 	r := gin.Default()
 	r := gin.Default()
@@ -87,28 +87,31 @@ BenchmarkZeus_GithubAll 		| 2000 		| 944234 	| 300688 	| 2648
 
 
 1. Download and install it:
 1. Download and install it:
 
 
-    ```sh
-    $ go get gopkg.in/gin-gonic/gin.v1
-    ```
+```sh
+$ go get github.com/gin-gonic/gin
+```
 
 
 2. Import it in your code:
 2. Import it in your code:
 
 
-    ```go
-    import "gopkg.in/gin-gonic/gin.v1"
-    ```
+```go
+import "github.com/gin-gonic/gin"
+```
 
 
 3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
 3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`.
 
 
-    ```go
-    import "net/http"
-    ```
+```go
+import "net/http"
+```
 
 
 ## API Examples
 ## API Examples
 
 
-#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
+### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
 
 
 ```go
 ```go
 func main() {
 func main() {
+	// Disable Console Color
+	// gin.DisableConsoleColor()
+
 	// Creates a gin router with default middleware:
 	// Creates a gin router with default middleware:
 	// logger and recovery (crash-free) middleware
 	// logger and recovery (crash-free) middleware
 	router := gin.Default()
 	router := gin.Default()
@@ -128,7 +131,7 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Parameters in path
+### Parameters in path
 
 
 ```go
 ```go
 func main() {
 func main() {
@@ -153,7 +156,8 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Querystring parameters
+### Querystring parameters
+
 ```go
 ```go
 func main() {
 func main() {
 	router := gin.Default()
 	router := gin.Default()
@@ -220,34 +224,66 @@ func main() {
 id: 1234; page: 1; name: manu; message: this_is_great
 id: 1234; page: 1; name: manu; message: this_is_great
 ```
 ```
 
 
-### Another example: upload file
+### Upload files
+
+#### Single file
 
 
-References issue [#548](https://github.com/gin-gonic/gin/issues/548).
+References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
 
 
 ```go
 ```go
 func main() {
 func main() {
 	router := gin.Default()
 	router := gin.Default()
+	router.POST("/upload", func(c *gin.Context) {
+		// single file
+		file, _ := c.FormFile("file")
+		log.Println(file.Filename)
+
+		c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
+	})
+	router.Run(":8080")
+}
+```
 
 
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+  -F "file=@/Users/appleboy/test.zip" \
+  -H "Content-Type: multipart/form-data"
+```
+
+#### Multiple files
+
+See the detail [example code](examples/upload-file/multiple).
+
+```go
+func main() {
+	router := gin.Default()
 	router.POST("/upload", func(c *gin.Context) {
 	router.POST("/upload", func(c *gin.Context) {
+		// Multipart form
+		form, _ := c.MultipartForm()
+		files := form.File["upload[]"]
 
 
-	        file, header , err := c.Request.FormFile("upload")
-	        filename := header.Filename
-	        fmt.Println(header.Filename)
-	        out, err := os.Create("./tmp/"+filename+".png")
-	        if err != nil {
-	            log.Fatal(err)
-	        }
-	        defer out.Close()
-	        _, err = io.Copy(out, file)
-	        if err != nil {
-	            log.Fatal(err)
-	        }   
+		for _, file := range files {
+			log.Println(file.Filename)
+		}
+		c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files)))
 	})
 	})
 	router.Run(":8080")
 	router.Run(":8080")
 }
 }
 ```
 ```
 
 
-#### Grouping routes
+How to `curl`:
+
+```bash
+curl -X POST http://localhost:8080/upload \
+  -F "upload[]=@/Users/appleboy/test1.zip" \
+  -F "upload[]=@/Users/appleboy/test2.zip" \
+  -H "Content-Type: multipart/form-data"
+```
+
+### Grouping routes
+
 ```go
 ```go
 func main() {
 func main() {
 	router := gin.Default()
 	router := gin.Default()
@@ -272,14 +308,14 @@ func main() {
 }
 }
 ```
 ```
 
 
-
-#### Blank Gin without middleware by default
+### Blank Gin without middleware by default
 
 
 Use
 Use
 
 
 ```go
 ```go
 r := gin.New()
 r := gin.New()
 ```
 ```
+
 instead of
 instead of
 
 
 ```go
 ```go
@@ -287,7 +323,7 @@ r := gin.Default()
 ```
 ```
 
 
 
 
-#### Using middleware
+### Using middleware
 ```go
 ```go
 func main() {
 func main() {
 	// Creates a router without any middleware by default
 	// Creates a router without any middleware by default
@@ -322,7 +358,7 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Model binding and validation
+### Model binding and validation
 
 
 To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
 To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
 
 
@@ -372,13 +408,48 @@ func main() {
 }
 }
 ```
 ```
 
 
+### Bind Query String
+
+See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
+
+```go
+package main
+
+import "log"
+import "github.com/gin-gonic/gin"
+
+type Person struct {
+	Name    string `form:"name"`
+	Address string `form:"address"`
+}
+
+func main() {
+	route := gin.Default()
+	route.GET("/testing", startPage)
+	route.Run(":8085")
+}
+
+func startPage(c *gin.Context) {
+	var person Person
+	// If `GET`, only `Form` binding engine (`query`) used.
+	// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
+	// See more at https://github.com/gin-gonic/gin/blob/develop/binding/binding.go#L45
+	if c.Bind(&person) == nil {
+		log.Println(person.Name)
+		log.Println(person.Address)
+	}
+
+	c.String(200, "Success")
+}
+```
+
+### Multipart/Urlencoded binding
 
 
-###Multipart/Urlencoded binding
 ```go
 ```go
 package main
 package main
 
 
 import (
 import (
-	"gopkg.in/gin-gonic/gin.v1"
+	"github.com/gin-gonic/gin"
 )
 )
 
 
 type LoginForm struct {
 type LoginForm struct {
@@ -390,7 +461,7 @@ func main() {
 	router := gin.Default()
 	router := gin.Default()
 	router.POST("/login", func(c *gin.Context) {
 	router.POST("/login", func(c *gin.Context) {
 		// you can bind multipart form with explicit binding declaration:
 		// you can bind multipart form with explicit binding declaration:
-		// c.BindWith(&form, binding.Form)
+		// c.MustBindWith(&form, binding.Form)
 		// or you can simply use autobinding with Bind method:
 		// or you can simply use autobinding with Bind method:
 		var form LoginForm
 		var form LoginForm
 		// in this case proper binding will be automatically selected
 		// in this case proper binding will be automatically selected
@@ -411,8 +482,7 @@ Test it with:
 $ curl -v --form user=user --form password=password http://localhost:8080/login
 $ curl -v --form user=user --form password=password http://localhost:8080/login
 ```
 ```
 
 
-
-#### XML, JSON and YAML rendering
+### XML, JSON and YAML rendering
 
 
 ```go
 ```go
 func main() {
 func main() {
@@ -451,7 +521,7 @@ func main() {
 }
 }
 ```
 ```
 
 
-####Serving static files
+### Serving static files
 
 
 ```go
 ```go
 func main() {
 func main() {
@@ -465,9 +535,9 @@ func main() {
 }
 }
 ```
 ```
 
 
-####HTML rendering
+### HTML rendering
 
 
-Using LoadHTMLTemplates()
+Using LoadHTMLGlob() or LoadHTMLFiles()
 
 
 ```go
 ```go
 func main() {
 func main() {
@@ -482,7 +552,9 @@ func main() {
 	router.Run(":8080")
 	router.Run(":8080")
 }
 }
 ```
 ```
+
 templates/index.tmpl
 templates/index.tmpl
+
 ```html
 ```html
 <html>
 <html>
 	<h1>
 	<h1>
@@ -510,7 +582,9 @@ func main() {
 	router.Run(":8080")
 	router.Run(":8080")
 }
 }
 ```
 ```
+
 templates/posts/index.tmpl
 templates/posts/index.tmpl
+
 ```html
 ```html
 {{ define "posts/index.tmpl" }}
 {{ define "posts/index.tmpl" }}
 <html><h1>
 <html><h1>
@@ -520,7 +594,9 @@ templates/posts/index.tmpl
 </html>
 </html>
 {{ end }}
 {{ end }}
 ```
 ```
+
 templates/users/index.tmpl
 templates/users/index.tmpl
+
 ```html
 ```html
 {{ define "users/index.tmpl" }}
 {{ define "users/index.tmpl" }}
 <html><h1>
 <html><h1>
@@ -544,8 +620,59 @@ func main() {
 }
 }
 ```
 ```
 
 
+You may use custom delims
+
+```go
+	r := gin.Default()
+	r.Delims("{[{", "}]}")
+	r.LoadHTMLGlob("/path/to/templates"))
+```  
+
+#### Add custom template funcs
+
+main.go
+
+```go
+	...
+	
+	func formatAsDate(t time.Time) string {
+		year, month, day := t.Date()
+		return fmt.Sprintf("%d/%02d/%02d", year, month, day)
+	}
+	
+	...
+	
+	router.SetFuncMap(template.FuncMap{
+		"formatAsDate": formatAsDate,
+	})
+	
+	...
+	
+	router.GET("/raw", func(c *Context) {
+		c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+			"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+		})
+	})
+	
+	...
+```
+
+raw.tmpl
+
+```html
+Date: {[{.now | formatAsDate}]}
+```
+
+Result:
+```
+Date: 2017/07/01
+```
+
+### Multitemplate
+
+Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
 
 
-#### Redirects
+### Redirects
 
 
 Issuing a HTTP redirect is easy:
 Issuing a HTTP redirect is easy:
 
 
@@ -557,7 +684,7 @@ r.GET("/test", func(c *gin.Context) {
 Both internal and external locations are supported.
 Both internal and external locations are supported.
 
 
 
 
-#### Custom Middleware
+### Custom Middleware
 
 
 ```go
 ```go
 func Logger() gin.HandlerFunc {
 func Logger() gin.HandlerFunc {
@@ -597,7 +724,8 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Using BasicAuth() middleware
+### Using BasicAuth() middleware
+
 ```go
 ```go
 // simulate some private data
 // simulate some private data
 var secrets = gin.H{
 var secrets = gin.H{
@@ -635,8 +763,8 @@ func main() {
 }
 }
 ```
 ```
 
 
+### Goroutines inside a middleware
 
 
-#### Goroutines inside a middleware
 When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
 When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
 
 
 ```go
 ```go
@@ -668,7 +796,7 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Custom HTTP configuration
+### Custom HTTP configuration
 
 
 Use `http.ListenAndServe()` directly, like this:
 Use `http.ListenAndServe()` directly, like this:
 
 
@@ -695,7 +823,66 @@ func main() {
 }
 }
 ```
 ```
 
 
-#### Graceful restart or stop
+### Support Let's Encrypt
+
+example for 1-line LetsEncrypt HTTPS servers.
+
+[embedmd]:# (examples/auto-tls/example1.go go)
+```go
+package main
+
+import (
+	"log"
+
+	"github.com/gin-gonic/autotls"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+
+	// Ping handler
+	r.GET("/ping", func(c *gin.Context) {
+		c.String(200, "pong")
+	})
+
+	log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
+}
+```
+
+example for custom autocert manager.
+
+[embedmd]:# (examples/auto-tls/example2.go go)
+```go
+package main
+
+import (
+	"log"
+
+	"github.com/gin-gonic/autotls"
+	"github.com/gin-gonic/gin"
+	"golang.org/x/crypto/acme/autocert"
+)
+
+func main() {
+	r := gin.Default()
+
+	// Ping handler
+	r.GET("/ping", func(c *gin.Context) {
+		c.String(200, "pong")
+	})
+
+	m := autocert.Manager{
+		Prompt:     autocert.AcceptTOS,
+		HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
+		Cache:      autocert.DirCache("/var/www/.cache"),
+	}
+
+	log.Fatal(autotls.RunWithManager(r, m))
+}
+```
+
+### Graceful restart or stop
 
 
 Do you want to graceful restart or stop your web server?
 Do you want to graceful restart or stop your web server?
 There are some ways this can be done.
 There are some ways this can be done.
@@ -712,6 +899,62 @@ endless.ListenAndServe(":4242", router)
 An alternative to endless:
 An alternative to endless:
 
 
 * [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
 * [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully.
+* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server.
+* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers.
+
+If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](./examples/graceful-shutdown) example with gin.
+
+[embedmd]:# (examples/graceful-shutdown/graceful-shutdown/server.go go)
+```go
+// +build go1.8
+
+package main
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.GET("/", func(c *gin.Context) {
+		time.Sleep(5 * time.Second)
+		c.String(http.StatusOK, "Welcome Gin Server")
+	})
+
+	srv := &http.Server{
+		Addr:    ":8080",
+		Handler: router,
+	}
+
+	go func() {
+		// service connections
+		if err := srv.ListenAndServe(); err != nil {
+			log.Printf("listen: %s\n", err)
+		}
+	}()
+
+	// Wait for interrupt signal to gracefully shutdown the server with
+	// a timeout of 5 seconds.
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+	<-quit
+	log.Println("Shutdown Server ...")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		log.Fatal("Server Shutdown:", err)
+	}
+	log.Println("Server exist")
+}
+```
 
 
 ## Contributing 
 ## Contributing 
 
 
@@ -726,7 +969,7 @@ An alternative to endless:
   - You should add/modify tests to cover your proposed code changes.
   - You should add/modify tests to cover your proposed code changes.
   - If your pull request contains a new feature, please document it on the README.
   - If your pull request contains a new feature, please document it on the README.
 
 
-## Example
+## Users
 
 
 Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
 Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
 
 

+ 4 - 0
benchmarks_test.go

@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
 package gin
 package gin
 
 
 import (
 import (

+ 16 - 11
binding/binding.go

@@ -15,6 +15,8 @@ const (
 	MIMEPOSTForm          = "application/x-www-form-urlencoded"
 	MIMEPOSTForm          = "application/x-www-form-urlencoded"
 	MIMEMultipartPOSTForm = "multipart/form-data"
 	MIMEMultipartPOSTForm = "multipart/form-data"
 	MIMEPROTOBUF          = "application/x-protobuf"
 	MIMEPROTOBUF          = "application/x-protobuf"
+	MIMEMSGPACK           = "application/x-msgpack"
+	MIMEMSGPACK2          = "application/msgpack"
 )
 )
 
 
 type Binding interface {
 type Binding interface {
@@ -40,22 +42,25 @@ var (
 	FormPost      = formPostBinding{}
 	FormPost      = formPostBinding{}
 	FormMultipart = formMultipartBinding{}
 	FormMultipart = formMultipartBinding{}
 	ProtoBuf      = protobufBinding{}
 	ProtoBuf      = protobufBinding{}
+	MsgPack       = msgpackBinding{}
 )
 )
 
 
 func Default(method, contentType string) Binding {
 func Default(method, contentType string) Binding {
 	if method == "GET" {
 	if method == "GET" {
 		return Form
 		return Form
-	} else {
-		switch contentType {
-		case MIMEJSON:
-			return JSON
-		case MIMEXML, MIMEXML2:
-			return XML
-		case MIMEPROTOBUF:
-			return ProtoBuf
-		default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
-			return Form
-		}
+	}
+
+	switch contentType {
+	case MIMEJSON:
+		return JSON
+	case MIMEXML, MIMEXML2:
+		return XML
+	case MIMEPROTOBUF:
+		return ProtoBuf
+	case MIMEMSGPACK, MIMEMSGPACK2:
+		return MsgPack
+	default: //case MIMEPOSTForm, MIMEMultipartPOSTForm:
+		return Form
 	}
 	}
 }
 }
 
 

+ 43 - 3
binding/binding_test.go

@@ -12,17 +12,17 @@ import (
 
 
 	"github.com/gin-gonic/gin/binding/example"
 	"github.com/gin-gonic/gin/binding/example"
 	"github.com/golang/protobuf/proto"
 	"github.com/golang/protobuf/proto"
-
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"github.com/ugorji/go/codec"
 )
 )
 
 
 type FooStruct struct {
 type FooStruct struct {
-	Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"`
+	Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"`
 }
 }
 
 
 type FooBarStruct struct {
 type FooBarStruct struct {
 	FooStruct
 	FooStruct
-	Bar string `json:"bar" form:"bar" xml:"bar" binding:"required"`
+	Bar string `msgpack:"bar" json:"bar" form:"bar" xml:"bar" binding:"required"`
 }
 }
 
 
 func TestBindingDefault(t *testing.T) {
 func TestBindingDefault(t *testing.T) {
@@ -43,6 +43,9 @@ func TestBindingDefault(t *testing.T) {
 
 
 	assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf)
 	assert.Equal(t, Default("POST", MIMEPROTOBUF), ProtoBuf)
 	assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf)
 	assert.Equal(t, Default("PUT", MIMEPROTOBUF), ProtoBuf)
+
+	assert.Equal(t, Default("POST", MIMEMSGPACK), MsgPack)
+	assert.Equal(t, Default("PUT", MIMEMSGPACK2), MsgPack)
 }
 }
 
 
 func TestBindingJSON(t *testing.T) {
 func TestBindingJSON(t *testing.T) {
@@ -121,6 +124,26 @@ func TestBindingProtoBuf(t *testing.T) {
 		string(data), string(data[1:]))
 		string(data), string(data[1:]))
 }
 }
 
 
+func TestBindingMsgPack(t *testing.T) {
+	test := FooStruct{
+		Foo: "bar",
+	}
+
+	h := new(codec.MsgpackHandle)
+	assert.NotNil(t, h)
+	buf := bytes.NewBuffer([]byte{})
+	assert.NotNil(t, buf)
+	err := codec.NewEncoder(buf, h).Encode(test)
+	assert.NoError(t, err)
+
+	data := buf.Bytes()
+
+	testMsgPackBodyBinding(t,
+		MsgPack, "msgpack",
+		"/", "/",
+		string(data), string(data[1:]))
+}
+
 func TestValidationFails(t *testing.T) {
 func TestValidationFails(t *testing.T) {
 	var obj FooStruct
 	var obj FooStruct
 	req := requestWithBody("POST", "/", `{"bar": "foo"}`)
 	req := requestWithBody("POST", "/", `{"bar": "foo"}`)
@@ -213,6 +236,23 @@ func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, ba
 	assert.Error(t, err)
 	assert.Error(t, err)
 }
 }
 
 
+func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
+	assert.Equal(t, b.Name(), name)
+
+	obj := FooStruct{}
+	req := requestWithBody("POST", path, body)
+	req.Header.Add("Content-Type", MIMEMSGPACK)
+	err := b.Bind(req, &obj)
+	assert.NoError(t, err)
+	assert.Equal(t, obj.Foo, "bar")
+
+	obj = FooStruct{}
+	req = requestWithBody("POST", badPath, badBody)
+	req.Header.Add("Content-Type", MIMEMSGPACK)
+	err = MsgPack.Bind(req, &obj)
+	assert.Error(t, err)
+}
+
 func requestWithBody(method, path, body string) (req *http.Request) {
 func requestWithBody(method, path, body string) (req *http.Request) {
 	req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
 	req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
 	return
 	return

+ 4 - 0
binding/default_validator.go

@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
 package binding
 package binding
 
 
 import (
 import (

+ 32 - 0
binding/form_mapping.go

@@ -8,6 +8,7 @@ import (
 	"errors"
 	"errors"
 	"reflect"
 	"reflect"
 	"strconv"
 	"strconv"
+	"time"
 )
 )
 
 
 func mapForm(ptr interface{}, form map[string][]string) error {
 func mapForm(ptr interface{}, form map[string][]string) error {
@@ -52,6 +53,12 @@ func mapForm(ptr interface{}, form map[string][]string) error {
 			}
 			}
 			val.Field(i).Set(slice)
 			val.Field(i).Set(slice)
 		} else {
 		} else {
+			if _, isTime := structField.Interface().(time.Time); isTime {
+				if err := setTimeField(inputValue[0], typeField, structField); err != nil {
+					return err
+				}
+				continue
+			}
 			if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
 			if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil {
 				return err
 				return err
 			}
 			}
@@ -140,6 +147,31 @@ func setFloatField(val string, bitSize int, field reflect.Value) error {
 	return err
 	return err
 }
 }
 
 
+func setTimeField(val string, structField reflect.StructField, value reflect.Value) error {
+	timeFormat := structField.Tag.Get("time_format")
+	if timeFormat == "" {
+		return errors.New("Blank time format")
+	}
+
+	if val == "" {
+		value.Set(reflect.ValueOf(time.Time{}))
+		return nil
+	}
+
+	l := time.Local
+	if isUTC, _ := strconv.ParseBool(structField.Tag.Get("time_utc")); isUTC {
+		l = time.UTC
+	}
+
+	t, err := time.ParseInLocation(timeFormat, val, l)
+	if err != nil {
+		return err
+	}
+
+	value.Set(reflect.ValueOf(t))
+	return nil
+}
+
 // Don't pass in pointers to bind to. Can lead to bugs. See:
 // Don't pass in pointers to bind to. Can lead to bugs. See:
 // https://github.com/codegangsta/martini-contrib/issues/40
 // https://github.com/codegangsta/martini-contrib/issues/40
 // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659
 // https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659

+ 0 - 1
binding/json.go

@@ -6,7 +6,6 @@ package binding
 
 
 import (
 import (
 	"encoding/json"
 	"encoding/json"
-
 	"net/http"
 	"net/http"
 )
 )
 
 

+ 28 - 0
binding/msgpack.go

@@ -0,0 +1,28 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package binding
+
+import (
+	"net/http"
+
+	"github.com/ugorji/go/codec"
+)
+
+type msgpackBinding struct{}
+
+func (msgpackBinding) Name() string {
+	return "msgpack"
+}
+
+func (msgpackBinding) Bind(req *http.Request, obj interface{}) error {
+
+	if err := codec.NewDecoder(req.Body, new(codec.MsgpackHandle)).Decode(&obj); err != nil {
+		//var decoder *codec.Decoder = codec.NewDecoder(req.Body, &codec.MsgpackHandle)
+		//if err := decoder.Decode(&obj); err != nil {
+		return err
+	}
+	return validate(obj)
+
+}

+ 2 - 2
binding/protobuf.go

@@ -5,10 +5,10 @@
 package binding
 package binding
 
 
 import (
 import (
-	"github.com/golang/protobuf/proto"
-
 	"io/ioutil"
 	"io/ioutil"
 	"net/http"
 	"net/http"
+
+	"github.com/golang/protobuf/proto"
 )
 )
 
 
 type protobufBinding struct{}
 type protobufBinding struct{}

+ 201 - 36
context.go

@@ -7,17 +7,18 @@ package gin
 import (
 import (
 	"errors"
 	"errors"
 	"io"
 	"io"
+	"io/ioutil"
 	"math"
 	"math"
+	"mime/multipart"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"github.com/gin-contrib/sse"
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/binding"
 	"github.com/gin-gonic/gin/render"
 	"github.com/gin-gonic/gin/render"
-	"github.com/manucorporat/sse"
-	"golang.org/x/net/context"
 )
 )
 
 
 // Content-Type MIME of the most common data formats
 // Content-Type MIME of the most common data formats
@@ -31,7 +32,10 @@ const (
 	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
 	MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
 )
 )
 
 
-const abortIndex int8 = math.MaxInt8 / 2
+const (
+	defaultMemory      = 32 << 20 // 32 MB
+	abortIndex    int8 = math.MaxInt8 / 2
+)
 
 
 // Context is the most important part of gin. It allows us to pass variables between middleware,
 // Context is the most important part of gin. It allows us to pass variables between middleware,
 // manage the flow, validate the JSON of a request and render a JSON response for example.
 // manage the flow, validate the JSON of a request and render a JSON response for example.
@@ -50,8 +54,6 @@ type Context struct {
 	Accepted []string
 	Accepted []string
 }
 }
 
 
-var _ context.Context = &Context{}
-
 /************************************/
 /************************************/
 /********** CONTEXT CREATION ********/
 /********** CONTEXT CREATION ********/
 /************************************/
 /************************************/
@@ -67,7 +69,7 @@ func (c *Context) reset() {
 }
 }
 
 
 // Copy returns a copy of the current context that can be safely used outside the request's scope.
 // Copy returns a copy of the current context that can be safely used outside the request's scope.
-// This have to be used then the context has to be passed to a goroutine.
+// This has to be used when the context has to be passed to a goroutine.
 func (c *Context) Copy() *Context {
 func (c *Context) Copy() *Context {
 	var cp = *c
 	var cp = *c
 	cp.writermem.ResponseWriter = nil
 	cp.writermem.ResponseWriter = nil
@@ -83,13 +85,18 @@ func (c *Context) HandlerName() string {
 	return nameOfFunction(c.handlers.Last())
 	return nameOfFunction(c.handlers.Last())
 }
 }
 
 
+// Handler returns the main handler.
+func (c *Context) Handler() HandlerFunc {
+	return c.handlers.Last()
+}
+
 /************************************/
 /************************************/
 /*********** FLOW CONTROL ***********/
 /*********** FLOW CONTROL ***********/
 /************************************/
 /************************************/
 
 
 // Next should be used only inside middleware.
 // Next should be used only inside middleware.
 // It executes the pending handlers in the chain inside the calling handler.
 // It executes the pending handlers in the chain inside the calling handler.
-// See example in github.
+// See example in GitHub.
 func (c *Context) Next() {
 func (c *Context) Next() {
 	c.index++
 	c.index++
 	s := int8(len(c.handlers))
 	s := int8(len(c.handlers))
@@ -112,13 +119,20 @@ func (c *Context) Abort() {
 }
 }
 
 
 // AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
 // AbortWithStatus calls `Abort()` and writes the headers with the specified status code.
-// For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401).
+// For example, a failed attempt to authenticate a request could use: context.AbortWithStatus(401).
 func (c *Context) AbortWithStatus(code int) {
 func (c *Context) AbortWithStatus(code int) {
 	c.Status(code)
 	c.Status(code)
 	c.Writer.WriteHeaderNow()
 	c.Writer.WriteHeaderNow()
 	c.Abort()
 	c.Abort()
 }
 }
 
 
+// AbortWithStatusJSON calls `Abort()` and then `JSON` internally. This method stops the chain, writes the status code and return a JSON body
+// It also sets the Content-Type as "application/json".
+func (c *Context) AbortWithStatusJSON(code int, jsonObj interface{}) {
+	c.Abort()
+	c.JSON(code, jsonObj)
+}
+
 // AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
 // AbortWithError calls `AbortWithStatus()` and `Error()` internally. This method stops the chain, writes the status code and
 // pushes the specified error to `c.Errors`.
 // pushes the specified error to `c.Errors`.
 // See Context.Error() for more details.
 // See Context.Error() for more details.
@@ -154,7 +168,7 @@ func (c *Context) Error(err error) *Error {
 /******** METADATA MANAGEMENT********/
 /******** METADATA MANAGEMENT********/
 /************************************/
 /************************************/
 
 
-// Set is used to store a new key/value pair exclusivelly for this context.
+// Set is used to store a new key/value pair exclusively for this context.
 // It also lazy initializes  c.Keys if it was not used previously.
 // It also lazy initializes  c.Keys if it was not used previously.
 func (c *Context) Set(key string, value interface{}) {
 func (c *Context) Set(key string, value interface{}) {
 	if c.Keys == nil {
 	if c.Keys == nil {
@@ -166,9 +180,7 @@ func (c *Context) Set(key string, value interface{}) {
 // Get returns the value for the given key, ie: (value, true).
 // Get returns the value for the given key, ie: (value, true).
 // If the value does not exists it returns (nil, false)
 // If the value does not exists it returns (nil, false)
 func (c *Context) Get(key string) (value interface{}, exists bool) {
 func (c *Context) Get(key string) (value interface{}, exists bool) {
-	if c.Keys != nil {
-		value, exists = c.Keys[key]
-	}
+	value, exists = c.Keys[key]
 	return
 	return
 }
 }
 
 
@@ -180,6 +192,94 @@ func (c *Context) MustGet(key string) interface{} {
 	panic("Key \"" + key + "\" does not exist")
 	panic("Key \"" + key + "\" does not exist")
 }
 }
 
 
+// GetString returns the value associated with the key as a string.
+func (c *Context) GetString(key string) (s string) {
+	if val, ok := c.Get(key); ok && val != nil {
+		s, _ = val.(string)
+	}
+	return
+}
+
+// GetBool returns the value associated with the key as a boolean.
+func (c *Context) GetBool(key string) (b bool) {
+	if val, ok := c.Get(key); ok && val != nil {
+		b, _ = val.(bool)
+	}
+	return
+}
+
+// GetInt returns the value associated with the key as an integer.
+func (c *Context) GetInt(key string) (i int) {
+	if val, ok := c.Get(key); ok && val != nil {
+		i, _ = val.(int)
+	}
+	return
+}
+
+// GetInt64 returns the value associated with the key as an integer.
+func (c *Context) GetInt64(key string) (i64 int64) {
+	if val, ok := c.Get(key); ok && val != nil {
+		i64, _ = val.(int64)
+	}
+	return
+}
+
+// GetFloat64 returns the value associated with the key as a float64.
+func (c *Context) GetFloat64(key string) (f64 float64) {
+	if val, ok := c.Get(key); ok && val != nil {
+		f64, _ = val.(float64)
+	}
+	return
+}
+
+// GetTime returns the value associated with the key as time.
+func (c *Context) GetTime(key string) (t time.Time) {
+	if val, ok := c.Get(key); ok && val != nil {
+		t, _ = val.(time.Time)
+	}
+	return
+}
+
+// GetDuration returns the value associated with the key as a duration.
+func (c *Context) GetDuration(key string) (d time.Duration) {
+	if val, ok := c.Get(key); ok && val != nil {
+		d, _ = val.(time.Duration)
+	}
+	return
+}
+
+// GetStringSlice returns the value associated with the key as a slice of strings.
+func (c *Context) GetStringSlice(key string) (ss []string) {
+	if val, ok := c.Get(key); ok && val != nil {
+		ss, _ = val.([]string)
+	}
+	return
+}
+
+// GetStringMap returns the value associated with the key as a map of interfaces.
+func (c *Context) GetStringMap(key string) (sm map[string]interface{}) {
+	if val, ok := c.Get(key); ok && val != nil {
+		sm, _ = val.(map[string]interface{})
+	}
+	return
+}
+
+// GetStringMapString returns the value associated with the key as a map of strings.
+func (c *Context) GetStringMapString(key string) (sms map[string]string) {
+	if val, ok := c.Get(key); ok && val != nil {
+		sms, _ = val.(map[string]string)
+	}
+	return
+}
+
+// GetStringMapStringSlice returns the value associated with the key as a map to a slice of strings.
+func (c *Context) GetStringMapStringSlice(key string) (smss map[string][]string) {
+	if val, ok := c.Get(key); ok && val != nil {
+		smss, _ = val.(map[string][]string)
+	}
+	return
+}
+
 /************************************/
 /************************************/
 /************ INPUT DATA ************/
 /************ INPUT DATA ************/
 /************************************/
 /************************************/
@@ -195,7 +295,7 @@ func (c *Context) Param(key string) string {
 }
 }
 
 
 // Query returns the keyed url query value if it exists,
 // Query returns the keyed url query value if it exists,
-// othewise it returns an empty string `("")`.
+// otherwise it returns an empty string `("")`.
 // It is shortcut for `c.Request.URL.Query().Get(key)`
 // It is shortcut for `c.Request.URL.Query().Get(key)`
 // 		GET /path?id=1234&name=Manu&value=
 // 		GET /path?id=1234&name=Manu&value=
 // 		c.Query("id") == "1234"
 // 		c.Query("id") == "1234"
@@ -208,7 +308,7 @@ func (c *Context) Query(key string) string {
 }
 }
 
 
 // DefaultQuery returns the keyed url query value if it exists,
 // DefaultQuery returns the keyed url query value if it exists,
-// othewise it returns the specified defaultValue string.
+// otherwise it returns the specified defaultValue string.
 // See: Query() and GetQuery() for further information.
 // See: Query() and GetQuery() for further information.
 // 		GET /?name=Manu&lastname=
 // 		GET /?name=Manu&lastname=
 // 		c.DefaultQuery("name", "unknown") == "Manu"
 // 		c.DefaultQuery("name", "unknown") == "Manu"
@@ -223,7 +323,7 @@ func (c *Context) DefaultQuery(key, defaultValue string) string {
 
 
 // GetQuery is like Query(), it returns the keyed url query value
 // GetQuery is like Query(), it returns the keyed url query value
 // if it exists `(value, true)` (even when the value is an empty string),
 // if it exists `(value, true)` (even when the value is an empty string),
-// othewise it returns `("", false)`.
+// otherwise it returns `("", false)`.
 // It is shortcut for `c.Request.URL.Query().Get(key)`
 // It is shortcut for `c.Request.URL.Query().Get(key)`
 // 		GET /?name=Manu&lastname=
 // 		GET /?name=Manu&lastname=
 // 		("Manu", true) == c.GetQuery("name")
 // 		("Manu", true) == c.GetQuery("name")
@@ -296,7 +396,7 @@ func (c *Context) PostFormArray(key string) []string {
 func (c *Context) GetPostFormArray(key string) ([]string, bool) {
 func (c *Context) GetPostFormArray(key string) ([]string, bool) {
 	req := c.Request
 	req := c.Request
 	req.ParseForm()
 	req.ParseForm()
-	req.ParseMultipartForm(32 << 20) // 32 MB
+	req.ParseMultipartForm(defaultMemory)
 	if values := req.PostForm[key]; len(values) > 0 {
 	if values := req.PostForm[key]; len(values) > 0 {
 		return values, true
 		return values, true
 	}
 	}
@@ -308,6 +408,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
 	return []string{}, false
 	return []string{}, false
 }
 }
 
 
+// FormFile returns the first file for the provided form key.
+func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
+	_, fh, err := c.Request.FormFile(name)
+	return fh, err
+}
+
+// MultipartForm is the parsed multipart form, including file uploads.
+func (c *Context) MultipartForm() (*multipart.Form, error) {
+	err := c.Request.ParseMultipartForm(defaultMemory)
+	return c.Request.MultipartForm, err
+}
+
 // Bind checks the Content-Type to select a binding engine automatically,
 // Bind checks the Content-Type to select a binding engine automatically,
 // Depending the "Content-Type" header different bindings are used:
 // Depending the "Content-Type" header different bindings are used:
 // 		"application/json" --> JSON binding
 // 		"application/json" --> JSON binding
@@ -318,33 +430,38 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
 // Like ParseBody() but this method also writes a 400 error if the json is not valid.
 // Like ParseBody() but this method also writes a 400 error if the json is not valid.
 func (c *Context) Bind(obj interface{}) error {
 func (c *Context) Bind(obj interface{}) error {
 	b := binding.Default(c.Request.Method, c.ContentType())
 	b := binding.Default(c.Request.Method, c.ContentType())
-	return c.BindWith(obj, b)
+	return c.MustBindWith(obj, b)
 }
 }
 
 
-// BindJSON is a shortcut for c.BindWith(obj, binding.JSON)
+// BindJSON is a shortcut for c.MustBindWith(obj, binding.JSON)
 func (c *Context) BindJSON(obj interface{}) error {
 func (c *Context) BindJSON(obj interface{}) error {
-	return c.BindWith(obj, binding.JSON)
+	return c.MustBindWith(obj, binding.JSON)
 }
 }
 
 
-// BindWith binds the passed struct pointer using the specified binding engine.
+// MustBindWith binds the passed struct pointer using the specified binding
+// engine. It will abort the request with HTTP 400 if any error ocurrs.
 // See the binding package.
 // See the binding package.
-func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
-	if err := b.Bind(c.Request, obj); err != nil {
+func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
+	if err = c.ShouldBindWith(obj, b); err != nil {
 		c.AbortWithError(400, err).SetType(ErrorTypeBind)
 		c.AbortWithError(400, err).SetType(ErrorTypeBind)
-		return err
 	}
 	}
-	return nil
+
+	return
+}
+
+// ShouldBindWith binds the passed struct pointer using the specified binding
+// engine.
+// See the binding package.
+func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
+	return b.Bind(c.Request, obj)
 }
 }
 
 
 // ClientIP implements a best effort algorithm to return the real client IP, it parses
 // ClientIP implements a best effort algorithm to return the real client IP, it parses
 // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
 // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
+// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
 func (c *Context) ClientIP() string {
 func (c *Context) ClientIP() string {
 	if c.engine.ForwardedByClientIP {
 	if c.engine.ForwardedByClientIP {
-		clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip"))
-		if len(clientIP) > 0 {
-			return clientIP
-		}
-		clientIP = c.requestHeader("X-Forwarded-For")
+		clientIP := c.requestHeader("X-Forwarded-For")
 		if index := strings.IndexByte(clientIP, ','); index >= 0 {
 		if index := strings.IndexByte(clientIP, ','); index >= 0 {
 			clientIP = clientIP[0:index]
 			clientIP = clientIP[0:index]
 		}
 		}
@@ -352,10 +469,22 @@ func (c *Context) ClientIP() string {
 		if len(clientIP) > 0 {
 		if len(clientIP) > 0 {
 			return clientIP
 			return clientIP
 		}
 		}
+		clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
+		if len(clientIP) > 0 {
+			return clientIP
+		}
+	}
+
+	if c.engine.AppEngine {
+		if addr := c.Request.Header.Get("X-Appengine-Remote-Addr"); addr != "" {
+			return addr
+		}
 	}
 	}
+
 	if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
 	if ip, _, err := net.SplitHostPort(strings.TrimSpace(c.Request.RemoteAddr)); err == nil {
 		return ip
 		return ip
 	}
 	}
+
 	return ""
 	return ""
 }
 }
 
 
@@ -364,6 +493,16 @@ func (c *Context) ContentType() string {
 	return filterFlags(c.requestHeader("Content-Type"))
 	return filterFlags(c.requestHeader("Content-Type"))
 }
 }
 
 
+// IsWebsocket returns true if the request headers indicate that a websocket
+// handshake is being initiated by the client.
+func (c *Context) IsWebsocket() bool {
+	if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
+		strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
+		return true
+	}
+	return false
+}
+
 func (c *Context) requestHeader(key string) string {
 func (c *Context) requestHeader(key string) string {
 	if values, _ := c.Request.Header[key]; len(values) > 0 {
 	if values, _ := c.Request.Header[key]; len(values) > 0 {
 		return values[0]
 		return values[0]
@@ -375,6 +514,19 @@ func (c *Context) requestHeader(key string) string {
 /******** RESPONSE RENDERING ********/
 /******** RESPONSE RENDERING ********/
 /************************************/
 /************************************/
 
 
+// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
+func bodyAllowedForStatus(status int) bool {
+	switch {
+	case status >= 100 && status <= 199:
+		return false
+	case status == 204:
+		return false
+	case status == 304:
+		return false
+	}
+	return true
+}
+
 func (c *Context) Status(code int) {
 func (c *Context) Status(code int) {
 	c.writermem.WriteHeader(code)
 	c.writermem.WriteHeader(code)
 }
 }
@@ -390,6 +542,16 @@ func (c *Context) Header(key, value string) {
 	}
 	}
 }
 }
 
 
+// GetHeader returns value from request headers
+func (c *Context) GetHeader(key string) string {
+	return c.requestHeader(key)
+}
+
+// GetRawData return stream data
+func (c *Context) GetRawData() ([]byte, error) {
+	return ioutil.ReadAll(c.Request.Body)
+}
+
 func (c *Context) SetCookie(
 func (c *Context) SetCookie(
 	name string,
 	name string,
 	value string,
 	value string,
@@ -424,6 +586,13 @@ func (c *Context) Cookie(name string) (string, error) {
 
 
 func (c *Context) Render(code int, r render.Render) {
 func (c *Context) Render(code int, r render.Render) {
 	c.Status(code)
 	c.Status(code)
+
+	if !bodyAllowedForStatus(code) {
+		r.WriteContentType(c.Writer)
+		c.Writer.WriteHeaderNow()
+		return
+	}
+
 	if err := r.Render(c.Writer); err != nil {
 	if err := r.Render(c.Writer); err != nil {
 		panic(err)
 		panic(err)
 	}
 	}
@@ -439,7 +608,7 @@ func (c *Context) HTML(code int, name string, obj interface{}) {
 
 
 // IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
 // IndentedJSON serializes the given struct as pretty JSON (indented + endlines) into the response body.
 // It also sets the Content-Type as "application/json".
 // It also sets the Content-Type as "application/json".
-// WARNING: we recommend to use this only for development propuses since printing pretty JSON is
+// WARNING: we recommend to use this only for development purposes since printing pretty JSON is
 // more CPU and bandwidth consuming. Use Context.JSON() instead.
 // more CPU and bandwidth consuming. Use Context.JSON() instead.
 func (c *Context) IndentedJSON(code int, obj interface{}) {
 func (c *Context) IndentedJSON(code int, obj interface{}) {
 	c.Render(code, render.IndentedJSON{Data: obj})
 	c.Render(code, render.IndentedJSON{Data: obj})
@@ -448,10 +617,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
 // JSON serializes the given struct as JSON into the response body.
 // JSON serializes the given struct as JSON into the response body.
 // It also sets the Content-Type as "application/json".
 // It also sets the Content-Type as "application/json".
 func (c *Context) JSON(code int, obj interface{}) {
 func (c *Context) JSON(code int, obj interface{}) {
-	c.Status(code)
-	if err := render.WriteJSON(c.Writer, obj); err != nil {
-		panic(err)
-	}
+	c.Render(code, render.JSON{Data: obj})
 }
 }
 
 
 // XML serializes the given struct as XML into the response body.
 // XML serializes the given struct as XML into the response body.
@@ -467,8 +633,7 @@ func (c *Context) YAML(code int, obj interface{}) {
 
 
 // String writes the given string into the response body.
 // String writes the given string into the response body.
 func (c *Context) String(code int, format string, values ...interface{}) {
 func (c *Context) String(code int, format string, values ...interface{}) {
-	c.Status(code)
-	render.WriteString(c.Writer, format, values)
+	c.Render(code, render.String{Format: format, Data: values})
 }
 }
 
 
 // Redirect returns a HTTP redirect to the specific location.
 // Redirect returns a HTTP redirect to the specific location.

+ 11 - 0
context_appengine.go

@@ -0,0 +1,11 @@
+// +build appengine
+
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+func init() {
+	defaultAppEngine = true
+}

+ 509 - 56
context_test.go

@@ -7,18 +7,23 @@ package gin
 import (
 import (
 	"bytes"
 	"bytes"
 	"errors"
 	"errors"
+	"fmt"
 	"html/template"
 	"html/template"
 	"mime/multipart"
 	"mime/multipart"
 	"net/http"
 	"net/http"
 	"net/http/httptest"
 	"net/http/httptest"
+	"reflect"
 	"strings"
 	"strings"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
-	"github.com/manucorporat/sse"
+	"github.com/gin-contrib/sse"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"golang.org/x/net/context"
 )
 )
 
 
+var _ context.Context = &Context{}
+
 // Unit tests TODO
 // Unit tests TODO
 // func (c *Context) File(filepath string) {
 // func (c *Context) File(filepath string) {
 // func (c *Context) Negotiate(code int, config Negotiate) {
 // func (c *Context) Negotiate(code int, config Negotiate) {
@@ -38,6 +43,8 @@ func createMultipartRequest() *http.Request {
 	must(mw.WriteField("array", "first"))
 	must(mw.WriteField("array", "first"))
 	must(mw.WriteField("array", "second"))
 	must(mw.WriteField("array", "second"))
 	must(mw.WriteField("id", ""))
 	must(mw.WriteField("id", ""))
+	must(mw.WriteField("time_local", "31/12/2016 14:55"))
+	must(mw.WriteField("time_utc", "31/12/2016 14:55"))
 	req, err := http.NewRequest("POST", "/", body)
 	req, err := http.NewRequest("POST", "/", body)
 	must(err)
 	must(err)
 	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
 	req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -50,6 +57,37 @@ func must(err error) {
 	}
 	}
 }
 }
 
 
+func TestContextFormFile(t *testing.T) {
+	buf := new(bytes.Buffer)
+	mw := multipart.NewWriter(buf)
+	w, err := mw.CreateFormFile("file", "test")
+	if assert.NoError(t, err) {
+		w.Write([]byte("test"))
+	}
+	mw.Close()
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("POST", "/", buf)
+	c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+	f, err := c.FormFile("file")
+	if assert.NoError(t, err) {
+		assert.Equal(t, "test", f.Filename)
+	}
+}
+
+func TestContextMultipartForm(t *testing.T) {
+	buf := new(bytes.Buffer)
+	mw := multipart.NewWriter(buf)
+	mw.WriteField("foo", "bar")
+	mw.Close()
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("POST", "/", buf)
+	c.Request.Header.Set("Content-Type", mw.FormDataContentType())
+	f, err := c.MultipartForm()
+	if assert.NoError(t, err) {
+		assert.NotNil(t, f)
+	}
+}
+
 func TestContextReset(t *testing.T) {
 func TestContextReset(t *testing.T) {
 	router := New()
 	router := New()
 	c := router.allocateContext()
 	c := router.allocateContext()
@@ -74,7 +112,7 @@ func TestContextReset(t *testing.T) {
 }
 }
 
 
 func TestContextHandlers(t *testing.T) {
 func TestContextHandlers(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	assert.Nil(t, c.handlers)
 	assert.Nil(t, c.handlers)
 	assert.Nil(t, c.handlers.Last())
 	assert.Nil(t, c.handlers.Last())
 
 
@@ -95,7 +133,7 @@ func TestContextHandlers(t *testing.T) {
 // TestContextSetGet tests that a parameter is set correctly on the
 // TestContextSetGet tests that a parameter is set correctly on the
 // current context and can be retrieved using Get.
 // current context and can be retrieved using Get.
 func TestContextSetGet(t *testing.T) {
 func TestContextSetGet(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Set("foo", "bar")
 	c.Set("foo", "bar")
 
 
 	value, err := c.Get("foo")
 	value, err := c.Get("foo")
@@ -111,7 +149,7 @@ func TestContextSetGet(t *testing.T) {
 }
 }
 
 
 func TestContextSetGetValues(t *testing.T) {
 func TestContextSetGetValues(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Set("string", "this is a string")
 	c.Set("string", "this is a string")
 	c.Set("int32", int32(-42))
 	c.Set("int32", int32(-42))
 	c.Set("int64", int64(42424242424242))
 	c.Set("int64", int64(42424242424242))
@@ -131,8 +169,87 @@ func TestContextSetGetValues(t *testing.T) {
 
 
 }
 }
 
 
+func TestContextGetString(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("string", "this is a string")
+	assert.Equal(t, "this is a string", c.GetString("string"))
+}
+
+func TestContextSetGetBool(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("bool", true)
+	assert.Equal(t, true, c.GetBool("bool"))
+}
+
+func TestContextGetInt(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("int", 1)
+	assert.Equal(t, 1, c.GetInt("int"))
+}
+
+func TestContextGetInt64(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("int64", int64(42424242424242))
+	assert.Equal(t, int64(42424242424242), c.GetInt64("int64"))
+}
+
+func TestContextGetFloat64(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("float64", 4.2)
+	assert.Equal(t, 4.2, c.GetFloat64("float64"))
+}
+
+func TestContextGetTime(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	t1, _ := time.Parse("1/2/2006 15:04:05", "01/01/2017 12:00:00")
+	c.Set("time", t1)
+	assert.Equal(t, t1, c.GetTime("time"))
+}
+
+func TestContextGetDuration(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("duration", time.Second)
+	assert.Equal(t, time.Second, c.GetDuration("duration"))
+}
+
+func TestContextGetStringSlice(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Set("slice", []string{"foo"})
+	assert.Equal(t, []string{"foo"}, c.GetStringSlice("slice"))
+}
+
+func TestContextGetStringMap(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	var m = make(map[string]interface{})
+	m["foo"] = 1
+	c.Set("map", m)
+
+	assert.Equal(t, m, c.GetStringMap("map"))
+	assert.Equal(t, 1, c.GetStringMap("map")["foo"])
+}
+
+func TestContextGetStringMapString(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	var m = make(map[string]string)
+	m["foo"] = "bar"
+	c.Set("map", m)
+
+	assert.Equal(t, m, c.GetStringMapString("map"))
+	assert.Equal(t, "bar", c.GetStringMapString("map")["foo"])
+}
+
+func TestContextGetStringMapStringSlice(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	var m = make(map[string][]string)
+	m["foo"] = []string{"foo"}
+	c.Set("map", m)
+
+	assert.Equal(t, m, c.GetStringMapStringSlice("map"))
+	assert.Equal(t, []string{"foo"}, c.GetStringMapStringSlice("map")["foo"])
+}
+
 func TestContextCopy(t *testing.T) {
 func TestContextCopy(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.index = 2
 	c.index = 2
 	c.Request, _ = http.NewRequest("POST", "/hola", nil)
 	c.Request, _ = http.NewRequest("POST", "/hola", nil)
 	c.handlers = HandlersChain{func(c *Context) {}}
 	c.handlers = HandlersChain{func(c *Context) {}}
@@ -151,7 +268,7 @@ func TestContextCopy(t *testing.T) {
 }
 }
 
 
 func TestContextHandlerName(t *testing.T) {
 func TestContextHandlerName(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
 	c.handlers = HandlersChain{func(c *Context) {}, handlerNameTest}
 
 
 	assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
 	assert.Regexp(t, "^(.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest$", c.HandlerName())
@@ -161,8 +278,19 @@ func handlerNameTest(c *Context) {
 
 
 }
 }
 
 
+var handlerTest HandlerFunc = func(c *Context) {
+
+}
+
+func TestContextHandler(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.handlers = HandlersChain{func(c *Context) {}, handlerTest}
+
+	assert.Equal(t, reflect.ValueOf(handlerTest).Pointer(), reflect.ValueOf(c.Handler()).Pointer())
+}
+
 func TestContextQuery(t *testing.T) {
 func TestContextQuery(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
 	c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
 
 
 	value, ok := c.GetQuery("foo")
 	value, ok := c.GetQuery("foo")
@@ -197,7 +325,7 @@ func TestContextQuery(t *testing.T) {
 }
 }
 
 
 func TestContextQueryAndPostForm(t *testing.T) {
 func TestContextQueryAndPostForm(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
 	body := bytes.NewBufferString("foo=bar&page=11&both=&foo=second")
 	c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
 	c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main&id=omit&array[]=first&array[]=second", body)
 	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
 	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
@@ -270,15 +398,18 @@ func TestContextQueryAndPostForm(t *testing.T) {
 }
 }
 
 
 func TestContextPostFormMultipart(t *testing.T) {
 func TestContextPostFormMultipart(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request = createMultipartRequest()
 	c.Request = createMultipartRequest()
 
 
 	var obj struct {
 	var obj struct {
-		Foo      string   `form:"foo"`
-		Bar      string   `form:"bar"`
-		BarAsInt int      `form:"bar"`
-		Array    []string `form:"array"`
-		ID       string   `form:"id"`
+		Foo       string    `form:"foo"`
+		Bar       string    `form:"bar"`
+		BarAsInt  int       `form:"bar"`
+		Array     []string  `form:"array"`
+		ID        string    `form:"id"`
+		TimeLocal time.Time `form:"time_local" time_format:"02/01/2006 15:04"`
+		TimeUTC   time.Time `form:"time_utc" time_format:"02/01/2006 15:04" time_utc:"1"`
+		BlankTime time.Time `form:"blank_time" time_format:"02/01/2006 15:04"`
 	}
 	}
 	assert.NoError(t, c.Bind(&obj))
 	assert.NoError(t, c.Bind(&obj))
 	assert.Equal(t, obj.Foo, "bar")
 	assert.Equal(t, obj.Foo, "bar")
@@ -286,6 +417,11 @@ func TestContextPostFormMultipart(t *testing.T) {
 	assert.Equal(t, obj.BarAsInt, 10)
 	assert.Equal(t, obj.BarAsInt, 10)
 	assert.Equal(t, obj.Array, []string{"first", "second"})
 	assert.Equal(t, obj.Array, []string{"first", "second"})
 	assert.Equal(t, obj.ID, "")
 	assert.Equal(t, obj.ID, "")
+	assert.Equal(t, obj.TimeLocal.Format("02/01/2006 15:04"), "31/12/2016 14:55")
+	assert.Equal(t, obj.TimeLocal.Location(), time.Local)
+	assert.Equal(t, obj.TimeUTC.Format("02/01/2006 15:04"), "31/12/2016 14:55")
+	assert.Equal(t, obj.TimeUTC.Location(), time.UTC)
+	assert.True(t, obj.BlankTime.IsZero())
 
 
 	value, ok := c.GetQuery("foo")
 	value, ok := c.GetQuery("foo")
 	assert.False(t, ok)
 	assert.False(t, ok)
@@ -334,46 +470,115 @@ func TestContextPostFormMultipart(t *testing.T) {
 }
 }
 
 
 func TestContextSetCookie(t *testing.T) {
 func TestContextSetCookie(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
 	c.SetCookie("user", "gin", 1, "/", "localhost", true, true)
 	assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
 	assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
 }
 }
 
 
+func TestContextSetCookiePathEmpty(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.SetCookie("user", "gin", 1, "", "localhost", true, true)
+	assert.Equal(t, c.Writer.Header().Get("Set-Cookie"), "user=gin; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure")
+}
+
 func TestContextGetCookie(t *testing.T) {
 func TestContextGetCookie(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("GET", "/get", nil)
 	c.Request, _ = http.NewRequest("GET", "/get", nil)
 	c.Request.Header.Set("Cookie", "user=gin")
 	c.Request.Header.Set("Cookie", "user=gin")
 	cookie, _ := c.Cookie("user")
 	cookie, _ := c.Cookie("user")
 	assert.Equal(t, cookie, "gin")
 	assert.Equal(t, cookie, "gin")
+
+	_, err := c.Cookie("nokey")
+	assert.Error(t, err)
+}
+
+func TestContextBodyAllowedForStatus(t *testing.T) {
+	assert.Equal(t, false, bodyAllowedForStatus(102))
+	assert.Equal(t, false, bodyAllowedForStatus(204))
+	assert.Equal(t, false, bodyAllowedForStatus(304))
+	assert.Equal(t, true, bodyAllowedForStatus(500))
+}
+
+type TestPanicRender struct {
+}
+
+func (*TestPanicRender) Render(http.ResponseWriter) error {
+	return errors.New("TestPanicRender")
+}
+
+func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
+
+func TestContextRenderPanicIfErr(t *testing.T) {
+	defer func() {
+		r := recover()
+		assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
+	}()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.Render(http.StatusOK, &TestPanicRender{})
+
+	assert.Fail(t, "Panic not detected")
 }
 }
 
 
 // Tests that the response is serialized as JSON
 // Tests that the response is serialized as JSON
 // and Content-Type is set to application/json
 // and Content-Type is set to application/json
 func TestContextRenderJSON(t *testing.T) {
 func TestContextRenderJSON(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.JSON(201, H{"foo": "bar"})
 	c.JSON(201, H{"foo": "bar"})
 
 
-	assert.Equal(t, w.Code, 201)
-	assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
-	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
+	assert.Equal(t, 201, w.Code)
+	assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+// Tests that no JSON is rendered if code is 204
+func TestContextRenderNoContentJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.JSON(204, H{"foo": "bar"})
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
 }
 }
 
 
 // Tests that the response is serialized as JSON
 // Tests that the response is serialized as JSON
 // we change the content-type before
 // we change the content-type before
 func TestContextRenderAPIJSON(t *testing.T) {
 func TestContextRenderAPIJSON(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Header("Content-Type", "application/vnd.api+json")
 	c.Header("Content-Type", "application/vnd.api+json")
 	c.JSON(201, H{"foo": "bar"})
 	c.JSON(201, H{"foo": "bar"})
 
 
-	assert.Equal(t, w.Code, 201)
-	assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
+	assert.Equal(t, 201, w.Code)
+	assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type"))
+}
+
+// Tests that no Custom JSON is rendered if code is 204
+func TestContextRenderNoContentAPIJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.Header("Content-Type", "application/vnd.api+json")
+	c.JSON(204, H{"foo": "bar"})
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
 }
 }
 
 
 // Tests that the response is serialized as JSON
 // Tests that the response is serialized as JSON
 // and Content-Type is set to application/json
 // and Content-Type is set to application/json
 func TestContextRenderIndentedJSON(t *testing.T) {
 func TestContextRenderIndentedJSON(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
 	c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
 
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Code, 201)
@@ -381,10 +586,23 @@ func TestContextRenderIndentedJSON(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
 }
 }
 
 
+// Tests that no Custom JSON is rendered if code is 204
+func TestContextRenderNoContentIndentedJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
+}
+
 // Tests that the response executes the templates
 // Tests that the response executes the templates
 // and responds with Content-Type set to text/html
 // and responds with Content-Type set to text/html
 func TestContextRenderHTML(t *testing.T) {
 func TestContextRenderHTML(t *testing.T) {
-	c, w, router := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, router := CreateTestContext(w)
 	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
 	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
 	router.SetHTMLTemplate(templ)
 	router.SetHTMLTemplate(templ)
 
 
@@ -395,10 +613,26 @@ func TestContextRenderHTML(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 }
 }
 
 
+// Tests that no HTML is rendered if code is 204
+func TestContextRenderNoContentHTML(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, router := CreateTestContext(w)
+	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+	router.SetHTMLTemplate(templ)
+
+	c.HTML(204, "t", H{"name": "alexandernyquist"})
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
+}
+
 // TestContextXML tests that the response is serialized as XML
 // TestContextXML tests that the response is serialized as XML
 // and Content-Type is set to application/xml
 // and Content-Type is set to application/xml
 func TestContextRenderXML(t *testing.T) {
 func TestContextRenderXML(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.XML(201, H{"foo": "bar"})
 	c.XML(201, H{"foo": "bar"})
 
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Code, 201)
@@ -406,10 +640,24 @@ func TestContextRenderXML(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
 }
 }
 
 
+// Tests that no XML is rendered if code is 204
+func TestContextRenderNoContentXML(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.XML(204, H{"foo": "bar"})
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
+}
+
 // TestContextString tests that the response is returned
 // TestContextString tests that the response is returned
 // with Content-Type set to text/plain
 // with Content-Type set to text/plain
 func TestContextRenderString(t *testing.T) {
 func TestContextRenderString(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.String(201, "test %s %d", "string", 2)
 	c.String(201, "test %s %d", "string", 2)
 
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Code, 201)
@@ -417,10 +665,24 @@ func TestContextRenderString(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
 }
 }
 
 
+// Tests that no String is rendered if code is 204
+func TestContextRenderNoContentString(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.String(204, "test %s %d", "string", 2)
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
+}
+
 // TestContextString tests that the response is returned
 // TestContextString tests that the response is returned
 // with Content-Type set to text/html
 // with Content-Type set to text/html
 func TestContextRenderHTMLString(t *testing.T) {
 func TestContextRenderHTMLString(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Header("Content-Type", "text/html; charset=utf-8")
 	c.Header("Content-Type", "text/html; charset=utf-8")
 	c.String(201, "<html>%s %d</html>", "string", 3)
 	c.String(201, "<html>%s %d</html>", "string", 3)
 
 
@@ -429,10 +691,25 @@ func TestContextRenderHTMLString(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
 }
 }
 
 
+// Tests that no HTML String is rendered if code is 204
+func TestContextRenderNoContentHTMLString(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.Header("Content-Type", "text/html; charset=utf-8")
+	c.String(204, "<html>%s %d</html>", "string", 3)
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
+}
+
 // TestContextData tests that the response can be written from `bytesting`
 // TestContextData tests that the response can be written from `bytesting`
 // with specified MIME type
 // with specified MIME type
 func TestContextRenderData(t *testing.T) {
 func TestContextRenderData(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Data(201, "text/csv", []byte(`foo,bar`))
 	c.Data(201, "text/csv", []byte(`foo,bar`))
 
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Code, 201)
@@ -440,8 +717,22 @@ func TestContextRenderData(t *testing.T) {
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
 	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
 }
 }
 
 
+// Tests that no Custom Data is rendered if code is 204
+func TestContextRenderNoContentData(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
+	c.Data(204, "text/csv", []byte(`foo,bar`))
+
+	assert.Equal(t, 204, w.Code)
+	assert.Equal(t, "", w.Body.String())
+	assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
+}
+
 func TestContextRenderSSE(t *testing.T) {
 func TestContextRenderSSE(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.SSEvent("float", 1.5)
 	c.SSEvent("float", 1.5)
 	c.Render(-1, sse.Event{
 	c.Render(-1, sse.Event{
 		Id:   "123",
 		Id:   "123",
@@ -456,7 +747,9 @@ func TestContextRenderSSE(t *testing.T) {
 }
 }
 
 
 func TestContextRenderFile(t *testing.T) {
 func TestContextRenderFile(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("GET", "/", nil)
 	c.Request, _ = http.NewRequest("GET", "/", nil)
 	c.File("./gin.go")
 	c.File("./gin.go")
 
 
@@ -468,7 +761,9 @@ func TestContextRenderFile(t *testing.T) {
 // 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) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.YAML(201, H{"foo": "bar"})
 	c.YAML(201, H{"foo": "bar"})
 
 
 	assert.Equal(t, w.Code, 201)
 	assert.Equal(t, w.Code, 201)
@@ -477,7 +772,7 @@ func TestContextRenderYAML(t *testing.T) {
 }
 }
 
 
 func TestContextHeaders(t *testing.T) {
 func TestContextHeaders(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Header("Content-Type", "text/plain")
 	c.Header("Content-Type", "text/plain")
 	c.Header("X-Custom", "value")
 	c.Header("X-Custom", "value")
 
 
@@ -494,7 +789,9 @@ func TestContextHeaders(t *testing.T) {
 
 
 // TODO
 // TODO
 func TestContextRenderRedirectWithRelativePath(t *testing.T) {
 func TestContextRenderRedirectWithRelativePath(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	assert.Panics(t, func() { c.Redirect(299, "/new_path") })
 	assert.Panics(t, func() { c.Redirect(299, "/new_path") })
 	assert.Panics(t, func() { c.Redirect(309, "/new_path") })
 	assert.Panics(t, func() { c.Redirect(309, "/new_path") })
@@ -506,7 +803,9 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
 }
 }
 
 
 func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
 func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Redirect(302, "http://google.com")
 	c.Redirect(302, "http://google.com")
 	c.Writer.WriteHeaderNow()
 	c.Writer.WriteHeaderNow()
@@ -516,7 +815,9 @@ func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
 }
 }
 
 
 func TestContextRenderRedirectWith201(t *testing.T) {
 func TestContextRenderRedirectWith201(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Redirect(201, "/resource")
 	c.Redirect(201, "/resource")
 	c.Writer.WriteHeaderNow()
 	c.Writer.WriteHeaderNow()
@@ -526,7 +827,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
 }
 }
 
 
 func TestContextRenderRedirectAll(t *testing.T) {
 func TestContextRenderRedirectAll(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
 	assert.Panics(t, func() { c.Redirect(200, "/resource") })
 	assert.Panics(t, func() { c.Redirect(200, "/resource") })
 	assert.Panics(t, func() { c.Redirect(202, "/resource") })
 	assert.Panics(t, func() { c.Redirect(202, "/resource") })
@@ -536,8 +837,70 @@ func TestContextRenderRedirectAll(t *testing.T) {
 	assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
 	assert.NotPanics(t, func() { c.Redirect(308, "/resource") })
 }
 }
 
 
+func TestContextNegotiationWithJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "", nil)
+
+	c.Negotiate(200, Negotiate{
+		Offered: []string{MIMEJSON, MIMEXML},
+		Data:    H{"foo": "bar"},
+	})
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationWithXML(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "", nil)
+
+	c.Negotiate(200, Negotiate{
+		Offered: []string{MIMEXML, MIMEJSON},
+		Data:    H{"foo": "bar"},
+	})
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "<map><foo>bar</foo></map>", w.Body.String())
+	assert.Equal(t, "application/xml; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationWithHTML(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, router := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "", nil)
+	templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
+	router.SetHTMLTemplate(templ)
+
+	c.Negotiate(200, Negotiate{
+		Offered:  []string{MIMEHTML},
+		Data:     H{"name": "gin"},
+		HTMLName: "t",
+	})
+
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "Hello gin", w.Body.String())
+	assert.Equal(t, "text/html; charset=utf-8", w.HeaderMap.Get("Content-Type"))
+}
+
+func TestContextNegotiationNotSupport(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.Request, _ = http.NewRequest("POST", "", nil)
+
+	c.Negotiate(200, Negotiate{
+		Offered: []string{MIMEPOSTForm},
+	})
+
+	assert.Equal(t, 406, w.Code)
+	assert.Equal(t, c.index, abortIndex)
+	assert.True(t, c.IsAborted())
+}
+
 func TestContextNegotiationFormat(t *testing.T) {
 func TestContextNegotiationFormat(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "", nil)
 	c.Request, _ = http.NewRequest("POST", "", nil)
 
 
 	assert.Panics(t, func() { c.NegotiateFormat() })
 	assert.Panics(t, func() { c.NegotiateFormat() })
@@ -546,7 +909,7 @@ func TestContextNegotiationFormat(t *testing.T) {
 }
 }
 
 
 func TestContextNegotiationFormatWithAccept(t *testing.T) {
 func TestContextNegotiationFormatWithAccept(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	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")
 
 
@@ -556,7 +919,7 @@ func TestContextNegotiationFormatWithAccept(t *testing.T) {
 }
 }
 
 
 func TestContextNegotiationFormatCustum(t *testing.T) {
 func TestContextNegotiationFormatCustum(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	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")
 
 
@@ -569,7 +932,7 @@ func TestContextNegotiationFormatCustum(t *testing.T) {
 }
 }
 
 
 func TestContextIsAborted(t *testing.T) {
 func TestContextIsAborted(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	assert.False(t, c.IsAborted())
 	assert.False(t, c.IsAborted())
 
 
 	c.Abort()
 	c.Abort()
@@ -585,7 +948,9 @@ func TestContextIsAborted(t *testing.T) {
 // TestContextData tests that the response can be written from `bytesting`
 // TestContextData tests that the response can be written from `bytesting`
 // with specified MIME type
 // with specified MIME type
 func TestContextAbortWithStatus(t *testing.T) {
 func TestContextAbortWithStatus(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.index = 4
 	c.index = 4
 	c.AbortWithStatus(401)
 	c.AbortWithStatus(401)
 
 
@@ -595,8 +960,38 @@ func TestContextAbortWithStatus(t *testing.T) {
 	assert.True(t, c.IsAborted())
 	assert.True(t, c.IsAborted())
 }
 }
 
 
+type testJSONAbortMsg struct {
+	Foo string `json:"foo"`
+	Bar string `json:"bar"`
+}
+
+func TestContextAbortWithStatusJSON(t *testing.T) {
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+	c.index = 4
+
+	in := new(testJSONAbortMsg)
+	in.Bar = "barValue"
+	in.Foo = "fooValue"
+
+	c.AbortWithStatusJSON(415, in)
+
+	assert.Equal(t, c.index, abortIndex)
+	assert.Equal(t, c.Writer.Status(), 415)
+	assert.Equal(t, w.Code, 415)
+	assert.True(t, c.IsAborted())
+
+	contentType := w.Header().Get("Content-Type")
+	assert.Equal(t, contentType, "application/json; charset=utf-8")
+
+	buf := new(bytes.Buffer)
+	buf.ReadFrom(w.Body)
+	jsonStringBody := buf.String()
+	assert.Equal(t, fmt.Sprint(`{"foo":"fooValue","bar":"barValue"}`), jsonStringBody)
+}
+
 func TestContextError(t *testing.T) {
 func TestContextError(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	assert.Empty(t, c.Errors)
 	assert.Empty(t, c.Errors)
 
 
 	c.Error(errors.New("first error"))
 	c.Error(errors.New("first error"))
@@ -622,7 +1017,7 @@ func TestContextError(t *testing.T) {
 }
 }
 
 
 func TestContextTypedError(t *testing.T) {
 func TestContextTypedError(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
 	c.Error(errors.New("externo 0")).SetType(ErrorTypePublic)
 	c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
 	c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate)
 
 
@@ -636,7 +1031,9 @@ func TestContextTypedError(t *testing.T) {
 }
 }
 
 
 func TestContextAbortWithError(t *testing.T) {
 func TestContextAbortWithError(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
 	c.AbortWithError(401, errors.New("bad input")).SetMeta("some input")
 
 
 	assert.Equal(t, w.Code, 401)
 	assert.Equal(t, w.Code, 401)
@@ -645,27 +1042,37 @@ func TestContextAbortWithError(t *testing.T) {
 }
 }
 
 
 func TestContextClientIP(t *testing.T) {
 func TestContextClientIP(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "/", nil)
 	c.Request, _ = http.NewRequest("POST", "/", nil)
 
 
 	c.Request.Header.Set("X-Real-IP", " 10.10.10.10  ")
 	c.Request.Header.Set("X-Real-IP", " 10.10.10.10  ")
 	c.Request.Header.Set("X-Forwarded-For", "  20.20.20.20, 30.30.30.30")
 	c.Request.Header.Set("X-Forwarded-For", "  20.20.20.20, 30.30.30.30")
+	c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
 	c.Request.RemoteAddr = "  40.40.40.40:42123 "
 	c.Request.RemoteAddr = "  40.40.40.40:42123 "
 
 
-	assert.Equal(t, c.ClientIP(), "10.10.10.10")
+	assert.Equal(t, "20.20.20.20", c.ClientIP())
 
 
-	c.Request.Header.Del("X-Real-IP")
-	assert.Equal(t, c.ClientIP(), "20.20.20.20")
+	c.Request.Header.Del("X-Forwarded-For")
+	assert.Equal(t, "10.10.10.10", c.ClientIP())
 
 
 	c.Request.Header.Set("X-Forwarded-For", "30.30.30.30  ")
 	c.Request.Header.Set("X-Forwarded-For", "30.30.30.30  ")
-	assert.Equal(t, c.ClientIP(), "30.30.30.30")
+	assert.Equal(t, "30.30.30.30", c.ClientIP())
 
 
 	c.Request.Header.Del("X-Forwarded-For")
 	c.Request.Header.Del("X-Forwarded-For")
-	assert.Equal(t, c.ClientIP(), "40.40.40.40")
+	c.Request.Header.Del("X-Real-IP")
+	c.engine.AppEngine = true
+	assert.Equal(t, "50.50.50.50", c.ClientIP())
+
+	c.Request.Header.Del("X-Appengine-Remote-Addr")
+	assert.Equal(t, "40.40.40.40", c.ClientIP())
+
+	// no port
+	c.Request.RemoteAddr = "50.50.50.50"
+	assert.Equal(t, "", c.ClientIP())
 }
 }
 
 
 func TestContextContentType(t *testing.T) {
 func TestContextContentType(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "/", nil)
 	c.Request, _ = http.NewRequest("POST", "/", nil)
 	c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
 	c.Request.Header.Set("Content-Type", "application/json; charset=utf-8")
 
 
@@ -673,7 +1080,7 @@ func TestContextContentType(t *testing.T) {
 }
 }
 
 
 func TestContextAutoBindJSON(t *testing.T) {
 func TestContextAutoBindJSON(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 
 
@@ -688,7 +1095,9 @@ func TestContextAutoBindJSON(t *testing.T) {
 }
 }
 
 
 func TestContextBindWithJSON(t *testing.T) {
 func TestContextBindWithJSON(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 	c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
 
 
@@ -703,7 +1112,9 @@ func TestContextBindWithJSON(t *testing.T) {
 }
 }
 
 
 func TestContextBadAutoBind(t *testing.T) {
 func TestContextBadAutoBind(t *testing.T) {
-	c, w, _ := CreateTestContext()
+	w := httptest.NewRecorder()
+	c, _ := CreateTestContext(w)
+
 	c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 	c.Request.Header.Add("Content-Type", MIMEJSON)
 	var obj struct {
 	var obj struct {
@@ -722,7 +1133,7 @@ func TestContextBadAutoBind(t *testing.T) {
 }
 }
 
 
 func TestContextGolangContext(t *testing.T) {
 func TestContextGolangContext(t *testing.T) {
-	c, _, _ := CreateTestContext()
+	c, _ := CreateTestContext(httptest.NewRecorder())
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}"))
 	assert.NoError(t, c.Err())
 	assert.NoError(t, c.Err())
 	assert.Nil(t, c.Done())
 	assert.Nil(t, c.Done())
@@ -736,3 +1147,45 @@ func TestContextGolangContext(t *testing.T) {
 	assert.Equal(t, c.Value("foo"), "bar")
 	assert.Equal(t, c.Value("foo"), "bar")
 	assert.Nil(t, c.Value(1))
 	assert.Nil(t, c.Value(1))
 }
 }
+
+func TestWebsocketsRequired(t *testing.T) {
+	// Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("GET", "/chat", nil)
+	c.Request.Header.Set("Host", "server.example.com")
+	c.Request.Header.Set("Upgrade", "websocket")
+	c.Request.Header.Set("Connection", "Upgrade")
+	c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
+	c.Request.Header.Set("Origin", "http://example.com")
+	c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat")
+	c.Request.Header.Set("Sec-WebSocket-Version", "13")
+
+	assert.True(t, c.IsWebsocket())
+
+	// Normal request, no websocket required.
+	c, _ = CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("GET", "/chat", nil)
+	c.Request.Header.Set("Host", "server.example.com")
+
+	assert.False(t, c.IsWebsocket())
+}
+
+func TestGetRequestHeaderValue(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	c.Request, _ = http.NewRequest("GET", "/chat", nil)
+	c.Request.Header.Set("Gin-Version", "1.0.0")
+
+	assert.Equal(t, "1.0.0", c.GetHeader("Gin-Version"))
+	assert.Equal(t, "", c.GetHeader("Connection"))
+}
+
+func TestContextGetRawData(t *testing.T) {
+	c, _ := CreateTestContext(httptest.NewRecorder())
+	body := bytes.NewBufferString("Fetch binary post data")
+	c.Request, _ = http.NewRequest("POST", "/", body)
+	c.Request.Header.Add("Content-Type", MIMEPOSTForm)
+
+	data, err := c.GetRawData()
+	assert.Nil(t, err)
+	assert.Equal(t, "Fetch binary post data", string(data))
+}

+ 20 - 0
debug_test.go

@@ -7,6 +7,7 @@ package gin
 import (
 import (
 	"bytes"
 	"bytes"
 	"errors"
 	"errors"
+	"html/template"
 	"io"
 	"io"
 	"log"
 	"log"
 	"os"
 	"os"
@@ -66,6 +67,25 @@ 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$`, w.String())
 	assert.Regexp(t, `^\[GIN-debug\] GET    /path/to/route/:param     --> (.*/vendor/)?github.com/gin-gonic/gin.handlerNameTest \(2 handlers\)\n$`, w.String())
 }
 }
 
 
+func TestDebugPrintLoadTemplate(t *testing.T) {
+	var w bytes.Buffer
+	setup(&w)
+	defer teardown()
+
+	templ := template.Must(template.New("").Delims("{[{", "}]}").ParseGlob("./fixtures/basic/hello.tmpl"))
+	debugPrintLoadTemplate(templ)
+	assert.Equal(t, w.String(), "[GIN-debug] Loaded HTML Templates (2): \n\t- \n\t- hello.tmpl\n\n")
+}
+
+func TestDebugPrintWARNINGSetHTMLTemplate(t *testing.T) {
+	var w bytes.Buffer
+	setup(&w)
+	defer teardown()
+
+	debugPrintWARNINGSetHTMLTemplate()
+	assert.Equal(t, w.String(), "[GIN-debug] [WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called\nat initialization. ie. before any route is registered or the router is listening in a socket:\n\n\trouter := gin.Default()\n\trouter.SetHTMLTemplate(template) // << good place\n\n")
+}
+
 func setup(w io.Writer) {
 func setup(w io.Writer) {
 	SetMode(DebugMode)
 	SetMode(DebugMode)
 	log.SetOutput(w)
 	log.SetOutput(w)

+ 14 - 1
deprecated.go

@@ -4,9 +4,22 @@
 
 
 package gin
 package gin
 
 
-import "log"
+import (
+	"github.com/gin-gonic/gin/binding"
+	"log"
+)
 
 
 func (c *Context) GetCookie(name string) (string, error) {
 func (c *Context) GetCookie(name string) (string, error) {
 	log.Println("GetCookie() method is deprecated. Use Cookie() instead.")
 	log.Println("GetCookie() method is deprecated. Use Cookie() instead.")
 	return c.Cookie(name)
 	return c.Cookie(name)
 }
 }
+
+// BindWith binds the passed struct pointer using the specified binding engine.
+// See the binding package.
+func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
+	log.Println(`BindWith(\"interface{}, binding.Binding\") error is going to
+	be deprecated, please check issue #662 and either use MustBindWith() if you
+	want HTTP 400 to be automatically returned if any error occur, of use
+	ShouldBindWith() if you need to manage the error.`)
+	return c.MustBindWith(obj, b)
+}

+ 2 - 2
errors.go

@@ -72,7 +72,7 @@ func (msg *Error) MarshalJSON() ([]byte, error) {
 }
 }
 
 
 // Implements the error interface
 // Implements the error interface
-func (msg *Error) Error() string {
+func (msg Error) Error() string {
 	return msg.Err.Error()
 	return msg.Err.Error()
 }
 }
 
 
@@ -80,7 +80,7 @@ func (msg *Error) IsType(flags ErrorType) bool {
 	return (msg.Type & flags) > 0
 	return (msg.Type & flags) > 0
 }
 }
 
 
-// Returns a readonly copy filterd the byte.
+// 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 {
 	if len(a) == 0 {
 	if len(a) == 0 {

+ 7 - 0
errors_test.go

@@ -54,6 +54,13 @@ func TestError(t *testing.T) {
 		"status": "200",
 		"status": "200",
 		"data":   "some data",
 		"data":   "some data",
 	})
 	})
+
+	type customError struct {
+		status string
+		data   string
+	}
+	err.SetMeta(customError{status: "200", data: "other data"})
+	assert.Equal(t, err.JSON(), customError{status: "200", data: "other data"})
 }
 }
 
 
 func TestErrorSlice(t *testing.T) {
 func TestErrorSlice(t *testing.T) {

+ 2 - 1
examples/app-engine/hello.go

@@ -1,8 +1,9 @@
 package hello
 package hello
 
 
 import (
 import (
-	"github.com/gin-gonic/gin"
 	"net/http"
 	"net/http"
+
+	"github.com/gin-gonic/gin"
 )
 )
 
 
 // This function's name is a must. App Engine uses it to drive the requests properly.
 // This function's name is a must. App Engine uses it to drive the requests properly.

+ 19 - 0
examples/auto-tls/example1.go

@@ -0,0 +1,19 @@
+package main
+
+import (
+	"log"
+
+	"github.com/gin-gonic/autotls"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+
+	// Ping handler
+	r.GET("/ping", func(c *gin.Context) {
+		c.String(200, "pong")
+	})
+
+	log.Fatal(autotls.Run(r, "example1.com", "example2.com"))
+}

+ 26 - 0
examples/auto-tls/example2.go

@@ -0,0 +1,26 @@
+package main
+
+import (
+	"log"
+
+	"github.com/gin-gonic/autotls"
+	"github.com/gin-gonic/gin"
+	"golang.org/x/crypto/acme/autocert"
+)
+
+func main() {
+	r := gin.Default()
+
+	// Ping handler
+	r.GET("/ping", func(c *gin.Context) {
+		c.String(200, "pong")
+	})
+
+	m := autocert.Manager{
+		Prompt:     autocert.AcceptTOS,
+		HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"),
+		Cache:      autocert.DirCache("/var/www/.cache"),
+	}
+
+	log.Fatal(autotls.RunWithManager(r, m))
+}

+ 2 - 0
examples/basic/main.go

@@ -7,6 +7,8 @@ import (
 var DB = make(map[string]string)
 var DB = make(map[string]string)
 
 
 func main() {
 func main() {
+	// Disable Console Color
+	// gin.DisableConsoleColor()
 	r := gin.Default()
 	r := gin.Default()
 
 
 	// Ping test
 	// Ping test

+ 45 - 0
examples/graceful-shutdown/close/server.go

@@ -0,0 +1,45 @@
+// +build go1.8
+
+package main
+
+import (
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.GET("/", func(c *gin.Context) {
+		c.String(http.StatusOK, "Welcome Gin Server")
+	})
+
+	server := &http.Server{
+		Addr:    ":8080",
+		Handler: router,
+	}
+
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+
+	go func() {
+		<-quit
+		log.Println("receive interrupt signal")
+		if err := server.Close(); err != nil {
+			log.Fatal("Server Close:", err)
+		}
+	}()
+
+	if err := server.ListenAndServe(); err != nil {
+		if err == http.ErrServerClosed {
+			log.Println("Server closed under request")
+		} else {
+			log.Fatal("Server closed unexpect")
+		}
+	}
+
+	log.Println("Server exist")
+}

+ 48 - 0
examples/graceful-shutdown/graceful-shutdown/server.go

@@ -0,0 +1,48 @@
+// +build go1.8
+
+package main
+
+import (
+	"context"
+	"log"
+	"net/http"
+	"os"
+	"os/signal"
+	"time"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.GET("/", func(c *gin.Context) {
+		time.Sleep(5 * time.Second)
+		c.String(http.StatusOK, "Welcome Gin Server")
+	})
+
+	srv := &http.Server{
+		Addr:    ":8080",
+		Handler: router,
+	}
+
+	go func() {
+		// service connections
+		if err := srv.ListenAndServe(); err != nil {
+			log.Printf("listen: %s\n", err)
+		}
+	}()
+
+	// Wait for interrupt signal to gracefully shutdown the server with
+	// a timeout of 5 seconds.
+	quit := make(chan os.Signal)
+	signal.Notify(quit, os.Interrupt)
+	<-quit
+	log.Println("Shutdown Server ...")
+
+	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	if err := srv.Shutdown(ctx); err != nil {
+		log.Fatal("Server Shutdown:", err)
+	}
+	log.Println("Server exist")
+}

+ 10 - 0
examples/realtime-advanced/Makefile

@@ -0,0 +1,10 @@
+all: deps build
+
+.PHONY: deps
+deps:
+	go get -d -v github.com/dustin/go-broadcast/...
+	go get -d -v github.com/manucorporat/stats/...
+
+.PHONY: build
+build: deps
+	go build -o realtime-advanced main.go rooms.go routes.go stats.go

+ 0 - 1
examples/realtime-advanced/routes.go

@@ -11,7 +11,6 @@ import (
 )
 )
 
 
 func rateLimit(c *gin.Context) {
 func rateLimit(c *gin.Context) {
-
 	ip := c.ClientIP()
 	ip := c.ClientIP()
 	value := int(ips.Add(ip, 1))
 	value := int(ips.Add(ip, 1))
 	if value%50 == 0 {
 	if value%50 == 0 {

+ 7 - 5
examples/realtime-advanced/stats.go

@@ -8,11 +8,13 @@ import (
 	"github.com/manucorporat/stats"
 	"github.com/manucorporat/stats"
 )
 )
 
 
-var ips = stats.New()
-var messages = stats.New()
-var users = stats.New()
-var mutexStats sync.RWMutex
-var savedStats map[string]uint64
+var (
+	ips        = stats.New()
+	messages   = stats.New()
+	users      = stats.New()
+	mutexStats sync.RWMutex
+	savedStats map[string]uint64
+)
 
 
 func statsWorker() {
 func statsWorker() {
 	c := time.Tick(1 * time.Second)
 	c := time.Tick(1 * time.Second)

+ 9 - 0
examples/realtime-chat/Makefile

@@ -0,0 +1,9 @@
+all: deps build
+
+.PHONY: deps
+deps:
+	go get -d -v github.com/dustin/go-broadcast/...
+
+.PHONY: build
+build: deps
+	go build -o realtime-chat main.go rooms.go template.go

+ 39 - 0
examples/upload-file/multiple/main.go

@@ -0,0 +1,39 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.Static("/", "./public")
+	router.POST("/upload", func(c *gin.Context) {
+		name := c.PostForm("name")
+		email := c.PostForm("email")
+
+		// Multipart form
+		form, _ := c.MultipartForm()
+		files := form.File["files"]
+
+		for _, file := range files {
+			// Source
+			src, _ := file.Open()
+			defer src.Close()
+
+			// Destination
+			dst, _ := os.Create(file.Filename)
+			defer dst.Close()
+
+			// Copy
+			io.Copy(dst, src)
+		}
+
+		c.String(http.StatusOK, fmt.Sprintf("Uploaded successfully %d files with fields name=%s and email=%s.", len(files), name, email))
+	})
+	router.Run(":8080")
+}

+ 17 - 0
examples/upload-file/multiple/public/index.html

@@ -0,0 +1,17 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>Multiple file upload</title>
+</head>
+<body>
+<h1>Upload multiple files with fields</h1>
+
+<form action="/upload" method="post" enctype="multipart/form-data">
+    Name: <input type="text" name="name"><br>
+    Email: <input type="email" name="email"><br>
+    Files: <input type="file" name="files" multiple><br><br>
+    <input type="submit" value="Submit">
+</form>
+</body>
+</html>

+ 34 - 0
examples/upload-file/single/main.go

@@ -0,0 +1,34 @@
+package main
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	router := gin.Default()
+	router.Static("/", "./public")
+	router.POST("/upload", func(c *gin.Context) {
+		name := c.PostForm("name")
+		email := c.PostForm("email")
+
+		// Source
+		file, _ := c.FormFile("file")
+		src, _ := file.Open()
+		defer src.Close()
+
+		// Destination
+		dst, _ := os.Create(file.Filename)
+		defer dst.Close()
+
+		// Copy
+		io.Copy(dst, src)
+
+		c.String(http.StatusOK, fmt.Sprintf("File %s uploaded successfully with fields name=%s and email=%s.", file.Filename, name, email))
+	})
+	router.Run(":8080")
+}

+ 16 - 0
examples/upload-file/single/public/index.html

@@ -0,0 +1,16 @@
+<!doctype html>
+<html lang="en">
+<head>
+    <meta charset="utf-8">
+    <title>Single file upload</title>
+</head>
+<body>
+<h1>Upload single file with fields</h1>
+
+<form action="/upload" method="post" enctype="multipart/form-data">
+    Name: <input type="text" name="name"><br>
+    Email: <input type="email" name="email"><br>
+    Files: <input type="file" name="file"><br><br>
+    <input type="submit" value="Submit">
+</form>
+</body>

+ 1 - 0
fixtures/basic/hello.tmpl

@@ -0,0 +1 @@
+<h1>Hello {[{.name}]}</h1>

+ 1 - 0
fixtures/basic/raw.tmpl

@@ -0,0 +1 @@
+Date: {[{.now | formatAsDate}]}

+ 5 - 1
fs.go

@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
 package gin
 package gin
 
 
 import (
 import (
@@ -14,7 +18,7 @@ type (
 	}
 	}
 )
 )
 
 
-// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used interally
+// Dir returns a http.Filesystem that can be used by http.FileServer(). It is used internally
 // in router.Static().
 // in router.Static().
 // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
 // if listDirectory == true, then it works the same as http.Dir() otherwise it returns
 // a filesystem that prevents http.FileServer() to list the directory files.
 // a filesystem that prevents http.FileServer() to list the directory files.

+ 60 - 12
gin.go

@@ -15,10 +15,11 @@ import (
 )
 )
 
 
 // Version is Framework's version
 // Version is Framework's version
-const Version = "v1.0rc2"
+const Version = "v1.2"
 
 
 var default404Body = []byte("404 page not found")
 var default404Body = []byte("404 page not found")
 var default405Body = []byte("405 method not allowed")
 var default405Body = []byte("405 method not allowed")
+var defaultAppEngine bool
 
 
 type HandlerFunc func(*Context)
 type HandlerFunc func(*Context)
 type HandlersChain []HandlerFunc
 type HandlersChain []HandlerFunc
@@ -44,7 +45,9 @@ type (
 	// Create an instance of Engine, by using New() or Default()
 	// Create an instance of Engine, by using New() or Default()
 	Engine struct {
 	Engine struct {
 		RouterGroup
 		RouterGroup
+		delims      render.Delims
 		HTMLRender  render.HTMLRender
 		HTMLRender  render.HTMLRender
+		FuncMap     template.FuncMap
 		allNoRoute  HandlersChain
 		allNoRoute  HandlersChain
 		allNoMethod HandlersChain
 		allNoMethod HandlersChain
 		noRoute     HandlersChain
 		noRoute     HandlersChain
@@ -78,6 +81,17 @@ type (
 		// handler.
 		// handler.
 		HandleMethodNotAllowed bool
 		HandleMethodNotAllowed bool
 		ForwardedByClientIP    bool
 		ForwardedByClientIP    bool
+
+		// #726 #755 If enabled, it will thrust some headers starting with
+		// 'X-AppEngine...' for better integration with that PaaS.
+		AppEngine bool
+
+		// If enabled, the url.RawPath will be used to find parameters.
+		UseRawPath bool
+		// If true, the path value will be unescaped.
+		// If UseRawPath is false (by default), the UnescapePathValues effectively is true,
+		// as url.Path gonna be used, which is already unescaped.
+		UnescapePathValues bool
 	}
 	}
 )
 )
 
 
@@ -89,6 +103,8 @@ var _ IRouter = &Engine{}
 // - RedirectFixedPath:      false
 // - RedirectFixedPath:      false
 // - HandleMethodNotAllowed: false
 // - HandleMethodNotAllowed: false
 // - ForwardedByClientIP:    true
 // - ForwardedByClientIP:    true
+// - UseRawPath:             false
+// - UnescapePathValues:     true
 func New() *Engine {
 func New() *Engine {
 	debugPrintWARNINGNew()
 	debugPrintWARNINGNew()
 	engine := &Engine{
 	engine := &Engine{
@@ -97,11 +113,16 @@ func New() *Engine {
 			basePath: "/",
 			basePath: "/",
 			root:     true,
 			root:     true,
 		},
 		},
+		FuncMap:                template.FuncMap{},
 		RedirectTrailingSlash:  true,
 		RedirectTrailingSlash:  true,
 		RedirectFixedPath:      false,
 		RedirectFixedPath:      false,
 		HandleMethodNotAllowed: false,
 		HandleMethodNotAllowed: false,
 		ForwardedByClientIP:    true,
 		ForwardedByClientIP:    true,
+		AppEngine:              defaultAppEngine,
+		UseRawPath:             false,
+		UnescapePathValues:     true,
 		trees:                  make(methodTrees, 0, 9),
 		trees:                  make(methodTrees, 0, 9),
+		delims:                 render.Delims{"{{", "}}"},
 	}
 	}
 	engine.RouterGroup.engine = engine
 	engine.RouterGroup.engine = engine
 	engine.pool.New = func() interface{} {
 	engine.pool.New = func() interface{} {
@@ -121,21 +142,26 @@ func (engine *Engine) allocateContext() *Context {
 	return &Context{engine: engine}
 	return &Context{engine: engine}
 }
 }
 
 
+func (engine *Engine) Delims(left, right string) *Engine {
+	engine.delims = render.Delims{left, right}
+	return engine
+}
+
 func (engine *Engine) LoadHTMLGlob(pattern string) {
 func (engine *Engine) LoadHTMLGlob(pattern string) {
 	if IsDebugging() {
 	if IsDebugging() {
-		debugPrintLoadTemplate(template.Must(template.ParseGlob(pattern)))
-		engine.HTMLRender = render.HTMLDebug{Glob: pattern}
+		debugPrintLoadTemplate(template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern)))
+		engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
 	} else {
 	} else {
-		templ := template.Must(template.ParseGlob(pattern))
+		templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseGlob(pattern))
 		engine.SetHTMLTemplate(templ)
 		engine.SetHTMLTemplate(templ)
 	}
 	}
 }
 }
 
 
 func (engine *Engine) LoadHTMLFiles(files ...string) {
 func (engine *Engine) LoadHTMLFiles(files ...string) {
 	if IsDebugging() {
 	if IsDebugging() {
-		engine.HTMLRender = render.HTMLDebug{Files: files}
+		engine.HTMLRender = render.HTMLDebug{Files: files, FuncMap: engine.FuncMap, Delims: engine.delims}
 	} else {
 	} else {
-		templ := template.Must(template.ParseFiles(files...))
+		templ := template.Must(template.New("").Delims(engine.delims.Left, engine.delims.Right).Funcs(engine.FuncMap).ParseFiles(files...))
 		engine.SetHTMLTemplate(templ)
 		engine.SetHTMLTemplate(templ)
 	}
 	}
 }
 }
@@ -144,7 +170,12 @@ func (engine *Engine) SetHTMLTemplate(templ *template.Template) {
 	if len(engine.trees) > 0 {
 	if len(engine.trees) > 0 {
 		debugPrintWARNINGSetHTMLTemplate()
 		debugPrintWARNINGSetHTMLTemplate()
 	}
 	}
-	engine.HTMLRender = render.HTMLProduction{Template: templ}
+
+	engine.HTMLRender = render.HTMLProduction{Template: templ.Funcs(engine.FuncMap)}
+}
+
+func (engine *Engine) SetFuncMap(funcMap template.FuncMap) {
+	engine.FuncMap = funcMap
 }
 }
 
 
 // NoRoute adds handlers for NoRoute. It return a 404 code by default.
 // NoRoute adds handlers for NoRoute. It return a 404 code by default.
@@ -267,9 +298,26 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	engine.pool.Put(c)
 	engine.pool.Put(c)
 }
 }
 
 
+// Re-enter a context that has been rewritten.
+// This can be done by setting c.Request.Path to your new target.
+// Disclaimer: You can loop yourself to death with this, use wisely.
+func (engine *Engine) HandleContext(c *Context) {
+	c.reset()
+	engine.handleHTTPRequest(c)
+	engine.pool.Put(c)
+}
+
 func (engine *Engine) handleHTTPRequest(context *Context) {
 func (engine *Engine) handleHTTPRequest(context *Context) {
 	httpMethod := context.Request.Method
 	httpMethod := context.Request.Method
-	path := context.Request.URL.Path
+	var path string
+	var unescape bool
+	if engine.UseRawPath && len(context.Request.URL.RawPath) > 0 {
+		path = context.Request.URL.RawPath
+		unescape = engine.UnescapePathValues
+	} else {
+		path = context.Request.URL.Path
+		unescape = false
+	}
 
 
 	// Find root of the tree for the given HTTP method
 	// Find root of the tree for the given HTTP method
 	t := engine.trees
 	t := engine.trees
@@ -277,15 +325,15 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
 		if t[i].method == httpMethod {
 		if t[i].method == httpMethod {
 			root := t[i].root
 			root := t[i].root
 			// Find route in tree
 			// Find route in tree
-			handlers, params, tsr := root.getValue(path, context.Params)
+			handlers, params, tsr := root.getValue(path, context.Params, unescape)
 			if handlers != nil {
 			if handlers != nil {
 				context.handlers = handlers
 				context.handlers = handlers
 				context.Params = params
 				context.Params = params
 				context.Next()
 				context.Next()
 				context.writermem.WriteHeaderNow()
 				context.writermem.WriteHeaderNow()
 				return
 				return
-
-			} else if httpMethod != "CONNECT" && path != "/" {
+			}
+			if httpMethod != "CONNECT" && path != "/" {
 				if tsr && engine.RedirectTrailingSlash {
 				if tsr && engine.RedirectTrailingSlash {
 					redirectTrailingSlash(context)
 					redirectTrailingSlash(context)
 					return
 					return
@@ -302,7 +350,7 @@ func (engine *Engine) handleHTTPRequest(context *Context) {
 	if engine.HandleMethodNotAllowed {
 	if engine.HandleMethodNotAllowed {
 		for _, tree := range engine.trees {
 		for _, tree := range engine.trees {
 			if tree.method != httpMethod {
 			if tree.method != httpMethod {
-				if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil {
+				if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
 					context.handlers = engine.allNoMethod
 					context.handlers = engine.allNoMethod
 					serveError(context, 405, default405Body)
 					serveError(context, 405, default405Body)
 					return
 					return

+ 1 - 1
ginS/README.md

@@ -1,4 +1,4 @@
-#Gin Default Server
+# Gin Default Server
 
 
 This is API experiment for Gin.
 This is API experiment for Gin.
 
 

+ 18 - 14
gin_integration_test.go

@@ -1,3 +1,7 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
 package gin
 package gin
 
 
 import (
 import (
@@ -6,18 +10,18 @@ import (
 	"io/ioutil"
 	"io/ioutil"
 	"net"
 	"net"
 	"net/http"
 	"net/http"
+	"net/http/httptest"
 	"os"
 	"os"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
-	"net/http/httptest"
 )
 )
 
 
 func testRequest(t *testing.T, url string) {
 func testRequest(t *testing.T, url string) {
 	resp, err := http.Get(url)
 	resp, err := http.Get(url)
-	defer resp.Body.Close()
 	assert.NoError(t, err)
 	assert.NoError(t, err)
+	defer resp.Body.Close()
 
 
 	body, ioerr := ioutil.ReadAll(resp.Body)
 	body, ioerr := ioutil.ReadAll(resp.Body)
 	assert.NoError(t, ioerr)
 	assert.NoError(t, ioerr)
@@ -115,17 +119,17 @@ func TestWithHttptestWithAutoSelectedPort(t *testing.T) {
 	testRequest(t, ts.URL+"/example")
 	testRequest(t, ts.URL+"/example")
 }
 }
 
 
-func TestWithHttptestWithSpecifiedPort(t *testing.T) {
-	router := New()
-	router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
+// func TestWithHttptestWithSpecifiedPort(t *testing.T) {
+// 	router := New()
+// 	router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") })
 
 
-	l, _ := net.Listen("tcp", ":8033")
-	ts := httptest.Server{
-		Listener: l,
-		Config:   &http.Server{Handler: router},
-	}
-	ts.Start()
-	defer ts.Close()
+// 	l, _ := net.Listen("tcp", ":8033")
+// 	ts := httptest.Server{
+// 		Listener: l,
+// 		Config:   &http.Server{Handler: router},
+// 	}
+// 	ts.Start()
+// 	defer ts.Close()
 
 
-	testRequest(t, "http://localhost:8033/example")
-}
+// 	testRequest(t, "http://localhost:8033/example")
+// }

+ 111 - 1
gin_test.go

@@ -5,14 +5,98 @@
 package gin
 package gin
 
 
 import (
 import (
+	"fmt"
+	"html/template"
+	"io/ioutil"
+	"net/http"
 	"reflect"
 	"reflect"
 	"testing"
 	"testing"
+	"time"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
+func formatAsDate(t time.Time) string {
+	year, month, day := t.Date()
+	return fmt.Sprintf("%d/%02d/%02d", year, month, day)
+}
+
+func setupHTMLFiles(t *testing.T) func() {
+	go func() {
+		SetMode(TestMode)
+		router := New()
+		router.Delims("{[{", "}]}")
+		router.SetFuncMap(template.FuncMap{
+			"formatAsDate": formatAsDate,
+		})
+		router.LoadHTMLFiles("./fixtures/basic/hello.tmpl", "./fixtures/basic/raw.tmpl")
+		router.GET("/test", func(c *Context) {
+			c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
+		})
+		router.GET("/raw", func(c *Context) {
+			c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+				"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+			})
+		})
+		router.Run(":8888")
+	}()
+	t.Log("waiting 1 second for server startup")
+	time.Sleep(1 * time.Second)
+	return func() {}
+}
+
+func setupHTMLGlob(t *testing.T) func() {
+	go func() {
+		SetMode(DebugMode)
+		router := New()
+		router.Delims("{[{", "}]}")
+		router.SetFuncMap(template.FuncMap{
+			"formatAsDate": formatAsDate,
+		})
+		router.LoadHTMLGlob("./fixtures/basic/*")
+		router.GET("/test", func(c *Context) {
+			c.HTML(http.StatusOK, "hello.tmpl", map[string]string{"name": "world"})
+		})
+		router.GET("/raw", func(c *Context) {
+			c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{
+				"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
+			})
+		})
+		router.Run(":8888")
+	}()
+	t.Log("waiting 1 second for server startup")
+	time.Sleep(1 * time.Second)
+	return func() {}
+}
+
 //TODO
 //TODO
-// func (engine *Engine) LoadHTMLGlob(pattern string) {
+func TestLoadHTMLGlob(t *testing.T) {
+	td := setupHTMLGlob(t)
+	res, err := http.Get("http://127.0.0.1:8888/test")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	resp, _ := ioutil.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
+
+	td()
+}
+
+func TestLoadHTMLGlobFromFuncMap(t *testing.T) {
+	time.Now()
+	td := setupHTMLGlob(t)
+	res, err := http.Get("http://127.0.0.1:8888/raw")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	resp, _ := ioutil.ReadAll(res.Body)
+	assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
+
+	td()
+}
+
 // func (engine *Engine) LoadHTMLFiles(files ...string) {
 // func (engine *Engine) LoadHTMLFiles(files ...string) {
 // func (engine *Engine) RunTLS(addr string, cert string, key string) error {
 // func (engine *Engine) RunTLS(addr string, cert string, key string) error {
 
 
@@ -42,6 +126,32 @@ func TestCreateEngine(t *testing.T) {
 // 	SetMode(TestMode)
 // 	SetMode(TestMode)
 // }
 // }
 
 
+func TestLoadHTMLFiles(t *testing.T) {
+	td := setupHTMLFiles(t)
+	res, err := http.Get("http://127.0.0.1:8888/test")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	resp, _ := ioutil.ReadAll(res.Body)
+	assert.Equal(t, "<h1>Hello world</h1>", string(resp[:]))
+	td()
+}
+
+func TestLoadHTMLFilesFuncMap(t *testing.T) {
+	time.Now()
+	td := setupHTMLFiles(t)
+	res, err := http.Get("http://127.0.0.1:8888/raw")
+	if err != nil {
+		fmt.Println(err)
+	}
+
+	resp, _ := ioutil.ReadAll(res.Body)
+	assert.Equal(t, "Date: 2017/07/01\n", string(resp[:]))
+
+	td()
+}
+
 func TestLoadHTMLReleaseMode(t *testing.T) {
 func TestLoadHTMLReleaseMode(t *testing.T) {
 
 
 }
 }

+ 0 - 14
helpers_test.go

@@ -1,14 +0,0 @@
-package gin
-
-import (
-	"net/http/httptest"
-)
-
-func CreateTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) {
-	w = httptest.NewRecorder()
-	r = New()
-	c = r.allocateContext()
-	c.reset()
-	c.writermem.reset(w)
-	return
-}

+ 18 - 11
logger.go

@@ -14,16 +14,21 @@ import (
 )
 )
 
 
 var (
 var (
-	green   = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
-	white   = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
-	yellow  = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
-	red     = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
-	blue    = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
-	magenta = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
-	cyan    = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
-	reset   = string([]byte{27, 91, 48, 109})
+	green        = string([]byte{27, 91, 57, 55, 59, 52, 50, 109})
+	white        = string([]byte{27, 91, 57, 48, 59, 52, 55, 109})
+	yellow       = string([]byte{27, 91, 57, 55, 59, 52, 51, 109})
+	red          = string([]byte{27, 91, 57, 55, 59, 52, 49, 109})
+	blue         = string([]byte{27, 91, 57, 55, 59, 52, 52, 109})
+	magenta      = string([]byte{27, 91, 57, 55, 59, 52, 53, 109})
+	cyan         = string([]byte{27, 91, 57, 55, 59, 52, 54, 109})
+	reset        = string([]byte{27, 91, 48, 109})
+	disableColor = false
 )
 )
 
 
+func DisableConsoleColor() {
+	disableColor = true
+}
+
 func ErrorLogger() HandlerFunc {
 func ErrorLogger() HandlerFunc {
 	return ErrorLoggerT(ErrorTypeAny)
 	return ErrorLoggerT(ErrorTypeAny)
 }
 }
@@ -49,7 +54,9 @@ func Logger() HandlerFunc {
 func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
 func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
 	isTerm := true
 	isTerm := true
 
 
-	if w, ok := out.(*os.File); !ok || !isatty.IsTerminal(w.Fd()) {
+	if w, ok := out.(*os.File); !ok ||
+		(os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(w.Fd()) && !isatty.IsCygwinTerminal(w.Fd()))) ||
+		disableColor {
 		isTerm = false
 		isTerm = false
 	}
 	}
 
 
@@ -87,12 +94,12 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
 			}
 			}
 			comment := c.Errors.ByType(ErrorTypePrivate).String()
 			comment := c.Errors.ByType(ErrorTypePrivate).String()
 
 
-			fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s  %s %-7s %s\n%s",
+			fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s  %s %-7s %s\n%s",
 				end.Format("2006/01/02 - 15:04:05"),
 				end.Format("2006/01/02 - 15:04:05"),
 				statusColor, statusCode, reset,
 				statusColor, statusCode, reset,
 				latency,
 				latency,
 				clientIP,
 				clientIP,
-				methodColor, reset, method,
+				methodColor, method, reset,
 				path,
 				path,
 				comment,
 				comment,
 			)
 			)

+ 21 - 7
logger_test.go

@@ -36,37 +36,43 @@ func TestLogger(t *testing.T) {
 	// I wrote these first (extending the above) but then realized they are more
 	// I wrote these first (extending the above) but then realized they are more
 	// like integration tests because they test the whole logging process rather
 	// like integration tests because they test the whole logging process rather
 	// than individual functions.  Im not sure where these should go.
 	// than individual functions.  Im not sure where these should go.
-
+	buffer.Reset()
 	performRequest(router, "POST", "/example")
 	performRequest(router, "POST", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "POST")
 	assert.Contains(t, buffer.String(), "POST")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "PUT", "/example")
 	performRequest(router, "PUT", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "PUT")
 	assert.Contains(t, buffer.String(), "PUT")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "DELETE", "/example")
 	performRequest(router, "DELETE", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "DELETE")
 	assert.Contains(t, buffer.String(), "DELETE")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "PATCH", "/example")
 	performRequest(router, "PATCH", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "PATCH")
 	assert.Contains(t, buffer.String(), "PATCH")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "HEAD", "/example")
 	performRequest(router, "HEAD", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "HEAD")
 	assert.Contains(t, buffer.String(), "HEAD")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "OPTIONS", "/example")
 	performRequest(router, "OPTIONS", "/example")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "OPTIONS")
 	assert.Contains(t, buffer.String(), "OPTIONS")
 	assert.Contains(t, buffer.String(), "/example")
 	assert.Contains(t, buffer.String(), "/example")
 
 
+	buffer.Reset()
 	performRequest(router, "GET", "/notfound")
 	performRequest(router, "GET", "/notfound")
 	assert.Contains(t, buffer.String(), "404")
 	assert.Contains(t, buffer.String(), "404")
 	assert.Contains(t, buffer.String(), "GET")
 	assert.Contains(t, buffer.String(), "GET")
@@ -107,16 +113,16 @@ func TestErrorLogger(t *testing.T) {
 	})
 	})
 
 
 	w := performRequest(router, "GET", "/error")
 	w := performRequest(router, "GET", "/error")
-	assert.Equal(t, w.Code, 200)
-	assert.Equal(t, w.Body.String(), "{\"error\":\"this is an error\"}\n")
+	assert.Equal(t, 200, w.Code)
+	assert.Equal(t, "{\"error\":\"this is an error\"}", w.Body.String())
 
 
 	w = performRequest(router, "GET", "/abort")
 	w = performRequest(router, "GET", "/abort")
-	assert.Equal(t, w.Code, 401)
-	assert.Equal(t, w.Body.String(), "{\"error\":\"no authorized\"}\n")
+	assert.Equal(t, 401, w.Code)
+	assert.Equal(t, "{\"error\":\"no authorized\"}", w.Body.String())
 
 
 	w = performRequest(router, "GET", "/print")
 	w = performRequest(router, "GET", "/print")
-	assert.Equal(t, w.Code, 500)
-	assert.Equal(t, w.Body.String(), "hola!{\"error\":\"this is an error\"}\n")
+	assert.Equal(t, 500, w.Code)
+	assert.Equal(t, "hola!{\"error\":\"this is an error\"}", w.Body.String())
 }
 }
 
 
 func TestSkippingPaths(t *testing.T) {
 func TestSkippingPaths(t *testing.T) {
@@ -129,6 +135,14 @@ func TestSkippingPaths(t *testing.T) {
 	performRequest(router, "GET", "/logged")
 	performRequest(router, "GET", "/logged")
 	assert.Contains(t, buffer.String(), "200")
 	assert.Contains(t, buffer.String(), "200")
 
 
+	buffer.Reset()
 	performRequest(router, "GET", "/skipped")
 	performRequest(router, "GET", "/skipped")
 	assert.Contains(t, buffer.String(), "")
 	assert.Contains(t, buffer.String(), "")
 }
 }
+
+func TestDisableConsoleColor(t *testing.T) {
+	New()
+	assert.False(t, disableColor)
+	DisableConsoleColor()
+	assert.True(t, disableColor)
+}

+ 3 - 4
middleware_test.go

@@ -7,10 +7,9 @@ package gin
 import (
 import (
 	"errors"
 	"errors"
 	"strings"
 	"strings"
-
 	"testing"
 	"testing"
 
 
-	"github.com/manucorporat/sse"
+	"github.com/gin-contrib/sse"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
 )
 )
 
 
@@ -245,6 +244,6 @@ func TestMiddlewareWrite(t *testing.T) {
 
 
 	w := performRequest(router, "GET", "/")
 	w := performRequest(router, "GET", "/")
 
 
-	assert.Equal(t, w.Code, 400)
-	assert.Equal(t, strings.Replace(w.Body.String(), " ", "", -1), strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}\n{\"foo\":\"bar\"}\nevent:test\ndata:message\n\n", " ", "", -1))
+	assert.Equal(t, 400, w.Code)
+	assert.Equal(t, strings.Replace("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", "", -1), strings.Replace(w.Body.String(), " ", "", -1))
 }
 }

+ 3 - 3
mode.go

@@ -19,9 +19,9 @@ const (
 	TestMode    string = "test"
 	TestMode    string = "test"
 )
 )
 const (
 const (
-	debugCode   = iota
-	releaseCode 
-	testCode  
+	debugCode = iota
+	releaseCode
+	testCode
 )
 )
 
 
 // DefaultWriter is the default io.Writer used the Gin for debug output and
 // DefaultWriter is the default io.Writer used the Gin for debug output and

+ 1 - 1
path.go

@@ -1,7 +1,7 @@
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Based on the path package, Copyright 2009 The Go Authors.
 // Based on the path package, Copyright 2009 The Go Authors.
 // Use of this source code is governed by a BSD-style license that can be found
 // Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE.
 
 
 package gin
 package gin
 
 

+ 1 - 1
path_test.go

@@ -1,7 +1,7 @@
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Based on the path package, Copyright 2009 The Go Authors.
 // Based on the path package, Copyright 2009 The Go Authors.
 // Use of this source code is governed by a BSD-style license that can be found
 // Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
 
 
 package gin
 package gin
 
 

+ 9 - 6
render/data.go

@@ -11,10 +11,13 @@ type Data struct {
 	Data        []byte
 	Data        []byte
 }
 }
 
 
-func (r Data) Render(w http.ResponseWriter) error {
-	if len(r.ContentType) > 0 {
-		w.Header()["Content-Type"] = []string{r.ContentType}
-	}
-	w.Write(r.Data)
-	return nil
+// Render (Data) writes data with custom ContentType
+func (r Data) Render(w http.ResponseWriter) (err error) {
+	r.WriteContentType(w)
+	_, err = w.Write(r.Data)
+	return
+}
+
+func (r Data) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, []string{r.ContentType})
 }
 }

+ 21 - 5
render/html.go

@@ -10,17 +10,25 @@ import (
 )
 )
 
 
 type (
 type (
+	Delims struct {
+		Left  string
+		Right string
+	}
+
 	HTMLRender interface {
 	HTMLRender interface {
 		Instance(string, interface{}) Render
 		Instance(string, interface{}) Render
 	}
 	}
 
 
 	HTMLProduction struct {
 	HTMLProduction struct {
 		Template *template.Template
 		Template *template.Template
+		Delims   Delims
 	}
 	}
 
 
 	HTMLDebug struct {
 	HTMLDebug struct {
-		Files []string
-		Glob  string
+		Files   []string
+		Glob    string
+		Delims  Delims
+		FuncMap template.FuncMap
 	}
 	}
 
 
 	HTML struct {
 	HTML struct {
@@ -48,19 +56,27 @@ func (r HTMLDebug) Instance(name string, data interface{}) Render {
 	}
 	}
 }
 }
 func (r HTMLDebug) loadTemplate() *template.Template {
 func (r HTMLDebug) loadTemplate() *template.Template {
+	if r.FuncMap == nil {
+		r.FuncMap = template.FuncMap{}
+	}
 	if len(r.Files) > 0 {
 	if len(r.Files) > 0 {
-		return template.Must(template.ParseFiles(r.Files...))
+		return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseFiles(r.Files...))
 	}
 	}
 	if len(r.Glob) > 0 {
 	if len(r.Glob) > 0 {
-		return template.Must(template.ParseGlob(r.Glob))
+		return template.Must(template.New("").Delims(r.Delims.Left, r.Delims.Right).Funcs(r.FuncMap).ParseGlob(r.Glob))
 	}
 	}
 	panic("the HTML debug render was created without files or glob pattern")
 	panic("the HTML debug render was created without files or glob pattern")
 }
 }
 
 
 func (r HTML) Render(w http.ResponseWriter) error {
 func (r HTML) Render(w http.ResponseWriter) error {
-	writeContentType(w, htmlContentType)
+	r.WriteContentType(w)
+
 	if len(r.Name) == 0 {
 	if len(r.Name) == 0 {
 		return r.Template.Execute(w, r.Data)
 		return r.Template.Execute(w, r.Data)
 	}
 	}
 	return r.Template.ExecuteTemplate(w, r.Name, r.Data)
 	return r.Template.ExecuteTemplate(w, r.Name, r.Data)
 }
 }
+
+func (r HTML) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, htmlContentType)
+}

+ 21 - 5
render/json.go

@@ -21,12 +21,29 @@ type (
 
 
 var jsonContentType = []string{"application/json; charset=utf-8"}
 var jsonContentType = []string{"application/json; charset=utf-8"}
 
 
-func (r JSON) Render(w http.ResponseWriter) error {
-	return WriteJSON(w, r.Data)
+func (r JSON) Render(w http.ResponseWriter) (err error) {
+	if err = WriteJSON(w, r.Data); err != nil {
+		panic(err)
+	}
+	return
 }
 }
 
 
-func (r IndentedJSON) Render(w http.ResponseWriter) error {
+func (r JSON) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, jsonContentType)
+}
+
+func WriteJSON(w http.ResponseWriter, obj interface{}) error {
 	writeContentType(w, jsonContentType)
 	writeContentType(w, jsonContentType)
+	jsonBytes, err := json.Marshal(obj)
+	if err != nil {
+		return err
+	}
+	w.Write(jsonBytes)
+	return nil
+}
+
+func (r IndentedJSON) Render(w http.ResponseWriter) error {
+	r.WriteContentType(w)
 	jsonBytes, err := json.MarshalIndent(r.Data, "", "    ")
 	jsonBytes, err := json.MarshalIndent(r.Data, "", "    ")
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -35,7 +52,6 @@ func (r IndentedJSON) Render(w http.ResponseWriter) error {
 	return nil
 	return nil
 }
 }
 
 
-func WriteJSON(w http.ResponseWriter, obj interface{}) error {
+func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
 	writeContentType(w, jsonContentType)
 	writeContentType(w, jsonContentType)
-	return json.NewEncoder(w).Encode(obj)
 }
 }

+ 31 - 0
render/msgpack.go

@@ -0,0 +1,31 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package render
+
+import (
+	"net/http"
+
+	"github.com/ugorji/go/codec"
+)
+
+type MsgPack struct {
+	Data interface{}
+}
+
+var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
+
+func (r MsgPack) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, msgpackContentType)
+}
+
+func (r MsgPack) Render(w http.ResponseWriter) error {
+	return WriteMsgPack(w, r.Data)
+}
+
+func WriteMsgPack(w http.ResponseWriter, obj interface{}) error {
+	writeContentType(w, msgpackContentType)
+	var h codec.Handle = new(codec.MsgpackHandle)
+	return codec.NewEncoder(w, h).Encode(obj)
+}

+ 2 - 0
render/redirect.go

@@ -22,3 +22,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
 	http.Redirect(w, r.Request, r.Location, r.Code)
 	http.Redirect(w, r.Request, r.Location, r.Code)
 	return nil
 	return nil
 }
 }
+
+func (r Redirect) WriteContentType(http.ResponseWriter) {}

+ 3 - 0
render/render.go

@@ -8,6 +8,7 @@ import "net/http"
 
 
 type Render interface {
 type Render interface {
 	Render(http.ResponseWriter) error
 	Render(http.ResponseWriter) error
+	WriteContentType(w http.ResponseWriter)
 }
 }
 
 
 var (
 var (
@@ -21,6 +22,8 @@ var (
 	_ HTMLRender = HTMLDebug{}
 	_ HTMLRender = HTMLDebug{}
 	_ HTMLRender = HTMLProduction{}
 	_ HTMLRender = HTMLProduction{}
 	_ Render     = YAML{}
 	_ Render     = YAML{}
+	_ Render     = MsgPack{}
+	_ Render     = MsgPack{}
 )
 )
 
 
 func writeContentType(w http.ResponseWriter, value []string) {
 func writeContentType(w http.ResponseWriter, value []string) {

+ 25 - 2
render/render_test.go

@@ -5,17 +5,40 @@
 package render
 package render
 
 
 import (
 import (
+	"bytes"
 	"encoding/xml"
 	"encoding/xml"
 	"html/template"
 	"html/template"
 	"net/http/httptest"
 	"net/http/httptest"
 	"testing"
 	"testing"
 
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/assert"
+	"github.com/ugorji/go/codec"
 )
 )
 
 
 // TODO unit tests
 // TODO unit tests
 // test errors
 // test errors
 
 
+func TestRenderMsgPack(t *testing.T) {
+	w := httptest.NewRecorder()
+	data := map[string]interface{}{
+		"foo": "bar",
+	}
+
+	err := (MsgPack{data}).Render(w)
+
+	assert.NoError(t, err)
+
+	h := new(codec.MsgpackHandle)
+	assert.NotNil(t, h)
+	buf := bytes.NewBuffer([]byte{})
+	assert.NotNil(t, buf)
+	err = codec.NewEncoder(buf, h).Encode(data)
+
+	assert.NoError(t, err)
+	assert.Equal(t, w.Body.String(), string(buf.Bytes()))
+	assert.Equal(t, w.Header().Get("Content-Type"), "application/msgpack; charset=utf-8")
+}
+
 func TestRenderJSON(t *testing.T) {
 func TestRenderJSON(t *testing.T) {
 	w := httptest.NewRecorder()
 	w := httptest.NewRecorder()
 	data := map[string]interface{}{
 	data := map[string]interface{}{
@@ -25,8 +48,8 @@ func TestRenderJSON(t *testing.T) {
 	err := (JSON{data}).Render(w)
 	err := (JSON{data}).Render(w)
 
 
 	assert.NoError(t, err)
 	assert.NoError(t, err)
-	assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n")
-	assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8")
+	assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+	assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type"))
 }
 }
 
 
 func TestRenderIndentedJSON(t *testing.T) {
 func TestRenderIndentedJSON(t *testing.T) {

+ 4 - 1
render/text.go

@@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error {
 	return nil
 	return nil
 }
 }
 
 
-func WriteString(w http.ResponseWriter, format string, data []interface{}) {
+func (r String) WriteContentType(w http.ResponseWriter) {
 	writeContentType(w, plainContentType)
 	writeContentType(w, plainContentType)
+}
 
 
+func WriteString(w http.ResponseWriter, format string, data []interface{}) {
+	writeContentType(w, plainContentType)
 	if len(data) > 0 {
 	if len(data) > 0 {
 		fmt.Fprintf(w, format, data...)
 		fmt.Fprintf(w, format, data...)
 	} else {
 	} else {

+ 5 - 1
render/xml.go

@@ -16,6 +16,10 @@ type XML struct {
 var xmlContentType = []string{"application/xml; charset=utf-8"}
 var xmlContentType = []string{"application/xml; charset=utf-8"}
 
 
 func (r XML) Render(w http.ResponseWriter) error {
 func (r XML) Render(w http.ResponseWriter) error {
-	writeContentType(w, xmlContentType)
+	r.WriteContentType(w)
 	return xml.NewEncoder(w).Encode(r.Data)
 	return xml.NewEncoder(w).Encode(r.Data)
 }
 }
+
+func (r XML) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, xmlContentType)
+}

+ 5 - 1
render/yaml.go

@@ -17,7 +17,7 @@ type YAML struct {
 var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
 var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
 
 
 func (r YAML) Render(w http.ResponseWriter) error {
 func (r YAML) Render(w http.ResponseWriter) error {
-	writeContentType(w, yamlContentType)
+	r.WriteContentType(w)
 
 
 	bytes, err := yaml.Marshal(r.Data)
 	bytes, err := yaml.Marshal(r.Data)
 	if err != nil {
 	if err != nil {
@@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
 	w.Write(bytes)
 	w.Write(bytes)
 	return nil
 	return nil
 }
 }
+
+func (r YAML) WriteContentType(w http.ResponseWriter) {
+	writeContentType(w, yamlContentType)
+}

+ 39 - 0
routes_test.go

@@ -400,3 +400,42 @@ func TestRouterNotFound(t *testing.T) {
 	w = performRequest(router, "GET", "/")
 	w = performRequest(router, "GET", "/")
 	assert.Equal(t, w.Code, 404)
 	assert.Equal(t, w.Code, 404)
 }
 }
+
+func TestRouteRawPath(t *testing.T) {
+	route := New()
+	route.UseRawPath = true
+
+	route.POST("/project/:name/build/:num", func(c *Context) {
+		name := c.Params.ByName("name")
+		num := c.Params.ByName("num")
+
+		assert.Equal(t, c.Param("name"), name)
+		assert.Equal(t, c.Param("num"), num)
+
+		assert.Equal(t, "Some/Other/Project", name)
+		assert.Equal(t, "222", num)
+	})
+
+	w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/222")
+	assert.Equal(t, w.Code, 200)
+}
+
+func TestRouteRawPathNoUnescape(t *testing.T) {
+	route := New()
+	route.UseRawPath = true
+	route.UnescapePathValues = false
+
+	route.POST("/project/:name/build/:num", func(c *Context) {
+		name := c.Params.ByName("name")
+		num := c.Params.ByName("num")
+
+		assert.Equal(t, c.Param("name"), name)
+		assert.Equal(t, c.Param("num"), num)
+
+		assert.Equal(t, "Some%2FOther%2FProject", name)
+		assert.Equal(t, "333", num)
+	})
+
+	w := performRequest(route, "POST", "/project/Some%2FOther%2FProject/build/333")
+	assert.Equal(t, w.Code, 200)
+}

+ 17 - 0
test_helpers.go

@@ -0,0 +1,17 @@
+// Copyright 2017 Manu Martinez-Almeida.  All rights reserved.
+// Use of this source code is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package gin
+
+import (
+	"net/http"
+)
+
+func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
+	r = New()
+	c = r.allocateContext()
+	c.reset()
+	c.writermem.reset(w)
+	return
+}

+ 22 - 5
tree.go

@@ -1,10 +1,11 @@
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be found
 // Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
 
 
 package gin
 package gin
 
 
 import (
 import (
+	"net/url"
 	"strings"
 	"strings"
 	"unicode"
 	"unicode"
 )
 )
@@ -363,7 +364,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
 // If no handle can be found, a TSR (trailing slash redirect) recommendation is
 // If no handle can be found, a TSR (trailing slash redirect) recommendation is
 // made if a handle exists with an extra (without the) trailing slash for the
 // made if a handle exists with an extra (without the) trailing slash for the
 // given path.
 // given path.
-func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) {
+func (n *node) getValue(path string, po Params, unescape bool) (handlers HandlersChain, p Params, tsr bool) {
 	p = po
 	p = po
 walk: // Outer loop for walking the tree
 walk: // Outer loop for walking the tree
 	for {
 	for {
@@ -406,7 +407,15 @@ walk: // Outer loop for walking the tree
 					i := len(p)
 					i := len(p)
 					p = p[:i+1] // expand slice within preallocated capacity
 					p = p[:i+1] // expand slice within preallocated capacity
 					p[i].Key = n.path[1:]
 					p[i].Key = n.path[1:]
-					p[i].Value = path[:end]
+					val := path[:end]
+					if unescape {
+						var err error
+						if p[i].Value, err = url.QueryUnescape(val); err != nil {
+							p[i].Value = val // fallback, in case of error
+						}
+					} else {
+						p[i].Value = val
+					}
 
 
 					// we need to go deeper!
 					// we need to go deeper!
 					if end < len(path) {
 					if end < len(path) {
@@ -423,7 +432,8 @@ walk: // Outer loop for walking the tree
 
 
 					if handlers = n.handlers; handlers != nil {
 					if handlers = n.handlers; handlers != nil {
 						return
 						return
-					} else if len(n.children) == 1 {
+					}
+					if len(n.children) == 1 {
 						// No handle found. Check if a handle for this path + a
 						// No handle found. Check if a handle for this path + a
 						// trailing slash exists for TSR recommendation
 						// trailing slash exists for TSR recommendation
 						n = n.children[0]
 						n = n.children[0]
@@ -440,7 +450,14 @@ walk: // Outer loop for walking the tree
 					i := len(p)
 					i := len(p)
 					p = p[:i+1] // expand slice within preallocated capacity
 					p = p[:i+1] // expand slice within preallocated capacity
 					p[i].Key = n.path[2:]
 					p[i].Key = n.path[2:]
-					p[i].Value = path
+					if unescape {
+						var err error
+						if p[i].Value, err = url.QueryUnescape(path); err != nil {
+							p[i].Value = path // fallback, in case of error
+						}
+					} else {
+						p[i].Value = path
+					}
 
 
 					handlers = n.handlers
 					handlers = n.handlers
 					return
 					return

+ 51 - 7
tree_test.go

@@ -1,6 +1,6 @@
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Copyright 2013 Julien Schmidt. All rights reserved.
 // Use of this source code is governed by a BSD-style license that can be found
 // Use of this source code is governed by a BSD-style license that can be found
-// in the LICENSE file.
+// at https://github.com/julienschmidt/httprouter/blob/master/LICENSE
 
 
 package gin
 package gin
 
 
@@ -37,9 +37,14 @@ type testRequests []struct {
 	ps         Params
 	ps         Params
 }
 }
 
 
-func checkRequests(t *testing.T, tree *node, requests testRequests) {
+func checkRequests(t *testing.T, tree *node, requests testRequests, unescapes ...bool) {
+	unescape := false
+	if len(unescapes) >= 1 {
+		unescape = unescapes[0]
+	}
+
 	for _, request := range requests {
 	for _, request := range requests {
-		handler, ps, _ := tree.getValue(request.path, nil)
+		handler, ps, _ := tree.getValue(request.path, nil, unescape)
 
 
 		if handler == nil {
 		if handler == nil {
 			if !request.nilHandler {
 			if !request.nilHandler {
@@ -197,6 +202,45 @@ func TestTreeWildcard(t *testing.T) {
 	checkMaxParams(t, tree)
 	checkMaxParams(t, tree)
 }
 }
 
 
+func TestUnescapeParameters(t *testing.T) {
+	tree := &node{}
+
+	routes := [...]string{
+		"/",
+		"/cmd/:tool/:sub",
+		"/cmd/:tool/",
+		"/src/*filepath",
+		"/search/:query",
+		"/files/:dir/*filepath",
+		"/info/:user/project/:project",
+		"/info/:user",
+	}
+	for _, route := range routes {
+		tree.addRoute(route, fakeHandler(route))
+	}
+
+	//printChildren(tree, "")
+	unescape := true
+	checkRequests(t, tree, testRequests{
+		{"/", false, "/", nil},
+		{"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}},
+		{"/cmd/test", true, "", Params{Param{"tool", "test"}}},
+		{"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}},
+		{"/src/some/file+test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file test.png"}}},
+		{"/src/some/file++++%%%%test.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file++++%%%%test.png"}}},
+		{"/src/some/file%2Ftest.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file/test.png"}}},
+		{"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng in ünìcodé"}}},
+		{"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}},
+		{"/info/slash%2Fgordon", false, "/info/:user", Params{Param{"user", "slash/gordon"}}},
+		{"/info/slash%2Fgordon/project/Project%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash/gordon"}, Param{"project", "Project #1"}}},
+		{"/info/slash%%%%", false, "/info/:user", Params{Param{"user", "slash%%%%"}}},
+		{"/info/slash%%%%2Fgordon/project/Project%%%%20%231", false, "/info/:user/project/:project", Params{Param{"user", "slash%%%%2Fgordon"}, Param{"project", "Project%%%%20%231"}}},
+	}, unescape)
+
+	checkPriorities(t, tree)
+	checkMaxParams(t, tree)
+}
+
 func catchPanic(testFunc func()) (recv interface{}) {
 func catchPanic(testFunc func()) (recv interface{}) {
 	defer func() {
 	defer func() {
 		recv = recover()
 		recv = recover()
@@ -430,7 +474,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 		"/doc/",
 		"/doc/",
 	}
 	}
 	for _, route := range tsrRoutes {
 	for _, route := range tsrRoutes {
-		handler, _, tsr := tree.getValue(route, nil)
+		handler, _, tsr := tree.getValue(route, nil, false)
 		if handler != nil {
 		if handler != nil {
 			t.Fatalf("non-nil handler for TSR route '%s", route)
 			t.Fatalf("non-nil handler for TSR route '%s", route)
 		} else if !tsr {
 		} else if !tsr {
@@ -447,7 +491,7 @@ func TestTreeTrailingSlashRedirect(t *testing.T) {
 		"/api/world/abc",
 		"/api/world/abc",
 	}
 	}
 	for _, route := range noTsrRoutes {
 	for _, route := range noTsrRoutes {
-		handler, _, tsr := tree.getValue(route, nil)
+		handler, _, tsr := tree.getValue(route, nil, false)
 		if handler != nil {
 		if handler != nil {
 			t.Fatalf("non-nil handler for No-TSR route '%s", route)
 			t.Fatalf("non-nil handler for No-TSR route '%s", route)
 		} else if tsr {
 		} else if tsr {
@@ -466,7 +510,7 @@ func TestTreeRootTrailingSlashRedirect(t *testing.T) {
 		t.Fatalf("panic inserting test route: %v", recv)
 		t.Fatalf("panic inserting test route: %v", recv)
 	}
 	}
 
 
-	handler, _, tsr := tree.getValue("/", nil)
+	handler, _, tsr := tree.getValue("/", nil, false)
 	if handler != nil {
 	if handler != nil {
 		t.Fatalf("non-nil handler")
 		t.Fatalf("non-nil handler")
 	} else if tsr {
 	} else if tsr {
@@ -617,7 +661,7 @@ func TestTreeInvalidNodeType(t *testing.T) {
 
 
 	// normal lookup
 	// normal lookup
 	recv := catchPanic(func() {
 	recv := catchPanic(func() {
-		tree.getValue("/test", nil)
+		tree.getValue("/test", nil, false)
 	})
 	})
 	if rs, ok := recv.(string); !ok || rs != panicMsg {
 	if rs, ok := recv.(string); !ok || rs != panicMsg {
 		t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)
 		t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv)

+ 109 - 0
vendor/vendor.json

@@ -0,0 +1,109 @@
+{
+	"comment": "v1.2",
+	"ignore": "test",
+	"package": [
+		{
+			"checksumSHA1": "dvabztWVQX8f6oMLRyv4dLH+TGY=",
+			"comment": "v1.1.0",
+			"path": "github.com/davecgh/go-spew/spew",
+			"revision": "346938d642f2ec3594ed81d874461961cd0faa76",
+			"revisionTime": "2016-10-29T20:57:26Z"
+		},
+		{
+			"checksumSHA1": "7c3FuEadBInl/4ExSrB7iJMXpe4=",
+			"path": "github.com/dustin/go-broadcast",
+			"revision": "3bdf6d4a7164a50bc19d5f230e2981d87d2584f1",
+			"revisionTime": "2014-06-27T04:00:55Z"
+		},
+		{
+			"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=",
+			"path": "github.com/gin-contrib/sse",
+			"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
+			"revisionTime": "2017-01-09T09:34:21Z"
+		},
+		{
+			"checksumSHA1": "FJKrZuFmeLJp8HDeJc6UkIDBPUw=",
+			"path": "github.com/gin-gonic/autotls",
+			"revision": "5b3297bdcee778ff3bbdc99ab7c41e1c2677d22d",
+			"revisionTime": "2017-04-16T09:39:34Z"
+		},
+		{
+			"checksumSHA1": "qlPUeFabwF4RKAOF1H+yBFU1Veg=",
+			"path": "github.com/golang/protobuf/proto",
+			"revision": "5a0f697c9ed9d68fef0116532c6e05cfeae00e55",
+			"revisionTime": "2017-06-01T23:02:30Z"
+		},
+		{
+			"checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
+			"path": "github.com/manucorporat/stats",
+			"revision": "8f2d6ace262eba462e9beb552382c98be51d807b",
+			"revisionTime": "2015-05-31T20:46:25Z"
+		},
+		{
+			"checksumSHA1": "U6lX43KDDlNOn+Z0Yyww+ZzHfFo=",
+			"path": "github.com/mattn/go-isatty",
+			"revision": "57fdcb988a5c543893cc61bce354a6e24ab70022",
+			"revisionTime": "2017-03-07T16:30:44Z"
+		},
+		{
+			"checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=",
+			"comment": "v1.0.0",
+			"path": "github.com/pmezard/go-difflib/difflib",
+			"revision": "792786c7400a136282c1664665ae0a8db921c6c2",
+			"revisionTime": "2016-01-10T10:55:54Z"
+		},
+		{
+			"checksumSHA1": "Q2V7Zs3diLmLfmfbiuLpSxETSuY=",
+			"comment": "v1.1.4",
+			"path": "github.com/stretchr/testify/assert",
+			"revision": "976c720a22c8eb4eb6a0b4348ad85ad12491a506",
+			"revisionTime": "2016-09-25T22:06:09Z"
+		},
+		{
+			"checksumSHA1": "CoxdaTYdPZNJXr8mJfLxye428N0=",
+			"path": "github.com/ugorji/go/codec",
+			"revision": "c88ee250d0221a57af388746f5cf03768c21d6e2",
+			"revisionTime": "2017-02-15T20:11:44Z"
+		},
+		{
+			"checksumSHA1": "W0j4I7QpxXlChjyhAojZmFjU6Bg=",
+			"path": "golang.org/x/crypto/acme",
+			"revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
+			"revisionTime": "2017-06-19T06:03:41Z"
+		},
+		{
+			"checksumSHA1": "TrKJW+flz7JulXU3sqnBJjGzgQc=",
+			"path": "golang.org/x/crypto/acme/autocert",
+			"revision": "adbae1b6b6fb4b02448a0fc0dbbc9ba2b95b294d",
+			"revisionTime": "2017-06-19T06:03:41Z"
+		},
+		{
+			"checksumSHA1": "9jjO5GjLa0XF/nfWihF02RoH4qc=",
+			"comment": "release-branch.go1.7",
+			"path": "golang.org/x/net/context",
+			"revision": "d4c55e66d8c3a2f3382d264b08e3e3454a66355a",
+			"revisionTime": "2016-10-18T08:54:36Z"
+		},
+		{
+			"checksumSHA1": "TVEkpH3gq84iQ39I4R+mlDwjuVI=",
+			"path": "golang.org/x/sys/unix",
+			"revision": "99f16d856c9836c42d24e7ab64ea72916925fa97",
+			"revisionTime": "2017-03-08T15:04:45Z"
+		},
+		{
+			"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
+			"comment": "v8.18.1",
+			"path": "gopkg.in/go-playground/validator.v8",
+			"revision": "5f57d2222ad794d0dffb07e664ea05e2ee07d60c",
+			"revisionTime": "2016-07-18T13:41:25Z"
+		},
+		{
+			"checksumSHA1": "12GqsW8PiRPnezDDy0v4brZrndM=",
+			"comment": "v2",
+			"path": "gopkg.in/yaml.v2",
+			"revision": "a5b47d31c556af34a302ce5d659e6fea44d90de0",
+			"revisionTime": "2016-09-28T15:37:09Z"
+		}
+	],
+	"rootPath": "github.com/gin-gonic/gin"
+}