From 58c198c3c6910cefa89f5cda136aeedb696f9609 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Mar 2026 10:17:58 -0700 Subject: [PATCH 1/2] Support HEAD requests on all public routes Gin does not automatically handle HEAD for GET routes. Add a getAndHead helper that registers both methods, and use it for all public-facing routes (home, posts, tags, search, sitemap). Fixes curl -I and similar tools returning 404 instead of 200. Co-Authored-By: Claude Opus 4.6 (1M context) --- goblog.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/goblog.go b/goblog.go index 331bc90..79af42b 100644 --- a/goblog.go +++ b/goblog.go @@ -397,8 +397,8 @@ func main() { c.File(filepath.Join("themes", activeTheme, "static", fp)) }) - router.GET("/", goblog.rootHandler) - router.GET("/login", goblog.loginHandler) + getAndHead(router, "/", goblog.rootHandler) + getAndHead(router, "/login", goblog.loginHandler) router.GET("/wizard", goblog._wizard.SaveToken) router.POST("/wizard_db", updateDB) router.POST("/test_db", testDB) @@ -463,16 +463,16 @@ func (g goblog) addRoutes() { //the json API. The json API is tested more easily. Also javascript can //served in the html can be used to create and update posts by directly //working with the json API. - g.router.GET("/index.php", g._blog.Home) - g.router.GET("/posts/:yyyy/:mm/:dd/:slug", g._blog.Post) + getAndHead(g.router, "/index.php", g._blog.Home) + getAndHead(g.router, "/posts/:yyyy/:mm/:dd/:slug", g._blog.Post) // lets posts work with our without the word posts in front - g.router.GET("/:yyyy/:mm/:dd/:slug", g._blog.Post) + getAndHead(g.router, "/:yyyy/:mm/:dd/:slug", g._blog.Post) g.router.GET("/admin/posts/:yyyy/:mm/:dd/:slug", g._admin.Post) - g.router.GET("/tag/*name", g._blog.Tag) + getAndHead(g.router, "/tag/*name", g._blog.Tag) g.router.GET("/logout", g._blog.Logout) - g.router.GET("/search", g._blog.Search) - g.router.GET("/sitemap.xml", g._blog.Sitemap) + getAndHead(g.router, "/search", g._blog.Search) + getAndHead(g.router, "/sitemap.xml", g._blog.Sitemap) // lets old WordPress stuff stored at wp-content/uploads work g.router.Use(static.Serve("/wp-content", static.LocalFile("www", false))) @@ -489,6 +489,13 @@ func (g goblog) addRoutes() { g.router.NoRoute(g._blog.NoRoute) } +// getAndHead registers a handler for both GET and HEAD on the given path. +// HEAD is required by the HTTP spec for any resource that supports GET. +func getAndHead(router *gin.Engine, path string, handler gin.HandlerFunc) { + router.GET(path, handler) + router.HEAD(path, handler) +} + func CORS() gin.HandlerFunc { return func(c *gin.Context) { c.Writer.Header().Set("Access-Control-Allow-Origin", "*") From 1f2859cb56bbb998d81c1989555d0475ccb3d944 Mon Sep 17 00:00:00 2001 From: Jason Ernst Date: Thu, 19 Mar 2026 10:35:40 -0700 Subject: [PATCH 2/2] Address review feedback: sync.Once for routes, IRoutes interface - Replace handlersRegistered bool with sync.Once for thread-safe route registration, preventing potential race when concurrent GET and HEAD requests trigger addRoutes simultaneously - Change goblog methods to pointer receivers so sync.Once state persists - Accept gin.IRoutes instead of *gin.Engine in getAndHead for reuse with router groups Co-Authored-By: Claude Opus 4.6 (1M context) --- goblog.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/goblog.go b/goblog.go index 79af42b..97e560a 100644 --- a/goblog.go +++ b/goblog.go @@ -21,6 +21,7 @@ import ( "log" "net/http" "os" + "sync" "path/filepath" "strings" @@ -42,7 +43,7 @@ type goblog struct { _registry *gplugin.Registry sessionKey string router *gin.Engine - handlersRegistered bool + routesOnce sync.Once } func envFilePresent() bool { @@ -106,7 +107,7 @@ func attemptConnectDb() *gorm.DB { } // depending on if the env file is present or not, we will show the wizard or the main site -func (g goblog) rootHandler(c *gin.Context) { +func (g *goblog) rootHandler(c *gin.Context) { if !envFilePresent() { log.Println("Root handler: No .env file found") c.HTML(http.StatusOK, "wizard_db.html", gin.H{ @@ -186,7 +187,7 @@ func (g goblog) rootHandler(c *gin.Context) { } } -func (g goblog) loginHandler(c *gin.Context) { +func (g *goblog) loginHandler(c *gin.Context) { if !envFilePresent() { log.Println("Root handler: No .env file found") c.HTML(http.StatusOK, "wizard_db.html", gin.H{ @@ -316,7 +317,7 @@ func main() { registry.StartScheduledJobs() } - goblog := goblog{ + goblog := &goblog{ _wizard: &_wizard, _blog: &_blog, _auth: &_auth, @@ -422,12 +423,13 @@ func main() { } } -func (g goblog) addRoutes() { - if g.handlersRegistered { - log.Println("Handlers already registered") - return - } - g.handlersRegistered = true +func (g *goblog) addRoutes() { + g.routesOnce.Do(func() { + g.addRoutesInner() + }) +} + +func (g *goblog) addRoutesInner() { log.Println("Adding main blog routes") //all of this is the json api g.router.MaxMultipartMemory = 50 << 20 @@ -491,7 +493,7 @@ func (g goblog) addRoutes() { // getAndHead registers a handler for both GET and HEAD on the given path. // HEAD is required by the HTTP spec for any resource that supports GET. -func getAndHead(router *gin.Engine, path string, handler gin.HandlerFunc) { +func getAndHead(router gin.IRoutes, path string, handler gin.HandlerFunc) { router.GET(path, handler) router.HEAD(path, handler) }