refactored Engine.handleHTTPRequest so that two passes are made if the request is HEAD and there isn't a matching HEAD handler

added more documentation

re-ordered GET and HEAD methods to be adjacent

rendering is now suppressed for HEAD request, which means that wasteful processing is avoided
This commit is contained in:
Rick 2020-11-05 23:34:33 +00:00
parent 65ed60ed13
commit 652faa23c2
10 changed files with 191 additions and 94 deletions

View File

@ -196,6 +196,8 @@ You can find a number of ready-to-run examples at [Gin examples repository](http
### Using GET, POST, PUT, PATCH, DELETE and OPTIONS
For each HTTP method, there is a router method to set up handlers on specific paths.
```go
func main() {
// Creates a gin router with default middleware:
@ -217,8 +219,13 @@ func main() {
}
```
Note that every GET handler will also receive its equivalent HEAD requests automatically, so
although you can define HEAD handlers explicitly, you don't really need to.
### Parameters in path
Path parameters take the form `:param` and accept any string separated the adjacent slash(es).
```go
func main() {
router := gin.Default()
@ -247,7 +254,10 @@ func main() {
}
```
### Querystring parameters
### Query string parameters
The query string follows the `?` in the URL. The parameters are accessed using Gin `Context` methods
`Query` and `DefaultQuery`.
```go
func main() {
@ -315,7 +325,7 @@ func main() {
id: 1234; page: 1; name: manu; message: this_is_great
```
### Map as querystring or postform parameters
### Map as query string or post form parameters
```
POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1
@ -547,7 +557,7 @@ func main() {
c.String(200, "pong")
})
   router.Run(":8080")
router.Run(":8080")
}
```

View File

@ -856,7 +856,11 @@ func (c *Context) Cookie(name string) (string, error) {
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
if !bodyAllowedForStatus(code) {
// Rendering is suppressed for 1xx, 204 and 304 and all HEAD requests
// This avoids wasteful response processing, even though the standard
// net/http response processing would discard the response entity in
// (some of) these cases.
if !bodyAllowedForStatus(code) || c.Request.Method == http.MethodHead {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return

View File

@ -389,7 +389,7 @@ func TestContextHandler(t *testing.T) {
func TestContextQuery(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10&id=", nil)
c.Request, _ = http.NewRequest("GET", "/?foo=bar&page=10&id=", nil)
value, ok := c.GetQuery("foo")
assert.True(t, ok)
@ -675,6 +675,7 @@ func TestContextRenderPanicIfErr(t *testing.T) {
}()
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Render(http.StatusOK, &TestPanicRender{})
@ -687,6 +688,7 @@ func TestContextRenderPanicIfErr(t *testing.T) {
func TestContextRenderJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://example.com/", nil)
c.JSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
@ -740,6 +742,7 @@ func TestContextRenderNoContentJSON(t *testing.T) {
func TestContextRenderAPIJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "http://example.com/", nil)
c.Header("Content-Type", "application/vnd.api+json")
c.JSON(http.StatusCreated, H{"foo": "bar"})
@ -767,6 +770,7 @@ func TestContextRenderNoContentAPIJSON(t *testing.T) {
func TestContextRenderIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.IndentedJSON(http.StatusCreated, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
@ -779,6 +783,7 @@ func TestContextRenderIndentedJSON(t *testing.T) {
func TestContextRenderNoContentIndentedJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.IndentedJSON(http.StatusNoContent, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}})
@ -792,6 +797,7 @@ func TestContextRenderNoContentIndentedJSON(t *testing.T) {
func TestContextRenderSecureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
router.SecureJsonPrefix("&&&START&&&")
c.SecureJSON(http.StatusCreated, []string{"foo", "bar"})
@ -805,6 +811,7 @@ func TestContextRenderSecureJSON(t *testing.T) {
func TestContextRenderNoContentSecureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.SecureJSON(http.StatusNoContent, []string{"foo", "bar"})
@ -816,6 +823,7 @@ func TestContextRenderNoContentSecureJSON(t *testing.T) {
func TestContextRenderNoContentAsciiJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.AsciiJSON(http.StatusNoContent, []string{"lang", "Go语言"})
@ -830,6 +838,8 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) {
func TestContextRenderPureJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": "<b>"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"<b>\"}\n", w.Body.String())
@ -841,6 +851,7 @@ func TestContextRenderPureJSON(t *testing.T) {
func TestContextRenderHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
@ -855,6 +866,7 @@ func TestContextRenderHTML(t *testing.T) {
func TestContextRenderHTML2(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
// print debug warning log when Engine.trees > 0
router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}})
@ -880,6 +892,7 @@ func TestContextRenderHTML2(t *testing.T) {
func TestContextRenderNoContentHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
@ -895,6 +908,7 @@ func TestContextRenderNoContentHTML(t *testing.T) {
func TestContextRenderXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.XML(http.StatusCreated, H{"foo": "bar"})
@ -907,6 +921,7 @@ func TestContextRenderXML(t *testing.T) {
func TestContextRenderNoContentXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.XML(http.StatusNoContent, H{"foo": "bar"})
@ -920,6 +935,7 @@ func TestContextRenderNoContentXML(t *testing.T) {
func TestContextRenderString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.String(http.StatusCreated, "test %s %d", "string", 2)
@ -932,6 +948,7 @@ func TestContextRenderString(t *testing.T) {
func TestContextRenderNoContentString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.String(http.StatusNoContent, "test %s %d", "string", 2)
@ -945,6 +962,7 @@ func TestContextRenderNoContentString(t *testing.T) {
func TestContextRenderHTMLString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusCreated, "<html>%s %d</html>", "string", 3)
@ -958,6 +976,7 @@ func TestContextRenderHTMLString(t *testing.T) {
func TestContextRenderNoContentHTMLString(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Header("Content-Type", "text/html; charset=utf-8")
c.String(http.StatusNoContent, "<html>%s %d</html>", "string", 3)
@ -972,6 +991,7 @@ func TestContextRenderNoContentHTMLString(t *testing.T) {
func TestContextRenderData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Data(http.StatusCreated, "text/csv", []byte(`foo,bar`))
@ -984,6 +1004,7 @@ func TestContextRenderData(t *testing.T) {
func TestContextRenderNoContentData(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Data(http.StatusNoContent, "text/csv", []byte(`foo,bar`))
@ -995,6 +1016,7 @@ func TestContextRenderNoContentData(t *testing.T) {
func TestContextRenderSSE(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.SSEvent("float", 1.5)
c.Render(-1, sse.Event{
@ -1012,6 +1034,7 @@ func TestContextRenderSSE(t *testing.T) {
func TestContextRenderFile(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.File("./gin.go")
@ -1024,6 +1047,7 @@ func TestContextRenderFile(t *testing.T) {
func TestContextRenderFileFromFS(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Request, _ = http.NewRequest("GET", "/some/path", nil)
c.FileFromFS("./gin.go", Dir(".", false))
@ -1037,6 +1061,7 @@ func TestContextRenderFileFromFS(t *testing.T) {
func TestContextRenderAttachment(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
newFilename := "new_filename.go"
c.Request, _ = http.NewRequest("GET", "/", nil)
@ -1052,6 +1077,7 @@ func TestContextRenderAttachment(t *testing.T) {
func TestContextRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.YAML(http.StatusCreated, H{"foo": "bar"})
@ -1066,6 +1092,7 @@ func TestContextRenderYAML(t *testing.T) {
func TestContextRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
reps := []int64{int64(1), int64(2)}
label := "test"
@ -1086,6 +1113,7 @@ func TestContextRenderProtoBuf(t *testing.T) {
func TestContextHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Header("Content-Type", "text/plain")
c.Header("X-Custom", "value")
@ -1118,6 +1146,7 @@ func TestContextRenderRedirectWithRelativePath(t *testing.T) {
func TestContextRenderRedirectWithAbsolutePath(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Redirect(http.StatusFound, "http://google.com")
@ -1131,7 +1160,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Redirect(http.StatusCreated, "/resource")
c.Writer.WriteHeaderNow()
@ -1141,7 +1170,7 @@ func TestContextRenderRedirectWith201(t *testing.T) {
func TestContextRenderRedirectAll(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "http://example.com", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
assert.Panics(t, func() { c.Redirect(http.StatusOK, "/resource") })
assert.Panics(t, func() { c.Redirect(http.StatusAccepted, "/resource") })
assert.Panics(t, func() { c.Redirect(299, "/resource") })
@ -1153,7 +1182,7 @@ func TestContextRenderRedirectAll(t *testing.T) {
func TestContextNegotiationWithJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEJSON, MIMEXML, MIMEYAML},
@ -1168,7 +1197,7 @@ func TestContextNegotiationWithJSON(t *testing.T) {
func TestContextNegotiationWithXML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEXML, MIMEJSON, MIMEYAML},
@ -1183,7 +1212,7 @@ func TestContextNegotiationWithXML(t *testing.T) {
func TestContextNegotiationWithHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
templ := template.Must(template.New("t").Parse(`Hello {{.name}}`))
router.SetHTMLTemplate(templ)
@ -1201,7 +1230,7 @@ func TestContextNegotiationWithHTML(t *testing.T) {
func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEPOSTForm},
@ -1214,7 +1243,7 @@ func TestContextNegotiationNotSupport(t *testing.T) {
func TestContextNegotiationFormat(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("POST", "", nil)
c.Request, _ = http.NewRequest("POST", "/", nil)
assert.Panics(t, func() { c.NegotiateFormat() })
assert.Equal(t, MIMEJSON, c.NegotiateFormat(MIMEJSON, MIMEXML))
@ -1305,6 +1334,7 @@ type testJSONAbortMsg struct {
func TestContextAbortWithStatusJSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.index = 4
in := new(testJSONAbortMsg)
@ -1365,6 +1395,7 @@ func TestContextError(t *testing.T) {
func TestContextTypedError(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Request, _ = http.NewRequest("GET", "/", nil)
c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) // nolint: errcheck
c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) // nolint: errcheck
@ -1380,6 +1411,7 @@ func TestContextTypedError(t *testing.T) {
func TestContextAbortWithError(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
c.AbortWithError(http.StatusUnauthorized, errors.New("bad input")).SetMeta("some input") // nolint: errcheck
@ -1537,7 +1569,7 @@ func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
Foo string `json:"foo"`
@ -1670,7 +1702,7 @@ func TestContextBadAutoShouldBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}"))
c.Request.Header.Add("Content-Type", MIMEJSON)
var obj struct {
Foo string `json:"foo"`
@ -1734,7 +1766,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(
"POST", "http://example.com", bytes.NewBufferString(tt.bodyA),
"POST", "/", bytes.NewBufferString(tt.bodyA),
)
// When it binds to typeA and typeB, it finds the body is
// not typeB but typeA.
@ -1752,7 +1784,7 @@ func TestContextShouldBindBodyWith(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(
"POST", "http://example.com", bytes.NewBufferString(tt.bodyB),
"POST", "/", bytes.NewBufferString(tt.bodyB),
)
objA := typeA{}
assert.Error(t, c.ShouldBindBodyWith(&objA, tt.bindingA))
@ -1825,6 +1857,7 @@ func TestContextGetRawData(t *testing.T) {
func TestContextRenderDataFromReader(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
body := "#!PNG some raw data"
reader := strings.NewReader(body)
@ -1844,6 +1877,7 @@ func TestContextRenderDataFromReader(t *testing.T) {
func TestContextRenderDataFromReaderNoHeaders(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest("GET", "/", nil)
body := "#!PNG some raw data"
reader := strings.NewReader(body)

75
gin.go
View File

@ -390,7 +390,6 @@ func (engine *Engine) HandleContext(c *Context) {
}
func (engine *Engine) handleHTTPRequest(c *Context) {
httpMethod := c.Request.Method
rPath := c.Request.URL.Path
unescape := false
if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 {
@ -402,40 +401,20 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
rPath = cleanPath(rPath)
}
// Find root of the tree for the given HTTP method
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method != httpMethod {
continue
}
root := t[i].root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return
}
}
break
if engine.handleHTTPRequestForMethod(c, c.Request.Method, rPath, unescape) {
return
}
// For HEAD requests, reaching here means no HEAD handler had been set up, so we retry the
// equivalent GET handler as if this had been a GET request. As expected for HEAD requests,
// the response content will of course be empty (see net/http for implementation details).
if c.Request.Method == http.MethodHead && engine.handleHTTPRequestForMethod(c, http.MethodGet, rPath, unescape) {
return
}
if engine.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
if tree.method == c.Request.Method {
continue
}
if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil {
@ -449,6 +428,40 @@ func (engine *Engine) handleHTTPRequest(c *Context) {
serveError(c, http.StatusNotFound, default404Body)
}
func (engine *Engine) handleHTTPRequestForMethod(c *Context, httpMethod, rPath string, unescape bool) bool {
// Find root of the tree for the given HTTP method
for _, t := range engine.trees {
if t.method != httpMethod {
continue
}
root := t.root
// Find route in tree
value := root.getValue(rPath, c.params, unescape)
if value.params != nil {
c.Params = *value.params
}
if value.handlers != nil {
c.handlers = value.handlers
c.fullPath = value.fullPath
c.Next()
c.writermem.WriteHeaderNow()
return true
}
if httpMethod != "CONNECT" && rPath != "/" {
if value.tsr && engine.RedirectTrailingSlash {
redirectTrailingSlash(c)
return true
}
if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) {
return true
}
}
break
}
return false // probably 404 or 406
}
var mimePlain = []string{MIMEPlain}
func serveError(c *Context, code int, defaultMessage []byte) {

View File

@ -58,16 +58,23 @@ func Handle(httpMethod, relativePath string, handlers ...gin.HandlerFunc) gin.IR
return engine().Handle(httpMethod, relativePath, handlers...)
}
// GET is a shortcut for router.Handle("GET", path, handle)
// HEAD requests will also be handled automatically by this handler.
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().GET(relativePath, handlers...)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
// This is rarely needed because every GET handler will also handle HEAD requests.
func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...)
}
// POST is a shortcut for router.Handle("POST", path, handle)
func POST(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().POST(relativePath, handlers...)
}
// GET is a shortcut for router.Handle("GET", path, handle)
func GET(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().GET(relativePath, handlers...)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle)
func DELETE(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().DELETE(relativePath, handlers...)
@ -88,11 +95,6 @@ func OPTIONS(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().OPTIONS(relativePath, handlers...)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle)
func HEAD(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().HEAD(relativePath, handlers...)
}
// Any is a wrapper for Engine.Any.
func Any(relativePath string, handlers ...gin.HandlerFunc) gin.IRoutes {
return engine().Any(relativePath, handlers...)

View File

@ -450,7 +450,7 @@ func TestListOfRoutes(t *testing.T) {
list := router.Routes()
assert.Len(t, list, 7)
assert.Len(t, list, 6)
assertRoutePresent(t, list, RouteInfo{
Method: "GET",
Path: "/favicon.ico",

View File

@ -23,58 +23,58 @@ func TestLogger(t *testing.T) {
buffer := new(bytes.Buffer)
router := New()
router.Use(LoggerWithWriter(buffer))
router.GET("/example", func(c *Context) {})
router.POST("/example", func(c *Context) {})
router.PUT("/example", func(c *Context) {})
router.DELETE("/example", func(c *Context) {})
router.PATCH("/example", func(c *Context) {})
router.HEAD("/example", func(c *Context) {})
router.OPTIONS("/example", func(c *Context) {})
router.GET("/get", func(c *Context) {})
router.POST("/post", func(c *Context) {})
router.PUT("/put", func(c *Context) {})
router.DELETE("/delete", func(c *Context) {})
router.PATCH("/patch", func(c *Context) {})
router.HEAD("/head", func(c *Context) {})
router.OPTIONS("/options", func(c *Context) {})
performRequest(router, "GET", "/example?a=100")
performRequest(router, "GET", "/get?a=100")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "GET")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/get")
assert.Contains(t, buffer.String(), "a=100")
// I wrote these first (extending the above) but then realized they are more
// like integration tests because they test the whole logging process rather
// than individual functions. Im not sure where these should go.
buffer.Reset()
performRequest(router, "POST", "/example")
performRequest(router, "POST", "/post")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "POST")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/post")
buffer.Reset()
performRequest(router, "PUT", "/example")
performRequest(router, "PUT", "/put")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PUT")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/put")
buffer.Reset()
performRequest(router, "DELETE", "/example")
performRequest(router, "DELETE", "/delete")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "DELETE")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/delete")
buffer.Reset()
performRequest(router, "PATCH", "/example")
performRequest(router, "PATCH", "/patch")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "PATCH")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/patch")
buffer.Reset()
performRequest(router, "HEAD", "/example")
performRequest(router, "HEAD", "/head")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "HEAD")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/head")
buffer.Reset()
performRequest(router, "OPTIONS", "/example")
performRequest(router, "OPTIONS", "/options")
assert.Contains(t, buffer.String(), "200")
assert.Contains(t, buffer.String(), "OPTIONS")
assert.Contains(t, buffer.String(), "/example")
assert.Contains(t, buffer.String(), "/options")
buffer.Reset()
performRequest(router, "GET", "/notfound")

View File

@ -80,7 +80,7 @@ func (group *RouterGroup) handle(httpMethod, relativePath string, handlers Handl
// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes.
// See the example code in GitHub.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// For GET, HEAD, POST, PUT, PATCH, DELETE and OPTIONS requests the respective shortcut
// functions can be used.
//
// This function is intended for bulk loading and to allow the usage of less
@ -98,11 +98,18 @@ func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRo
return group.handle(http.MethodPost, relativePath, handlers)
}
// GET is a shortcut for router.Handle("GET", path, handle).
// GET is a shortcut for router.Handle("GET", path, handle). The equivalent
// HEAD requests will also be handled automatically by this handler.
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodGet, relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle). This is rarely needed
// because every GET handler will also handle HEAD requests.
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodHead, relativePath, handlers)
}
// DELETE is a shortcut for router.Handle("DELETE", path, handle).
func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodDelete, relativePath, handlers)
@ -123,11 +130,6 @@ func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc)
return group.handle(http.MethodOptions, relativePath, handlers)
}
// HEAD is a shortcut for router.Handle("HEAD", path, handle).
func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) IRoutes {
return group.handle(http.MethodHead, relativePath, handlers)
}
// Any registers a route that matches all the HTTP methods.
// GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE.
func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) IRoutes {
@ -152,8 +154,9 @@ func (group *RouterGroup) StaticFile(relativePath, filepath string) IRoutes {
handler := func(c *Context) {
c.File(filepath)
}
// Register GET (and HEAD) handler
group.GET(relativePath, handler)
group.HEAD(relativePath, handler)
return group.returnObj()
}
@ -176,9 +179,8 @@ func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) IRou
handler := group.createStaticHandler(relativePath, fs)
urlPattern := path.Join(relativePath, "/*filepath")
// Register GET and HEAD handlers
// Register GET (and HEAD) handler
group.GET(urlPattern, handler)
group.HEAD(urlPattern, handler)
return group.returnObj()
}

View File

@ -38,7 +38,6 @@ func TestRouterGroupBasicHandle(t *testing.T) {
performRequestInGroup(t, http.MethodPut)
performRequestInGroup(t, http.MethodPatch)
performRequestInGroup(t, http.MethodDelete)
performRequestInGroup(t, http.MethodHead)
performRequestInGroup(t, http.MethodOptions)
}
@ -70,9 +69,6 @@ func performRequestInGroup(t *testing.T, method string) {
case http.MethodDelete:
v1.DELETE("/test", handler)
login.DELETE("/test", handler)
case http.MethodHead:
v1.HEAD("/test", handler)
login.HEAD("/test", handler)
case http.MethodOptions:
v1.OPTIONS("/test", handler)
login.OPTIONS("/test", handler)
@ -89,6 +85,30 @@ func performRequestInGroup(t *testing.T, method string) {
assert.Equal(t, "the method was "+method+" and index 1", w.Body.String())
}
func TestRouterGroupBasicHandleHEAD(t *testing.T) {
router := New()
v1 := router.Group("v1", func(c *Context) {})
assert.Equal(t, "/v1", v1.BasePath())
login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {})
assert.Equal(t, "/v1/login/", login.BasePath())
handler := func(c *Context) {
c.String(http.StatusBadRequest, "the method was %s and index %d", c.Request.Method, c.index)
}
v1.HEAD("/test", handler)
login.HEAD("/test", handler)
w := performRequest(router, http.MethodHead, "/v1/login/test")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "", w.Body.String())
w = performRequest(router, http.MethodHead, "/v1/test")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, "", w.Body.String())
}
func TestRouterGroupInvalidStatic(t *testing.T) {
router := New()
assert.Panics(t, func() {

View File

@ -368,6 +368,18 @@ func TestRouterMiddlewareAndStatic(t *testing.T) {
assert.Equal(t, "Gin Framework", w.Header().Get("x-GIN"))
}
func TestRouteHeadUsesGetHandlerByDefault(t *testing.T) {
router := New()
router.GET("/path", func(c *Context) {
c.String(http.StatusOK, "responseText")
})
w := performRequest(router, http.MethodHead, "/path")
assert.Equal(t, http.StatusOK, w.Code)
// The response entity is blank because response rendering is suppressed for HEAD requests.
// Note that the Go net/http handlers would discard the entity for HEAD requests anyway.
assert.Equal(t, "", w.Body.String())
}
func TestRouteNotAllowedEnabled(t *testing.T) {
router := New()
router.HandleMethodNotAllowed = true
@ -494,7 +506,7 @@ func TestRouterStaticFSNotFound(t *testing.T) {
assert.Equal(t, "non existent", w.Body.String())
w = performRequest(router, http.MethodHead, "/nonexistent")
assert.Equal(t, "non existent", w.Body.String())
assert.Equal(t, "", w.Body.String())
}
func TestRouterStaticFSFileNotFound(t *testing.T) {