From 652faa23c2eb70de3173f5ee85e05059616afd58 Mon Sep 17 00:00:00 2001 From: Rick <1358735+rickb777@users.noreply.github.com> Date: Thu, 5 Nov 2020 23:34:33 +0000 Subject: [PATCH] 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 --- README.md | 16 ++++++++-- context.go | 6 +++- context_test.go | 58 +++++++++++++++++++++++++++-------- gin.go | 75 ++++++++++++++++++++++++++------------------- ginS/gins.go | 22 +++++++------ gin_test.go | 2 +- logger_test.go | 42 ++++++++++++------------- routergroup.go | 22 +++++++------ routergroup_test.go | 28 ++++++++++++++--- routes_test.go | 14 ++++++++- 10 files changed, 191 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 18b19430..8c2db7e4 100644 --- a/README.md +++ b/README.md @@ -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") } ``` diff --git a/context.go b/context.go index 71fb5937..4d2af07f 100644 --- a/context.go +++ b/context.go @@ -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 diff --git a/context_test.go b/context_test.go index 8e1e3b57..bfc75b14 100644 --- a/context_test.go +++ b/context_test.go @@ -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": ""}) @@ -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": ""}) assert.Equal(t, http.StatusCreated, w.Code) assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\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, "%s %d", "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, "%s %d", "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) diff --git a/gin.go b/gin.go index 1e126179..5a63281b 100644 --- a/gin.go +++ b/gin.go @@ -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) { diff --git a/ginS/gins.go b/ginS/gins.go index 3080fd34..cadae606 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -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...) diff --git a/gin_test.go b/gin_test.go index 11bdd79c..21d5ddd0 100644 --- a/gin_test.go +++ b/gin_test.go @@ -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", diff --git a/logger_test.go b/logger_test.go index 0d40666e..1bcab310 100644 --- a/logger_test.go +++ b/logger_test.go @@ -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") diff --git a/routergroup.go b/routergroup.go index 15d9930d..cb61122e 100644 --- a/routergroup.go +++ b/routergroup.go @@ -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() } diff --git a/routergroup_test.go b/routergroup_test.go index 0e49d65b..e00ce108 100644 --- a/routergroup_test.go +++ b/routergroup_test.go @@ -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() { diff --git a/routes_test.go b/routes_test.go index 11ff71a6..9c5e6f81 100644 --- a/routes_test.go +++ b/routes_test.go @@ -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) {