Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: user-configurable querycache and formcache #3695

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
68 changes: 54 additions & 14 deletions context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -526,16 +539,43 @@ 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)
return
}
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
Expand Down
223 changes: 223 additions & 0 deletions context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -613,6 +672,170 @@ func TestContextPostFormMultipart(t *testing.T) {
assert.Equal(t, 0, len(dicts))
}

func TestInitFormCache(t *testing.T) {
tests := []struct {
name string
cacheEnabled bool
existingCache bool
request *http.Request
expectFormCache bool
}{
{"Cache disabled", false, false, nil, false},
{"Existing cache", true, true, nil, false},
{"Nil request", true, false, nil, false},
{"Successful parsing", true, false, &http.Request{Method: "POST"}, true},
}

// Create a specific request with an error other than ErrNotMultipart
req, err := http.NewRequest("POST", "/", nil)
if err != nil {
t.Fatal(err)
}
req.Header.Set("Content-Type", "multipart/form-data")
tests = append(tests, struct {
name string
cacheEnabled bool
existingCache bool
request *http.Request
expectFormCache bool
}{"Error other than ErrNotMultipart", true, false, req, false})

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
engine := &Engine{
cacheConfig: CacheConfig{EnableFormCache: tt.cacheEnabled},
}

c := &Context{
engine: engine,
}

if tt.existingCache {
c.formCache = make(url.Values)
}

c.Request = tt.request

c.initFormCache()

if tt.expectFormCache && c.formCache == nil {
t.Errorf("Expected formCache to be initialized, but it was nil")
}
})
}
}

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)
Expand Down
22 changes: 22 additions & 0 deletions gin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -168,6 +184,7 @@ type Engine struct {
maxSections uint16
trustedProxies []string
trustedCIDRs []*net.IPNet
cacheConfig CacheConfig
}

var _ IRouter = (*Engine)(nil)
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down
14 changes: 14 additions & 0 deletions gin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,17 @@ func assertRoutePresent(t *testing.T, gotRoutes RoutesInfo, wantRoute RouteInfo)

func handlerTest1(c *Context) {}
func handlerTest2(c *Context) {}

func TestSetCacheConfig(t *testing.T) {
engine := &Engine{}
config := CacheConfig{
EnableFormCache: true,
// Other fields can be filled as needed
}

engine.SetCacheConfig(config)

if !reflect.DeepEqual(engine.cacheConfig, config) {
t.Errorf("Expected engine.cacheConfig to be %v, but got %v", config, engine.cacheConfig)
}
}