Merge branch 'master' into logger_test

This commit is contained in:
thinkerou 2019-08-29 08:13:48 +08:00 committed by GitHub
commit 933c55e124
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 529 additions and 126 deletions

View File

@ -3,8 +3,6 @@ language: go
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- go: 1.8.x
- go: 1.9.x
- go: 1.10.x - go: 1.10.x
- go: 1.11.x - go: 1.11.x
env: GO111MODULE=on env: GO111MODULE=on

View File

@ -2,7 +2,7 @@
### Gin 1.4.0 ### Gin 1.4.0
- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) - [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569)
- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) - [NEW] Refactor of form mapping multipart request [#1829](https://github.com/gin-gonic/gin/pull/1829)
- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) - [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830)
- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) - [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802)
- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) - [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264)

114
README.md
View File

@ -40,6 +40,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
- [Only Bind Query String](#only-bind-query-string) - [Only Bind Query String](#only-bind-query-string)
- [Bind Query String or Post Data](#bind-query-string-or-post-data) - [Bind Query String or Post Data](#bind-query-string-or-post-data)
- [Bind Uri](#bind-uri) - [Bind Uri](#bind-uri)
- [Bind Header](#bind-header)
- [Bind HTML checkboxes](#bind-html-checkboxes) - [Bind HTML checkboxes](#bind-html-checkboxes)
- [Multipart/Urlencoded binding](#multiparturlencoded-binding) - [Multipart/Urlencoded binding](#multiparturlencoded-binding)
- [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering)
@ -69,7 +70,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi
To install Gin package, you need to install Go and set your Go workspace first. To install Gin package, you need to install Go and set your Go workspace first.
1. The first need [Go](https://golang.org/) installed (**version 1.8+ is required**), then you can use the below Go command to install Gin. 1. The first need [Go](https://golang.org/) installed (**version 1.10+ is required**), then you can use the below Go command to install Gin.
```sh ```sh
$ go get -u github.com/gin-gonic/gin $ go get -u github.com/gin-gonic/gin
@ -755,7 +756,7 @@ func bookableDate(
) bool { ) bool {
if date, ok := field.Interface().(time.Time); ok { if date, ok := field.Interface().(time.Time); ok {
today := time.Now() today := time.Now()
if today.Year() > date.Year() || today.YearDay() > date.YearDay() { if today.After(date) {
return false return false
} }
} }
@ -845,9 +846,11 @@ import (
) )
type Person struct { type Person struct {
Name string `form:"name"` Name string `form:"name"`
Address string `form:"address"` Address string `form:"address"`
Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
} }
func main() { func main() {
@ -861,11 +864,13 @@ func startPage(c *gin.Context) {
// If `GET`, only `Form` binding engine (`query`) used. // If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). // 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/master/binding/binding.go#L48 // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48
if c.ShouldBind(&person) == nil { if c.ShouldBind(&person) == nil {
log.Println(person.Name) log.Println(person.Name)
log.Println(person.Address) log.Println(person.Address)
log.Println(person.Birthday) log.Println(person.Birthday)
} log.Println(person.CreateTime)
log.Println(person.UnixTime)
}
c.String(200, "Success") c.String(200, "Success")
} }
@ -873,7 +878,7 @@ func startPage(c *gin.Context) {
Test it with: Test it with:
```sh ```sh
$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" $ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15&createTime=1562400033000000123&unixTime=1562400033"
``` ```
### Bind Uri ### Bind Uri
@ -910,6 +915,43 @@ $ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3
$ curl -v localhost:8088/thinkerou/not-uuid $ curl -v localhost:8088/thinkerou/not-uuid
``` ```
### Bind Header
```go
package main
import (
"fmt"
"github.com/gin-gonic/gin"
)
type testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
}
func main() {
r := gin.Default()
r.GET("/", func(c *gin.Context) {
h := testHeader{}
if err := c.ShouldBindHeader(&h); err != nil {
c.JSON(200, err)
}
fmt.Printf("%#v\n", h)
c.JSON(200, gin.H{"Rate": h.Rate, "Domain": h.Domain})
})
r.Run()
// client
// curl -H "rate:300" -H "domain:music" 127.0.0.1:8080/
// output
// {"Domain":"music","Rate":300}
}
```
### Bind HTML checkboxes ### Bind HTML checkboxes
See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092)
@ -959,32 +1001,36 @@ result:
### Multipart/Urlencoded binding ### Multipart/Urlencoded binding
```go ```go
package main type ProfileForm struct {
Name string `form:"name" binding:"required"`
Avatar *multipart.FileHeader `form:"avatar" binding:"required"`
import ( // or for multiple files
"github.com/gin-gonic/gin" // Avatars []*multipart.FileHeader `form:"avatar" binding:"required"`
)
type LoginForm struct {
User string `form:"user" binding:"required"`
Password string `form:"password" binding:"required"`
} }
func main() { func main() {
router := gin.Default() router := gin.Default()
router.POST("/login", func(c *gin.Context) { router.POST("/profile", func(c *gin.Context) {
// you can bind multipart form with explicit binding declaration: // you can bind multipart form with explicit binding declaration:
// c.ShouldBindWith(&form, binding.Form) // c.ShouldBindWith(&form, binding.Form)
// or you can simply use autobinding with ShouldBind method: // or you can simply use autobinding with ShouldBind method:
var form LoginForm var form ProfileForm
// in this case proper binding will be automatically selected // in this case proper binding will be automatically selected
if c.ShouldBind(&form) == nil { if err := c.ShouldBind(&form); err != nil {
if form.User == "user" && form.Password == "password" { c.String(http.StatusBadRequest, "bad request")
c.JSON(200, gin.H{"status": "you are logged in"}) return
} else {
c.JSON(401, gin.H{"status": "unauthorized"})
}
} }
err := c.SaveUploadedFile(form.Avatar, form.Avatar.Filename)
if err != nil {
c.String(http.StatusInternalServerError, "unknown error")
return
}
// db.Save(&form)
c.String(http.StatusOK, "ok")
}) })
router.Run(":8080") router.Run(":8080")
} }
@ -992,7 +1038,7 @@ func main() {
Test it with: Test it with:
```sh ```sh
$ curl -v --form user=user --form password=password http://localhost:8080/login $ curl -X POST -v --form name=user --form "avatar=@./avatar.png" http://localhost:8080/profile
``` ```
### XML, JSON, YAML and ProtoBuf rendering ### XML, JSON, YAML and ProtoBuf rendering
@ -1077,8 +1123,8 @@ Using JSONP to request data from a server in a different domain. Add callback t
func main() { func main() {
r := gin.Default() r := gin.Default()
r.GET("/JSONP?callback=x", func(c *gin.Context) { r.GET("/JSONP", func(c *gin.Context) {
data := map[string]interface{}{ data := gin.H{
"foo": "bar", "foo": "bar",
} }
@ -1089,6 +1135,9 @@ func main() {
// Listen and serve on 0.0.0.0:8080 // Listen and serve on 0.0.0.0:8080
r.Run(":8080") r.Run(":8080")
// client
// curl http://127.0.0.1:8080/JSONP?callback=x
} }
``` ```
@ -1101,7 +1150,7 @@ func main() {
r := gin.Default() r := gin.Default()
r.GET("/someJSON", func(c *gin.Context) { r.GET("/someJSON", func(c *gin.Context) {
data := map[string]interface{}{ data := gin.H{
"lang": "GO语言", "lang": "GO语言",
"tag": "<br>", "tag": "<br>",
} }
@ -1310,7 +1359,7 @@ func main() {
router.LoadHTMLFiles("./testdata/template/raw.tmpl") router.LoadHTMLFiles("./testdata/template/raw.tmpl")
router.GET("/raw", func(c *gin.Context) { router.GET("/raw", func(c *gin.Context) {
c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ c.HTML(http.StatusOK, "raw.tmpl", gin.H{
"now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC),
}) })
}) })
@ -2069,3 +2118,4 @@ Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framewor
* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. * [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow.
* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. * [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares.
* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. * [picfit](https://github.com/thoas/picfit): An image resizing server written in Go.
* [brigade](https://github.com/brigadecore/brigade): Event-based Scripting for Kubernetes.

View File

@ -5,7 +5,6 @@
package gin package gin
import ( import (
"crypto/subtle"
"encoding/base64" "encoding/base64"
"net/http" "net/http"
"strconv" "strconv"
@ -86,11 +85,3 @@ func authorizationHeader(user, password string) string {
base := user + ":" + password base := user + ":" + password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) return "Basic " + base64.StdEncoding.EncodeToString([]byte(base))
} }
func secureCompare(given, actual string) bool {
if subtle.ConstantTimeEq(int32(len(given)), int32(len(actual))) == 1 {
return subtle.ConstantTimeCompare([]byte(given), []byte(actual)) == 1
}
// Securely compare actual to itself to keep constant time, but always return false.
return subtle.ConstantTimeCompare([]byte(actual), []byte(actual)) == 1 && false
}

View File

@ -81,13 +81,6 @@ func TestBasicAuthAuthorizationHeader(t *testing.T) {
assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password")) assert.Equal(t, "Basic YWRtaW46cGFzc3dvcmQ=", authorizationHeader("admin", "password"))
} }
func TestBasicAuthSecureCompare(t *testing.T) {
assert.True(t, secureCompare("1234567890", "1234567890"))
assert.False(t, secureCompare("123456789", "1234567890"))
assert.False(t, secureCompare("12345678900", "1234567890"))
assert.False(t, secureCompare("1234567891", "1234567890"))
}
func TestBasicAuthSucceed(t *testing.T) { func TestBasicAuthSucceed(t *testing.T) {
accounts := Accounts{"admin": "password"} accounts := Accounts{"admin": "password"}
router := New() router := New()

View File

@ -78,6 +78,7 @@ var (
MsgPack = msgpackBinding{} MsgPack = msgpackBinding{}
YAML = yamlBinding{} YAML = yamlBinding{}
Uri = uriBinding{} Uri = uriBinding{}
Header = headerBinding{}
) )
// Default returns the appropriate Binding instance based on the HTTP method // Default returns the appropriate Binding instance based on the HTTP method

View File

@ -65,8 +65,15 @@ type FooStructUseNumber struct {
} }
type FooBarStructForTimeType struct { type FooBarStructForTimeType struct {
TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"` TimeFoo time.Time `form:"time_foo" time_format:"2006-01-02" time_utc:"1" time_location:"Asia/Chongqing"`
TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"` TimeBar time.Time `form:"time_bar" time_format:"2006-01-02" time_utc:"1"`
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
}
type FooStructForTimeTypeNotUnixFormat struct {
CreateTime time.Time `form:"createTime" time_format:"unixNano"`
UnixTime time.Time `form:"unixTime" time_format:"unix"`
} }
type FooStructForTimeTypeNotFormat struct { type FooStructForTimeTypeNotFormat struct {
@ -226,7 +233,10 @@ func TestBindingFormDefaultValue2(t *testing.T) {
func TestBindingFormForTime(t *testing.T) { func TestBindingFormForTime(t *testing.T) {
testFormBindingForTime(t, "POST", testFormBindingForTime(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15&time_bar=", "bar2=foo") "time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "bar2=foo")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "POST", testFormBindingForTimeNotFormat(t, "POST",
"/", "/", "/", "/",
"time_foo=2017-11-15", "bar2=foo") "time_foo=2017-11-15", "bar2=foo")
@ -240,8 +250,11 @@ func TestBindingFormForTime(t *testing.T) {
func TestBindingFormForTime2(t *testing.T) { func TestBindingFormForTime2(t *testing.T) {
testFormBindingForTime(t, "GET", testFormBindingForTime(t, "GET",
"/?time_foo=2017-11-15&time_bar=", "/?bar2=foo", "/?time_foo=2017-11-15&time_bar=&createTime=1562400033000000123&unixTime=1562400033", "/?bar2=foo",
"", "") "", "")
testFormBindingForTimeNotUnixFormat(t, "POST",
"/", "/",
"time_foo=2017-11-15&createTime=bad&unixTime=bad", "bar2=foo")
testFormBindingForTimeNotFormat(t, "GET", testFormBindingForTimeNotFormat(t, "GET",
"/?time_foo=2017-11-15", "/?bar2=foo", "/?time_foo=2017-11-15", "/?bar2=foo",
"", "") "", "")
@ -667,6 +680,31 @@ func TestExistsFails(t *testing.T) {
assert.Error(t, err) assert.Error(t, err)
} }
func TestHeaderBinding(t *testing.T) {
h := Header
assert.Equal(t, "header", h.Name())
type tHeader struct {
Limit int `header:"limit"`
}
var theader tHeader
req := requestWithBody("GET", "/", "")
req.Header.Add("limit", "1000")
assert.NoError(t, h.Bind(req, &theader))
assert.Equal(t, 1000, theader.Limit)
req = requestWithBody("GET", "/", "")
req.Header.Add("fail", `{fail:fail}`)
type failStruct struct {
Fail map[string]interface{} `header:"fail"`
}
err := h.Bind(req, &failStruct{})
assert.Error(t, err)
}
func TestUriBinding(t *testing.T) { func TestUriBinding(t *testing.T) {
b := Uri b := Uri
assert.Equal(t, "uri", b.Name()) assert.Equal(t, "uri", b.Name())
@ -824,6 +862,8 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String()) assert.Equal(t, "Asia/Chongqing", obj.TimeFoo.Location().String())
assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix()) assert.Equal(t, int64(-62135596800), obj.TimeBar.Unix())
assert.Equal(t, "UTC", obj.TimeBar.Location().String()) assert.Equal(t, "UTC", obj.TimeBar.Location().String())
assert.Equal(t, int64(1562400033000000123), obj.CreateTime.UnixNano())
assert.Equal(t, int64(1562400033), obj.UnixTime.Unix())
obj = FooBarStructForTimeType{} obj = FooBarStructForTimeType{}
req = requestWithBody(method, badPath, badBody) req = requestWithBody(method, badPath, badBody)
@ -831,6 +871,24 @@ func testFormBindingForTime(t *testing.T, method, path, badPath, body, badBody s
assert.Error(t, err) assert.Error(t, err)
} }
func testFormBindingForTimeNotUnixFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form
assert.Equal(t, "form", b.Name())
obj := FooStructForTimeTypeNotUnixFormat{}
req := requestWithBody(method, path, body)
if method == "POST" {
req.Header.Add("Content-Type", MIMEPOSTForm)
}
err := b.Bind(req, &obj)
assert.Error(t, err)
obj = FooStructForTimeTypeNotUnixFormat{}
req = requestWithBody(method, badPath, badBody)
err = JSON.Bind(req, &obj)
assert.Error(t, err)
}
func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) { func testFormBindingForTimeNotFormat(t *testing.T, method, path, badPath, body, badBody string) {
b := Form b := Form
assert.Equal(t, "form", b.Name()) assert.Equal(t, "form", b.Name())

View File

@ -5,9 +5,7 @@
package binding package binding
import ( import (
"mime/multipart"
"net/http" "net/http"
"reflect"
) )
const defaultMemory = 32 * 1024 * 1024 const defaultMemory = 32 * 1024 * 1024
@ -63,27 +61,3 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error {
return validate(obj) return validate(obj)
} }
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
var (
multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{})
)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if value.Type() == multipartFileHeaderStructType {
_, file, err := (*http.Request)(r).FormFile(key)
if err != nil {
return false, err
}
if file != nil {
value.Set(reflect.ValueOf(*file))
return true, nil
}
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}

View File

@ -266,6 +266,24 @@ func setTimeField(val string, structField reflect.StructField, value reflect.Val
timeFormat = time.RFC3339 timeFormat = time.RFC3339
} }
switch tf := strings.ToLower(timeFormat); tf {
case "unix", "unixnano":
tv, err := strconv.ParseInt(val, 10, 0)
if err != nil {
return err
}
d := time.Duration(1)
if tf == "unixnano" {
d = time.Second
}
t := time.Unix(tv/int64(d), tv%int64(d))
value.Set(reflect.ValueOf(t))
return nil
}
if val == "" { if val == "" {
value.Set(reflect.ValueOf(time.Time{})) value.Set(reflect.ValueOf(time.Time{}))
return nil return nil

34
binding/header.go Normal file
View File

@ -0,0 +1,34 @@
package binding
import (
"net/http"
"net/textproto"
"reflect"
)
type headerBinding struct{}
func (headerBinding) Name() string {
return "header"
}
func (headerBinding) Bind(req *http.Request, obj interface{}) error {
if err := mapHeader(obj, req.Header); err != nil {
return err
}
return validate(obj)
}
func mapHeader(ptr interface{}, h map[string][]string) error {
return mappingByPtr(ptr, headerSource(h), "header")
}
type headerSource map[string][]string
var _ setter = headerSource(nil)
func (hs headerSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) {
return setByForm(value, field, hs, textproto.CanonicalMIMEHeaderKey(tagValue), opt)
}

View File

@ -0,0 +1,66 @@
// Copyright 2019 Gin Core Team. 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 (
"errors"
"mime/multipart"
"net/http"
"reflect"
)
type multipartRequest http.Request
var _ setter = (*multipartRequest)(nil)
// TrySet tries to set a value by the multipart request with the binding a form file
func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) {
if files := r.MultipartForm.File[key]; len(files) != 0 {
return setByMultipartFormFile(value, field, files)
}
return setByForm(value, field, r.MultipartForm.Value, key, opt)
}
func setByMultipartFormFile(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
switch value.Kind() {
case reflect.Ptr:
switch value.Interface().(type) {
case *multipart.FileHeader:
value.Set(reflect.ValueOf(files[0]))
return true, nil
}
case reflect.Struct:
switch value.Interface().(type) {
case multipart.FileHeader:
value.Set(reflect.ValueOf(*files[0]))
return true, nil
}
case reflect.Slice:
slice := reflect.MakeSlice(value.Type(), len(files), len(files))
isSetted, err = setArrayOfMultipartFormFiles(slice, field, files)
if err != nil || !isSetted {
return isSetted, err
}
value.Set(slice)
return true, nil
case reflect.Array:
return setArrayOfMultipartFormFiles(value, field, files)
}
return false, errors.New("unsupported field type for multipart.FileHeader")
}
func setArrayOfMultipartFormFiles(value reflect.Value, field reflect.StructField, files []*multipart.FileHeader) (isSetted bool, err error) {
if value.Len() != len(files) {
return false, errors.New("unsupported len of array for []*multipart.FileHeader")
}
for i := range files {
setted, err := setByMultipartFormFile(value.Index(i), field, files[i:i+1])
if err != nil || !setted {
return setted, err
}
}
return true, nil
}

View File

@ -0,0 +1,138 @@
// Copyright 2019 Gin Core Team. 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 (
"bytes"
"io/ioutil"
"mime/multipart"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
func TestFormMultipartBindingBindOneFile(t *testing.T) {
var s struct {
FileValue multipart.FileHeader `form:"file"`
FilePtr *multipart.FileHeader `form:"file"`
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [1]multipart.FileHeader `form:"file"`
ArrayPtrs [1]*multipart.FileHeader `form:"file"`
}
file := testFile{"file", "file1", []byte("hello")}
req := createRequestMultipartFiles(t, file)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assertMultipartFileHeader(t, &s.FileValue, file)
assertMultipartFileHeader(t, s.FilePtr, file)
assert.Len(t, s.SliceValues, 1)
assertMultipartFileHeader(t, &s.SliceValues[0], file)
assert.Len(t, s.SlicePtrs, 1)
assertMultipartFileHeader(t, s.SlicePtrs[0], file)
assertMultipartFileHeader(t, &s.ArrayValues[0], file)
assertMultipartFileHeader(t, s.ArrayPtrs[0], file)
}
func TestFormMultipartBindingBindTwoFiles(t *testing.T) {
var s struct {
SliceValues []multipart.FileHeader `form:"file"`
SlicePtrs []*multipart.FileHeader `form:"file"`
ArrayValues [2]multipart.FileHeader `form:"file"`
ArrayPtrs [2]*multipart.FileHeader `form:"file"`
}
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, &s)
assert.NoError(t, err)
assert.Len(t, s.SliceValues, len(files))
assert.Len(t, s.SlicePtrs, len(files))
assert.Len(t, s.ArrayValues, len(files))
assert.Len(t, s.ArrayPtrs, len(files))
for i, file := range files {
assertMultipartFileHeader(t, &s.SliceValues[i], file)
assertMultipartFileHeader(t, s.SlicePtrs[i], file)
assertMultipartFileHeader(t, &s.ArrayValues[i], file)
assertMultipartFileHeader(t, s.ArrayPtrs[i], file)
}
}
func TestFormMultipartBindingBindError(t *testing.T) {
files := []testFile{
{"file", "file1", []byte("hello")},
{"file", "file2", []byte("world")},
}
for _, tt := range []struct {
name string
s interface{}
}{
{"wrong type", &struct {
Files int `form:"file"`
}{}},
{"wrong array size", &struct {
Files [1]*multipart.FileHeader `form:"file"`
}{}},
{"wrong slice type", &struct {
Files []int `form:"file"`
}{}},
} {
req := createRequestMultipartFiles(t, files...)
err := FormMultipart.Bind(req, tt.s)
assert.Error(t, err)
}
}
type testFile struct {
Fieldname string
Filename string
Content []byte
}
func createRequestMultipartFiles(t *testing.T, files ...testFile) *http.Request {
var body bytes.Buffer
mw := multipart.NewWriter(&body)
for _, file := range files {
fw, err := mw.CreateFormFile(file.Fieldname, file.Filename)
assert.NoError(t, err)
n, err := fw.Write(file.Content)
assert.NoError(t, err)
assert.Equal(t, len(file.Content), n)
}
err := mw.Close()
assert.NoError(t, err)
req, err := http.NewRequest("POST", "/", &body)
assert.NoError(t, err)
req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+mw.Boundary())
return req
}
func assertMultipartFileHeader(t *testing.T, fh *multipart.FileHeader, file testFile) {
assert.Equal(t, file.Filename, fh.Filename)
// assert.Equal(t, int64(len(file.Content)), fh.Size) // fh.Size does not exist on go1.8
fl, err := fh.Open()
assert.NoError(t, err)
body, err := ioutil.ReadAll(fl)
assert.NoError(t, err)
assert.Equal(t, string(file.Content), string(body))
err = fl.Close()
assert.NoError(t, err)
}

View File

@ -83,6 +83,7 @@ func (c *Context) reset() {
c.Errors = c.Errors[0:0] c.Errors = c.Errors[0:0]
c.Accepted = nil c.Accepted = nil
c.queryCache = nil c.queryCache = nil
c.formCache = nil
} }
// 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.
@ -582,6 +583,11 @@ func (c *Context) BindYAML(obj interface{}) error {
return c.MustBindWith(obj, binding.YAML) return c.MustBindWith(obj, binding.YAML)
} }
// BindHeader is a shortcut for c.MustBindWith(obj, binding.Header).
func (c *Context) BindHeader(obj interface{}) error {
return c.MustBindWith(obj, binding.Header)
}
// BindUri binds the passed struct pointer using binding.Uri. // BindUri binds the passed struct pointer using binding.Uri.
// It will abort the request with HTTP 400 if any error occurs. // It will abort the request with HTTP 400 if any error occurs.
func (c *Context) BindUri(obj interface{}) error { func (c *Context) BindUri(obj interface{}) error {
@ -636,6 +642,11 @@ func (c *Context) ShouldBindYAML(obj interface{}) error {
return c.ShouldBindWith(obj, binding.YAML) return c.ShouldBindWith(obj, binding.YAML)
} }
// ShouldBindHeader is a shortcut for c.ShouldBindWith(obj, binding.Header).
func (c *Context) ShouldBindHeader(obj interface{}) error {
return c.ShouldBindWith(obj, binding.Header)
}
// ShouldBindUri binds the passed struct pointer using the specified binding engine. // ShouldBindUri binds the passed struct pointer using the specified binding engine.
func (c *Context) ShouldBindUri(obj interface{}) error { func (c *Context) ShouldBindUri(obj interface{}) error {
m := make(map[string][]string) m := make(map[string][]string)

View File

@ -6,6 +6,7 @@ package gin
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
@ -24,7 +25,6 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/golang/protobuf/proto" "github.com/golang/protobuf/proto"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context"
testdata "github.com/gin-gonic/gin/testdata/protoexample" testdata "github.com/gin-gonic/gin/testdata/protoexample"
) )
@ -1436,6 +1436,28 @@ func TestContextBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.BindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithQuery(t *testing.T) { func TestContextBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -1543,6 +1565,28 @@ func TestContextShouldBindWithXML(t *testing.T) {
assert.Equal(t, 0, w.Body.Len()) assert.Equal(t, 0, w.Body.Len())
} }
func TestContextShouldBindHeader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Request.Header.Add("rate", "8000")
c.Request.Header.Add("domain", "music")
c.Request.Header.Add("limit", "1000")
var testHeader struct {
Rate int `header:"Rate"`
Domain string `header:"Domain"`
Limit int `header:"limit"`
}
assert.NoError(t, c.ShouldBindHeader(&testHeader))
assert.Equal(t, 8000, testHeader.Rate)
assert.Equal(t, "music", testHeader.Domain)
assert.Equal(t, 1000, testHeader.Limit)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithQuery(t *testing.T) { func TestContextShouldBindWithQuery(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)

View File

@ -5,7 +5,6 @@
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"html/template" "html/template"
"runtime" "runtime"
@ -13,7 +12,7 @@ import (
"strings" "strings"
) )
const ginSupportMinGoVer = 8 const ginSupportMinGoVer = 10
// IsDebugging returns true if the framework is running in debug mode. // IsDebugging returns true if the framework is running in debug mode.
// Use SetMode(gin.ReleaseMode) to disable debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode.
@ -38,7 +37,7 @@ func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) {
func debugPrintLoadTemplate(tmpl *template.Template) { func debugPrintLoadTemplate(tmpl *template.Template) {
if IsDebugging() { if IsDebugging() {
var buf bytes.Buffer var buf strings.Builder
for _, tmpl := range tmpl.Templates() { for _, tmpl := range tmpl.Templates() {
buf.WriteString("\t- ") buf.WriteString("\t- ")
buf.WriteString(tmpl.Name()) buf.WriteString(tmpl.Name())
@ -68,7 +67,7 @@ func getMinVer(v string) (uint64, error) {
func debugPrintWARNINGDefault() { func debugPrintWARNINGDefault() {
if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer {
debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. debugPrint(`[WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.
`) `)
} }

View File

@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) {
}) })
m, e := getMinVer(runtime.Version()) m, e := getMinVer(runtime.Version())
if e == nil && m <= ginSupportMinGoVer { if e == nil && m <= ginSupportMinGoVer {
assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.10 or later and Go 1.11 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} else { } else {
assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re)
} }

View File

@ -5,9 +5,9 @@
package gin package gin
import ( import (
"bytes"
"fmt" "fmt"
"reflect" "reflect"
"strings"
"github.com/gin-gonic/gin/internal/json" "github.com/gin-gonic/gin/internal/json"
) )
@ -158,7 +158,7 @@ func (a errorMsgs) String() string {
if len(a) == 0 { if len(a) == 0 {
return "" return ""
} }
var buffer bytes.Buffer var buffer strings.Builder
for i, msg := range a { for i, msg := range a {
fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err) fmt.Fprintf(&buffer, "Error #%02d: %s\n", i+1, msg.Err)
if msg.Meta != nil { if msg.Meta != nil {

1
gin.go
View File

@ -437,7 +437,6 @@ func serveError(c *Context, code int, defaultMessage []byte) {
return return
} }
c.writermem.WriteHeaderNow() c.writermem.WriteHeaderNow()
return
} }
func redirectTrailingSlash(c *Context) { func redirectTrailingSlash(c *Context) {

7
go.mod
View File

@ -3,15 +3,14 @@ module github.com/gin-gonic/gin
go 1.12 go 1.12
require ( require (
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 github.com/gin-contrib/sse v0.1.0
github.com/golang/protobuf v1.3.1 github.com/golang/protobuf v1.3.1
github.com/json-iterator/go v1.1.6 github.com/json-iterator/go v1.1.6
github.com/mattn/go-isatty v0.0.7 github.com/mattn/go-isatty v0.0.9
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/stretchr/testify v1.3.0 github.com/stretchr/testify v1.3.0
github.com/ugorji/go v1.1.4 github.com/ugorji/go/codec v1.1.7
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c
gopkg.in/go-playground/assert.v1 v1.2.1 // indirect gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
gopkg.in/go-playground/validator.v8 v8.18.2 gopkg.in/go-playground/validator.v8 v8.18.2
gopkg.in/yaml.v2 v2.2.2 gopkg.in/yaml.v2 v2.2.2

23
go.sum
View File

@ -1,13 +1,13 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
@ -17,15 +17,12 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=

View File

@ -21,7 +21,9 @@ type Reader struct {
// Render (Reader) writes data with custom ContentType and headers. // Render (Reader) writes data with custom ContentType and headers.
func (r Reader) Render(w http.ResponseWriter) (err error) { func (r Reader) Render(w http.ResponseWriter) (err error) {
r.WriteContentType(w) r.WriteContentType(w)
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10) if r.ContentLength >= 0 {
r.Headers["Content-Length"] = strconv.FormatInt(r.ContentLength, 10)
}
r.writeHeaders(w, r.Headers) r.writeHeaders(w, r.Headers)
_, err = io.Copy(w, r.Reader) _, err = io.Copy(w, r.Reader)
return return

View File

@ -45,7 +45,7 @@ func TestRenderMsgPack(t *testing.T) {
err = codec.NewEncoder(buf, h).Encode(data) err = codec.NewEncoder(buf, h).Encode(data)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, w.Body.String(), string(buf.Bytes())) assert.Equal(t, w.Body.String(), buf.String())
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type")) assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
} }
@ -498,3 +498,26 @@ func TestRenderReader(t *testing.T) {
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition")) assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id")) assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
} }
func TestRenderReaderNoContentLength(t *testing.T) {
w := httptest.NewRecorder()
body := "#!PNG some raw data"
headers := make(map[string]string)
headers["Content-Disposition"] = `attachment; filename="filename.png"`
headers["x-request-id"] = "requestId"
err := (Reader{
ContentLength: -1,
ContentType: "image/png",
Reader: strings.NewReader(body),
Headers: headers,
}).Render(w)
assert.NoError(t, err)
assert.Equal(t, body, w.Body.String())
assert.Equal(t, "image/png", w.Header().Get("Content-Type"))
assert.NotContains(t, "Content-Length", w.Header())
assert.Equal(t, headers["Content-Disposition"], w.Header().Get("Content-Disposition"))
assert.Equal(t, headers["x-request-id"], w.Header().Get("x-request-id"))
}

View File

@ -560,10 +560,15 @@ func TestRouteContextHoldsFullPath(t *testing.T) {
// Test routes // Test routes
routes := []string{ routes := []string{
"/",
"/simple", "/simple",
"/project/:name", "/project/:name",
"/",
"/news/home",
"/news",
"/simple-two/one",
"/simple-two/one-two",
"/project/:name/build/*params", "/project/:name/build/*params",
"/project/:name/bui",
} }
for _, route := range routes { for _, route := range routes {

View File

@ -128,6 +128,8 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n.priority++ n.priority++
numParams := countParams(path) numParams := countParams(path)
parentFullPathIndex := 0
// non-empty tree // non-empty tree
if len(n.path) > 0 || len(n.children) > 0 { if len(n.path) > 0 || len(n.children) > 0 {
walk: walk:
@ -155,7 +157,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
children: n.children, children: n.children,
handlers: n.handlers, handlers: n.handlers,
priority: n.priority - 1, priority: n.priority - 1,
fullPath: fullPath, fullPath: n.fullPath,
} }
// Update maxParams (max of all children) // Update maxParams (max of all children)
@ -171,6 +173,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
n.path = path[:i] n.path = path[:i]
n.handlers = nil n.handlers = nil
n.wildChild = false n.wildChild = false
n.fullPath = fullPath[:parentFullPathIndex+i]
} }
// Make new node a child of this node // Make new node a child of this node
@ -178,6 +181,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
path = path[i:] path = path[i:]
if n.wildChild { if n.wildChild {
parentFullPathIndex += len(n.path)
n = n.children[0] n = n.children[0]
n.priority++ n.priority++
@ -211,6 +215,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
// slash after param // slash after param
if n.nType == param && c == '/' && len(n.children) == 1 { if n.nType == param && c == '/' && len(n.children) == 1 {
parentFullPathIndex += len(n.path)
n = n.children[0] n = n.children[0]
n.priority++ n.priority++
continue walk continue walk
@ -219,6 +224,7 @@ func (n *node) addRoute(path string, handlers HandlersChain) {
// Check if a child with the next path byte exists // Check if a child with the next path byte exists
for i := 0; i < len(n.indices); i++ { for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] { if c == n.indices[i] {
parentFullPathIndex += len(n.path)
i = n.incrementChildPrio(i) i = n.incrementChildPrio(i)
n = n.children[i] n = n.children[i]
continue walk continue walk
@ -369,6 +375,7 @@ func (n *node) insertChild(numParams uint8, path string, fullPath string, handle
// insert remaining path part and handle to the leaf // insert remaining path part and handle to the leaf
n.path = path[offset:] n.path = path[offset:]
n.handlers = handlers n.handlers = handlers
n.fullPath = fullPath
} }
// nodeValue holds return values of (*Node).getValue method // nodeValue holds return values of (*Node).getValue method

View File

@ -146,6 +146,6 @@ func resolveAddress(addr []string) string {
case 1: case 1:
return addr[0] return addr[0]
default: default:
panic("too much parameters") panic("too many parameters")
} }
} }

24
vendor/vendor.json vendored
View File

@ -11,10 +11,12 @@
"versionExact": "v1.1.1" "versionExact": "v1.1.1"
}, },
{ {
"checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", "checksumSHA1": "qlEzrgKgIkh7y0ePm9BNo1cNdXo=",
"path": "github.com/gin-contrib/sse", "path": "github.com/gin-contrib/sse",
"revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae", "revision": "54d8467d122d380a14768b6b4e5cd7ca4755938f",
"revisionTime": "2019-03-01T06:25:29Z" "revisionTime": "2019-06-02T15:02:53Z",
"version": "v0.1",
"versionExact": "v0.1.0"
}, },
{ {
"checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=", "checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=",
@ -81,18 +83,12 @@
"versionExact": "v1.2.2" "versionExact": "v1.2.2"
}, },
{ {
"checksumSHA1": "csplo594qomjp2IZj82y7mTueOw=", "checksumSHA1": "S4ei9eSqVThDio0Jn2sav6yUbvg=",
"path": "github.com/ugorji/go/codec", "path": "github.com/ugorji/go/codec",
"revision": "2adff0894ba3bc2eeb9f9aea45fefd49802e1a13", "revision": "82dbfaf494e3b01d2d481376f11f6a5c8cf9599f",
"revisionTime": "2019-04-08T19:08:48Z", "revisionTime": "2019-07-02T14:15:27Z",
"version": "v1.1", "version": "v1.1",
"versionExact": "v1.1.4" "versionExact": "v1.1.6"
},
{
"checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=",
"path": "golang.org/x/net/context",
"revision": "f4e77d36d62c17c2336347bb2670ddbd02d092b7",
"revisionTime": "2019-05-02T22:26:14Z"
}, },
{ {
"checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=", "checksumSHA1": "2gaep1KNRDNyDA3O+KgPTQsGWvs=",
@ -118,4 +114,4 @@
} }
], ],
"rootPath": "github.com/gin-gonic/gin" "rootPath": "github.com/gin-gonic/gin"
} }