mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-16 21:32:11 +08:00
Merge branch 'develop' of https://github.com/gin-gonic/gin into AbortWithStatusJSON
This commit is contained in:
commit
111701baf8
144
README.md
144
README.md
@ -102,9 +102,16 @@ BenchmarkZeus_GithubAll | 2000 | 944234 | 300688 | 2648
|
||||
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
|
||||
|
||||
#### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
|
||||
### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -130,7 +137,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Parameters in path
|
||||
### Parameters in path
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -155,7 +162,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Querystring parameters
|
||||
### Querystring parameters
|
||||
```go
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
@ -222,34 +229,66 @@ func main() {
|
||||
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
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
|
||||
router.POST("/upload", func(c *gin.Context) {
|
||||
// single file
|
||||
file, _ := c.FormFile("file")
|
||||
log.Println(file.Filename)
|
||||
|
||||
file, header , err := c.Request.FormFile("upload")
|
||||
filename := header.Filename
|
||||
fmt.Println(header.Filename)
|
||||
out, err := os.Create("./tmp/"+filename+".png")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer out.Close()
|
||||
_, err = io.Copy(out, file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
c.String(http.StatusOK, fmt.Printf("'%s' uploaded!", file.Filename))
|
||||
})
|
||||
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
|
||||
func main() {
|
||||
router := gin.Default()
|
||||
@ -275,7 +314,7 @@ func main() {
|
||||
```
|
||||
|
||||
|
||||
#### Blank Gin without middleware by default
|
||||
### Blank Gin without middleware by default
|
||||
|
||||
Use
|
||||
|
||||
@ -289,7 +328,7 @@ r := gin.Default()
|
||||
```
|
||||
|
||||
|
||||
#### Using middleware
|
||||
### Using middleware
|
||||
```go
|
||||
func main() {
|
||||
// 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).
|
||||
|
||||
@ -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
|
||||
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
|
||||
func main() {
|
||||
@ -453,7 +528,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
####Serving static files
|
||||
### Serving static files
|
||||
|
||||
```go
|
||||
func main() {
|
||||
@ -467,7 +542,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
####HTML rendering
|
||||
### HTML rendering
|
||||
|
||||
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:
|
||||
|
||||
@ -559,7 +637,7 @@ r.GET("/test", func(c *gin.Context) {
|
||||
Both internal and external locations are supported.
|
||||
|
||||
|
||||
#### Custom Middleware
|
||||
### Custom Middleware
|
||||
|
||||
```go
|
||||
func Logger() gin.HandlerFunc {
|
||||
@ -599,7 +677,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Using BasicAuth() middleware
|
||||
### Using BasicAuth() middleware
|
||||
```go
|
||||
// simulate some private data
|
||||
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.
|
||||
|
||||
```go
|
||||
@ -670,7 +748,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
#### Custom HTTP configuration
|
||||
### Custom HTTP configuration
|
||||
|
||||
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?
|
||||
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.
|
||||
- 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.
|
||||
|
||||
|
73
context.go
73
context.go
@ -8,6 +8,7 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@ -16,7 +17,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"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
|
||||
@ -30,7 +31,10 @@ const (
|
||||
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,
|
||||
// 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.
|
||||
// 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 {
|
||||
var cp = *c
|
||||
cp.writermem.ResponseWriter = nil
|
||||
@ -298,7 +302,7 @@ func (c *Context) PostFormArray(key string) []string {
|
||||
func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||
req := c.Request
|
||||
req.ParseForm()
|
||||
req.ParseMultipartForm(32 << 20) // 32 MB
|
||||
req.ParseMultipartForm(defaultMemory)
|
||||
if values := req.PostForm[key]; len(values) > 0 {
|
||||
return values, true
|
||||
}
|
||||
@ -310,6 +314,18 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) {
|
||||
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,
|
||||
// Depending the "Content-Type" header different bindings are used:
|
||||
// "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
|
||||
// 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 {
|
||||
if c.engine.ForwardedByClientIP {
|
||||
clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip"))
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
clientIP = c.requestHeader("X-Forwarded-For")
|
||||
clientIP := c.requestHeader("X-Forwarded-For")
|
||||
if index := strings.IndexByte(clientIP, ','); index >= 0 {
|
||||
clientIP = clientIP[0:index]
|
||||
}
|
||||
@ -354,6 +367,10 @@ func (c *Context) ClientIP() string {
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
clientIP = strings.TrimSpace(c.requestHeader("X-Real-Ip"))
|
||||
if len(clientIP) > 0 {
|
||||
return clientIP
|
||||
}
|
||||
}
|
||||
|
||||
if c.engine.AppEngine {
|
||||
@ -374,6 +391,16 @@ func (c *Context) ContentType() string {
|
||||
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 {
|
||||
if values, _ := c.Request.Header[key]; len(values) > 0 {
|
||||
return values[0]
|
||||
@ -385,6 +412,19 @@ func (c *Context) requestHeader(key string) string {
|
||||
/******** 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) {
|
||||
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) {
|
||||
c.Status(code)
|
||||
|
||||
if !bodyAllowedForStatus(code) {
|
||||
r.WriteContentType(c.Writer)
|
||||
c.Writer.WriteHeaderNow()
|
||||
return
|
||||
}
|
||||
|
||||
if err := r.Render(c.Writer); err != nil {
|
||||
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.
|
||||
// It also sets the Content-Type as "application/json".
|
||||
func (c *Context) JSON(code int, obj interface{}) {
|
||||
c.Status(code)
|
||||
if err := render.WriteJSON(c.Writer, obj); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
c.Render(code, render.JSON{Data: obj})
|
||||
}
|
||||
|
||||
// XML serializes the given struct as XML into the response body.
|
||||
@ -477,8 +521,7 @@ func (c *Context) YAML(code int, obj interface{}) {
|
||||
|
||||
// String writes the given string into the response body.
|
||||
func (c *Context) String(code int, format string, values ...interface{}) {
|
||||
c.Status(code)
|
||||
render.WriteString(c.Writer, format, values)
|
||||
c.Render(code, render.String{Format: format, Data: values})
|
||||
}
|
||||
|
||||
// Redirect returns a HTTP redirect to the specific location.
|
||||
|
209
context_test.go
209
context_test.go
@ -16,9 +16,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/manucorporat/sse"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"golang.org/x/net/context"
|
||||
"gopkg.in/gin-contrib/sse.v0"
|
||||
)
|
||||
|
||||
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) {
|
||||
router := New()
|
||||
c := router.allocateContext()
|
||||
@ -351,6 +382,35 @@ func TestContextGetCookie(t *testing.T) {
|
||||
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
|
||||
// and Content-Type is set to application/json
|
||||
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"))
|
||||
}
|
||||
|
||||
// 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
|
||||
// we change the content-type before
|
||||
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"))
|
||||
}
|
||||
|
||||
// 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
|
||||
// and Content-Type is set to application/json
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// and responds with Content-Type set to text/html
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// and Content-Type is set to application/xml
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// with Content-Type set to text/plain
|
||||
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")
|
||||
}
|
||||
|
||||
// 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
|
||||
// with Content-Type set to text/html
|
||||
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")
|
||||
}
|
||||
|
||||
// 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`
|
||||
// with specified MIME type
|
||||
func TestContextRenderData(t *testing.T) {
|
||||
@ -459,6 +607,18 @@ func TestContextRenderData(t *testing.T) {
|
||||
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) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
@ -631,11 +791,7 @@ func TestContextAbortWithStatus(t *testing.T) {
|
||||
func TestContextAbortWithStatusJSON(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := CreateTestContext(w)
|
||||
<<<<<<< HEAD
|
||||
|
||||
=======
|
||||
|
||||
>>>>>>> d08400d4d4a552c9521b7e5379d79b18a409694e
|
||||
c.index = 4
|
||||
type inboundJsonType struct {
|
||||
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.RemoteAddr = " 40.40.40.40:42123 "
|
||||
|
||||
assert.Equal(t, c.ClientIP(), "10.10.10.10")
|
||||
|
||||
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")
|
||||
assert.Equal(t, "20.20.20.20", c.ClientIP())
|
||||
|
||||
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
|
||||
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")
|
||||
assert.Equal(t, c.ClientIP(), "40.40.40.40")
|
||||
assert.Equal(t, "40.40.40.40", c.ClientIP())
|
||||
|
||||
// no port
|
||||
c.Request.RemoteAddr = "50.50.50.50"
|
||||
assert.Equal(t, c.ClientIP(), "")
|
||||
assert.Equal(t, "", c.ClientIP())
|
||||
}
|
||||
|
||||
func TestContextContentType(t *testing.T) {
|
||||
@ -818,3 +975,25 @@ func TestContextGolangContext(t *testing.T) {
|
||||
assert.Equal(t, c.Value("foo"), "bar")
|
||||
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())
|
||||
}
|
||||
|
39
examples/upload-file/multiple/main.go
Normal file
39
examples/upload-file/multiple/main.go
Normal 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")
|
||||
}
|
17
examples/upload-file/multiple/public/index.html
Normal file
17
examples/upload-file/multiple/public/index.html
Normal 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>
|
34
examples/upload-file/single/main.go
Normal file
34
examples/upload-file/single/main.go
Normal 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")
|
||||
}
|
16
examples/upload-file/single/public/index.html
Normal file
16
examples/upload-file/single/public/index.html
Normal 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
9
gin.go
@ -273,6 +273,15 @@ func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
|
||||
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) {
|
||||
httpMethod := context.Request.Method
|
||||
path := context.Request.URL.Path
|
||||
|
@ -92,7 +92,7 @@ func LoggerWithWriter(out io.Writer, notlogged ...string) HandlerFunc {
|
||||
}
|
||||
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"),
|
||||
statusColor, statusCode, reset,
|
||||
latency,
|
||||
|
@ -10,8 +10,8 @@ import (
|
||||
|
||||
"testing"
|
||||
|
||||
"github.com/manucorporat/sse"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gopkg.in/gin-contrib/sse.v0"
|
||||
)
|
||||
|
||||
func TestMiddlewareGeneralCase(t *testing.T) {
|
||||
|
@ -11,10 +11,13 @@ type Data struct {
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func (r Data) Render(w http.ResponseWriter) error {
|
||||
if len(r.ContentType) > 0 {
|
||||
w.Header()["Content-Type"] = []string{r.ContentType}
|
||||
}
|
||||
w.Write(r.Data)
|
||||
return nil
|
||||
// Render (Data) writes data with custom ContentType
|
||||
func (r Data) Render(w http.ResponseWriter) (err error) {
|
||||
r.WriteContentType(w)
|
||||
_, err = w.Write(r.Data)
|
||||
return
|
||||
}
|
||||
|
||||
func (r Data) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, []string{r.ContentType})
|
||||
}
|
||||
|
@ -58,9 +58,14 @@ func (r HTMLDebug) loadTemplate() *template.Template {
|
||||
}
|
||||
|
||||
func (r HTML) Render(w http.ResponseWriter) error {
|
||||
writeContentType(w, htmlContentType)
|
||||
r.WriteContentType(w)
|
||||
|
||||
if len(r.Name) == 0 {
|
||||
return r.Template.Execute(w, r.Data)
|
||||
}
|
||||
return r.Template.ExecuteTemplate(w, r.Name, r.Data)
|
||||
}
|
||||
|
||||
func (r HTML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, htmlContentType)
|
||||
}
|
||||
|
@ -21,18 +21,15 @@ type (
|
||||
|
||||
var jsonContentType = []string{"application/json; charset=utf-8"}
|
||||
|
||||
func (r JSON) Render(w http.ResponseWriter) error {
|
||||
return WriteJSON(w, r.Data)
|
||||
func (r JSON) Render(w http.ResponseWriter) (err error) {
|
||||
if err = WriteJSON(w, r.Data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r IndentedJSON) Render(w http.ResponseWriter) error {
|
||||
func (r JSON) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, jsonContentType)
|
||||
jsonBytes, err := json.MarshalIndent(r.Data, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.Write(jsonBytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||
@ -44,3 +41,17 @@ func WriteJSON(w http.ResponseWriter, obj interface{}) error {
|
||||
w.Write(jsonBytes)
|
||||
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)
|
||||
}
|
||||
|
@ -22,3 +22,5 @@ func (r Redirect) Render(w http.ResponseWriter) error {
|
||||
http.Redirect(w, r.Request, r.Location, r.Code)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r Redirect) WriteContentType(http.ResponseWriter) {}
|
||||
|
@ -8,6 +8,7 @@ import "net/http"
|
||||
|
||||
type Render interface {
|
||||
Render(http.ResponseWriter) error
|
||||
WriteContentType(w http.ResponseWriter)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -22,9 +22,12 @@ func (r String) Render(w http.ResponseWriter) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r String) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, plainContentType)
|
||||
}
|
||||
|
||||
func WriteString(w http.ResponseWriter, format string, data []interface{}) {
|
||||
writeContentType(w, plainContentType)
|
||||
|
||||
if len(data) > 0 {
|
||||
fmt.Fprintf(w, format, data...)
|
||||
} else {
|
||||
|
@ -16,6 +16,10 @@ type XML struct {
|
||||
var xmlContentType = []string{"application/xml; charset=utf-8"}
|
||||
|
||||
func (r XML) Render(w http.ResponseWriter) error {
|
||||
writeContentType(w, xmlContentType)
|
||||
r.WriteContentType(w)
|
||||
return xml.NewEncoder(w).Encode(r.Data)
|
||||
}
|
||||
|
||||
func (r XML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, xmlContentType)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ type YAML struct {
|
||||
var yamlContentType = []string{"application/x-yaml; charset=utf-8"}
|
||||
|
||||
func (r YAML) Render(w http.ResponseWriter) error {
|
||||
writeContentType(w, yamlContentType)
|
||||
r.WriteContentType(w)
|
||||
|
||||
bytes, err := yaml.Marshal(r.Data)
|
||||
if err != nil {
|
||||
@ -27,3 +27,7 @@ func (r YAML) Render(w http.ResponseWriter) error {
|
||||
w.Write(bytes)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r YAML) WriteContentType(w http.ResponseWriter) {
|
||||
writeContentType(w, yamlContentType)
|
||||
}
|
||||
|
12
vendor/vendor.json
vendored
12
vendor/vendor.json
vendored
@ -21,12 +21,6 @@
|
||||
"revision": "8ee79997227bf9b34611aee7946ae64735e6fd93",
|
||||
"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=",
|
||||
"path": "github.com/manucorporat/stats",
|
||||
@ -66,6 +60,12 @@
|
||||
"revision": "478fcf54317e52ab69f40bb4c7a1520288d7f7ea",
|
||||
"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=",
|
||||
"comment": "v8.18.1",
|
||||
|
Loading…
x
Reference in New Issue
Block a user