When the timeout middleware is registered globally with engine.Use(...), any request that does not match a registered route is answered with 200 OK and an empty body instead of gin's default 404 page not found.
Gin's own access logger still reports 404, so the internal status and the status actually sent on the wire disagree, which makes this very confusing to debug behind a reverse proxy (our ingress logged 200 while the app logged 404).
Registering the middleware per-route (as in _example/example01) does not trigger the bug, because route-level handlers never run for unmatched routes. The examples therefore never exercise this path.
How to reproduce
package main
import (
"net/http"
"time"
"github.com/gin-contrib/timeout"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.New()
r.Use(gin.Logger(), timeout.New(timeout.WithTimeout(30*time.Second)))
r.GET("/hello", func(c *gin.Context) {
c.String(http.StatusOK, "world")
})
_ = http.ListenAndServe(":8080", r)
}
$ curl -si http://127.0.0.1:8080/no/such/route
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 0
Server log for the same request (note the 404):
[GIN] 2026/07/30 - 17:14:57 | 404 | 57.607µs | 127.0.0.1 | GET "/no/such/route"
When the timeout middleware is registered globally with
engine.Use(...), any request that does not match a registered route is answered with200 OKand an empty body instead of gin's default404 page not found.Gin's own access logger still reports
404, so the internal status and the status actually sent on the wire disagree, which makes this very confusing to debug behind a reverse proxy (our ingress logged 200 while the app logged 404).Registering the middleware per-route (as in
_example/example01) does not trigger the bug, because route-level handlers never run for unmatched routes. The examples therefore never exercise this path.How to reproduce
Server log for the same request (note the 404):