@@ -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+
14261745func benchmarkEchoRoutes (b * testing.B , routes []testRoute ) {
14271746 e := New ()
14281747 req := httptest .NewRequest (http .MethodGet , "/" , nil )
0 commit comments