Merge branch 'develop' of https://github.com/gin-gonic/gin into AbortWithStatusJSON

This commit is contained in:
PABLO JOSE GONZALEZ 2017-02-13 15:23:02 +01:00
commit 111701baf8
19 changed files with 538 additions and 90 deletions

144
README.md
View File

@ -102,9 +102,16 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
import "net/http" import "net/http"
``` ```
4. (Optional) Use latest changes (note: they may be broken and/or unstable):
   ```sh
$ GIN_PATH=$GOPATH/src/gopkg.in/gin-gonic/gin.v1
$ git -C $GIN_PATH checkout develop
$ git -C $GIN_PATH pull origin develop
   ```
## API Examples ## API Examples
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
```go ```go
func main() { func main() {
@ -130,7 +137,7 @@ func main() {
} }
``` ```
#### Parameters in path ### Parameters in path
```go ```go
func main() { func main() {
@ -155,7 +162,7 @@ func main() {
} }
``` ```
#### Querystring parameters ### Querystring parameters
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -222,34 +229,66 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great id: 1234; page: 1; name: manu; message: this_is_great
``` ```
### Another example: upload file ### Upload files
References issue [#548](https://github.com/gin-gonic/gin/issues/548). #### Single file
References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](examples/upload-file/single).
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
router.POST("/upload", func(c *gin.Context) { router.POST("/upload", func(c *gin.Context) {
// single file
file, _ := c.FormFile("file")
log.Println(file.Filename)
file, header , err := c.Request.FormFile("upload") c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
filename := header.Filename
fmt.Println(header.Filename)
out, err := os.Create("./tmp/"+filename+".png")
if err != nil {
log.Fatal(err)
}
defer out.Close()
_, err = io.Copy(out, file)
if err != nil {
log.Fatal(err)
}
}) })
router.Run(":8080") router.Run(":8080")
} }
``` ```
#### Grouping routes How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "file=@/Users/appleboy/test.zip" \
-H "Content-Type: multipart/form-data"
```
#### Multiple files
See the detail [example code](examples/upload-file/multiple).
```go
func main() {
router := gin.Default()
router.POST("/upload", func(c *gin.Context) {
// Multipart form
form, _ := c.MultipartForm()
files := form.File["upload[]"]
for _, file := range files {
log.Println(file.Filename)
}
c.String(http.StatusOK, fmt.Printf("%d files uploaded!", len(files)))
})
router.Run(":8080")
}
```
How to `curl`:
```bash
curl -X POST http://localhost:8080/upload \
-F "upload[]=@/Users/appleboy/test1.zip" \
-F "upload[]=@/Users/appleboy/test2.zip" \
-H "Content-Type: multipart/form-data"
```
### Grouping routes
```go ```go
func main() { func main() {
router := gin.Default() router := gin.Default()
@ -275,7 +314,7 @@ func main() {
``` ```
#### Blank Gin without middleware by default ### Blank Gin without middleware by default
Use Use
@ -289,7 +328,7 @@ r := gin.Default()
``` ```
#### Using middleware ### Using middleware
```go ```go
func main() { func main() {
// Creates a router without any middleware by default // Creates a router without any middleware by default
@ -324,7 +363,7 @@ func main() {
} }
``` ```
#### Model binding and validation ### Model binding and validation
To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz). To bind a request body into a type, use model binding. We currently support binding of JSON, XML and standard form values (foo=bar&boo=baz).
@ -374,8 +413,44 @@ func main() {
} }
``` ```
### Bind Query String
See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292).
```go
package main
import "log"
import "github.com/gin-gonic/gin"
type Person struct {
Name string `form:"name"`
Address string `form:"address"`
}
func main() {
route := gin.Default()
route.GET("/testing", startPage)
route.Run(":8085")
}
func startPage(c *gin.Context) {
var person Person
// If `GET`, only `Form` binding engine (`query`) used.
// If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`).
// See more at https://github.com/gin-gonic/gin/blob/develop/binding/binding.go#L45
if c.Bind(&person) == nil {
log.Println(person.Name)
log.Println(person.Address)
}
c.String(200, "Success")
}
```
### Multipart/Urlencoded binding
###Multipart/Urlencoded binding
```go ```go
package main package main
@ -414,7 +489,7 @@ $ curl -v --form user=user --form password=password http://localhost:8080/login
``` ```
#### XML, JSON and YAML rendering ### XML, JSON and YAML rendering
```go ```go
func main() { func main() {
@ -453,7 +528,7 @@ func main() {
} }
``` ```
####Serving static files ### Serving static files
```go ```go
func main() { func main() {
@ -467,7 +542,7 @@ func main() {
} }
``` ```
####HTML rendering ### HTML rendering
Using LoadHTMLGlob() or LoadHTMLFiles() Using LoadHTMLGlob() or LoadHTMLFiles()
@ -546,8 +621,11 @@ func main() {
} }
``` ```
### Multitemplate
#### Redirects Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`.
### Redirects
Issuing a HTTP redirect is easy: Issuing a HTTP redirect is easy:
@ -559,7 +637,7 @@ r.GET("/test", func(c *gin.Context) {
Both internal and external locations are supported. Both internal and external locations are supported.
#### Custom Middleware ### Custom Middleware
```go ```go
func Logger() gin.HandlerFunc { func Logger() gin.HandlerFunc {
@ -599,7 +677,7 @@ func main() {
} }
``` ```
#### Using BasicAuth() middleware ### Using BasicAuth() middleware
```go ```go
// simulate some private data // simulate some private data
var secrets = gin.H{ var secrets = gin.H{
@ -638,7 +716,7 @@ func main() {
``` ```
#### Goroutines inside a middleware ### Goroutines inside a middleware
When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. When starting inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy.
```go ```go
@ -670,7 +748,7 @@ func main() {
} }
``` ```
#### Custom HTTP configuration ### Custom HTTP configuration
Use `http.ListenAndServe()` directly, like this: Use `http.ListenAndServe()` directly, like this:
@ -697,7 +775,7 @@ func main() {
} }
``` ```
#### Graceful restart or stop ### Graceful restart or stop
Do you want to graceful restart or stop your web server? Do you want to graceful restart or stop your web server?
There are some ways this can be done. There are some ways this can be done.
@ -728,7 +806,7 @@ An alternative to endless:
- You should add/modify tests to cover your proposed code changes. - You should add/modify tests to cover your proposed code changes.
- If your pull request contains a new feature, please document it on the README. - If your pull request contains a new feature, please document it on the README.
## Example ## Users
Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework.

View File

@ -8,6 +8,7 @@ import (
"errors" "errors"
"io" "io"
"math" "math"
"mime/multipart"
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
@ -16,7 +17,7 @@ import (
"github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render" "github.com/gin-gonic/gin/render"
"github.com/manucorporat/sse" "gopkg.in/gin-contrib/sse.v0"
) )
// Content-Type MIME of the most common data formats // Content-Type MIME of the most common data formats
@ -30,7 +31,10 @@ const (
MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm
) )
const abortIndex int8 = math.MaxInt8 / 2 const (
defaultMemory = 32 << 20 // 32 MB
abortIndex int8 = math.MaxInt8 / 2
)
// Context is the most important part of gin. It allows us to pass variables between middleware, // Context is the most important part of gin. It allows us to pass variables between middleware,
// manage the flow, validate the JSON of a request and render a JSON response for example. // manage the flow, validate the JSON of a request and render a JSON response for example.
@ -64,7 +68,7 @@ func (c *Context) reset() {
} }
// Copy returns a copy of the current context that can be safely used outside the request's scope. // Copy returns a copy of the current context that can be safely used outside the request's scope.
// This have to be used then the context has to be passed to a goroutine. // This has to be used when the context has to be passed to a goroutine.
func (c *Context) Copy() *Context { func (c *Context) Copy() *Context {
var cp = *c var cp = *c
cp.writermem.ResponseWriter = nil cp.writermem.ResponseWriter = nil
@ -298,7 +302,7 @@ func (c *Context) PostFormArray(key string) []string {
func (c *Context) GetPostFormArray(key string) ([]string, bool) { func (c *Context) GetPostFormArray(key string) ([]string, bool) {
req := c.Request req := c.Request
req.ParseForm() req.ParseForm()
req.ParseMultipartForm(32 << 20) // 32 MB req.ParseMultipartForm(defaultMemory)
if values := req.PostForm[key]; len(values) > 0 { if values := req.PostForm[key]; len(values) > 0 {
return values, true return values, true
} }
@ -310,6 +314,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
return []string{}, false return []string{}, false
} }
// FormFile returns the first file for the provided form key.
func (c *Context) FormFile(name string) (*multipart.FileHeader, error) {
_, fh, err := c.Request.FormFile(name)
return fh, err
}
// MultipartForm is the parsed multipart form, including file uploads.
func (c *Context) MultipartForm() (*multipart.Form, error) {
err := c.Request.ParseMultipartForm(defaultMemory)
return c.Request.MultipartForm, err
}
// Bind checks the Content-Type to select a binding engine automatically, // Bind checks the Content-Type to select a binding engine automatically,
// Depending the "Content-Type" header different bindings are used: // Depending the "Content-Type" header different bindings are used:
// "application/json" --> JSON binding // "application/json" --> JSON binding
@ -340,13 +356,10 @@ func (c *Context) BindWith(obj interface{}, b binding.Binding) error {
// ClientIP implements a best effort algorithm to return the real client IP, it parses // ClientIP implements a best effort algorithm to return the real client IP, it parses
// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. // X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy.
// Use X-Forwarded-For before X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP.
func (c *Context) ClientIP() string { func (c *Context) ClientIP() string {
if c.engine.ForwardedByClientIP { if c.engine.ForwardedByClientIP {
clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) clientIP := c.requestHeader("X-Forwarded-For")
if len(clientIP) > 0 {
return clientIP
}
clientIP = c.requestHeader("X-Forwarded-For")
if index := strings.IndexByte(clientIP, ','); index >= 0 { if index := strings.IndexByte(clientIP, ','); index >= 0 {
clientIP = clientIP[0:index] clientIP = clientIP[0:index]
} }
@ -354,6 +367,10 @@ func (c *Context) ClientIP() string {
if len(clientIP) > 0 { if len(clientIP) > 0 {
return clientIP return clientIP
} }
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
if len(clientIP) > 0 {
return clientIP
}
} }
if c.engine.AppEngine { if c.engine.AppEngine {
@ -374,6 +391,16 @@ func (c *Context) ContentType() string {
return filterFlags(c.requestHeader("Content-Type")) return filterFlags(c.requestHeader("Content-Type"))
} }
// IsWebsocket returns true if the request headers indicate that a websocket
// handshake is being initiated by the client.
func (c *Context) IsWebsocket() bool {
if strings.Contains(strings.ToLower(c.requestHeader("Connection")), "upgrade") &&
strings.ToLower(c.requestHeader("Upgrade")) == "websocket" {
return true
}
return false
}
func (c *Context) requestHeader(key string) string { func (c *Context) requestHeader(key string) string {
if values, _ := c.Request.Header[key]; len(values) > 0 { if values, _ := c.Request.Header[key]; len(values) > 0 {
return values[0] return values[0]
@ -385,6 +412,19 @@ func (c *Context) requestHeader(key string) string {
/******** RESPONSE RENDERING ********/ /******** RESPONSE RENDERING ********/
/************************************/ /************************************/
// bodyAllowedForStatus is a copy of http.bodyAllowedForStatus non-exported function
func bodyAllowedForStatus(status int) bool {
switch {
case status >= 100 && status <= 199:
return false
case status == 204:
return false
case status == 304:
return false
}
return true
}
func (c *Context) Status(code int) { func (c *Context) Status(code int) {
c.writermem.WriteHeader(code) c.writermem.WriteHeader(code)
} }
@ -434,6 +474,13 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) { func (c *Context) Render(code int, r render.Render) {
c.Status(code) c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil { if err := r.Render(c.Writer); err != nil {
panic(err) panic(err)
} }
@ -458,10 +505,7 @@ func (c *Context) IndentedJSON(code int, obj interface{}) {
// JSON serializes the given struct as JSON into the response body. // JSON serializes the given struct as JSON into the response body.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) JSON(code int, obj interface{}) { func (c *Context) JSON(code int, obj interface{}) {
c.Status(code) c.Render(code, render.JSON{Data: obj})
if err := render.WriteJSON(c.Writer, obj); err != nil {
panic(err)
}
} }
// XML serializes the given struct as XML into the response body. // XML serializes the given struct as XML into the response body.
@ -477,8 +521,7 @@ func (c *Context) YAML(code int, obj interface{}) {
// String writes the given string into the response body. // String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...interface{}) { func (c *Context) String(code int, format string, values ...interface{}) {
c.Status(code) c.Render(code, render.String{Format: format, Data: values})
render.WriteString(c.Writer, format, values)
} }
// Redirect returns a HTTP redirect to the specific location. // Redirect returns a HTTP redirect to the specific location.

View File

@ -16,9 +16,9 @@ import (
"testing" "testing"
"time" "time"
"github.com/manucorporat/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/net/context" "golang.org/x/net/context"
"gopkg.in/gin-contrib/sse.v0"
) )
var _ context.Context = &Context{} var _ context.Context = &Context{}
@ -54,6 +54,37 @@ func must(err error) {
} }
} }
func TestContextFormFile(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
w, err := mw.CreateFormFile("file", "test")
if assert.NoError(t, err) {
w.Write([]byte("test"))
}
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.FormFile("file")
if assert.NoError(t, err) {
assert.Equal(t, "test", f.Filename)
}
}
func TestContextMultipartForm(t *testing.T) {
buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf)
mw.WriteField("foo", "bar")
mw.Close()
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "/", buf)
c.Request.Header.Set("Content-Type", mw.FormDataContentType())
f, err := c.MultipartForm()
if assert.NoError(t, err) {
assert.NotNil(t, f)
}
}
func TestContextReset(t *testing.T) { func TestContextReset(t *testing.T) {
router := New() router := New()
c := router.allocateContext() c := router.allocateContext()
@ -351,6 +382,35 @@ func TestContextGetCookie(t *testing.T) {
assert.Equal(t, cookie, "gin") assert.Equal(t, cookie, "gin")
} }
func TestContextBodyAllowedForStatus(t *testing.T) {
assert.Equal(t, false, bodyAllowedForStatus(102))
assert.Equal(t, false, bodyAllowedForStatus(204))
assert.Equal(t, false, bodyAllowedForStatus(304))
assert.Equal(t, true, bodyAllowedForStatus(500))
}
type TestPanicRender struct {
}
func (*TestPanicRender) Render(http.ResponseWriter) error {
return errors.New("TestPanicRender")
}
func (*TestPanicRender) WriteContentType(http.ResponseWriter) {}
func TestContextRenderPanicIfErr(t *testing.T) {
defer func() {
r := recover()
assert.Equal(t, "TestPanicRender", fmt.Sprint(r))
}()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Render(http.StatusOK, &TestPanicRender{})
assert.Fail(t, "Panic not detected")
}
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderJSON(t *testing.T) { func TestContextRenderJSON(t *testing.T) {
@ -364,6 +424,18 @@ func TestContextRenderJSON(t *testing.T) {
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no JSON is rendered if code is 204
func TestContextRenderNoContentJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, "application/json; charset=utf-8", w.HeaderMap.Get("Content-Type"))
}
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// we change the content-type before // we change the content-type before
func TestContextRenderAPIJSON(t *testing.T) { func TestContextRenderAPIJSON(t *testing.T) {
@ -378,6 +450,19 @@ func TestContextRenderAPIJSON(t *testing.T) {
assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type")) assert.Equal(t, "application/vnd.api+json", w.HeaderMap.Get("Content-Type"))
} }
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentAPIJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json")
}
// Tests that the response is serialized as JSON // Tests that the response is serialized as JSON
// and Content-Type is set to application/json // and Content-Type is set to application/json
func TestContextRenderIndentedJSON(t *testing.T) { func TestContextRenderIndentedJSON(t *testing.T) {
@ -391,6 +476,18 @@ func TestContextRenderIndentedJSON(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
} }
// Tests that no Custom JSON is rendered if code is 204
func TestContextRenderNoContentIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.IndentedJSON(204, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8")
}
// Tests that the response executes the templates // Tests that the response executes the templates
// and responds with Content-Type set to text/html // and responds with Content-Type set to text/html
func TestContextRenderHTML(t *testing.T) { func TestContextRenderHTML(t *testing.T) {
@ -406,6 +503,20 @@ func TestContextRenderHTML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
} }
// Tests that no HTML is rendered if code is 204
func TestContextRenderNoContentHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
c.HTML(204, "t", H{"name": "alexandernyquist"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextXML tests that the response is serialized as XML // TestContextXML tests that the response is serialized as XML
// and Content-Type is set to application/xml // and Content-Type is set to application/xml
func TestContextRenderXML(t *testing.T) { func TestContextRenderXML(t *testing.T) {
@ -419,6 +530,18 @@ func TestContextRenderXML(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
} }
// Tests that no XML is rendered if code is 204
func TestContextRenderNoContentXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.XML(204, H{"foo": "bar"})
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8")
}
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
// with Content-Type set to text/plain // with Content-Type set to text/plain
func TestContextRenderString(t *testing.T) { func TestContextRenderString(t *testing.T) {
@ -432,6 +555,18 @@ func TestContextRenderString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
} }
// Tests that no String is rendered if code is 204
func TestContextRenderNoContentString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.String(204, "test %s %d", "string", 2)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8")
}
// TestContextString tests that the response is returned // TestContextString tests that the response is returned
// with Content-Type set to text/html // with Content-Type set to text/html
func TestContextRenderHTMLString(t *testing.T) { func TestContextRenderHTMLString(t *testing.T) {
@ -446,6 +581,19 @@ func TestContextRenderHTMLString(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
} }
// Tests that no HTML String is rendered if code is 204
func TestContextRenderNoContentHTMLString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(204, "<html>%s %d</html>", "string", 3)
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8")
}
// TestContextData tests that the response can be written from `bytesting` // TestContextData tests that the response can be written from `bytesting`
// with specified MIME type // with specified MIME type
func TestContextRenderData(t *testing.T) { func TestContextRenderData(t *testing.T) {
@ -459,6 +607,18 @@ func TestContextRenderData(t *testing.T) {
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
} }
// Tests that no Custom Data is rendered if code is 204
func TestContextRenderNoContentData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Data(204, "text/csv", []byte(`foo,bar`))
assert.Equal(t, 204, w.Code)
assert.Equal(t, "", w.Body.String())
assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv")
}
func TestContextRenderSSE(t *testing.T) { func TestContextRenderSSE(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
@ -631,11 +791,7 @@ func TestContextAbortWithStatus(t *testing.T) {
func TestContextAbortWithStatusJSON(t *testing.T) { func TestContextAbortWithStatusJSON(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
c, _ := CreateTestContext(w) c, _ := CreateTestContext(w)
<<<<<<< HEAD
=======
>>>>>>> d08400d4d4a552c9521b7e5379d79b18a409694e
c.index = 4 c.index = 4
type inboundJsonType struct { type inboundJsonType struct {
Foo string `json:"foo"` Foo string `json:"foo"`
@ -722,24 +878,25 @@ func TestContextClientIP(t *testing.T) {
c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50") c.Request.Header.Set("X-Appengine-Remote-Addr", "50.50.50.50")
c.Request.RemoteAddr = " 40.40.40.40:42123 " c.Request.RemoteAddr = " 40.40.40.40:42123 "
assert.Equal(t, c.ClientIP(), "10.10.10.10") assert.Equal(t, "20.20.20.20", c.ClientIP())
c.Request.Header.Del("X-Real-IP")
assert.Equal(t, c.ClientIP(), "20.20.20.20")
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, c.ClientIP(), "30.30.30.30")
c.Request.Header.Del("X-Forwarded-For") c.Request.Header.Del("X-Forwarded-For")
assert.Equal(t, "10.10.10.10", c.ClientIP())
c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ")
assert.Equal(t, "30.30.30.30", c.ClientIP())
c.Request.Header.Del("X-Forwarded-For")
c.Request.Header.Del("X-Real-IP")
c.engine.AppEngine = true c.engine.AppEngine = true
assert.Equal(t, c.ClientIP(), "50.50.50.50") assert.Equal(t, "50.50.50.50", c.ClientIP())
c.Request.Header.Del("X-Appengine-Remote-Addr") c.Request.Header.Del("X-Appengine-Remote-Addr")
assert.Equal(t, c.ClientIP(), "40.40.40.40") assert.Equal(t, "40.40.40.40", c.ClientIP())
// no port // no port
c.Request.RemoteAddr = "50.50.50.50" c.Request.RemoteAddr = "50.50.50.50"
assert.Equal(t, c.ClientIP(), "") assert.Equal(t, "", c.ClientIP())
} }
func TestContextContentType(t *testing.T) { func TestContextContentType(t *testing.T) {
@ -818,3 +975,25 @@ func TestContextGolangContext(t *testing.T) {
assert.Equal(t, c.Value("foo"), "bar") assert.Equal(t, c.Value("foo"), "bar")
assert.Nil(t, c.Value(1)) assert.Nil(t, c.Value(1))
} }
func TestWebsocketsRequired(t *testing.T) {
// Example request from spec: https://tools.ietf.org/html/rfc6455#section-1.2
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
c.Request.Header.Set("Upgrade", "websocket")
c.Request.Header.Set("Connection", "Upgrade")
c.Request.Header.Set("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ==")
c.Request.Header.Set("Origin", "http://example.com")
c.Request.Header.Set("Sec-WebSocket-Protocol", "chat, superchat")
c.Request.Header.Set("Sec-WebSocket-Version", "13")
assert.True(t, c.IsWebsocket())
// Normal request, no websocket required.
c, _ = CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/chat", nil)
c.Request.Header.Set("Host", "server.example.com")
assert.False(t, c.IsWebsocket())
}

View File

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

View File

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

View File

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

View File

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

9
gin.go
View File

@ -273,6 +273,15 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
engine.pool.Put(c) engine.pool.Put(c)
} }
// Re-enter a context that has been rewritten.
// This can be done by setting c.Request.Path to your new target.
// Disclaimer: You can loop yourself to death with this, use wisely.
func (engine *Engine) HandleContext(c *Context) {
c.reset()
engine.handleHTTPRequest(c)
engine.pool.Put(c)
}
func (engine *Engine) handleHTTPRequest(context *Context) { func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method httpMethod := context.Request.Method
path := context.Request.URL.Path path := context.Request.URL.Path

View File

@ -92,7 +92,7 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
} }
comment := c.Errors.ByType(ErrorTypePrivate).String() comment := c.Errors.ByType(ErrorTypePrivate).String()
fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %15s |%s %s %-7s %s\n%s",
end.Format("2006/01/02 - 15:04:05"), end.Format("2006/01/02 - 15:04:05"),
statusColor, statusCode, reset, statusColor, statusCode, reset,
latency, latency,

View File

@ -10,8 +10,8 @@ import (
"testing" "testing"
"github.com/manucorporat/sse"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gopkg.in/gin-contrib/sse.v0"
) )
func TestMiddlewareGeneralCase(t *testing.T) { func TestMiddlewareGeneralCase(t *testing.T) {

View File

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

View File

@ -58,9 +58,14 @@ func (r HTMLDebug) loadTemplate() *template.Template {
} }
func (r HTML) Render(w http.ResponseWriter) error { func (r HTML) Render(w http.ResponseWriter) error {
writeContentType(w, htmlContentType) r.WriteContentType(w)
if len(r.Name) == 0 { if len(r.Name) == 0 {
return r.Template.Execute(w, r.Data) return r.Template.Execute(w, r.Data)
} }
return r.Template.ExecuteTemplate(w, r.Name, r.Data) return r.Template.ExecuteTemplate(w, r.Name, r.Data)
} }
func (r HTML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, htmlContentType)
}

View File

@ -21,18 +21,15 @@ type (
var jsonContentType = []string{"application/json; charset=utf-8"} var jsonContentType = []string{"application/json; charset=utf-8"}
func (r JSON) Render(w http.ResponseWriter) error { func (r JSON) Render(w http.ResponseWriter) (err error) {
return WriteJSON(w, r.Data) if err = WriteJSON(w, r.Data); err != nil {
panic(err)
}
return
} }
func (r IndentedJSON) Render(w http.ResponseWriter) error { func (r JSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType) writeContentType(w, jsonContentType)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
} }
func WriteJSON(w http.ResponseWriter, obj interface{}) error { func WriteJSON(w http.ResponseWriter, obj interface{}) error {
@ -44,3 +41,17 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error {
w.Write(jsonBytes) w.Write(jsonBytes)
return nil return nil
} }
func (r IndentedJSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
if err != nil {
return err
}
w.Write(jsonBytes)
return nil
}
func (r IndentedJSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, jsonContentType)
}

View File

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

View File

@ -8,6 +8,7 @@ import "net/http"
type Render interface { type Render interface {
Render(http.ResponseWriter) error Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
} }
var ( var (

View File

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

View File

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

View File

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

12
vendor/vendor.json vendored
View File

@ -21,12 +21,6 @@
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93", "revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
"revisionTime": "2016-11-17T03:31:26Z" "revisionTime": "2016-11-17T03:31:26Z"
}, },
{
"checksumSHA1": "b0T0Hzd+zYk+OCDTFMps+jwa/nY=",
"path": "github.com/manucorporat/sse",
"revision": "ee05b128a739a0fb76c7ebd3ae4810c1de808d6d",
"revisionTime": "2016-01-26T18:01:36Z"
},
{ {
"checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=", "checksumSHA1": "9if9IBLsxkarJ804NPWAzgskIAk=",
"path": "github.com/manucorporat/stats", "path": "github.com/manucorporat/stats",
@ -66,6 +60,12 @@
"revision": "478fcf54317e52ab69f40bb4c7a1520288d7f7ea", "revision": "478fcf54317e52ab69f40bb4c7a1520288d7f7ea",
"revisionTime": "2016-12-05T15:46:50Z" "revisionTime": "2016-12-05T15:46:50Z"
}, },
{
"checksumSHA1": "pyAPYrymvmZl0M/Mr4yfjOQjA8I=",
"path": "gopkg.in/gin-contrib/sse.v0",
"revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae",
"revisionTime": "2017-01-09T09:34:21Z"
},
{ {
"checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=", "checksumSHA1": "39V1idWER42Lmcmg2Uy40wMzOlo=",
"comment": "v8.18.1", "comment": "v8.18.1",