From cdd02fa9d65c15de42f1a4522fcb9af28fe56cf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sun, 1 Jul 2018 21:10:48 +0800 Subject: [PATCH 01/17] update error(err) to err (#1416) the pull request update `return error(err)` to `return err`, and remove `kindOfData`. --- binding/binding.go | 5 ++--- binding/default_validator.go | 19 ++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) 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 -} From 1c4cbfae59e0f62a6812b7808839eed45b66515f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 2 Jul 2018 11:06:56 +0800 Subject: [PATCH 02/17] chore: remove duplicate code (#1418) --- gin.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/gin.go b/gin.go index 65d6d446..3ee8018d 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) } @@ -425,11 +425,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 From d17a12591fb637da47576b8669fabb8752fe656d Mon Sep 17 00:00:00 2001 From: vz Date: Tue, 3 Jul 2018 15:39:18 +0800 Subject: [PATCH 03/17] update assert param(expect, actual) position (#1421) - update assert param(expect, actual) position --- errors_test.go | 18 +++++++++--------- response_writer_test.go | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) 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/response_writer_test.go b/response_writer_test.go index cec27338..4289a7c1 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()) From 85221af84cf6ee99dc900bc41912abc09acfc1b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rex=20Lee=28=E6=9D=8E=E4=BF=8A=29?= Date: Tue, 3 Jul 2018 17:17:08 +0800 Subject: [PATCH 04/17] add json ASCII string render (#1358) add a json render that rendering json as ASCII string --- README.md | 23 +++++++++++++++++++++++ context.go | 6 ++++++ context_test.go | 11 +++++++++++ render/json.go | 32 ++++++++++++++++++++++++++++++++ render/render_test.go | 29 +++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+) diff --git a/README.md b/README.md index e09a51f2..93896469 100644 --- a/README.md +++ b/README.md @@ -900,6 +900,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 diff --git a/context.go b/context.go index 6fc5d25f..d0b4e87f 100644 --- a/context.go +++ b/context.go @@ -704,6 +704,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..6f37682e 100644 --- a/context_test.go +++ b/context_test.go @@ -686,6 +686,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) { 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_test.go b/render/render_test.go index 40ec806e..2f728441 100755 --- a/render/render_test.go +++ b/render/render_test.go @@ -167,6 +167,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 From 220e8d34538f5dfdcc299dda79bf83cf52e633f6 Mon Sep 17 00:00:00 2001 From: solos Date: Sat, 21 Jul 2018 00:52:55 +0800 Subject: [PATCH 05/17] return json if jsonp has not callback (#1438) return json if jsonp has not callback --- context.go | 7 ++++++- context_test.go | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/context.go b/context.go index d0b4e87f..720c4251 100644 --- a/context.go +++ b/context.go @@ -695,7 +695,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. diff --git a/context_test.go b/context_test.go index 6f37682e..e29569fd 100644 --- a/context_test.go +++ b/context_test.go @@ -596,6 +596,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() From 631cfbd1ef2374f5a01f5d17ed8c8f08779ad6fe Mon Sep 17 00:00:00 2001 From: Dmitry Dorogin Date: Sun, 5 Aug 2018 08:29:26 +0300 Subject: [PATCH 06/17] Simplify context error (#1431) Hello! Looking through context package and found a little bit complicated switch block. And tried to make it easier. Thanks! --- context.go | 9 ++++----- ginS/gins.go | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/context.go b/context.go index 720c4251..779484b1 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 } 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) } From 647535cd9b780b6ccbeccfedfba36896e1a1da37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Mon, 6 Aug 2018 12:07:11 +0800 Subject: [PATCH 07/17] Support map as query string or post form parameters (#1383) * support query map * add GetQueryMap and unittest * support post-form map * add readme for query map * attempt to fix bug for post-form map when go version is 1.6 * remove duplicate code * remove comment --- README.md | 29 +++++++++++++++++++++++++++++ context.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ context_test.go | 45 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 121 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 93896469..11c91c9a 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,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 +324,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 diff --git a/context.go b/context.go index 779484b1..724ded79 100644 --- a/context.go +++ b/context.go @@ -360,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 { @@ -415,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) diff --git a/context_test.go b/context_test.go index e29569fd..589ab336 100644 --- a/context_test.go +++ b/context_test.go @@ -47,6 +47,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 +373,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 +442,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 +542,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) { From e2b4cf6e2dffb4e59f95f96b78e25e65f9037dec Mon Sep 17 00:00:00 2001 From: grapeVine Date: Mon, 6 Aug 2018 23:08:01 +0800 Subject: [PATCH 08/17] interface implement type check (#1459) interface implement type check --- render/render.go | 1 + 1 file changed, 1 insertion(+) 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) { From 9b7e7bdce6e073d47635d57fbacfb3205cb3127e Mon Sep 17 00:00:00 2001 From: Dmitry Dorogin Date: Tue, 7 Aug 2018 01:44:32 +0300 Subject: [PATCH 09/17] Add tests for context.Stream (#1433) --- context_test.go | 36 ++++++++++++++++++++++++++++++++++++ test_helpers.go | 21 +++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/context_test.go b/context_test.go index 589ab336..40ae5bf8 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{} @@ -1558,3 +1559,38 @@ 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")) } + +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/test_helpers.go b/test_helpers.go index 2aed37f2..554568d9 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -6,6 +6,7 @@ package gin import ( "net/http" + "net/http/httptest" ) // CreateTestContext returns a fresh engine and context for testing purposes @@ -16,3 +17,23 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { c.writermem.reset(w) return } + +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), + } +} From 0552c3bc3a6de6b802684b024ae49c3195cc70dd Mon Sep 17 00:00:00 2001 From: zhanweidu Date: Tue, 7 Aug 2018 12:41:28 +0800 Subject: [PATCH 10/17] flush operation will overwrite the origin status code (#1460) The status of responseWriter will be overwrite if flush was called. This is caused by the Flush of http.response.Flush(). --- response_writer.go | 1 + response_writer_test.go | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) 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 4289a7c1..fcc48b3b 100644 --- a/response_writer_test.go +++ b/response_writer_test.go @@ -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) +} From 9666ba67383f6cc7bed3bb18691c2382717d51ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Tue, 7 Aug 2018 13:49:31 +0800 Subject: [PATCH 11/17] chore: update top bar header (#1461) --- README.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 11c91c9a..c2cfe2d0 100644 --- a/README.md +++ b/README.md @@ -3,10 +3,11 @@ [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) - [![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) - [![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) - [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) - [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![codecov](https://codecov.io/gh/gin-gonic/gin/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-gonic/gin) +[![Go Report Card](https://goreportcard.com/badge/github.com/gin-gonic/gin)](https://goreportcard.com/report/github.com/gin-gonic/gin) +[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) +[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) [![Open Source Helpers](https://www.codetriage.com/gin-gonic/gin/badges/users.svg)](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. @@ -1811,7 +1812,7 @@ func TestPingRoute(t *testing.T) { } ``` -## Users [![Sourcegraph](https://sourcegraph.com/github.com/gin-gonic/gin/-/badge.svg)](https://sourcegraph.com/github.com/gin-gonic/gin?badge) +## Users Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. From 1f1bc429ed7a4d591a7374f0e267b59747b177bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Thu, 9 Aug 2018 17:20:06 +0800 Subject: [PATCH 12/17] chore: add test case for source/function of recovery.go (#1467) --- recovery_test.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) 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) +} From 8fc8ce047243dbbbc5eb3d01b9fd026fc3b08c82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Fri, 10 Aug 2018 20:50:23 +0800 Subject: [PATCH 13/17] small enhance for cleanPath (#1469) from httprouter patch: https://github.com/julienschmidt/httprouter/pull/243 --- path.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 7e64d32269d817b645d5d4694100a8a8ad6960cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sun, 12 Aug 2018 10:12:33 +0800 Subject: [PATCH 14/17] Attempt to fix #1462 (#1463) #1462 --- context_test.go | 20 ++++++++++++++++++++ test_helpers.go | 25 +------------------------ 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/context_test.go b/context_test.go index 40ae5bf8..0185507f 100644 --- a/context_test.go +++ b/context_test.go @@ -1560,6 +1560,26 @@ func TestContextRenderDataFromReader(t *testing.T) { 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) diff --git a/test_helpers.go b/test_helpers.go index 554568d9..3a7a5ddf 100644 --- a/test_helpers.go +++ b/test_helpers.go @@ -4,10 +4,7 @@ package gin -import ( - "net/http" - "net/http/httptest" -) +import "net/http" // CreateTestContext returns a fresh engine and context for testing purposes func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { @@ -17,23 +14,3 @@ func CreateTestContext(w http.ResponseWriter) (c *Context, r *Engine) { c.writermem.reset(w) return } - -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), - } -} From e5bb4f62a284426de199d68d71900fcbd1285691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sun, 12 Aug 2018 21:17:57 +0800 Subject: [PATCH 15/17] chore: add return or remove else for reduce indent (#1470) --- gin.go | 69 ++++++++++++++++++++++++++------------------------ routes_test.go | 10 ++++++++ 2 files changed, 46 insertions(+), 33 deletions(-) diff --git a/gin.go b/gin.go index 3ee8018d..fc4d6564 100644 --- a/gin.go +++ b/gin.go @@ -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) 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 From 202db4fb117768795c0fcc909cc9c3d75b9172b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sun, 12 Aug 2018 21:38:07 +0800 Subject: [PATCH 16/17] improve utils code coverage (#1473) --- utils_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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) +} From 1ae32f3a2cfa53d1ae3e5c10a72841673bedc449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Sun, 12 Aug 2018 22:02:37 +0800 Subject: [PATCH 17/17] improve render code coverage (#1474) all code coverage > 99% --- render/render_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/render/render_test.go b/render/render_test.go index 2f728441..9e768850 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)