diff --git a/gin.go b/gin.go index 16067e55..2e033bf3 100644 --- a/gin.go +++ b/gin.go @@ -135,10 +135,16 @@ type Engine struct { AppEngine bool // UseRawPath if enabled, the url.RawPath will be used to find parameters. + // The RawPath is only a hint, EscapedPath() should be use instead. (https://pkg.go.dev/net/url@master#URL) + // Only use RawPath if you know what you are doing. UseRawPath bool + // UseEscapedPath if enable, the url.EscapedPath() will be used to find parameters + // It overrides UseRawPath + UseEscapedPath bool + // UnescapePathValues if true, the path value will be unescaped. - // If UseRawPath is false (by default), the UnescapePathValues effectively is true, + // If UseRawPath and UseEscapedPath are false (by default), the UnescapePathValues effectively is true, // as url.Path gonna be used, which is already unescaped. UnescapePathValues bool @@ -191,6 +197,7 @@ var _ IRouter = (*Engine)(nil) // - HandleMethodNotAllowed: false // - ForwardedByClientIP: true // - UseRawPath: false +// - UseEscapedPath: false // - UnescapePathValues: true func New(opts ...OptionFunc) *Engine { debugPrintWARNINGNew() @@ -208,6 +215,7 @@ func New(opts ...OptionFunc) *Engine { RemoteIPHeaders: []string{"X-Forwarded-For", "X-Real-IP"}, TrustedPlatform: defaultPlatform, UseRawPath: false, + UseEscapedPath: false, RemoveExtraSlash: false, UnescapePathValues: true, MaxMultipartMemory: defaultMultipartMemory, @@ -683,7 +691,11 @@ func (engine *Engine) handleHTTPRequest(c *Context) { httpMethod := c.Request.Method rPath := c.Request.URL.Path unescape := false - if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { + + if engine.UseEscapedPath { + rPath = c.Request.URL.EscapedPath() + unescape = engine.UnescapePathValues + } else if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { rPath = c.Request.URL.RawPath unescape = engine.UnescapePathValues } diff --git a/ginS/gins_test.go b/ginS/gins_test.go new file mode 100644 index 00000000..ffde85d2 --- /dev/null +++ b/ginS/gins_test.go @@ -0,0 +1,246 @@ +// Copyright 2025 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package ginS + +import ( + "html/template" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" +) + +func init() { + gin.SetMode(gin.TestMode) +} + +func TestGET(t *testing.T) { + GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "test") + }) + + req := httptest.NewRequest(http.MethodGet, "/test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "test", w.Body.String()) +} + +func TestPOST(t *testing.T) { + POST("/post", func(c *gin.Context) { + c.String(http.StatusCreated, "created") + }) + + req := httptest.NewRequest(http.MethodPost, "/post", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "created", w.Body.String()) +} + +func TestPUT(t *testing.T) { + PUT("/put", func(c *gin.Context) { + c.String(http.StatusOK, "updated") + }) + + req := httptest.NewRequest(http.MethodPut, "/put", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "updated", w.Body.String()) +} + +func TestDELETE(t *testing.T) { + DELETE("/delete", func(c *gin.Context) { + c.String(http.StatusOK, "deleted") + }) + + req := httptest.NewRequest(http.MethodDelete, "/delete", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "deleted", w.Body.String()) +} + +func TestPATCH(t *testing.T) { + PATCH("/patch", func(c *gin.Context) { + c.String(http.StatusOK, "patched") + }) + + req := httptest.NewRequest(http.MethodPatch, "/patch", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "patched", w.Body.String()) +} + +func TestOPTIONS(t *testing.T) { + OPTIONS("/options", func(c *gin.Context) { + c.String(http.StatusOK, "options") + }) + + req := httptest.NewRequest(http.MethodOptions, "/options", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "options", w.Body.String()) +} + +func TestHEAD(t *testing.T) { + HEAD("/head", func(c *gin.Context) { + c.String(http.StatusOK, "head") + }) + + req := httptest.NewRequest(http.MethodHead, "/head", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestAny(t *testing.T) { + Any("/any", func(c *gin.Context) { + c.String(http.StatusOK, "any") + }) + + req := httptest.NewRequest(http.MethodGet, "/any", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "any", w.Body.String()) +} + +func TestHandle(t *testing.T) { + Handle(http.MethodGet, "/handle", func(c *gin.Context) { + c.String(http.StatusOK, "handle") + }) + + req := httptest.NewRequest(http.MethodGet, "/handle", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "handle", w.Body.String()) +} + +func TestGroup(t *testing.T) { + group := Group("/group") + group.GET("/test", func(c *gin.Context) { + c.String(http.StatusOK, "group test") + }) + + req := httptest.NewRequest(http.MethodGet, "/group/test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "group test", w.Body.String()) +} + +func TestUse(t *testing.T) { + var middlewareExecuted bool + Use(func(c *gin.Context) { + middlewareExecuted = true + c.Next() + }) + + GET("/middleware-test", func(c *gin.Context) { + c.String(http.StatusOK, "ok") + }) + + req := httptest.NewRequest(http.MethodGet, "/middleware-test", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.True(t, middlewareExecuted) + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestNoRoute(t *testing.T) { + NoRoute(func(c *gin.Context) { + c.String(http.StatusNotFound, "custom 404") + }) + + req := httptest.NewRequest(http.MethodGet, "/nonexistent", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusNotFound, w.Code) + assert.Equal(t, "custom 404", w.Body.String()) +} + +func TestNoMethod(t *testing.T) { + NoMethod(func(c *gin.Context) { + c.String(http.StatusMethodNotAllowed, "method not allowed") + }) + + // This just verifies that NoMethod is callable + // Testing the actual behavior would require a separate engine instance + assert.NotNil(t, engine()) +} + +func TestRoutes(t *testing.T) { + GET("/routes-test", func(c *gin.Context) {}) + + routes := Routes() + assert.NotEmpty(t, routes) + + found := false + for _, route := range routes { + if route.Path == "/routes-test" && route.Method == http.MethodGet { + found = true + break + } + } + assert.True(t, found) +} + +func TestSetHTMLTemplate(t *testing.T) { + tmpl := template.Must(template.New("test").Parse("Hello {{.}}")) + SetHTMLTemplate(tmpl) + + // Verify engine has template set + assert.NotNil(t, engine()) +} + +func TestStaticFile(t *testing.T) { + StaticFile("/static-file", "../testdata/test_file.txt") + + req := httptest.NewRequest(http.MethodGet, "/static-file", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestStatic(t *testing.T) { + Static("/static-dir", "../testdata") + + req := httptest.NewRequest(http.MethodGet, "/static-dir/test_file.txt", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} + +func TestStaticFS(t *testing.T) { + fs := http.Dir("../testdata") + StaticFS("/static-fs", fs) + + req := httptest.NewRequest(http.MethodGet, "/static-fs/test_file.txt", nil) + w := httptest.NewRecorder() + engine().ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) +} diff --git a/gin_test.go b/gin_test.go index cee1f3cc..21bf71d8 100644 --- a/gin_test.go +++ b/gin_test.go @@ -720,6 +720,55 @@ func TestEngineHandleContextPreventsMiddlewareReEntry(t *testing.T) { assert.Equal(t, int64(1), handlerCounterV2) } +func TestEngineHandleContextUseEscapedPathPercentEncoded(t *testing.T) { + r := New() + r.UseEscapedPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + // Path is Escaped, the %25 is not interpreted as % + assert.Equal(t, "foo%252Fbar", c.Param("path")) + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) +} + +func TestEngineHandleContextUseRawPathPercentEncoded(t *testing.T) { + r := New() + r.UseRawPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + // Path is used, the %25 is interpreted as % + assert.Equal(t, "foo%2Fbar", c.Param("path")) + c.Status(http.StatusOK) + }) + + req := httptest.NewRequest(http.MethodGet, "/v1/foo%252Fbar", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) +} + +func TestEngineHandleContextUseEscapedPathOverride(t *testing.T) { + r := New() + r.UseEscapedPath = true + r.UseRawPath = true + r.UnescapePathValues = false + + r.GET("/v1/:path", func(c *Context) { + assert.Equal(t, "foo%25bar", c.Param("path")) + c.Status(http.StatusOK) + }) + + assert.NotPanics(t, func() { + w := PerformRequest(r, http.MethodGet, "/v1/foo%25bar") + assert.Equal(t, 200, w.Code) + }) +} + func TestPrepareTrustedCIRDsWith(t *testing.T) { r := New()