gin/middleware_test.go
BitWeaverDev 641abb68da feat(engine): add SkipMethodNotAllowedMiddleware option
Middleware registered via Use() is prepended to the NoMethod handlers by
rebuild405Handlers, so a global middleware that aborts the request answers
before the NoMethod handler can reply. A middleware validating a header that
only some methods carry therefore turns every 405 into its own error
response.

Add an opt-in Engine.SkipMethodNotAllowedMiddleware flag that runs only the
NoMethod handlers for 405 responses. It is disabled by default, so existing
behaviour is unchanged.

The flag is resolved when the request is served rather than when the chain is
built, so it applies regardless of the order in which Use(), NoMethod() and
the flag are set. Requests handled by NoRoute() keep running the global
middleware.

Fixes #4189
2026-08-19 21:25:12 +02:00

382 lines
8.2 KiB
Go

// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package gin
import (
"errors"
"net/http"
"strings"
"testing"
"github.com/gin-contrib/sse"
"github.com/stretchr/testify/assert"
)
func TestMiddlewareGeneralCase(t *testing.T) {
signature := ""
router := New()
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
})
router.GET("/", func(c *Context) {
signature += "D"
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.NoMethod(func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "ACDB", signature)
}
func TestMiddlewareNoRoute(t *testing.T) {
signature := ""
router := New()
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
c.Next()
c.Next()
c.Next()
c.Next()
signature += "D"
})
router.NoRoute(func(c *Context) {
signature += "E"
c.Next()
signature += "F"
}, func(c *Context) {
signature += "G"
c.Next()
signature += "H"
})
router.NoMethod(func(c *Context) {
signature += " X "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "ACEGHFDB", signature)
}
func TestMiddlewareNoMethodEnabled(t *testing.T) {
signature := ""
router := New()
router.HandleMethodNotAllowed = true
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
c.Next()
signature += "D"
})
router.NoMethod(func(c *Context) {
signature += "E"
c.Next()
signature += "F"
}, func(c *Context) {
signature += "G"
c.Next()
signature += "H"
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "ACEGHFDB", signature)
}
func TestMiddlewareNoMethodDisabled(t *testing.T) {
signature := ""
router := New()
// NoMethod disabled
router.HandleMethodNotAllowed = false
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
c.Next()
signature += "D"
})
router.NoMethod(func(c *Context) {
signature += "E"
c.Next()
signature += "F"
}, func(c *Context) {
signature += "G"
c.Next()
signature += "H"
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "AC X DB", signature)
}
// Test the fix for https://github.com/gin-gonic/gin/issues/4189
func TestMiddlewareNoMethodSkipped(t *testing.T) {
signature := ""
router := New()
router.HandleMethodNotAllowed = true
router.SkipMethodNotAllowedMiddleware = true
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
c.Next()
signature += "D"
})
router.NoMethod(func(c *Context) {
signature += "E"
c.Next()
signature += "F"
}, func(c *Context) {
signature += "G"
c.Next()
signature += "H"
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, http.MethodPost, w.Header().Get("Allow"))
assert.Equal(t, "EGHF", signature)
}
// The flag is read when the request is served, so it takes effect even when it
// is set after Use() and NoMethod() have already built the handlers chain.
func TestMiddlewareNoMethodSkippedSetAfterRegistration(t *testing.T) {
signature := ""
router := New()
router.HandleMethodNotAllowed = true
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.NoMethod(func(c *Context) {
signature += "E"
c.Next()
signature += "F"
})
router.POST("/", func(c *Context) {
signature += " XX "
})
router.SkipMethodNotAllowedMiddleware = true
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "EF", signature)
}
func TestMiddlewareNoMethodSkippedWithoutNoMethodHandlers(t *testing.T) {
signature := ""
router := New()
router.HandleMethodNotAllowed = true
router.SkipMethodNotAllowedMiddleware = true
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusMethodNotAllowed, w.Code)
assert.Equal(t, "405 method not allowed", w.Body.String())
assert.Empty(t, signature)
}
// The flag only covers 405 responses, requests falling through to NoRoute must
// keep running the global middleware.
func TestMiddlewareNoMethodSkippedDoesNotAffectNoRoute(t *testing.T) {
signature := ""
router := New()
router.HandleMethodNotAllowed = true
router.SkipMethodNotAllowedMiddleware = true
router.Use(func(c *Context) {
signature += "A"
c.Next()
signature += "B"
})
router.Use(func(c *Context) {
signature += "C"
c.Next()
signature += "D"
})
router.NoMethod(func(c *Context) {
signature += " E "
})
router.NoRoute(func(c *Context) {
signature += " X "
})
router.POST("/", func(c *Context) {
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/not-registered")
// TEST
assert.Equal(t, http.StatusNotFound, w.Code)
assert.Equal(t, "AC X DB", signature)
}
func TestMiddlewareAbort(t *testing.T) {
signature := ""
router := New()
router.Use(func(c *Context) {
signature += "A"
})
router.Use(func(c *Context) {
signature += "C"
c.AbortWithStatus(http.StatusUnauthorized)
c.Next()
signature += "D"
})
router.GET("/", func(c *Context) {
signature += " X "
c.Next()
signature += " XX "
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusUnauthorized, w.Code)
assert.Equal(t, "ACD", signature)
}
func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) {
signature := ""
router := New()
router.Use(func(c *Context) {
signature += "A"
c.Next()
c.AbortWithStatus(http.StatusGone)
signature += "B"
})
router.GET("/", func(c *Context) {
signature += "C"
c.Next()
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusGone, w.Code)
assert.Equal(t, "ACB", signature)
}
// TestMiddlewareFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as
// as well as Abort
func TestMiddlewareFailHandlersChain(t *testing.T) {
// SETUP
signature := ""
router := New()
router.Use(func(context *Context) {
signature += "A"
context.AbortWithError(http.StatusInternalServerError, errors.New("foo")) //nolint: errcheck
})
router.Use(func(context *Context) {
signature += "B"
context.Next()
signature += "C"
})
// RUN
w := PerformRequest(router, http.MethodGet, "/")
// TEST
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Equal(t, "A", signature)
}
func TestMiddlewareWrite(t *testing.T) {
router := New()
router.Use(func(c *Context) {
c.String(http.StatusBadRequest, "hola\n")
})
router.Use(func(c *Context) {
c.XML(http.StatusBadRequest, H{"foo": "bar"})
})
router.Use(func(c *Context) {
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
})
router.GET("/", func(c *Context) {
c.JSON(http.StatusBadRequest, H{"foo": "bar"})
}, func(c *Context) {
c.Render(http.StatusBadRequest, sse.Event{
Event: "test",
Data: "message",
})
})
w := PerformRequest(router, http.MethodGet, "/")
assert.Equal(t, http.StatusBadRequest, w.Code)
assert.Equal(t, strings.ReplaceAll("hola\n<map><foo>bar</foo></map>{\"foo\":\"bar\"}{\"foo\":\"bar\"}event:test\ndata:message\n\n", " ", ""), strings.ReplaceAll(w.Body.String(), " ", ""))
}