Skip to content

Commit c97f73c

Browse files
atharvaSharma17aldas
authored andcommitted
AutoHandleHEAD enables automatic handling of HTTP HEAD requests by falling back to the corresponding GET route
1 parent 030f1b3 commit c97f73c

4 files changed

Lines changed: 774 additions & 15 deletions

File tree

echo_test.go

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,6 +1423,325 @@ func TestDefaultHTTPErrorHandler_CommitedResponse(t *testing.T) {
14231423
assert.Equal(t, http.StatusOK, resp.Code)
14241424
}
14251425

1426+
func TestRouterAutoHandleHEADFullHTTPHandlerFlow(t *testing.T) {
1427+
tests := []struct {
1428+
name string
1429+
givenAutoHandleHEAD bool
1430+
whenMethod string
1431+
expectBody string
1432+
expectCode int
1433+
expectContentLength string
1434+
}{
1435+
{
1436+
name: "AutoHandleHEAD disabled - HEAD returns 405",
1437+
givenAutoHandleHEAD: false,
1438+
whenMethod: http.MethodHead,
1439+
expectCode: http.StatusMethodNotAllowed,
1440+
expectBody: "",
1441+
},
1442+
{
1443+
name: "AutoHandleHEAD enabled - HEAD returns 200 with Content-Length",
1444+
givenAutoHandleHEAD: true,
1445+
whenMethod: http.MethodHead,
1446+
expectCode: http.StatusOK,
1447+
expectBody: "",
1448+
expectContentLength: "4",
1449+
},
1450+
{
1451+
name: "GET request works normally with AutoHandleHEAD enabled",
1452+
givenAutoHandleHEAD: true,
1453+
whenMethod: http.MethodGet,
1454+
expectCode: http.StatusOK,
1455+
expectBody: "test",
1456+
},
1457+
}
1458+
1459+
for _, tc := range tests {
1460+
t.Run(tc.name, func(t *testing.T) {
1461+
e := NewWithConfig(Config{
1462+
Router: NewRouter(RouterConfig{
1463+
AutoHandleHEAD: tc.givenAutoHandleHEAD,
1464+
}),
1465+
})
1466+
1467+
e.GET("/hello", func(c *Context) error {
1468+
return c.String(http.StatusOK, "test")
1469+
})
1470+
1471+
req := httptest.NewRequest(tc.whenMethod, "/hello", nil)
1472+
rec := httptest.NewRecorder()
1473+
1474+
e.ServeHTTP(rec, req)
1475+
1476+
assert.Equal(t, tc.expectCode, rec.Code)
1477+
assert.Equal(t, tc.expectContentLength, rec.Header().Get(HeaderContentLength))
1478+
assert.Equal(t, tc.expectBody, rec.Body.String())
1479+
})
1480+
}
1481+
}
1482+
1483+
func TestAutoHeadExplicitHeadTakesPrecedence(t *testing.T) {
1484+
e := NewWithConfig(Config{
1485+
Router: NewRouter(RouterConfig{
1486+
AutoHandleHEAD: true,
1487+
}),
1488+
})
1489+
1490+
// Register explicit HEAD route FIRST with custom behavior
1491+
e.HEAD("/api/users", func(c *Context) error {
1492+
c.Response().Header().Set("X-Custom-Header", "explicit-head")
1493+
return c.NoContent(http.StatusTeapot)
1494+
})
1495+
1496+
e.GET("/api/users", func(c *Context) error {
1497+
return c.JSON(http.StatusNotFound, map[string]string{"name": "John"})
1498+
})
1499+
1500+
req := httptest.NewRequest(http.MethodHead, "/api/users", nil)
1501+
rec := httptest.NewRecorder()
1502+
e.ServeHTTP(rec, req)
1503+
1504+
assert.Equal(t, http.StatusTeapot, rec.Code)
1505+
assert.Equal(t, "explicit-head", rec.Header().Get("X-Custom-Header"))
1506+
assert.Equal(t, "", rec.Body.String())
1507+
}
1508+
1509+
func TestRouterAutoHandleHEAD_PathParams(t *testing.T) {
1510+
e := NewWithConfig(Config{
1511+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1512+
})
1513+
1514+
e.GET("/users/:id", func(c *Context) error {
1515+
return c.String(http.StatusOK, "id="+c.Param("id"))
1516+
})
1517+
1518+
req := httptest.NewRequest(http.MethodHead, "/users/42", nil)
1519+
rec := httptest.NewRecorder()
1520+
e.ServeHTTP(rec, req)
1521+
1522+
assert.Equal(t, http.StatusOK, rec.Code)
1523+
assert.Equal(t, fmt.Sprintf("%d", len("id=42")), rec.Header().Get(HeaderContentLength))
1524+
assert.Equal(t, "", rec.Body.String())
1525+
}
1526+
1527+
func TestRouterAutoHandleHEAD_MultipleWriteCalls(t *testing.T) {
1528+
e := NewWithConfig(Config{
1529+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1530+
})
1531+
1532+
e.GET("/multi", func(c *Context) error {
1533+
c.Response().WriteHeader(http.StatusOK)
1534+
c.Response().Write([]byte("foo")) // 3
1535+
c.Response().Write([]byte("bar")) // 3
1536+
return nil
1537+
})
1538+
1539+
req := httptest.NewRequest(http.MethodHead, "/multi", nil)
1540+
rec := httptest.NewRecorder()
1541+
e.ServeHTTP(rec, req)
1542+
1543+
assert.Equal(t, http.StatusOK, rec.Code)
1544+
assert.Equal(t, "6", rec.Header().Get(HeaderContentLength))
1545+
assert.Equal(t, "", rec.Body.String())
1546+
}
1547+
1548+
func TestRouterAutoHandleHEAD_ExplicitContentLength(t *testing.T) {
1549+
e := NewWithConfig(Config{
1550+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1551+
})
1552+
1553+
e.GET("/explicit", func(c *Context) error {
1554+
c.Response().Header().Set(HeaderContentLength, "1000")
1555+
return c.String(http.StatusOK, "short")
1556+
})
1557+
1558+
req := httptest.NewRequest(http.MethodHead, "/explicit", nil)
1559+
rec := httptest.NewRecorder()
1560+
e.ServeHTTP(rec, req)
1561+
1562+
assert.Equal(t, "1000", rec.Header().Get(HeaderContentLength))
1563+
}
1564+
1565+
func TestRouterAutoHandleHEAD_TransferEncoding(t *testing.T) {
1566+
e := NewWithConfig(Config{
1567+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1568+
})
1569+
1570+
e.GET("/chunked", func(c *Context) error {
1571+
c.Response().Header().Set("Transfer-Encoding", "chunked")
1572+
return c.String(http.StatusOK, "data")
1573+
})
1574+
1575+
req := httptest.NewRequest(http.MethodHead, "/chunked", nil)
1576+
rec := httptest.NewRecorder()
1577+
e.ServeHTTP(rec, req)
1578+
1579+
assert.Equal(t, "", rec.Header().Get(HeaderContentLength))
1580+
}
1581+
1582+
func TestRouterAutoHandleHEAD_204Response(t *testing.T) {
1583+
e := NewWithConfig(Config{
1584+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1585+
})
1586+
1587+
e.GET("/nocontent", func(c *Context) error {
1588+
return c.NoContent(http.StatusNoContent)
1589+
})
1590+
1591+
req := httptest.NewRequest(http.MethodHead, "/nocontent", nil)
1592+
rec := httptest.NewRecorder()
1593+
e.ServeHTTP(rec, req)
1594+
1595+
assert.Equal(t, http.StatusNoContent, rec.Code)
1596+
assert.Equal(t, "", rec.Header().Get(HeaderContentLength))
1597+
}
1598+
1599+
func TestRouterAutoHandleHEAD_304Response(t *testing.T) {
1600+
e := NewWithConfig(Config{
1601+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1602+
})
1603+
1604+
e.GET("/notmodified", func(c *Context) error {
1605+
return c.NoContent(http.StatusNotModified)
1606+
})
1607+
1608+
req := httptest.NewRequest(http.MethodHead, "/notmodified", nil)
1609+
rec := httptest.NewRecorder()
1610+
e.ServeHTTP(rec, req)
1611+
1612+
assert.Equal(t, http.StatusNotModified, rec.Code)
1613+
assert.Equal(t, "", rec.Header().Get(HeaderContentLength))
1614+
}
1615+
1616+
func TestRouterAutoHandleHEAD_CustomResponseHeaders(t *testing.T) {
1617+
e := NewWithConfig(Config{
1618+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1619+
})
1620+
1621+
e.GET("/headers", func(c *Context) error {
1622+
c.Response().Header().Set("X-Foo", "bar")
1623+
return c.String(http.StatusOK, "body")
1624+
})
1625+
1626+
req := httptest.NewRequest(http.MethodHead, "/headers", nil)
1627+
rec := httptest.NewRecorder()
1628+
e.ServeHTTP(rec, req)
1629+
1630+
assert.Equal(t, "bar", rec.Header().Get("X-Foo"))
1631+
assert.Equal(t, "", rec.Body.String())
1632+
}
1633+
1634+
func TestRouterAutoHandleHEAD_ContentType(t *testing.T) {
1635+
e := NewWithConfig(Config{
1636+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1637+
})
1638+
1639+
e.GET("/json", func(c *Context) error {
1640+
return c.JSON(http.StatusOK, map[string]string{"key": "value"})
1641+
})
1642+
1643+
req := httptest.NewRequest(http.MethodHead, "/json", nil)
1644+
rec := httptest.NewRecorder()
1645+
e.ServeHTTP(rec, req)
1646+
1647+
assert.Equal(t, http.StatusOK, rec.Code)
1648+
assert.Contains(t, rec.Header().Get(HeaderContentType), MIMEApplicationJSON)
1649+
assert.Equal(t, "", rec.Body.String())
1650+
}
1651+
1652+
func TestRouterAutoHandleHEAD_WithMiddleware(t *testing.T) {
1653+
e := NewWithConfig(Config{
1654+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1655+
})
1656+
e.Use(func(next HandlerFunc) HandlerFunc {
1657+
return func(c *Context) error {
1658+
c.Response().Header().Set("X-Middleware", "ran")
1659+
return next(c)
1660+
}
1661+
})
1662+
1663+
e.GET("/mw", func(c *Context) error {
1664+
return c.String(http.StatusOK, "body")
1665+
})
1666+
1667+
req := httptest.NewRequest(http.MethodHead, "/mw", nil)
1668+
rec := httptest.NewRecorder()
1669+
e.ServeHTTP(rec, req)
1670+
1671+
assert.Equal(t, "ran", rec.Header().Get("X-Middleware"))
1672+
assert.Equal(t, "", rec.Body.String())
1673+
}
1674+
1675+
func TestRouterAutoHandleHEAD_HandlerError_ErrorHandlerRuns(t *testing.T) {
1676+
e := NewWithConfig(Config{
1677+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1678+
})
1679+
1680+
e.GET("/err", func(c *Context) error {
1681+
return ErrBadRequest
1682+
})
1683+
1684+
req := httptest.NewRequest(http.MethodHead, "/err", nil)
1685+
rec := httptest.NewRecorder()
1686+
e.ServeHTTP(rec, req)
1687+
1688+
assert.Equal(t, http.StatusBadRequest, rec.Code)
1689+
assert.Equal(t, "", rec.Body.String())
1690+
}
1691+
1692+
func TestRouterAutoHandleHEAD_NoGetRoute_Returns405(t *testing.T) {
1693+
e := NewWithConfig(Config{
1694+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1695+
})
1696+
1697+
e.POST("/post-only", func(c *Context) error {
1698+
return c.String(http.StatusOK, "ok")
1699+
})
1700+
1701+
req := httptest.NewRequest(http.MethodHead, "/post-only", nil)
1702+
rec := httptest.NewRecorder()
1703+
e.ServeHTTP(rec, req)
1704+
1705+
assert.Equal(t, http.StatusMethodNotAllowed, rec.Code)
1706+
}
1707+
1708+
func TestRouterAutoHandleHEAD_AnyRoute_NotWrapped(t *testing.T) {
1709+
e := NewWithConfig(Config{
1710+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1711+
})
1712+
1713+
e.Any("/resource", func(c *Context) error {
1714+
return c.String(http.StatusOK, "any-body")
1715+
})
1716+
1717+
req := httptest.NewRequest(http.MethodHead, "/resource", nil)
1718+
rec := httptest.NewRecorder()
1719+
e.ServeHTTP(rec, req)
1720+
1721+
assert.Equal(t, http.StatusOK, rec.Code)
1722+
assert.Equal(t, "any-body", rec.Body.String())
1723+
// Content-Length must NOT be injected by wrapHeadHandler for RouteAny
1724+
assert.Equal(t, "", rec.Header().Get(HeaderContentLength))
1725+
}
1726+
1727+
func TestRouterAutoHandleHEAD_CatchAllRoute(t *testing.T) {
1728+
e := NewWithConfig(Config{
1729+
Router: NewRouter(RouterConfig{AutoHandleHEAD: true}),
1730+
})
1731+
1732+
e.GET("/*", func(c *Context) error {
1733+
return c.String(http.StatusOK, "wildcard")
1734+
})
1735+
1736+
req := httptest.NewRequest(http.MethodHead, "/anything/here", nil)
1737+
rec := httptest.NewRecorder()
1738+
e.ServeHTTP(rec, req)
1739+
1740+
assert.Equal(t, http.StatusOK, rec.Code)
1741+
assert.Equal(t, fmt.Sprintf("%d", len("wildcard")), rec.Header().Get(HeaderContentLength))
1742+
assert.Equal(t, "", rec.Body.String())
1743+
}
1744+
14261745
func benchmarkEchoRoutes(b *testing.B, routes []testRoute) {
14271746
e := New()
14281747
req := httptest.NewRequest(http.MethodGet, "/", nil)

0 commit comments

Comments
 (0)