From 6d7e535a30e5d577e74308694686ca6d090c1778 Mon Sep 17 00:00:00 2001 From: Leonardo Araujo Date: Fri, 11 Aug 2023 02:19:42 -0300 Subject: [PATCH] feat: user-configurable querycache and formcache --- context.go | 67 +++++++++++++++---- context_test.go | 170 ++++++++++++++++++++++++++++++++++++++++++++++++ gin.go | 22 +++++++ 3 files changed, 245 insertions(+), 14 deletions(-) diff --git a/context.go b/context.go index 420ff1678f..e3d7ed18f3 100644 --- a/context.go +++ b/context.go @@ -456,15 +456,28 @@ func (c *Context) QueryArray(key string) (values []string) { } func (c *Context) initQueryCache() { - if c.queryCache == nil { - if c.Request != nil { - c.queryCache = c.Request.URL.Query() - } else { - c.queryCache = url.Values{} - } + if !c.engine.cacheConfig.EnableQueryCache || c.queryCache != nil { + return + } + + if c.Request != nil { + c.queryCache = c.Request.URL.Query() + } else { + c.queryCache = url.Values{} } } +// SetQuery sets the values for a given query key in the context's query cache. +// It ensures that the query cache is initialized before setting the values. +// If the query key already exists, its values will be overwritten with the provided values. +func (c *Context) SetQuery(key string, values []string) { + if !c.engine.cacheConfig.EnableQueryCache { + return // If query caching is disabled, just return without modifying the cache + } + c.initQueryCache() + c.queryCache[key] = values +} + // GetQueryArray returns a slice of strings for a given query key, plus // a boolean value whether at least one value exists for the given key. func (c *Context) GetQueryArray(key string) (values []string, ok bool) { @@ -526,16 +539,42 @@ func (c *Context) PostFormArray(key string) (values []string) { } func (c *Context) initFormCache() { - if c.formCache == nil { - c.formCache = make(url.Values) - req := c.Request - if err := req.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { - if !errors.Is(err, http.ErrNotMultipart) { - debugPrint("error on parse multipart form array: %v", err) - } + if !c.engine.cacheConfig.EnableFormCache || c.formCache != nil { + return + } + + if c.Request == nil { + return // If the Request is nil, exit early to avoid a nil pointer dereference + } + + c.formCache = make(url.Values) + if err := c.Request.ParseMultipartForm(c.engine.MaxMultipartMemory); err != nil { + if !errors.Is(err, http.ErrNotMultipart) { + debugPrint("error on parse multipart form array: %v", err) } - c.formCache = req.PostForm } + c.formCache = c.Request.PostForm +} + +// SetForm sets the values for a given form key in the context's form cache. +// It ensures that the form cache is initialized before setting the values. +// If the form key already exists, its values will be overwritten with the provided values. +func (c *Context) SetForm(key string, values []string) { + c.initFormCache() + if c.formCache != nil { // Only set values if the form cache is enabled + c.formCache[key] = values + } +} + +// GetForm retrieves the values associated with a given form key from the context's form cache. +// If the form cache has not been initialized, it does so first. +// If the form key does not exist, an empty slice is returned. +func (c *Context) GetForm(key string) []string { + c.initFormCache() + if c.formCache == nil { + return []string{} // Return an empty slice when the form cache is disabled + } + return c.formCache[key] } // GetPostFormArray returns a slice of strings for a given form key, plus diff --git a/context_test.go b/context_test.go index 70d4758377..85366ea089 100644 --- a/context_test.go +++ b/context_test.go @@ -521,6 +521,65 @@ func TestContextQueryAndPostForm(t *testing.T) { assert.Equal(t, 0, len(dicts)) } +func TestSetQuery(t *testing.T) { + // Create a Context instance with initialized query cache + c := &Context{ + engine: &Engine{ + cacheConfig: NewCacheConfig(true, true), + }, + Request: &http.Request{ + URL: &url.URL{ + RawQuery: "existingKey=value1", + }, + }, + } + + // Key and values to set + key := "testKey" + values := []string{"value1", "value2"} + + // Call SetQuery method + c.SetQuery(key, values) + + // Retrieve values from the query cache + retrievedValues, ok := c.queryCache[key] + + // Check if the values were correctly set + if !ok { + t.Errorf("Key %s not found in query cache", key) + } + + if !reflect.DeepEqual(retrievedValues, values) { + t.Errorf("Expected values %v, got %v", values, retrievedValues) + } +} + +func TestSetQueryWithCacheDisabled(t *testing.T) { + // Create a Context instance with query cache disabled + c := &Context{ + engine: &Engine{ + cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches + }, + Request: &http.Request{ + URL: &url.URL{ + RawQuery: "existingKey=value1", + }, + }, + } + + // Key and values to set + key := "testKey" + values := []string{"value1", "value2"} + + // Call SetQuery method + c.SetQuery(key, values) + + // Since the query cache is disabled, we expect the query cache to be nil + if c.queryCache != nil { + t.Errorf("Expected query cache to be nil, got %v", c.queryCache) + } +} + func TestContextPostFormMultipart(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Request = createMultipartRequest() @@ -613,6 +672,117 @@ func TestContextPostFormMultipart(t *testing.T) { assert.Equal(t, 0, len(dicts)) } +func TestSetForm(t *testing.T) { + req, _ := http.NewRequest("POST", "/", nil) + c := &Context{ + Request: req, + engine: &Engine{ + cacheConfig: NewCacheConfig(true, true), // Enabling both query and form caches + }, + } + + key := "testKey" + values := []string{"value1", "value2"} + + c.SetForm(key, values) + retrievedValues := c.formCache[key] + + if !reflect.DeepEqual(values, retrievedValues) { + t.Errorf("Expected values %v, got %v", values, retrievedValues) + } +} + +func TestGetForm(t *testing.T) { + // Create a Context instance with initialized form cache + c := &Context{ + engine: &Engine{ + cacheConfig: NewCacheConfig(true, true), // Assuming this initializes both caches + }, + formCache: map[string][]string{ + "existingKey": {"existingValue"}, + }, + } + + // Test retrieval of existing key + existingKey := "existingKey" + retrievedValues := c.GetForm(existingKey) + if !reflect.DeepEqual(retrievedValues, []string{"existingValue"}) { + t.Errorf("Expected values %v, got %v", []string{"existingValue"}, retrievedValues) + } + + // Test retrieval of non-existing key + nonExistingKey := "nonExistingKey" + retrievedValues = c.GetForm(nonExistingKey) + if len(retrievedValues) != 0 { + t.Errorf("Expected an empty slice, got %v", retrievedValues) + } +} + +func TestSetFormWithCacheDisabled(t *testing.T) { + c := &Context{ + engine: &Engine{ + cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches + }, + } + + key := "testKey" + values := []string{"value1", "value2"} + + c.SetForm(key, values) + + // Since the form cache is disabled, we expect the form cache to be nil + if c.formCache != nil { + t.Errorf("Expected form cache to be nil, got %v", c.formCache) + } +} + +func TestGetFormWithCacheDisabled(t *testing.T) { + c := &Context{ + engine: &Engine{ + cacheConfig: NewCacheConfig(false, false), // Disabling both query and form caches + }, + } + + key := "existingKey" + + // Call GetForm method + retrievedValues := c.GetForm(key) + + // Since the form cache is disabled and no pre-existing values were set, we expect the retrieved values to be an empty slice + if len(retrievedValues) != 0 { + t.Errorf("Expected an empty slice, got %v", retrievedValues) + } +} + +func TestChangeUrlParam(t *testing.T) { + ChangeUrlParam := func(c *Context) { + params, _ := url.ParseQuery(c.Request.URL.RawQuery) + params.Set("xxx", "yyy") + c.Request.URL.RawQuery = params.Encode() + } + + r := Default() + r.Use(ChangeUrlParam) + r.GET("/test", func(c *Context) { + // Dummy handler + }) + + // Create a test request with original parameters + req, err := http.NewRequest("GET", "/test?param1=value1", nil) + if err != nil { + t.Fatal(err) + } + + // Record the response + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + // Check that the URL parameter "xxx" has been set to "yyy" + if got := req.URL.Query().Get("xxx"); got != "yyy" { + t.Errorf("ChangeUrlParam() = %v, want %v", got, "yyy") + } +} + func TestContextSetCookie(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.SetSameSite(http.SameSiteLaxMode) diff --git a/gin.go b/gin.go index ed8b6dad75..b9be0804c7 100644 --- a/gin.go +++ b/gin.go @@ -79,6 +79,22 @@ const ( PlatformCloudflare = "CF-Connecting-IP" ) +// CacheConfig represents the configuration options for managing query and form caches +// within a Gin context. It provides flags to enable or disable the caching mechanisms +// for query parameters and form data, allowing for greater control over caching behavior. +type CacheConfig struct { + EnableQueryCache bool + EnableFormCache bool +} + +// NewCacheConfig returns a new instance of CacheConfig. +func NewCacheConfig(enableQueryCache, enableFormCache bool) CacheConfig { + return CacheConfig{ + EnableQueryCache: enableQueryCache, + EnableFormCache: enableFormCache, + } +} + // Engine is the framework's instance, it contains the muxer, middleware and configuration settings. // Create an instance of Engine, by using New() or Default() type Engine struct { @@ -168,6 +184,7 @@ type Engine struct { maxSections uint16 trustedProxies []string trustedCIDRs []*net.IPNet + cacheConfig CacheConfig } var _ IRouter = (*Engine)(nil) @@ -204,6 +221,7 @@ func New() *Engine { secureJSONPrefix: "while(1);", trustedProxies: []string{"0.0.0.0/0", "::/0"}, trustedCIDRs: defaultTrustedCIDRs, + cacheConfig: CacheConfig{true, true}, } engine.RouterGroup.engine = engine engine.pool.New = func() any { @@ -649,6 +667,10 @@ func (engine *Engine) handleHTTPRequest(c *Context) { serveError(c, http.StatusNotFound, default404Body) } +func (engine *Engine) SetCacheConfig(config CacheConfig) { + engine.cacheConfig = config +} + var mimePlain = []string{MIMEPlain} func serveError(c *Context, code int, defaultMessage []byte) {