diff --git a/README.md b/README.md
index fb749026..bf1bbb49 100644
--- a/README.md
+++ b/README.md
@@ -3,10 +3,11 @@
[](https://travis-ci.org/gin-gonic/gin)
- [](https://codecov.io/gh/gin-gonic/gin)
- [](https://goreportcard.com/report/github.com/gin-gonic/gin)
- [](https://godoc.org/github.com/gin-gonic/gin)
- [](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://codecov.io/gh/gin-gonic/gin)
+[](https://goreportcard.com/report/github.com/gin-gonic/gin)
+[](https://godoc.org/github.com/gin-gonic/gin)
+[](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
[](https://www.codetriage.com/gin-gonic/gin)
Gin is a web framework written in Go (Golang). It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin.
@@ -27,6 +28,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Querystring parameters](#querystring-parameters)
- [Multipart/Urlencoded Form](#multiparturlencoded-form)
- [Another example: query + post form](#another-example-query--post-form)
+ - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters)
- [Upload files](#upload-files)
- [Grouping routes](#grouping-routes)
- [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default)
@@ -323,6 +325,34 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
+### Map as querystring or postform parameters
+
+```
+POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
+Content-Type: application/x-www-form-urlencoded
+
+names[first]=thinkerou&names[second]=tianou
+```
+
+```go
+func main() {
+ router := gin.Default()
+
+ router.POST("/post", func(c *gin.Context) {
+
+ ids := c.QueryMap("ids")
+ names := c.PostFormMap("names")
+
+ fmt.Printf("ids: %v; names: %v", ids, names)
+ })
+ router.Run(":8080")
+}
+```
+
+```
+ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou]
+```
+
### Upload files
#### Single file
@@ -900,6 +930,29 @@ func main() {
}
```
+#### AsciiJSON
+
+Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters.
+
+```go
+func main() {
+ r := gin.Default()
+
+ r.GET("/someJSON", func(c *gin.Context) {
+ data := map[string]interface{}{
+ "lang": "GO语言",
+ "tag": "
",
+ }
+
+ // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"}
+ c.AsciiJSON(http.StatusOK, data)
+ })
+
+ // Listen and serve on 0.0.0.0:8080
+ r.Run(":8080")
+}
+```
+
### Serving static files
```go
@@ -1759,7 +1812,7 @@ func TestPingRoute(t *testing.T) {
}
```
-## Users [](https://sourcegraph.com/github.com/gin-gonic/gin?badge)
+## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.
diff --git a/binding/binding.go b/binding/binding.go
index 1a984777..3a2aad9c 100644
--- a/binding/binding.go
+++ b/binding/binding.go
@@ -4,10 +4,9 @@
package binding
-import (
- "net/http"
-)
+import "net/http"
+// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
diff --git a/binding/default_validator.go b/binding/default_validator.go
index c67aa8a3..e7a302de 100644
--- a/binding/default_validator.go
+++ b/binding/default_validator.go
@@ -18,11 +18,17 @@ type defaultValidator struct {
var _ StructValidator = &defaultValidator{}
+// ValidateStruct receives any kind of type, but only performed struct or pointer to struct type.
func (v *defaultValidator) ValidateStruct(obj interface{}) error {
- if kindOfData(obj) == reflect.Struct {
+ value := reflect.ValueOf(obj)
+ valueType := value.Kind()
+ if valueType == reflect.Ptr {
+ valueType = value.Elem().Kind()
+ }
+ if valueType == reflect.Struct {
v.lazyinit()
if err := v.validate.Struct(obj); err != nil {
- return error(err)
+ return err
}
}
return nil
@@ -43,12 +49,3 @@ func (v *defaultValidator) lazyinit() {
v.validate = validator.New(config)
})
}
-
-func kindOfData(data interface{}) reflect.Kind {
- value := reflect.ValueOf(data)
- valueType := value.Kind()
- if valueType == reflect.Ptr {
- valueType = value.Elem().Kind()
- }
- return valueType
-}
diff --git a/context.go b/context.go
index 6fc5d25f..724ded79 100644
--- a/context.go
+++ b/context.go
@@ -159,16 +159,15 @@ func (c *Context) Error(err error) *Error {
if err == nil {
panic("err is nil")
}
- var parsedError *Error
- switch err.(type) {
- case *Error:
- parsedError = err.(*Error)
- default:
+
+ parsedError, ok := err.(*Error)
+ if !ok {
parsedError = &Error{
Err: err,
Type: ErrorTypePrivate,
}
}
+
c.Errors = append(c.Errors, parsedError)
return parsedError
}
@@ -361,6 +360,18 @@ func (c *Context) GetQueryArray(key string) ([]string, bool) {
return []string{}, false
}
+// QueryMap returns a map for a given query key.
+func (c *Context) QueryMap(key string) map[string]string {
+ dicts, _ := c.GetQueryMap(key)
+ return dicts
+}
+
+// GetQueryMap returns a map for a given query key, plus a boolean value
+// whether at least one value exists for the given key.
+func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
+ return c.get(c.Request.URL.Query(), key)
+}
+
// PostForm returns the specified key from a POST urlencoded form or multipart form
// when it exists, otherwise it returns an empty string `("")`.
func (c *Context) PostForm(key string) string {
@@ -416,6 +427,42 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false
}
+// PostFormMap returns a map for a given form key.
+func (c *Context) PostFormMap(key string) map[string]string {
+ dicts, _ := c.GetPostFormMap(key)
+ return dicts
+}
+
+// GetPostFormMap returns a map for a given form key, plus a boolean value
+// whether at least one value exists for the given key.
+func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
+ req := c.Request
+ req.ParseForm()
+ req.ParseMultipartForm(c.engine.MaxMultipartMemory)
+ dicts, exist := c.get(req.PostForm, key)
+
+ if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil {
+ dicts, exist = c.get(req.MultipartForm.Value, key)
+ }
+
+ return dicts, exist
+}
+
+// get is an internal method and returns a map which satisfy conditions.
+func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) {
+ dicts := make(map[string]string)
+ exist := false
+ for k, v := range m {
+ if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
+ if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
+ exist = true
+ dicts[k[i+1:][:j]] = v[0]
+ }
+ }
+ }
+ return dicts, exist
+}
+
// 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)
@@ -695,7 +742,12 @@ func (c *Context) SecureJSON(code int, obj interface{}) {
// It add padding to response body to request data from a server residing in a different domain than the client.
// It also sets the Content-Type as "application/javascript".
func (c *Context) JSONP(code int, obj interface{}) {
- c.Render(code, render.JsonpJSON{Callback: c.DefaultQuery("callback", ""), Data: obj})
+ callback := c.DefaultQuery("callback", "")
+ if callback == "" {
+ c.Render(code, render.JSON{Data: obj})
+ } else {
+ c.Render(code, render.JsonpJSON{Callback: callback, Data: obj})
+ }
}
// JSON serializes the given struct as JSON into the response body.
@@ -704,6 +756,12 @@ func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
+// AsciiJSON serializes the given struct as JSON into the response body with unicode to ASCII string.
+// It also sets the Content-Type as "application/json".
+func (c *Context) AsciiJSON(code int, obj interface{}) {
+ c.Render(code, render.AsciiJSON{Data: obj})
+}
+
// XML serializes the given struct as XML into the response body.
// It also sets the Content-Type as "application/xml".
func (c *Context) XML(code int, obj interface{}) {
diff --git a/context_test.go b/context_test.go
index 12e02fa0..0185507f 100644
--- a/context_test.go
+++ b/context_test.go
@@ -21,6 +21,7 @@ import (
"github.com/gin-gonic/gin/binding"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
+ "io"
)
var _ context.Context = &Context{}
@@ -47,6 +48,8 @@ func createMultipartRequest() *http.Request {
must(mw.WriteField("time_local", "31/12/2016 14:55"))
must(mw.WriteField("time_utc", "31/12/2016 14:55"))
must(mw.WriteField("time_location", "31/12/2016 14:55"))
+ must(mw.WriteField("names[a]", "thinkerou"))
+ must(mw.WriteField("names[b]", "tianou"))
req, err := http.NewRequest("POST", "/", body)
must(err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary)
@@ -371,7 +374,8 @@ func TestContextQuery(t *testing.T) {
func TestContextQueryAndPostForm(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
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&ids[a]=hi&ids[b]=3.14", body)
c.Request.Header.Add("Content-Type", MIMEPOSTForm)
assert.Equal(t, "bar", c.DefaultPostForm("foo", "none"))
@@ -439,6 +443,30 @@ func TestContextQueryAndPostForm(t *testing.T) {
values = c.QueryArray("both")
assert.Equal(t, 1, len(values))
assert.Equal(t, "GET", values[0])
+
+ dicts, ok := c.GetQueryMap("ids")
+ assert.True(t, ok)
+ assert.Equal(t, "hi", dicts["a"])
+ assert.Equal(t, "3.14", dicts["b"])
+
+ dicts, ok = c.GetQueryMap("nokey")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts, ok = c.GetQueryMap("both")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts, ok = c.GetQueryMap("array")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts = c.QueryMap("ids")
+ assert.Equal(t, "hi", dicts["a"])
+ assert.Equal(t, "3.14", dicts["b"])
+
+ dicts = c.QueryMap("nokey")
+ assert.Equal(t, 0, len(dicts))
}
func TestContextPostFormMultipart(t *testing.T) {
@@ -515,6 +543,22 @@ func TestContextPostFormMultipart(t *testing.T) {
values = c.PostFormArray("foo")
assert.Equal(t, 1, len(values))
assert.Equal(t, "bar", values[0])
+
+ dicts, ok := c.GetPostFormMap("names")
+ assert.True(t, ok)
+ assert.Equal(t, "thinkerou", dicts["a"])
+ assert.Equal(t, "tianou", dicts["b"])
+
+ dicts, ok = c.GetPostFormMap("nokey")
+ assert.False(t, ok)
+ assert.Equal(t, 0, len(dicts))
+
+ dicts = c.PostFormMap("names")
+ assert.Equal(t, "thinkerou", dicts["a"])
+ assert.Equal(t, "tianou", dicts["b"])
+
+ dicts = c.PostFormMap("nokey")
+ assert.Equal(t, 0, len(dicts))
}
func TestContextSetCookie(t *testing.T) {
@@ -596,6 +640,20 @@ func TestContextRenderJSONP(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
+// Tests that the response is serialized as JSONP
+// and Content-Type is set to application/json
+func TestContextRenderJSONPWithoutCallback(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+ c.Request, _ = http.NewRequest("GET", "http://example.com", nil)
+
+ c.JSONP(201, H{"foo": "bar"})
+
+ 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()
@@ -686,6 +744,17 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
+func TestContextRenderNoContentAsciiJSON(t *testing.T) {
+ w := httptest.NewRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"})
+
+ assert.Equal(t, http.StatusNoContent, w.Code)
+ assert.Empty(t, w.Body.String())
+ assert.Equal(t, "application/json", w.HeaderMap.Get("Content-Type"))
+}
+
// Tests that the response executes the templates
// and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) {
@@ -1490,3 +1559,58 @@ func TestContextRenderDataFromReader(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%d", contentLength), w.HeaderMap.Get("Content-Length"))
assert.Equal(t, extraHeaders["Content-Disposition"], w.HeaderMap.Get("Content-Disposition"))
}
+
+type TestResponseRecorder struct {
+ *httptest.ResponseRecorder
+ closeChannel chan bool
+}
+
+func (r *TestResponseRecorder) CloseNotify() <-chan bool {
+ return r.closeChannel
+}
+
+func (r *TestResponseRecorder) closeClient() {
+ r.closeChannel <- true
+}
+
+func CreateTestResponseRecorder() *TestResponseRecorder {
+ return &TestResponseRecorder{
+ httptest.NewRecorder(),
+ make(chan bool, 1),
+ }
+}
+
+func TestContextStream(t *testing.T) {
+ w := CreateTestResponseRecorder()
+ c, _ := CreateTestContext(w)
+
+ stopStream := true
+ c.Stream(func(w io.Writer) bool {
+ defer func() {
+ stopStream = false
+ }()
+
+ w.Write([]byte("test"))
+
+ return stopStream
+ })
+
+ assert.Equal(t, "testtest", w.Body.String())
+}
+
+func TestContextStreamWithClientGone(t *testing.T) {
+ w := CreateTestResponseRecorder()
+ c, _ := CreateTestContext(w)
+
+ c.Stream(func(writer io.Writer) bool {
+ defer func() {
+ w.closeClient()
+ }()
+
+ writer.Write([]byte("test"))
+
+ return true
+ })
+
+ assert.Equal(t, "test", w.Body.String())
+}
diff --git a/errors_test.go b/errors_test.go
index a666d7c1..0626611f 100644
--- a/errors_test.go
+++ b/errors_test.go
@@ -19,17 +19,17 @@ func TestError(t *testing.T) {
Type: ErrorTypePrivate,
}
assert.Equal(t, err.Error(), baseError.Error())
- assert.Equal(t, err.JSON(), H{"error": baseError.Error()})
+ assert.Equal(t, H{"error": baseError.Error()}, err.JSON())
assert.Equal(t, err.SetType(ErrorTypePublic), err)
- assert.Equal(t, err.Type, ErrorTypePublic)
+ assert.Equal(t, ErrorTypePublic, err.Type)
assert.Equal(t, err.SetMeta("some data"), err)
- assert.Equal(t, err.Meta, "some data")
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, "some data", err.Meta)
+ assert.Equal(t, H{
"error": baseError.Error(),
"meta": "some data",
- })
+ }, err.JSON())
jsonBytes, _ := json.Marshal(err)
assert.Equal(t, "{\"error\":\"test error\",\"meta\":\"some data\"}", string(jsonBytes))
@@ -38,22 +38,22 @@ func TestError(t *testing.T) {
"status": "200",
"data": "some data",
})
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, H{
"error": baseError.Error(),
"status": "200",
"data": "some data",
- })
+ }, err.JSON())
err.SetMeta(H{
"error": "custom error",
"status": "200",
"data": "some data",
})
- assert.Equal(t, err.JSON(), H{
+ assert.Equal(t, H{
"error": "custom error",
"status": "200",
"data": "some data",
- })
+ }, err.JSON())
type customError struct {
status string
diff --git a/gin.go b/gin.go
index 65d6d446..fc4d6564 100644
--- a/gin.go
+++ b/gin.go
@@ -171,14 +171,14 @@ func (engine *Engine) SecureJsonPrefix(prefix string) *Engine {
func (engine *Engine) LoadHTMLGlob(pattern string) {
left := engine.delims.Left
right := engine.delims.Right
+ templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
if IsDebugging() {
- debugPrintLoadTemplate(template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern)))
+ debugPrintLoadTemplate(templ)
engine.HTMLRender = render.HTMLDebug{Glob: pattern, FuncMap: engine.FuncMap, Delims: engine.delims}
return
}
- templ := template.Must(template.New("").Delims(left, right).Funcs(engine.FuncMap).ParseGlob(pattern))
engine.SetHTMLTemplate(templ)
}
@@ -349,38 +349,40 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
- if t[i].method == httpMethod {
- root := t[i].root
- // Find route in tree
- handlers, params, tsr := root.getValue(path, c.Params, unescape)
- if handlers != nil {
- c.handlers = handlers
- c.Params = params
- c.Next()
- c.writermem.WriteHeaderNow()
+ if t[i].method != httpMethod {
+ continue
+ }
+ root := t[i].root
+ // Find route in tree
+ handlers, params, tsr := root.getValue(path, c.Params, unescape)
+ if handlers != nil {
+ c.handlers = handlers
+ c.Params = params
+ c.Next()
+ c.writermem.WriteHeaderNow()
+ return
+ }
+ if httpMethod != "CONNECT" && path != "/" {
+ if tsr && engine.RedirectTrailingSlash {
+ redirectTrailingSlash(c)
return
}
- if httpMethod != "CONNECT" && path != "/" {
- if tsr && engine.RedirectTrailingSlash {
- redirectTrailingSlash(c)
- return
- }
- if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
- return
- }
+ if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
+ return
}
- break
}
+ break
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
- if tree.method != httpMethod {
- if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
- c.handlers = engine.allNoMethod
- serveError(c, http.StatusMethodNotAllowed, default405Body)
- return
- }
+ if tree.method == httpMethod {
+ continue
+ }
+ if handlers, _, _ := tree.root.getValue(path, nil, unescape); handlers != nil {
+ c.handlers = engine.allNoMethod
+ serveError(c, http.StatusMethodNotAllowed, default405Body)
+ return
}
}
}
@@ -393,14 +395,16 @@ var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) {
c.writermem.status = code
c.Next()
- if !c.writermem.Written() {
- if c.writermem.Status() == code {
- c.writermem.Header()["Content-Type"] = mimePlain
- c.Writer.Write(defaultMessage)
- } else {
- c.writermem.WriteHeaderNow()
- }
+ if c.writermem.Written() {
+ return
}
+ if c.writermem.Status() == code {
+ c.writermem.Header()["Content-Type"] = mimePlain
+ c.Writer.Write(defaultMessage)
+ return
+ }
+ c.writermem.WriteHeaderNow()
+ return
}
func redirectTrailingSlash(c *Context) {
@@ -411,10 +415,9 @@ func redirectTrailingSlash(c *Context) {
code = http.StatusTemporaryRedirect
}
+ req.URL.Path = path + "/"
if length := len(path); length > 1 && path[length-1] == '/' {
req.URL.Path = path[:length-1]
- } else {
- req.URL.Path = path + "/"
}
debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String())
http.Redirect(c.Writer, req, req.URL.String(), code)
@@ -425,11 +428,7 @@ func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
req := c.Request
path := req.URL.Path
- fixedPath, found := root.findCaseInsensitivePath(
- cleanPath(path),
- trailingSlash,
- )
- if found {
+ if fixedPath, ok := root.findCaseInsensitivePath(cleanPath(path), trailingSlash); ok {
code := http.StatusMovedPermanently // Permanent redirect, request with GET method
if req.Method != "GET" {
code = http.StatusTemporaryRedirect
diff --git a/ginS/gins.go b/ginS/gins.go
index ee00b381..a7686f23 100644
--- a/ginS/gins.go
+++ b/ginS/gins.go
@@ -128,7 +128,7 @@ func Run(addr ...string) (err error) {
// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests.
// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router)
// Note: this method will block the calling goroutine undefinitelly unless an error happens.
-func RunTLS(addr string, certFile string, keyFile string) (err error) {
+func RunTLS(addr, certFile, keyFile string) (err error) {
return engine().RunTLS(addr, certFile, keyFile)
}
diff --git a/path.go b/path.go
index 1c2b8498..d1f59622 100644
--- a/path.go
+++ b/path.go
@@ -59,11 +59,11 @@ func cleanPath(p string) string {
case p[r] == '.' && p[r+1] == '/':
// . element
- r++
+ r += 2
case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
// .. element: remove to last /
- r += 2
+ r += 3
if w > 1 {
// can backtrack
diff --git a/recovery_test.go b/recovery_test.go
index de3b62a5..fa065b13 100644
--- a/recovery_test.go
+++ b/recovery_test.go
@@ -41,3 +41,23 @@ func TestPanicWithAbort(t *testing.T) {
// TEST
assert.Equal(t, 400, w.Code)
}
+
+func TestSource(t *testing.T) {
+ bs := source(nil, 0)
+ assert.Equal(t, []byte("???"), bs)
+
+ in := [][]byte{
+ []byte("Hello world."),
+ []byte("Hi, gin.."),
+ }
+ bs = source(in, 10)
+ assert.Equal(t, []byte("???"), bs)
+
+ bs = source(in, 1)
+ assert.Equal(t, []byte("Hello world."), bs)
+}
+
+func TestFunction(t *testing.T) {
+ bs := function(1)
+ assert.Equal(t, []byte("???"), bs)
+}
diff --git a/render/json.go b/render/json.go
index 3a2e8b2f..6e5089a0 100755
--- a/render/json.go
+++ b/render/json.go
@@ -6,6 +6,7 @@ package render
import (
"bytes"
+ "fmt"
"html/template"
"net/http"
@@ -30,10 +31,15 @@ type JsonpJSON struct {
Data interface{}
}
+type AsciiJSON struct {
+ Data interface{}
+}
+
type SecureJSONPrefix string
var jsonContentType = []string{"application/json; charset=utf-8"}
var jsonpContentType = []string{"application/javascript; charset=utf-8"}
+var jsonAsciiContentType = []string{"application/json"}
func (r JSON) Render(w http.ResponseWriter) (err error) {
if err = WriteJSON(w, r.Data); err != nil {
@@ -112,3 +118,29 @@ func (r JsonpJSON) Render(w http.ResponseWriter) (err error) {
func (r JsonpJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonpContentType)
}
+
+func (r AsciiJSON) Render(w http.ResponseWriter) (err error) {
+ r.WriteContentType(w)
+ ret, err := json.Marshal(r.Data)
+ if err != nil {
+ return err
+ }
+
+ var buffer bytes.Buffer
+ for _, r := range string(ret) {
+ cvt := ""
+ if r < 128 {
+ cvt = string(r)
+ } else {
+ cvt = fmt.Sprintf("\\u%04x", int64(r))
+ }
+ buffer.WriteString(cvt)
+ }
+
+ w.Write(buffer.Bytes())
+ return nil
+}
+
+func (r AsciiJSON) WriteContentType(w http.ResponseWriter) {
+ writeContentType(w, jsonAsciiContentType)
+}
diff --git a/render/render.go b/render/render.go
index 7caf9bba..4ff1c7b6 100755
--- a/render/render.go
+++ b/render/render.go
@@ -26,6 +26,7 @@ var (
_ Render = YAML{}
_ Render = MsgPack{}
_ Render = Reader{}
+ _ Render = AsciiJSON{}
)
func writeContentType(w http.ResponseWriter, value []string) {
diff --git a/render/render_test.go b/render/render_test.go
index c4c49bb7..8fad12b3 100755
--- a/render/render_test.go
+++ b/render/render_test.go
@@ -158,6 +158,21 @@ func TestRenderJsonpJSON(t *testing.T) {
assert.Equal(t, "application/javascript; charset=utf-8", w2.Header().Get("Content-Type"))
}
+func TestRenderJsonpJSONError2(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := map[string]interface{}{
+ "foo": "bar",
+ }
+ (JsonpJSON{"", data}).WriteContentType(w)
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+
+ e := (JsonpJSON{"", data}).Render(w)
+ assert.NoError(t, e)
+
+ assert.Equal(t, "{\"foo\":\"bar\"}", w.Body.String())
+ assert.Equal(t, "application/javascript; charset=utf-8", w.Header().Get("Content-Type"))
+}
+
func TestRenderJsonpJSONFail(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
@@ -167,6 +182,35 @@ func TestRenderJsonpJSONFail(t *testing.T) {
assert.Error(t, err)
}
+func TestRenderAsciiJSON(t *testing.T) {
+ w1 := httptest.NewRecorder()
+ data1 := map[string]interface{}{
+ "lang": "GO语言",
+ "tag": "
",
+ }
+
+ err := (AsciiJSON{data1}).Render(w1)
+
+ assert.NoError(t, err)
+ assert.Equal(t, "{\"lang\":\"GO\\u8bed\\u8a00\",\"tag\":\"\\u003cbr\\u003e\"}", w1.Body.String())
+ assert.Equal(t, "application/json", w1.Header().Get("Content-Type"))
+
+ w2 := httptest.NewRecorder()
+ data2 := float64(3.1415926)
+
+ err = (AsciiJSON{data2}).Render(w2)
+ assert.NoError(t, err)
+ assert.Equal(t, "3.1415926", w2.Body.String())
+}
+
+func TestRenderAsciiJSONFail(t *testing.T) {
+ w := httptest.NewRecorder()
+ data := make(chan int)
+
+ // json: unsupported type: chan int
+ assert.Error(t, (AsciiJSON{data}).Render(w))
+}
+
type xmlmap map[string]interface{}
// Allows type H to be used with xml.Marshal
diff --git a/response_writer.go b/response_writer.go
index 166ae8c3..923b53f8 100644
--- a/response_writer.go
+++ b/response_writer.go
@@ -110,5 +110,6 @@ func (w *responseWriter) CloseNotify() <-chan bool {
// Flush implements the http.Flush interface.
func (w *responseWriter) Flush() {
+ w.WriteHeaderNow()
w.ResponseWriter.(http.Flusher).Flush()
}
diff --git a/response_writer_test.go b/response_writer_test.go
index cec27338..fcc48b3b 100644
--- a/response_writer_test.go
+++ b/response_writer_test.go
@@ -51,7 +51,7 @@ func TestResponseWriterWriteHeader(t *testing.T) {
w.WriteHeader(300)
assert.False(t, w.Written())
assert.Equal(t, 300, w.Status())
- assert.NotEqual(t, testWritter.Code, 300)
+ assert.NotEqual(t, 300, testWritter.Code)
w.WriteHeader(-1)
assert.Equal(t, 300, w.Status())
@@ -113,3 +113,19 @@ func TestResponseWriterHijack(t *testing.T) {
w.Flush()
}
+
+func TestResponseWriterFlush(t *testing.T) {
+ testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ writer := &responseWriter{}
+ writer.reset(w)
+
+ writer.WriteHeader(http.StatusInternalServerError)
+ writer.Flush()
+ }))
+ defer testServer.Close()
+
+ // should return 500
+ resp, err := http.Get(testServer.URL)
+ assert.NoError(t, err)
+ assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
+}
diff --git a/routes_test.go b/routes_test.go
index 81293907..a2389f9b 100644
--- a/routes_test.go
+++ b/routes_test.go
@@ -333,6 +333,16 @@ func TestRouteNotAllowedEnabled(t *testing.T) {
assert.Equal(t, http.StatusTeapot, w.Code)
}
+func TestRouteNotAllowedEnabled2(t *testing.T) {
+ router := New()
+ router.HandleMethodNotAllowed = true
+ // add one methodTree to trees
+ router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}})
+ router.GET("/path2", func(c *Context) {})
+ w := performRequest(router, "POST", "/path2")
+ assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
+}
+
func TestRouteNotAllowedDisabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = false
diff --git a/test_helpers.go b/test_helpers.go
index 2aed37f2..3a7a5ddf 100644
--- a/test_helpers.go
+++ b/test_helpers.go
@@ -4,9 +4,7 @@
package gin
-import (
- "net/http"
-)
+import "net/http"
// CreateTestContext returns a fresh engine and context for testing purposes
func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) {
diff --git a/utils_test.go b/utils_test.go
index 3d019e7e..95b5de5e 100644
--- a/utils_test.go
+++ b/utils_test.go
@@ -5,6 +5,8 @@
package gin
import (
+ "bytes"
+ "encoding/xml"
"fmt"
"net/http"
"testing"
@@ -124,3 +126,14 @@ func TestBindMiddleware(t *testing.T) {
Bind(&bindTestStruct{})
})
}
+
+func TestMarshalXMLforH(t *testing.T) {
+ h := H{
+ "": "test",
+ }
+ var b bytes.Buffer
+ enc := xml.NewEncoder(&b)
+ var x xml.StartElement
+ e := h.MarshalXML(enc, x)
+ assert.Error(t, e)
+}