diff --git a/echo.go b/echo.go index 4855e8429..f07a6adcc 100644 --- a/echo.go +++ b/echo.go @@ -92,6 +92,9 @@ type Echo struct { // formParseMaxMemory is passed to Context for multipart form parsing (See http.Request.ParseMultipartForm) formParseMaxMemory int64 + + // automatically registers a HEAD request within GET + autoHeadInGet bool } // JSONSerializer is the interface that encodes and decodes JSON to and from interfaces. @@ -330,6 +333,7 @@ func New() *Echo { Binder: &DefaultBinder{}, JSONSerializer: &DefaultJSONSerializer{}, formParseMaxMemory: defaultMemory, + autoHeadInGet: true, } e.serveHTTPFunc = e.serveHTTP @@ -341,6 +345,14 @@ func New() *Echo { return e } +// AutoHeadCancel turns the flag autoHeadInGet to false. +// +// This flag is used to register HEAD request automatically +// everytime a GET request is registered. +func (e *Echo) AutoHeadCancel() { + e.autoHeadInGet = false +} + // NewContext returns a new Context instance. // // Note: both request and response can be left to nil as Echo.ServeHTTP will call c.Reset(req,resp) anyway @@ -437,7 +449,14 @@ func (e *Echo) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo // GET registers a new GET route for a path with matching handler in the router // with optional route-level middleware. Panics on error. +// +// Note: if autoHeadInGet flag is true, it will also register a HEAD request +// to the same path. func (e *Echo) GET(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo { + if e.autoHeadInGet { + _ = e.HEAD(path, h, m...) + } + return e.Add(http.MethodGet, path, h, m...) } @@ -639,7 +658,7 @@ func (e *Echo) Add(method, path string, handler HandlerFunc, middleware ...Middl // Group creates a new router group with prefix and optional group-level middleware. func (e *Echo) Group(prefix string, m ...MiddlewareFunc) (g *Group) { - g = &Group{prefix: prefix, echo: e} + g = &Group{prefix: prefix, echo: e, autoHeadInGet: true} g.Use(m...) return } diff --git a/echo_test.go b/echo_test.go index f26eed8e2..48af01dba 100644 --- a/echo_test.go +++ b/echo_test.go @@ -529,6 +529,16 @@ func TestEchoWrapMiddleware(t *testing.T) { assert.Equal(t, "/:id", actualPattern) } +func TestAutoHeadCancel(t *testing.T) { + e := New() + + assert.Equal(t, true, e.autoHeadInGet) + + e.AutoHeadCancel() + + assert.Equal(t, false, e.autoHeadInGet) +} + func TestEchoConnect(t *testing.T) { e := New() @@ -580,6 +590,24 @@ func TestEchoGet(t *testing.T) { assert.Equal(t, "OK", body) } +func TestEchoAutoHead(t *testing.T) { + e := New() + + assert.Equal(t, true, e.autoHeadInGet) // guarantees the flag is true + ri := e.GET("/", func(c *Context) error { + return c.String(http.StatusTeapot, "OK") + }) + + assert.Equal(t, http.MethodGet, ri.Method) + assert.Equal(t, "/", ri.Path) + assert.Equal(t, http.MethodGet+":/", ri.Name) + assert.Nil(t, ri.Parameters) + + status, body := request(http.MethodHead, "/", e) + assert.Equal(t, http.StatusTeapot, status) + assert.Equal(t, "OK", body) +} + func TestEchoHead(t *testing.T) { e := New() @@ -909,7 +937,7 @@ func TestEchoMethodNotAllowed(t *testing.T) { e.ServeHTTP(rec, req) assert.Equal(t, http.StatusMethodNotAllowed, rec.Code) - assert.Equal(t, "OPTIONS, GET", rec.Header().Get(HeaderAllow)) + assert.Equal(t, "OPTIONS, GET, HEAD", rec.Header().Get(HeaderAllow)) } func TestEcho_OnAddRoute(t *testing.T) { @@ -950,6 +978,7 @@ func TestEcho_OnAddRoute(t *testing.T) { t.Run(tc.name, func(t *testing.T) { e := New() + e.AutoHeadCancel() added := make([]string, 0) cnt := 0 diff --git a/group.go b/group.go index d81cd9163..7a39ba5cb 100644 --- a/group.go +++ b/group.go @@ -12,9 +12,15 @@ import ( // routes that share a common middleware or functionality that should be separate // from the parent echo instance while still inheriting from it. type Group struct { - echo *Echo - prefix string - middleware []MiddlewareFunc + echo *Echo + prefix string + middleware []MiddlewareFunc + autoHeadInGet bool +} + +// AutoHeadCancel implements `Echo#AutoHeadCancel()` for the Group struct. +func (g *Group) AutoHeadCancel() { + g.autoHeadInGet = false } // Use implements `Echo#Use()` for sub-routes within the Group. @@ -35,6 +41,10 @@ func (g *Group) DELETE(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInf // GET implements `Echo#GET()` for sub-routes within the Group. Panics on error. func (g *Group) GET(path string, h HandlerFunc, m ...MiddlewareFunc) RouteInfo { + if g.autoHeadInGet { + _ = g.HEAD(path, h, m...) + } + return g.Add(http.MethodGet, path, h, m...) } @@ -105,6 +115,7 @@ func (g *Group) Group(prefix string, middleware ...MiddlewareFunc) (sg *Group) { m = append(m, g.middleware...) m = append(m, middleware...) sg = g.echo.Group(g.prefix+prefix, m...) + sg.autoHeadInGet = true return } diff --git a/group_test.go b/group_test.go index 7078b6497..bde75b8ae 100644 --- a/group_test.go +++ b/group_test.go @@ -162,6 +162,17 @@ func TestGroupRouteMiddlewareWithMatchAny(t *testing.T) { } +func TestAutoHeadCancelInGroup(t *testing.T) { + e := New() + g := e.Group("/group") + + assert.Equal(t, true, g.autoHeadInGet) + + g.AutoHeadCancel() + + assert.Equal(t, false, g.autoHeadInGet) +} + func TestGroup_CONNECT(t *testing.T) { e := New() @@ -198,6 +209,25 @@ func TestGroup_DELETE(t *testing.T) { assert.Equal(t, `OK`, body) } +func TestGroup_AutoHEAD_in_GET(t *testing.T) { + e := New() + + users := e.Group("/users") + ri := users.GET("/activate", func(c *Context) error { + return c.String(http.StatusTeapot, "OK") + }) + + assert.Equal(t, true, users.autoHeadInGet) + assert.Equal(t, http.MethodGet, ri.Method) + assert.Equal(t, "/users/activate", ri.Path) + assert.Equal(t, http.MethodGet+":/users/activate", ri.Name) + assert.Nil(t, ri.Parameters) + + status, body := request(http.MethodHead, "/users/activate", e) + assert.Equal(t, http.StatusTeapot, status) + assert.Equal(t, "OK", body) +} + func TestGroup_HEAD(t *testing.T) { e := New()